wpfxamlmvvmdata-bindingviewmodel

WPF control doesn't update when property is changed


I am developing a WPF app for my final university project (thesis). One of the goals is to be able to dynamically change the language of the UI. I suppose that it is not a very good idea to do it the way I am trying as it requires a lot of code, but for the sake of learning, I will go through with it. The idea is to bind text properties in the XAML to properties in the LanguageViewModel and when the user changes the current language from a dropdown menu, all strings in the UI should change to the chosen language. With the code bellow, the OnPropertyChanged method is executed, but the UI elements don't update and after a lot of debugging, I still don't have any clue why.

I'm currently experimenting with a single TextBlock control. I have placed it between comments to make it easier to find.

This is my MainWindow.xaml

<Window x:Class="AdjustrixWPF.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:AdjustrixWPF"
    xmlns:userControls="clr-namespace:AdjustrixWPF.View.UserControls"
    mc:Ignorable="d"
    Title="MainWindow" WindowState="Maximized" Height="650" Width="850" WindowStyle="None"
    MouseLeftButtonDown="Window_MouseLeftButtonDown"
    Background="{DynamicResource BackgroundBrush}">
<Grid Name="MainGrid"
      Margin="5">
    <Grid.RowDefinitions>
        <RowDefinition Height="40"/>
        <RowDefinition Height="25*"/>
        <RowDefinition Height="75*"/>
        <RowDefinition Height="25"/>
    </Grid.RowDefinitions>
    <TabControl Grid.Row="0" Width="{Binding ElementName=MainGrid, Path=ActualWidth}" 
                HorizontalAlignment="Left" Grid.RowSpan="2" Background="{DynamicResource BackgroundBrush}" Style="{StaticResource TabControlStyle}">
        <TabItem Background="{DynamicResource BackgroundBrush}" 
                 Style="{StaticResource TabItemStyle}">
            <TabItem.Header>
                <userControls:TabHeader DataContext="{Binding languageViewModel}"
                    Text="File" 
                                        IsMouseOver="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType=TabItem}}"
                                        IsSelected="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=TabItem}}"
                                        Style="{StaticResource TabHeaderStyle}"
                                        Height="20"/>
            </TabItem.Header>
            <StackPanel Orientation="Horizontal">
                <Border BorderBrush="{DynamicResource ForegroundBrush}"
                        BorderThickness="0 0 1 0"
                        Margin="5">
                    <Grid Height="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=StackPanel}}"
                          Width="300"
                          x:Name="TabPanelGrid">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="50*"/>
                            <ColumnDefinition Width="25*"/>
                        </Grid.ColumnDefinitions>
                        <Grid Height="{Binding ElementName=TabPanelGrid, Path=ActualHeight}">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="10*"/>
                                <RowDefinition Height="45*"/>
                                <RowDefinition Height="45*"/>
                            </Grid.RowDefinitions>
                            <!-- =========================================================== -->
                            <!-- This is the control I am currently experimenting with-->
                            <TextBlock x:Name="AppearanceBlock"
                                Style="{StaticResource TextBlockTabPanel}"
                                       Text="{Binding languageViewModel.Appearance, UpdateSourceTrigger=PropertyChanged}"
                                       Margin="0 0 0 3"/>
                            <!-- =========================================================== -->
                            <userControls:LabeledComboBox LabelText="Language"
                                                          ComboBoxItemsSource="{Binding languageViewModel.Languages}"
                                                          ComboBoxSelectedItem="{Binding languageViewModel.CurrentLanguage, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                                          Grid.Row="1"
                                                          x:Name="LanguageBox"/>
                            <userControls:LabeledComboBox LabelText="Theme"
                                                          ComboBoxItemsSource="{Binding Path=themeViewModel.Themes, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                                          ComboBoxSelectedItem="{Binding Path=themeViewModel.SelectedTheme, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                                          Grid.Row="2"
                                                          x:Name="ThemeBox"/>
                        </Grid>
                    </Grid>
                </Border>
            </StackPanel>
        </TabItem>
        <TabItem Style="{StaticResource TabItemStyle}">
            <TabItem.Header>
                <userControls:TabHeader Text="Data"
                                        IsMouseOver="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType=TabItem}}"
                                        IsSelected="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=TabItem}}"
                                        Style="{StaticResource TabHeaderStyle}"
                                        Height="20"/>
            </TabItem.Header>
            <StackPanel>

            </StackPanel>
        </TabItem>
    </TabControl>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
        <Button Content="_" Style="{DynamicResource TitleBarButtonStyle}" Name="Minimize" Click="Minimize_Click"/>
        <Button Content="🗖" Style="{DynamicResource TitleBarButtonStyle}" Name="Restore" Click="Restore_Click"/>
        <Button Content="╳" Style="{DynamicResource TitleBarButtonStyle}" Name="Close" Click="Close_Click"/>
    </StackPanel>
</Grid>

This is the code behind where the data context is being set:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.MaxHeight = SystemParameters.MaximizedPrimaryScreenHeight;
        MainWindowViewModel mainWindowViewModel = new();
        this.DataContext = mainWindowViewModel;
    }

This is the MainWindowViewModel:

public class MainWindowViewModel : ViewModelBase
{

    public ThemeViewModel themeViewModel { get; set; }

    public LanguageViewModel languageViewModel { get; set; }

    public MainWindowViewModel()
    {
        themeViewModel = new ThemeViewModel();
        languageViewModel = new LanguageViewModel();
    }
}

This is the language view model:

internal class StringEntry
{
    public string Bulgarian { get; set; }
    public string English { get; set; }
    public StringEntry(string bg, string en)
    {
        Bulgarian = bg;
        English = en;
    }

    public string GetString(Language currentLanguage)
    {
        if (currentLanguage == Language.English)
        {
            return English;
        }
        return Bulgarian;
    }
}

public class LanguageViewModel : ViewModelBase
{
    private StringEntry file = new("Файл", "File");
    private StringEntry data = new("Данни", "Data");
    private StringEntry appearance = new("Изглед", "Appearance");
    private Language currentLanguage;

    public LanguageViewModel()
    {
        File = file.GetString(currentLanguage);
        Data = data.GetString(currentLanguage);
        Appearance = appearance.GetString(currentLanguage);
        //todo: later load it from AppData/Local
        currentLanguage = Language.English;
    }

    public string CurrentLanguage
    {
        get
        {
            return currentLanguage.ToString();
        }
        set
        {
            currentLanguage = Language.English.ToString() == value ? Language.English : Language.Bulgarian;
            File = Language.English == App.Language ? file.English : file.Bulgarian;
            Data = data.GetString(currentLanguage);
            Appearance = appearance.GetString(currentLanguage);
            OnPropertyChanged();
            OnPropertyChanged(nameof(File));
        }
    }

    public string File
    {
        get { return file.GetString(currentLanguage); }
        set
        {
            _ = value;
            OnPropertyChanged();
        }
    }

    public string Data
    {
        get { return data.GetString(currentLanguage); }
        set
        {
            _ = value;
            OnPropertyChanged();
        }
    }

    public string Appearance
    {
        get { return appearance.GetString(currentLanguage); }
        set { _ = value; OnPropertyChanged(); }
    }
}

And this is the base view model class:

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    public void OnPropertyChanged([CallerMemberName] string member = null)
    {
        PropertyChanged?.Invoke(member, new PropertyChangedEventArgs(member));
    }
}

Any help will be greatly appreciated!


Solution

  • Your OnPropertyChanged implementation is wrong. Instead of the property name you must pass this as first argument to the Invoke method:

    public void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }