WPF/MVVM How do I change the DataContext of Commands to a different ViewModel

Anonymous
2023-08-04T16:00:35.8733333+00:00

Hello, I am trying to figure out navigation in my project and am encountering an issue with the datacontext of my commands. I am new to wpf and am still learning it as I make my project. What I want to do is navigate between three user control Views, using three different buttons. I have defined the buttons as a seperate View component NavigationButtons.xaml with it's own ViewModel NavButtonsVM. When I run the program, it compiles and opens but nothing happens when I click on buttons, then I noticed that the Commands never execute. When I dig a little deeper I notice I get errors during runtime where it says the DataContext is MainVM and cannot find Property "NavigateToHomeCommand" in MainVM.

<UserControl
    x:Class="CAMUI.Components.NavigationButtons"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:componentvm="clr-namespace:CAMUI.ViewModels.ComponentVM"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:g="clr-namespace:CAMUI"
    xmlns:local="clr-namespace:CAMUI.Components"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:viewsvm="clr-namespace:CAMUI.ViewModels.ViewsVM"
    d:DataContext="{d:DesignInstance Type=componentvm:NavButtonsVM}"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">



    <Grid>

        <StackPanel Background="Transparent" Orientation="Horizontal">
            <Button Command="{Binding NavigateToHomeCommand}" Content="Home" />
            <Button Command="{Binding NavigateToAccountCommand}" Content="Accounts" />
            <Button Command="{Binding NavigateToContactsCommand}" Content="Contacts" />
        </StackPanel>
    </Grid>
</UserControl>


using CAMUI.Commands;
using CAMUI.Stores;
using System.Windows.Input;

namespace CAMUI.ViewModels.ComponentVM
{
    public class NavButtonsVM : ViewModelBase
    {

        /*  handles button click commands in NavigationButtons Component  */
        public ICommand? NavigateToHomeCommand { get; }
        public ICommand? NavigateToAccountCommand { get; }
        public ICommand? NavigateToContactsCommand { get; }

        public NavButtonsVM(NavStore navStore)
        {
            NavigateToHomeCommand = new NavigateToHomeCommand(navStore);
            NavigateToAccountCommand = new NavigateToAccountCommand(navStore);
            NavigateToContactsCommand = new NavigateToContactsCommand(navStore);

        }

    }
}

I want the data context for the commands to be NavButtonsVM but it is set to MainVM, which is the VIewModel for my MainWindow.xaml

<Window
    x:Class="CAMUI.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:components="clr-namespace:CAMUI.Components"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:CAMUI"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:views="clr-namespace:CAMUI.Views"
    xmlns:vm="clr-namespace:CAMUI.ViewModels.ViewsVM" d:DataContext="{d:DesignInstance Type=vm:MainVM}"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    
    <Grid>
       
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <components:MenuComponent Grid.Row="0" />

        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>

            <components:NavigationButtons Grid.Column="0" />
            <components:SettingsButton Grid.Column="1" HorizontalAlignment="Right" />
        </Grid>

        <ContentControl Grid.Row="2" Content="{Binding CurrentVM}">
            <ContentControl.Resources>
                <DataTemplate DataType="{x:Type vm:HomePageVM}">
                    <views:HomeView />
                </DataTemplate>

                <DataTemplate DataType="{x:Type vm:AccountsPageVM}">
                    <views:AccountsView />
                </DataTemplate>

                <DataTemplate DataType="{x:Type vm:ContactsPageVM}">
                    <views:ContactsView />
                </DataTemplate>
            </ContentControl.Resources>
        </ContentControl>


    </Grid>
</Window>


using CAMUI.Stores;
using CAMUI.ViewModels.ComponentVM;

namespace CAMUI.ViewModels.ViewsVM
{
    public class MainVM : ViewModelBase
    {
        /*  handles button click commands in main view
         *  handles components view models*/

        public MenuVM? MenuVM { get; } //readonly cause they won't change

        public NavButtonsVM? NavButtonsVM { get; }

        private readonly NavStore _navStore;
        public ViewModelBase? CurrentVM => _navStore.CurrentVM;

        public MainVM(NavStore navStore)
        {
            _navStore = navStore;

            MenuVM = new MenuVM();
            NavButtonsVM = new NavButtonsVM(_navStore);


            _navStore.CurrentVMChanged += OnCurrentVMChanged;
        }

        protected override void Dispose()
        {
            _navStore.CurrentVMChanged -= OnCurrentVMChanged;

            base.Dispose();
        }

        private void OnCurrentVMChanged()
        {
            OnPropertyChanged(nameof(CurrentVM));
        }
    }
}

NavStore is what I use to communicate between the viewmodels that the currentVM is changed so update the view.

using CAMUI.ViewModels;
using System;

namespace CAMUI.Stores
{
    public class NavStore
    {
        public event Action? CurrentVMChanged;
        private ViewModelBase? _currVM;
        public ViewModelBase? CurrentVM
        {
            get => _currVM;
            set
            {
                _currVM = value;
                OnCurrentVMChanged();
            }
        }

        private void OnCurrentVMChanged()
        {
            CurrentVMChanged?.Invoke();
        }
    }
}

What I want to happen is when I click a button NavButton executes the command and changes the CurrentVM. This change is then noticed by MainVM where we then change the view in MainWindow. This is an example of what all the commands look like.

using CAMUI.Stores;
using CAMUI.ViewModels.ViewsVM;

namespace CAMUI.Commands
{
    public class NavigateToAccountCommand : CommandBase
    {
        private readonly NavStore _navStore;

        public NavigateToAccountCommand(NavStore navStore)
        {
            _navStore = navStore;

        }

        public override void Execute(object? parameter)
        {

            _navStore.CurrentVM = new AccountsPageVM();
        }
    }
}

How Do I change the data context so it stops looking for the property in MainVM and starts looking for it in NavButtonsVM, as I think that will solve the issue I am having.

Developer technologies Windows Presentation Foundation
Developer technologies .NET Other
Developer technologies Visual Studio Other
Developer technologies XAML
Developer technologies C#
0 comments No comments
{count} votes

Accepted answer
  1. Hui Liu-MSFT 48,676 Reputation points Microsoft External Staff
    2023-08-07T07:21:16.81+00:00

    Hi,@Zarif J Nafee .Welcome Microsoft Q&A.

    I can't reproduce the problem due to incomplete code. You could try to refer to the sample below.

    If you still have problems, please show me the complete code and project structure that can reproduce your problem for analysis.

    project structure: (You can change the namespace according to your project structure.)

    User's image

    HomePage:

     <UserControl.DataContext>
            <local:HomePageViewModel/>
        </UserControl.DataContext>
        <Grid >
            <TextBlock Text="{Binding Name}" Width="300" Height="50" Background="AliceBlue"/>
        </Grid>
    
    
    

    AboutPage:

     <UserControl.DataContext>
            <local:AboutPageViewModel/>
        </UserControl.DataContext>
        <Grid>
            <TextBlock Text="{Binding Setting}" Background="Pink"/>
        </Grid>
    

    MainWindow.xaml:

     <Window.Resources>
    
            <DataTemplate DataType="{x:Type local:HomePageViewModel}">
    
                <local:HomePage/>
    
            </DataTemplate>
    
            <DataTemplate DataType="{x:Type local:AboutPageViewModel}">
    
                <local:AboutPage />
    
            </DataTemplate>
    
        </Window.Resources>
    
        <DockPanel LastChildFill="True">
    
            <StackPanel x:Name="navigation" DockPanel.Dock="Left">
    
                <Button Content="Home" Command="{Binding NavigateToHomeCommand}"></Button>
    
                <Button Content="About" Command="{Binding NavigateToAboutCommand}"></Button>
    
            </StackPanel>
    
            <ContentControl x:Name="Pages" DockPanel.Dock="Right" Content="{Binding CurrentViewModel}"/>
    
        </DockPanel>
    

    MainWindow.xaml.cs:

     public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
                DataContext=new MainWindowViewModel();
                
            }
        }
        public class MainWindowViewModel : ViewModelBase
        {
            private object _currentViewModel;
    
            public RelayCommand NavigateToHomeCommand { get; }
            public RelayCommand NavigateToAboutCommand { get; }
        
    
            public object CurrentViewModel
            {
                get => _currentViewModel;
                set
                {
                    _currentViewModel = value;
                    OnPropertyChanged("CurrentViewModel");
                }
            }
    
            public MainWindowViewModel()
            {
                NavigateToHomeCommand = new RelayCommand(OpenHome);
                NavigateToAboutCommand = new RelayCommand(OpenAbout);
              
            }
    
           
    
    
    
           
    
            private void OpenHome(object obj)
    
            {
    
                CurrentViewModel = new HomePageViewModel();
    
            }
    
            private void OpenAbout(object obj)
    
            {
    
                CurrentViewModel = new AboutPageViewModel();
    
            }
    
            
        }
      
        public class ViewModelBase : INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;
    
            protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    
        public class HomePageViewModel : ViewModelBase { public string Name { get; set; } = "name"; }
    
        public class AboutPageViewModel : ViewModelBase { public string Setting { get; set; } = "setting"; }
      
    
                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");
                _execute = execute;
                _canExecute = canExecute;
            }
    
            public bool CanExecute(object parameter)
            {
                return _canExecute == null ? true : _canExecute(parameter);
            }
    
            public event EventHandler CanExecuteChanged
            {
                add { CommandManager.RequerySuggested += value; }
                remove { CommandManager.RequerySuggested -= value; }
            }
    
            public void Execute(object parameter)
            {
                _execute(parameter);
            }
        }
    

    The result:

    6


    If the response is helpful, please click "Accept Answer" and upvote it.

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    1 person found this answer helpful.

0 additional answers

Sort by: Most helpful

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.