Search code examples
c#wpfxamlmvvmuser-controls

How to communicate between UserControl in MVVM - WPF Application


I want to create an application with a menu on the left that will change the content on the right. For that, I Have a MainWindow with two ContentControl (One that will Content a UserControl 'Menu' and the other one that will Content the selected UserControl 'Red' or 'Green'.

The problem is that the Content on the right does not Change...

I have made some research and saw concepts like Dependency Injection, Delegate, Event, Message Bus, ViewModelLocator... but I don't know which one would be the most suitable in this case and how to implement it. (I don't want to use MVVMLight or any PlugIn like that)

Thx in advance for your interest.

For that I use the MVVM pattern and DataTemplate Binding:

App.xaml

<Application.Resources>
    <DataTemplate DataType="{x:Type viewModel:MainViewModel}">
        <view:MainWindow />
    </DataTemplate>
    <DataTemplate DataType="{x:Type viewModelMenu:LeftViewModel}">
        <viewMenu:Left />
    </DataTemplate>
    <DataTemplate DataType="{x:Type viewModelContent:RedViewModel}">
        <viewContent:Red />
    </DataTemplate>
</Application.Resources>

ViewModel.cs

public abstract class ViewModel : INotifyPropertyChanged
{
    #region Properties

    private ViewModel _mainContent;

    public ViewModel MainContent
    {
        get => _mainContent;
        set
        {
            _mainContent = value;
            OnPropertyChanged();
            MessageBox.Show(nameof(MainContent));
        }
    }

    #endregion Properties

    public ViewModel()
    {
        InitCommands();
    }

    protected abstract void InitCommands();

    #region Factory Method - CreateCommand

    protected ICommand CreateCommand(Action execute, Func<bool> canExecute)
    {
        return new RelayCommand(
            execute: execute,
            canExecute: canExecute
            );
    }

    protected ICommand CreateCommand<T>(Action<T> execute, Predicate<T> canExecute)
    {
        return new RelayCommand<T>(
            execute: execute,
            canExecute: canExecute
            );
    }

    #endregion Factory Method - CreateCommand

    public event PropertyChangedEventHandler PropertyChanged;

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

MainViewModel.cs

internal class MainViewModel : ViewModel
{
    private ViewModel _leftMenu;
    public ViewModel LeftMenu
    {
        get => _leftMenu;
        set
        {
            _leftMenu = value;
            OnPropertyChanged();
        }
    }

    public MainViewModel()
    {
        LeftMenu = new LeftViewModel();
    }

    protected override void InitCommands()
    {
    }

LeftViewModel.cs

internal class LeftViewModel : ViewModel
{
    public ICommand ChangeContentToRed { get; set; }
    public ICommand ChangeContentToGreen { get; set; }

    protected override void InitCommands()
    {
        ChangeContentToRed = new RelayCommand(
            execute: () => MainContent = new RedViewModel(),
            canExecute: () => !(MainContent is RedViewModel)
            );

        ChangeContentToGreen = new RelayCommand(
            execute: () => MainContent = new GreenViewModel(),
            canExecute: () => !(MainContent is GreenViewModel)
            );
    }
}

RedViewModel and GreenViewModel are empty so I don't show the code but extend ViewModel

Window.xaml

 <Window.DataContext>
    <viewModel:MainViewModel />
</Window.DataContext>

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="3*" />
    </Grid.ColumnDefinitions>

    <ContentControl Grid.Column="0" Content="{Binding Path=LeftMenu}" />
    <ContentControl Grid.Column="1" Content="{Binding Path=MainContent}" />
</Grid>

Left.xaml

<UserControl.DataContext>
    <viewModel:LeftViewModel />
</UserControl.DataContext>

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Button
        Grid.Row="0"
        Command="{Binding Path=ChangeContentToRed}"
        Content="Red" />
    <Button
        Grid.Row="1"
        Command="{Binding Path=ChangeContentToGreen}"
        Content="Green" />
</Grid>

Red and Green are only two UserControl with a red and a green grid


Solution

  • When you have a DataTemplate like

    <DataTemplate DataType="{x:Type viewModelMenu:LeftViewModel}">
        <viewMenu:Left />
    </DataTemplate>
    

    and then assign a value of type LeftViewModel to the Content property of a ContentControl, like

    <ContentControl Content="{Binding Path=LeftMenu}"/>
    

    the DataTemplate is assigned to the ContentTemplate of the ContentControl, and the element in the instantiated DataTemplate (i.e. your UserControl) inherits the DataContext of the ContentPresenter in the ControlTemplate of the ContentControl, which then holds the Content value.

    However, this only works if you do not explicitly assign the UserControl's DataContext and thus break the value inheritance of the DataContext property.

    You have to remove the explicit DataContext assignment from the UserControl, i.e. this:

    <UserControl.DataContext>
        <viewModel:LeftViewModel />
    </UserControl.DataContext>