wpfmvvmuser-controlsrelaycommandcommandbinding

Execute a method in another Control using MVVM


I have built a dummy UserControl that has a method in its code-behind to display a message. I have used this control in my main window and want to execute its method when I click a Button using Commands and MVVM. Is this a good design, and if not, how can I improve it? My current code looks like this:

<UserControl x:Class="ControlBining.Control1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid DataContext="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}">
    </Grid>
</UserControl>


public partial class Control1 : UserControl
   {
      public Control1()
      {
         InitializeComponent();
      }

      public void ShowMessage()
      {
         MessageBox.Show("Called from other control!");
      }
   }

<Window x:Class="ControlBining.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:ControlBining"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:ViewModel/>
    </Window.DataContext>
    <StackPanel Margin="0 50 0 0">
        <local:Control1 Width="100"/>
        <Button Width="100" Content="Show Message"/>
    </StackPanel>
</Window>

   public class RelayCommand : ICommand
   {
      private readonly Predicate<object> m_canExecute;
      private readonly Action<object> m_execute;

      public RelayCommand(Predicate<object> canExecute, Action<object> execute)
      {
         m_canExecute = canExecute;
         m_execute = execute;
      }

      public event EventHandler CanExecuteChanged
      {
         add => CommandManager.RequerySuggested += value;
         remove => CommandManager.RequerySuggested -= value;
      }

      public bool CanExecute(object parameter)
      {
         return m_canExecute(parameter);
      }

      public void Execute(object parameter)
      {
         m_execute(parameter);
      }
   }

Currently, I have made it work using the following code, but I am not sure if this is a good design:

  private void Control1_Loaded(object sender, RoutedEventArgs e)
  {

     ViewModel m = (ViewModel)DataContext;
     m.ShowMessage += M_ShowMessage;
  }

  private void M_ShowMessage()
  {
     ShowMessage();
  }

  public event Action ShowMessage;

  private ICommand m_showMessageCommand;
  public ICommand ShowMessageCommand
  {
     get
     {
        return m_showMessageCommand ?? (m_showMessageCommand = new RelayCommand(
                  p => true,
                  p => ShowMessage?.Invoke()));
     }
  }

Solution

  • If you simply need to show a message, you should move the ShowMessage() method to the view model and use a message service to do this from the view model class.

    If you really want to call some method that it only makes sense to define in the view, this can be done by implementing an interface in the view and inject the view model with this interface. For example when you invoke the command:

    public interface IView
    {
        void ShowMessage();
    }
    
    public partial class Control1 : UserControl, IView
    {
        public Control1()
        {
            InitializeComponent();
        }
    
        public void ShowMessage()
        {
            MessageBox.Show("Called from other control!");
        }
    }
    

    View Model:

    public ICommand ShowMessageCommand
    {
        get
        {
            return m_showMessageCommand ?? (m_showMessageCommand = new RelayCommand(
                      p => true,
                      p =>
                      {
                          IView view as IView;
                          if (view != null)
                          {
                              //...
                              view.ShowMessage();
                          }
                      }));
        }
    }
    

    The view model knows nothing about the view, it only knows about an interface which of course may be called something else than IView.

    The other option is to use an event aggregator or a messenger to send an event or a message from the view model to the view in a lously coupled way. Please refer to this blog post for more information about this.

    Neither approach break the MVVM pattern.