Search code examples
c#wpfmvvmicommand

WPF MVVM changing parent window viewmodel from icommand execution


I'm currently in the process of mastering the C# WPF MVVM pattern and have stumbled upon a pretty big hurdle...

What I am trying to do fire off a LoginCommand that when successfully executed will allow me to change the parent window's viewmodel. The only issue is I can't quite think of a way to change the parent window's viewmodel without breaking the MVVM design pattern because I can't access the parent window's ContentControl that sets its path to the active UserControlViewModel in the window.

Here's the scenario:

In our App.xaml we have two DataTemplates:
<DataTemplate DataType="{x:Type ViewModels:LoginViewModel}"> <Views:LoginView /> </DataTemplate> <DataTemplate DataType="{x:Type ViewModels:LoggedInViewModel}"> <Views:LoggedView /> </DataTemplate>

In our MainWindow we have:
<ContentControl Content="{Binding ViewModel}" />
The MainWindow code behind will set the ViewModel = LoginViewModel

In our LoginViewModel we have:
<Button Command="{Binding LoginCommand}" CommandParameter="{Binding ElementName=pwPasswordBoxControlInXaml}" />

Now for the money... the LoginCommand:
public void Execute(object parameter) { // Do some validation // Async login task stuff // ... // Logged in... change the MainWindow's ViewModel to the LoggedInViewModel }

How can I make the Execute method change the window's viewmodel without breaking the MVVM pattern?

Things I've tried thus far:

  • Making the MainWindow have a static Instance singleton that I can access and then change the ViewModel property from the command.
  • Attempting to implement some form of routed command listener in the MainWindow and then have commands fire off routed command events to be handled by the parent window.

Solution

  • I've done a quick demo to show one way of doing it. I've kept it as simple as possible to give the general idea. There are lots of different ways of accomplishing the same thing (e.g. you could hold a reference to MainWindowViewModel inside LoginViewModel, handle everything there then call a method on MainWindowViewModel to trigger the workspace change, or you could use Events/Messages, etc).

    Definitely have a read of Navigation with MVVM though. That's a really good introduction that I found helpful when I was getting started with it.

    The key thing to take away from this is to have an outer MainWindowViewModel or ApplicationViewModel which handles the navigation, holds references to workspaces, etc. Then the choice of how you interact with this is up to you.

    In the code below, I've left out the clutter from defining Window, UserControl, etc. to keep it shorter.

    Window:

    <DockPanel>
        <ContentControl Content="{Binding CurrentWorkspace}"/>
    </DockPanel>
    

    MainWindowViewModel (this should be set as the DataContext for the Window):

    public class MainWindowViewModel : ObservableObject
    {
        LoginViewModel loginViewModel = new LoginViewModel();
        LoggedInViewModel loggedInViewModel = new LoggedInViewModel();
    
        public MainWindowViewModel()
        {
            CurrentWorkspace = loginViewModel;
    
            LoginCommand = new RelayCommand((p) => DoLogin());
        }
    
        private WorkspaceViewModel currentWorkspace;
        public WorkspaceViewModel CurrentWorkspace
        {
            get { return currentWorkspace; }
            set
            {
                if (currentWorkspace != value)
                {
                    currentWorkspace = value;
                    OnPropertyChanged();
                }
            }
        }
    
        public ICommand LoginCommand { get; set; }
    
        public void DoLogin()
        {
            bool isValidated = loginViewModel.Validate();
            if (isValidated)
            {
                CurrentWorkspace = loggedInViewModel;
            }
        }
    }
    

    LoginView:

    In this example I'm binding a Button on the LoginView to the LoginCommand on the Window DataContext (i.e. MainWindowViewModel).

    <StackPanel Orientation="Vertical">
        <TextBox Text="{Binding UserName}"/>
        <Button Content="Login" Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.LoginCommand}"/>
    </StackPanel>
    

    LoginViewModel:

    public class LoginViewModel : WorkspaceViewModel
    {
        private string userName;
        public string UserName
        {
            get { return userName; }
            set
            {
                if (userName != value)
                {
                    userName = value;
                    OnPropertyChanged();
                }
            }
        }
    
        public bool Validate()
        {
            if (UserName == "bob")
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    }
    

    LoggedInView:

    <StackPanel Orientation="Vertical">
        <TextBox Text="{Binding RestrictedData}"/>
    </StackPanel>
    

    LoggedInViewModel:

    public class LoggedInViewModel : WorkspaceViewModel
    {
        private string restrictedData = "Some restricted data";
        public string RestrictedData
        {
            get { return restrictedData; }
            set
            {
                if (restrictedData != value)
                {
                    restrictedData = value;
                    OnPropertyChanged();
                }
            }
        }
    }
    

    WorkspaceViewModel:

    public abstract class WorkspaceViewModel : ObservableObject
    {
    }
    

    Then some other classes you probably already have implemented (or alternatives).

    ObservableObject:

    public abstract class ObservableObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this,
                new PropertyChangedEventArgs(propertyName));
        }
    }
    

    RelayCommand:

    public class RelayCommand : ICommand
    {
        private readonly Action<object> execute;
        private readonly Predicate<object> canExecute;
    
        public RelayCommand(Action<object> execute)
            : this(execute, null)
        { }
    
        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
            {
                throw new ArgumentNullException("execute");
            }
    
            this.execute = execute;
            this.canExecute = canExecute;
        }
    
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
    
        [DebuggerStepThrough]
        public bool CanExecute(object parameter)
        {
            return canExecute == null ? true : canExecute(parameter);
        }
    
        public void Execute(object parameter)
        {
            execute(parameter);
        }
    }
    

    App.Xaml:

        <DataTemplate DataType="{x:Type ViewModels:LoginViewModel}">
            <Views:LoginView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type ViewModels:LoggedInViewModel}">
            <Views:LoggedInView />
        </DataTemplate>