Search code examples
c#wpfmvvmbinding

WPF binding doesn't work when property changes


This is my first question here, please understand. I've spent on this problem hours of digging nothing works for me, maybe somebody will explain me this strange (for me) problem? I've made my app in WPF with MVVM

I got in MainWindow.xaml with usercontrol which loads view with binding:

<UserControl Content="{Binding CurrentView}" />

MainWindow DataContext is MainViewModel, which derives from BaseViewModel, where i set and get CurrentView from and implement INotifyPropertyChanged.

First CurrentView is LoginViewModel - it loads in constructor of MainViewModel properly and set the view (Usercontrol Loginview.xaml). And I don't understand why when I change CurrentView property from this loaded LoginViewModel (it definitely changes - I checked it and NotifyPropertyChanged raises) - my view doesn't change - it's still LoginView, but should be WelcomeView. But when I change the same property with the same code from MainViewModel - my view changes properly. Somebody could point where's an error? Is it impossible to change CurrentView property from outside of MainViewModel even it's not the part of MainViemodel but another class or what? What I'm missing here?

CODE:

    public class BaseViewModel : NotifyPropertyChanged 
    {
        private object? _currentView;
        public object? CurrentView
        {
            get { return _currentView; }
            set
            {
                _currentView = value;
                OnPropertyChanged(nameof(CurrentView));
            }
        }
    }
    public class MainViewModel : BaseViewModel
    {
        public LoginViewModel LoginViewModel { get; set; }
        public WelcomeViewModel WelcomeViewModel { get; set; }
    [..]
        public ICommand LoginCommand { get; set; } //- this works 
        public MainViewModel()
        {
            LoginViewModel = new();
            WelcomeViewModel = new();
            CurrentView = LoginViewModel;
            // COMMANDS
            LoginCommand = new RelayCommand(o => DoLogin(), o => CanLogin()); // this works
        }
        private bool CanLogin()
        {
            return true;
        }
        private void DoLogin()
        {
            CurrentView = WelcomeViewModel;
        } 
    }
    public class LoginViewModel : BaseViewModel
    {
    [...]
        public WelcomeViewModel WelcomeViewModel { get; set; }
        // COMMANDS PROPERTIES
        public ICommand LoginCommand { get; set; }
        public LoginViewModel()
        {
            WelcomeViewModel = new();
            LoginCommand = new RelayCommand(o => DoLogin(), o => CanLogin());
        }
        private bool CanLogin()
        {
            return true;
        }
        private void DoLogin()
        {
            MessageBox.Show("Login!");  // message box test showes
            // here will be some authentication
            CurrentView = WelcomeViewModel; // property CurrentView changes
            // CurrentView = new MainViewModel().WelcomeViewModel; // this way also doesn't work
        }
    }

and finally XAML from UserControl LoginView.xaml (command runs properly, property CurrentView changes, but view remains the same:

<Button
    Width="200"
    Height="50"
    Margin="10"
    Command="{Binding LoginCommand}"
    Content="Login"
    FontSize="18" />
<!--  Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}, 
Path=DataContext.LoginCommand}" THIS WORKS! -->

App.xaml has:

 <DataTemplate DataType="{x:Type vievmodels:LoginViewModel}">
            <viewscontents:LoginView/>
        </DataTemplate>
 <DataTemplate DataType="{x:Type vievmodels:WelcomeViewModel}">
            <viewscontents:WelcomeView/>
        </DataTemplate>

Solution

  • The question is: how to set DataContext .... Do this.

    Main ViewModel:

        public class MainViewModel : BaseInpc
        {
            #region CurrentContent
            private object? _currentContent;
            public object? CurrentContent { get => _currentContent; set => Set(ref _currentContent, value); }
    
            private RelayCommand _setCurrentCommand;
            public RelayCommand SetCurrentCommand => _setCurrentCommand
                ??= new RelayCommand(content => CurrentContent = content);
            #endregion
    
            public LoginViewModel LoginViewModel { get; } = new LoginViewModel();
            public WelcomeViewModel WelcomeViewModel { get; } = new WelcomeViewModel();
    
            public MainViewModel()
            {
                CurrentContent = WelcomeViewModel;
            }
        }
    
        public class WelcomeViewModel: BaseInpc // Derived not from MainViewModel!
        {
            // Some Code
        }
    
        public class LoginViewModel: BaseInpc // Derived not from MainViewModel!
        {
            // Some Code
        }
    

    Create an instance of MainViewModel in the application resources:

        <Application.Resources>
            <local:MainViewModel x:Key="mainVM"/>
        </Application.Resources>
    

    In Windows XAML:

    <Window ------------
            ------------
            DataContext="{DynamicResource mainVM}">
        <Window.Resources>
            <DataTemplate DataType="{x:Type local:LoginViewModel}">
                <local:LoginViewUserControl/>
            </DataTemplate>
            <DataTemplate DataType="{x:Type WelcomeViewModel}">
                <local:WelcomeViewUserControl/>
            </DataTemplate>
        </Window.Resources>
    
        <ContentControl Content="{Binding CurrentContent}"/>
    

    An example of a button toggling CurrentContent:

        <Button Command="{Binding SetCurrentCommand, Source={StaticResource mainVM}}"
                CommandParameter="{Binding LoginViewModel, Source={StaticResource mainVM}}"/>
    

    BaseInpc and RelayCommand classes.