Anup Anup - 5 months ago 23
C# Question

What is the MVVM way to call WPF command from another viewmodel?

I'm trying to learn more about MVVM implementation in WPF and currently need some guidance on navigation using ViewModels. I'm following WPF navigation example from Rachel's blog and need a way to call Command of ApplicationViewModel from other ViewModel.

As per the blog, switching views from MainWindow is pretty clear, but I want to know more about inter-view navigation i.e. say I've Home, Product and Contact button on MainWindow along with View and ViewModel classes, now I want to open Contact page from some button inside Home view instead of MainWindow. I have written some code in Home ViewModel to achieve the same but I doubt whether this is the best practice of MVVM. And is there any way to achieve the same from HomeView.XAML?

Code Snippet from blog - ApplicationViewModel.cs

private ICommand _changePageCommand;

private IPageViewModel _currentPageViewModel;
private List<IPageViewModel> _pageViewModels;

public ApplicationViewModel()
{
// Add available pages in c'tor
PageViewModels.Add(new HomeViewModel(this));
PageViewModels.Add(new ProductsViewModel());
PageViewModels.Add(new ContactViewModel());
}

public ICommand ChangePageCommand
{
get
{
if (_changePageCommand == null)
_changePageCommand = new RelayCommand(
p => ChangeViewModel((IPageViewModel)p), p => p is IPageViewModel);

return _changePageCommand;
}
}

private void ChangeViewModel(IPageViewModel viewModel)
{
if (!PageViewModels.Contains(viewModel))
PageViewModels.Add(viewModel);

CurrentPageViewModel = PageViewModels.FirstOrDefault(vm => vm == viewModel);
}


Code Snippet from blog - ApplicationView.xaml

<Window.Resources>
<DataTemplate DataType="{x:Type local:HomeViewModel}">
<local:HomeView />
</DataTemplate>

<!-- Data template for other views -->
</Window.Resources>

<DockPanel>
<Border DockPanel.Dock="Left" BorderBrush="Black" BorderThickness="0,0,1,0">
<ItemsControl ItemsSource="{Binding PageViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Name}"
Command="{Binding DataContext.ChangePageCommand,
RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
CommandParameter="{Binding }"/>

<!--All closing tags-->


My code inside HomeViewModel.cs

// This is the command to get bind with my button inside Home view to invoke Contact view

private ICommand _loadContactCommand;
public ICommand LoadContactCommand
{
get
{
if (_loadContactCommand == null)
_loadContactCommand = new RelayCommand(p => LoadOtherView());

return _loadContactCommand;
}
}

private void LoadOtherView()
{
// _appVM is the instance of 'ApplicationViewModel' which is being set from c'tor
// Even I'm thinking to pass Contact view member of ApplicationViewModel class here,
// as I need exactly the same instance of the Contact which has been created earlier

_appVM.ChangePageCommand.Execute(new ContactViewModel());
}

Joe Joe
Answer Source

There's a couple of ways I'd do this.

The first, if the action is a service type of interaction, which I think this is a reasonably good example of, I would describe the action in an interface and inject it as a dependency into the ViewModels that need it.

This is effectively what you are doing, but it's worth abstracting it out into an interface. This provides less tight coupling between the two ViewModels.

Here is an example of wrapping up the functionality in an IPageDisplay interface:

public interface IPageDisplay
{
    IPageViewModel GetCurrentPage();
    void ChangeViewModel(IPageViewModel newPage);
}

Your ApplicationViewModel implements it and has the exact same methods it did before:

public class ApplicationViewModel: IPageDisplay
{
    // implement like you are doing

You're HomeViewModel then takes as an interface, not the 'whole' ViewModel:

class HomeViewModel
{
    HomeViewModel(IPageDisplay pageDisplay) {//constructor stuff}

private void LoadOtherView()
{
    // Instead of interacting with a whole ViewModel, we just use the interface
    _pageDisplay.ChangePageCommand.Execute(new ContactViewModel());
}

This is 'safer' as it's more abstract. You can test HomeViewModel without creating a AppViewModel by just mocking the IPageDisplay. You can change how pages are displayed or the implementation of AppViewModel, you can also display your pages in any other kind of location, by having some other implementation of IPageDisplay.

It's worth noting that any page that needs to perform navigation actions will require an IPageDisplay. It can be troublesome matching up all these dependencies if you have many of them - that's where something like a Dependency Injection framework can really help out.

The second would be a mediator pattern as suggested in the comments. You could have a common mediator PageManager that defines the ChangeViewModel(IPageViewModel newPage); method and fires a ChangeViewModelRequest event or callback. The ApplicationViewModel, and any other ViewModels that want to change the current page accept the PageManager instance as a dependency. ApplicationViewModel listens to the event, the other's call ChangeViewModelRequest to trigger it.

Again, a Dependency Injection will need to be managed effectively if this is in a complex application.

This naturally leads onto the third. Which is a extension of the mediator pattern, an Event Aggregator.

An event aggregator is a generic service that allows all different ViewModels to raise, or subscribe to application wide events. It's definitely worth looking at.

Here, your ApplicationViewModel subscribes to the event:

public class ApplicationViewModel
{
    private EventAgregator _eventAggregator;

    ApplicationViewModel(EventAgregator eventAggregator)
    {
        this._eventAggregator = eventAggregator;
        _eventAggregator.Subscribe('ChangeViewModelRequest', (EventArgs eventArgs) => ChangeViewModel(eventArgs.Parameter))
    }

    private void ChangeViewModel(IPageViewModel viewModel)
    {
        if (!PageViewModels.Contains(viewModel))
            PageViewModels.Add(viewModel);

        CurrentPageViewModel = PageViewModels.FirstOrDefault(vm => vm == viewModel);
    }
}

And the HomeViewModel publishes to the event:

private void LoadOtherView()
{
    _eventAggregator.Publish("ChangeViewModelRequest", new EventArgs(new ContactViewModel()));
}

There are plenty of Event Aggregators you can use, some built into MVVM frameworks like Prism.

While, like all the others, this is a dependency - it's a very generic one. Chances are, most of your ViewModels will need access to the aggregator instance and have it as a dependency, as it could be used for almost all inter-view-model communication. Simply having all VMs pass it to any created VMs in the constructor could work for a simple application. But I'd still say something that supports dependency injection (say, factory pattern?) would be worth implementing.

Edit:

Here's what you need for your HomeViewModel:

public class HomeViewModel : IPageViewModel // doesn't implement IPageDisplay
{
    private IPageDisplay _pageDisplay;
    public HomeViewModel(IPageDisplay pageDisplay)
    {
        // HomeViewModel doesn't implement IPageDisplay, it *consumes* one
        // as a dependency (instead of the previous ApplicationViewModel).
        // Note, that the instance you're passing still is the ApplicationViewModel,
        // so not much has actually changed - but it means you can have another
        // implementation of IPageDisplay. You're only linking the classes together
        // by the functionality of displaying a page.
        _pageDisplay= pageDisplay;
    }

    public string Name
    {
        get
        {
            return "Home Page";
        }
    }

    private ICommand _loadDashboardCommand;
    public ICommand LoadDashboardCommand
    {
        get
        {
            if (_loadDashboardCommand == null)
            {
                _loadDashboardCommand = new RelayCommand(
                    p => LoadOtherView());
            }
            return _loadDashboardCommand;
        }
    }

    private void LoadOtherView()
    {
        // Here you have the context of ApplicatiomViewModel like you required
        // but it can be replaced by any other implementation of IPageDisplay
        // as you're only linking the little bit of interface, not the whole class

        _pageDisplay.ChangeViewModel(new DashboardViewModel());
    }
}

}