When you are using the MVVM pattern, at some time, you have to send data between ViewModels. For example, a detail ViewModel must tell the collection ViewModel that the current item should be deleted, or when you have a main view that opens details views and keeps track of them and must be acknowledged of any of them closing.
This is a very common pattern and usually is solved by coupling the two ViewModels.
That’s a solution, but not the best one, because coupling makes difficult maintenance and testing. For example, we’ll use the WPF project at https://github.com/bsonnino/MessengerToolkit/tree/Original.

This is a WPF project with a main window that shows a grid with all the customers:

When you click the button at the right, it opens a secondary window with the details:

This project is composed by two ViewModels, MainViewModel and CustomerViewModel. MainViewModel serves the main view and performs all actions for the buttons: Filter the data, Add a new customer and Save the data:

public partial class MainViewModel : ObservableObject
{
    private readonly ICustomerRepository _customerRepository;
    private readonly INavigationService _navigationService;
    
    public MainViewModel(ICustomerRepository customerRepository, INavigationService navigationService)
    {
        _customerRepository = customerRepository ?? 
                              throw new ArgumentNullException("customerRepository");
        Customers = new ObservableCollection<CustomerViewModel>(
            _customerRepository.Customers.Select(c => new CustomerViewModel(c)));
        _navigationService = navigationService;
    }

    [ObservableProperty]
    private ObservableCollection<CustomerViewModel> _customers;

    [ObservableProperty]
    private int _windowCount;

    [RelayCommand]
    private void Add()
    {
        var customer = new Customer();
        _customerRepository.Add(customer);
        var vm = new CustomerViewModel(customer);
        Customers.Add(vm);
        _navigationService.Navigate(vm);
    }

    [RelayCommand]
    private void Save()
    {
        _customerRepository.Commit();
    }

    [RelayCommand]
    private void Search(string textToSearch)
    {
        var coll = CollectionViewSource.GetDefaultView(Customers);
        if (!string.IsNullOrWhiteSpace(textToSearch))
            coll.Filter = c => ((CustomerViewModel)c).Country?.ToLower().Contains(textToSearch.ToLower()) == true;
        else
            coll.Filter = null;
    }

    [RelayCommand]
    private void ShowDetails(CustomerViewModel vm)
    {
        _navigationService.Navigate(vm);
        WindowCount++;
    }
}

It uses the features available in version 8 of the MVVM toolkit. In this code, we can see two things:

  • The ViewModel maintains the open windows count. When you click on the button to show the details, it will increase the window count. The problem here is to decrease the count once the window is closed. Right now, the count only increases 😃
  • To open the detail window, we could use something like:
private void ShowDetails(CustomerViewModel vm)
{
    var detailWindow = new Detail { DataContext = vm };
    detailWindow.Show();
    WindowCount++;
}

This approach has two flaws:

  • We are coupling the ViewModel to the Detail view
  • We are calling View details in the ViewModel

This makes this code to be completely untestable and defeats all purpose of the MVVM pattern. So, we must find another solution. What I devised is a NavigationService, that will open a Window, depending on the type of the ViewModel that is passed to the Navigate method. This service is very simple, and has only one method,Navigate:

public interface INavigationService
{
    void Navigate(object arg);
}

public class NavigationService : INavigationService
{
    private readonly Dictionary<Type, Type> viewMapping = new()
    {
        [typeof(MainViewModel)] = typeof(MainWindow),
        [typeof(CustomerViewModel)] = typeof(Detail),
    };

    public void Navigate(object arg)
    {
        Type vmType = arg.GetType();
        if (viewMapping.ContainsKey(vmType))
        {
            var windowType = viewMapping[vmType];
            var window = (System.Windows.Window)Activator.CreateInstance(windowType);
            window.DataContext = arg;
            window.Show();
        }
    }
}

We’ve defined an interface, INavigationService, and created a class that implements it, NavigationService. This class has a dictionary with all the ViewModel types that have a corresponding window type, with their corresponding window type as the value of the item.

When we call Navigate and pass the instance of the ViewModel, the method will check its type, verify if it exists in the dictionary and instantiate the window, setting the VM as its DataContext and opening the Window. That way, we decouple the ViewModel from the views. The navigation service is injected in the constructor of MainViewModel by the use of dependency injection.

In App.xaml.cs we are registering the services and the VM. That way, we get the right data injected to the main VM:

public IServiceProvider Services { get; }

private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    services.AddSingleton<INavigationService, NavigationService>();
    services.AddSingleton<ICustomerRepository, CustomerRepository>();
    services.AddSingleton<MainViewModel>();

    return services.BuildServiceProvider();
}

One note is ShowDetails, which in fact is a command that should be sent to the CustomerViewModel, because it’s tied to every CustomerViewModel in the grid. What we did is to tie it to the ViewModel of the main view with:

<DataGridTemplateColumn>
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <Button Command="{Binding DataContext.ShowDetailsCommand, 
                RelativeSource={RelativeSource AncestorType=Window}}" 
                    CommandParameter="{Binding}" ToolTip="Details">
                <TextBlock Text="" FontFamily="Segoe UI Symbol" />
            </Button>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

We set the command to ShowDetailsCommand in the datacontext of the parent window, and pass the current CustomerViewModel as a parameter. That suffices to do the trick.

Now, onto our problems:

  • The Window Closing event is just that – an event, not a command and there is no direct way to handle it in the ViewModel
  • Even if we can handle it in the ViewModel, there is no direct way to warn the main viewmodel about the change
  • When we click the Remove button in the Detail view, we cannot remove the item from the list, because the list is on the Main ViewModel, which is not accessible from the Customer ViewModel

For the first problem, we could handle the Closing event in the code behind of the Detail view:

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    var vm = DataContext as CustomerViewModel;
    vm.ClosingCommand.Execute(null);
}

or, better, send the closing signal directly to the main viewmodel:

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    var vm = App.Current.MainVM;
    vm.ClosingCommand.Execute(null);
}

This would work fine, but purists would say that it’s a code smell. So, let’s do it in the MVVM way. For that, we must install the Microsoft.Xaml.Behaviors.WPF NuGet package, that has some behaviors we will need in the detail window:

<Window x:Class="CustomerApp.Detail"
        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"
        xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
        mc:Ignorable="d"
        Height="440"
        Width="520"
        Background="AliceBlue">
    <b:Interaction.Triggers>
        <b:EventTrigger EventName="Closing">
            <b:InvokeCommandAction Command="{Binding ClosingCommand}" />
        </b:EventTrigger>
    </b:Interaction.Triggers>

There, we have Interaction.Triggers, to add the triggers we want. We want to use the EventTrigger, that is triggered whe an event is fired. In our case, it’s the Closing event. When it’s fired, it will invoke the command ClosingCommand in the detail window. Problem solved. No more code behind.

To solve the second and third problem, we would have to couple our Detail ViewModel to the Main ViewModel and have a variable that stores its instance. One way would be to pass the VM as parameter:

private readonly MainViewModel _mainvm;

public CustomerViewModel(Customer customer, MainViewModel mainvm)
{
    _customer = customer;
    _mainvm = mainvm;
    CustomerId = _customer.CustomerId;
    CompanyName = _customer.CompanyName;
    ContactName = _customer.ContactName;
    ContactTitle = _customer.ContactTitle;
    Address = _customer.Address;
    City = _customer.City;
    Region = _customer.Region;
    PostalCode = _customer.PostalCode;
    Country = _customer.Country;
    Phone = _customer.Phone;
    Fax = _customer.Fax;
}

One other way would be to get the App property:

private readonly MainViewModel _mainvm = App.Current.MainVM;

or to use the Service Locator to get the instance:

private readonly MainViewModel _mainvm = (MainViewModel)App.Current.Services.GetService(typeof(MainViewModel));

No matter with way we are using it, we are coupling the two viewmodels and that will make testing difficult. Passing the VM as a parameter for the constructor will be the more testable way, the other two would get us in trouble: we should have an App created for the second option and have an App created and a Service Locator in place to get our instance. But wouldn’t it be better if we could get rid of the instance of Main ViewModel and do not have them coupled?

In fact there is: Messages. Messaging is a mechanism that decouples the sender and receiver of a message, there is not even need to have a receiver for the message, or there may be many receivers from it – the sender doesn’t care.

The messenger is based on the Mediator pattern, where the Messenger class acts as the mediator for the messages. The potential receivers subscribe for receiving some kind of messages and, when some sender sends a message, the messenger will send it to the subscribers that are willing to receive that kind of message.

In the MVVM Toolkit, you can define messengers that implement the IMessenger interface, but the class WeakReferenceMessenger exposes a Default property, which offers a thread safe implementation for this interface, which makes it easier to implement. This class has a Register method, which will register the client for some message type and will pass the callback function that will be called when a message of the desired type is received. If you wat toknow more about that, you can take a look at https://docs.microsoft.com/en-us/windows/communitytoolkit/mvvm/messenger.

For our needs, we have to define two types of different messages: a message for when a customer should be deleted and the other one, when the window is closed:

public class WindowClosedMessage: ValueChangedMessage<CustomerViewModel>
{
    public WindowClosedMessage(CustomerViewModel vm) : base(vm)
    {

    }
}

public class ViewModelDeletedMessage : ValueChangedMessage<CustomerViewModel>
{
    public ViewModelDeletedMessage(CustomerViewModel vm) : base(vm)
    {

    }
}

The messages inherit from ValueChangedMessage. This kind of messages are sent to the recipients and the execution goes on. If, on the other way, the Sender needs an answer from the receiver, it will send a message of the RequestMessage type. In this case, the sender will wait the response that the receiver will send using the Reply method of the message. In our case, both messages don’t need a reply, so we use the ValueChangedMessage type. We must create two different types of message, because we want different processing for each one.

Now, we must send the messages in CustomerViewModel, in the Closing and Delete commands:

[RelayCommand]
private void Delete()
{
    WeakReferenceMessenger.Default.Send(new ViewModelDeletedMessage(this));
}

[RelayCommand]
private void Closing()
{
    WeakReferenceMessenger.Default.Send(new WindowClosedMessage(this));
}

The processing for these messages is done in the MainViewModel class:

public MainViewModel(ICustomerRepository customerRepository, INavigationService navigationService)
{
    _customerRepository = customerRepository ?? 
                          throw new ArgumentNullException("customerRepository");
    Customers = new ObservableCollection<CustomerViewModel>(
        _customerRepository.Customers.Select(c => new CustomerViewModel(c)));
    _navigationService = navigationService;
    WeakReferenceMessenger.Default.Register<WindowClosedMessage>(this, (r, m) =>
    {
        WindowCount--;
    });
    WeakReferenceMessenger.Default.Register<ViewModelDeletedMessage>(this, (r, m) =>
    {
        DeleteCustomer(m.Value);
    });
}

private void DeleteCustomer(CustomerViewModel vm)
{
    Customers.Remove(vm);
    var deletedCustomer = _customerRepository.Customers.FirstOrDefault(c => c.CustomerId == vm.CustomerId);
    if (deletedCustomer != null)
    {
        _customerRepository.Remove(deletedCustomer);
    }
}

Now, when you run the app, you will see that when you close the detail window, the window count decreases. We could even keep a list of open windows, with some code like this:

[ObservableProperty]
private ObservableCollection<CustomerViewModel> _openWindows = new ObservableCollection<CustomerViewModel>();

public MainViewModel(ICustomerRepository customerRepository, INavigationService navigationService)
{
    _customerRepository = customerRepository ?? 
                          throw new ArgumentNullException("customerRepository");
    Customers = new ObservableCollection<CustomerViewModel>(
        _customerRepository.Customers.Select(c => new CustomerViewModel(c)));
    _navigationService = navigationService;
    WeakReferenceMessenger.Default.Register<WindowClosedMessage>(this, (r, m) =>
    {
        WindowCount--;
        _openWindows.Remove(m.Value);
    });
    WeakReferenceMessenger.Default.Register<ViewModelDeletedMessage>(this, (r, m) =>
    {
        DeleteCustomer(m.Value);
    });
}

....

[RelayCommand]
private void ShowDetails(CustomerViewModel vm)
{
    _navigationService.Navigate(vm);
    WindowCount++;
    _openWindows.Add(vm);
}

When you click the Delete button in the detail window, the corresponding item is deleted in the main list, but the window remains open, and it should not ne there anymore, as the corresponding customer does not exist anymore. We could use some code behind to close the window, but I’d prefer another approach: change the navigation service to allow closing the windows. For that, we should keep the lists of open windows and have another method, Close, to close the selected window:

public interface INavigationService
{
    void Navigate(object arg);
    void Close(object arg);
}

public class NavigationService : INavigationService
{
    private readonly Dictionary<Type, Type> viewMapping = new()
    {
        [typeof(MainViewModel)] = typeof(MainWindow),
        [typeof(CustomerViewModel)] = typeof(Detail),
    };

    private readonly Dictionary<object, List<System.Windows.Window>> _openWindows = new();

    public void Navigate(object arg)
    {
        Type vmType = arg.GetType();
        if (viewMapping.ContainsKey(vmType))
        {
            var windowType = viewMapping[vmType];
            var window = (System.Windows.Window)Activator.CreateInstance(windowType);
            window.DataContext = arg;
            if (!_openWindows.ContainsKey(arg))
            {
                _openWindows.Add(arg, new List<System.Windows.Window>());
            }
            _openWindows[arg].Add(window);
            window.Show();
        }
    }

    public void Close(object arg)
    {
        if (_openWindows.ContainsKey(arg))
        {
            foreach(var window in _openWindows[arg])
            {
                window.Close();
            }
        }
    }
}

In this case, we keep a list of windows open for each customer. If we try to open another window for the same customer, the corresponding window will be added to the corresponding list. When the close method is called, all windows for that customer will be closed at once. The DeleteCustomer in MainViewModel becomes

private void DeleteCustomer(CustomerViewModel vm)
{
    Customers.Remove(vm);
    var deletedCustomer = _customerRepository.Customers.FirstOrDefault(c => c.CustomerId == vm.CustomerId);
    if (deletedCustomer != null)
    {
        _customerRepository.Remove(deletedCustomer);
    }
    _navigationService.Close(vm);
}

And now everything works fine.

But, what about testing? We did all this to ease testing, let’s test the VMs. In the Test project, let’s create a class CustomerViewModelTests.cs and add some tests:

[TestClass]
public class CustomerViewModelTests
{
    [TestMethod]
    public void Constructor_NullCustomer_ShouldThrow()
    {
        Action act = () => new CustomerViewModel(null);

        act.Should().Throw<ArgumentNullException>()
            .Where(e => e.Message.Contains("customer"));
    }
}

If we run this test, we’ll see it doesn’t pass. We haven’t made a check for null customers, so we have to change that in the code:

public CustomerViewModel(Customer customer)
{
    if (customer == null)
        throw new ArgumentNullException("customer");
    _customer = customer;
    CustomerId = _customer.CustomerId;
    CompanyName = _customer.CompanyName;
    ContactName = _customer.ContactName;
    ContactTitle = _customer.ContactTitle;
    Address = _customer.Address;
    City = _customer.City;
    Region = _customer.Region;
    PostalCode = _customer.PostalCode;
    Country = _customer.Country;
    Phone = _customer.Phone;
    Fax = _customer.Fax;
}

The test now passes and we can continue. For the next test, I’d like to have a customer filled with data, and FakeItEasy (which we are using for our test mocks) fills the data with nulls and that’s not what we want. In this article I show how to use Bogus to create fake data. We can use AutoBogus.FakeItEasy to integrate it with FakeItEasy. Install the package AutoBogus.FakeItEasy and let’s create the second test

[TestMethod]
public void Constructor_ShouldSetFields()
{
    var customer = AutoFaker.Generate<Customer>();
    var customerVM = new CustomerViewModel(customer);
    customerVM.CustomerId.Should().Be(customer.CustomerId);
    customerVM.CompanyName.Should().Be(customer.CompanyName);
    customerVM.ContactName.Should().Be(customer.ContactName);
    customerVM.ContactTitle.Should().Be(customer.ContactTitle);
    customerVM.Address.Should().Be(customer.Address);
    customerVM.City.Should().Be(customer.City);
    customerVM.Region.Should().Be(customer.Region);
    customerVM.PostalCode.Should().Be(customer.PostalCode);
    customerVM.Country.Should().Be(customer.Country);
    customerVM.Phone.Should().Be(customer.Phone);
    customerVM.Fax.Should().Be(customer.Fax);
}

For the third test, we will be testing the Delete command, and we want to check if if sends the correct message. For that, we must install the _Community.Toolkit.MVVM in the test project. Then, we can create the tests for the two commands:

[TestMethod]
public void DeleteCommand_ShouldSendMessageWithVM()
{
    var customer = AutoFaker.Generate<Customer>();
    var customerVM = new CustomerViewModel(customer);
    object callbackResponse = null;
    var waitEvent = new AutoResetEvent(false);
    WeakReferenceMessenger.Default.Register<ViewModelDeletedMessage>(this, (r, m) =>
    {
        callbackResponse = customerVM;
        waitEvent.Set();
    });
    customerVM.DeleteCommand.Execute(null);
    waitEvent.WaitOne(100);
    callbackResponse.Should().Be(customerVM);
}

[TestMethod]
public void CloseCommand_ShouldSendMessageWithVM()
{
    var customer = AutoFaker.Generate<Customer>();
    var customerVM = new CustomerViewModel(customer);
    object callbackResponse = null;
    var waitEvent = new AutoResetEvent(false);
    WeakReferenceMessenger.Default.Register<WindowClosedMessage>(this, (r, m) =>
    {
        callbackResponse = customerVM;
        waitEvent.Set();
    });
    customerVM.ClosingCommand.Execute(null);
    waitEvent.WaitOne(100);
    callbackResponse.Should().Be(customerVM);
}

These two tests have a particularity: the result is set in the callback, so we may not have it immediately, so we can have a flaky test if we don’t wait some time before testing the value – sometimes it may pass and sometimes not. For this issue, I’ve used an AutoResetEvent to be set when the callback is called and wait 100ms to see if it’s called. That makes the trick.

For the MainViewModel tests, we can do this:

[TestClass]
public class MainViewModelTests
{
    [TestMethod]
    public void Constructor_NullRepository_ShouldThrow()
    {
        Action act = () => new MainViewModel(null, null);

        act.Should().Throw<ArgumentNullException>()
            .Where(e => e.Message.Contains("customerRepository"));
    }

    [TestMethod]
    public void Constructor_Customers_ShouldHaveValue()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);

        vm.Customers.Count.Should().Be(customers.Count);
    }

    [TestMethod]
    public void AddCommand_ShouldAddInRepository()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.AddCommand.Execute(null);
        A.CallTo(() => repository.Add(A<Customer>._)).MustHaveHappened();
    }

    [TestMethod]
    public void AddCommand_ShouldAddInCollection()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.AddCommand.Execute(null);
        vm.Customers.Count.Should().Be(11);
    }

    [TestMethod]
    public void AddCommand_ShouldCallNavigate()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.AddCommand.Execute(null);
        A.CallTo(() => navigationService.Navigate(A<CustomerViewModel>.Ignored)).MustHaveHappened();
    }

    [TestMethod]
    public void SaveCommand_ShouldCommitInRepository()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var vm = new MainViewModel(repository, navigationService);
        vm.SaveCommand.Execute(null);
        A.CallTo(() => repository.Commit()).MustHaveHappened();
    }

    [TestMethod]
    public void SearchCommand_WithText_ShouldSetFilter()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.SearchCommand.Execute("text");
        var coll = CollectionViewSource.GetDefaultView(vm.Customers);
        coll.Filter.Should().NotBeNull();
    }

    [TestMethod]
    public void SearchCommand_WithoutText_ShouldSetFilter()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.SearchCommand.Execute("");
        var coll = CollectionViewSource.GetDefaultView(vm.Customers);
        coll.Filter.Should().BeNull();
    }

    [TestMethod]
    public void ShowDetailsCommand_ShouldCallNavigate()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        A.CallTo(() => navigationService.Navigate(customerVM)).MustHaveHappened();
    }

    [TestMethod]
    public void ShowDetailsCommand_ShouldIncrementWindowCount()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        vm.WindowCount.Should().Be(1);
    }

    [TestMethod]
    public void ShowDetailsCommand_ShouldAddToOpenWindows()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        vm.OpenWindows.Count.Should().Be(1);
        vm.OpenWindows[0].Should().Be(customerVM);
    }

    [TestMethod]
    public void CustomerCloseCommand_ShouldDecreaseWindowCount()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.ClosingCommand.Execute(null);
        vm.WindowCount.Should().Be(0);
    }

    [TestMethod]
    public void CustomerCloseCommand_ShouldRemoveFromOpenWindows()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.ClosingCommand.Execute(null);
        vm.OpenWindows.Count.Should().Be(0);
    }

    [TestMethod]
    public void CustomerDeleteCommand_ShouldCallNavigationClose()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.DeleteCommand.Execute(null);
        A.CallTo(() => navigationService.Close(customerVM)).MustHaveHappened();
    }

    [TestMethod]
    public void CustomerDeleteCommand_ShouldRemoveCustomer()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.DeleteCommand.Execute(null);
        vm.Customers.Count.Should().Be(9);
        vm.Customers.Should().NotContain(customerVM);
    }

    [TestMethod]
    public void CustomerDeleteCommand_ShouldCallRemoveFromRepository()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.DeleteCommand.Execute(null);
        A.CallTo(() => repository.Remove(A<Customer>.Ignored)).MustHaveHappened();
    }
}

If you run the tests, all will pass.

As you can see, we have decoupled the two ViewModels with the Messenger implemented in the MVVM Toolkit, our code is completely testable and it runs fine. You can check this code here.

We can still go one step further (yes, we can always do that 😃): inject the IMessenger in the constructor of the ViewModels.

In App.xaml.cs we add the registration for IMessenger:

private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    services.AddSingleton<INavigationService, NavigationService>();
    services.AddSingleton<ICustomerRepository, CustomerRepository>();
    services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
    services.AddSingleton<MainViewModel>();

    return services.BuildServiceProvider();
}

And inject it in the constructor of MainViewModel and CustomerViewModel:

public MainViewModel(ICustomerRepository customerRepository, 
    INavigationService navigationService,
    IMessenger messenger)
{
    _customerRepository = customerRepository ?? 
                          throw new ArgumentNullException("customerRepository");
    _navigationService = navigationService ?? 
        throw new ArgumentNullException("navigationService"); 
    _messenger = messenger ??
        throw new ArgumentNullException("messenger");
    Customers = new ObservableCollection<CustomerViewModel>(
        _customerRepository.Customers.Select(c => new CustomerViewModel(c, messenger)));
    messenger.Register<WindowClosedMessage>(this, (r, m) =>
    {
        WindowCount--;
        _openWindows.Remove(m.Value);
    });
    messenger.Register<ViewModelDeletedMessage>(this, (r, m) =>
    {
        DeleteCustomer(m.Value);
    });
}
private readonly IMessenger _messenger;

public CustomerViewModel(Customer customer, IMessenger messenger)
{
    if (customer == null)
        throw new ArgumentNullException("customer");
    if (messenger == null)
        throw new ArgumentNullException("messenger");
    _messenger = messenger;
    _customer = customer;
    CustomerId = _customer.CustomerId;
    CompanyName = _customer.CompanyName;
    ContactName = _customer.ContactName;
    ContactTitle = _customer.ContactTitle;
    Address = _customer.Address;
    City = _customer.City;
    Region = _customer.Region;
    PostalCode = _customer.PostalCode;
    Country = _customer.Country;
    Phone = _customer.Phone;
    Fax = _customer.Fax;
}

We have decoupled the ViewModels from the default messenger. There are some changes to do in the code and in the tests, but you can check them in the final project, here. The code now is decoupled, testable and works fine 😃

All the source code for this article is at https://github.com/bsonnino/MessengerToolkit

I was working with the WPF project, converted to UWP in this article, in order to check usage of the new MVVM toolkit, explained in my last article and use the bindings with x:Bind, that are not supported in WPF (in fact, they are, but not natively. You can add the CompiledBindings.WPF package to your WPF project to have them).
I converted it, changed the Views to use the x:Bind bindings and ended up with this project.
There are some notes, here with the conversion:

  • This is an UWP project and, by default, uses C# 7. I wanted to use C# 9 t get the source generator goodies in the MVVM Toolkit. So I upgraded the language version using the LangVersion tag in the project. To do this, open the csproj file (you must unload the project and edit the file) and add this PropertyGroup:
<PropertyGroup>
  <LangVersion>9.0</LangVersion>
</PropertyGroup>
  • I removed the details view, by adding a ContentControl with a template with the Customer details:
<ContentControl Grid.Row="2" Content="{x:Bind master.SelectedItem, Mode=OneWay}" 
                ContentTemplate="{StaticResource DetailTemplate}" Margin="5" />
<Page.Resources>
    <c:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
    <DataTemplate x:Key="DetailTemplate" x:DataType="customerlib:Customer">
        <Grid HorizontalAlignment="Stretch">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="600" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <TextBlock Text="Customer Id:" Grid.Column="0" Grid.Row="0" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="0" Margin="3" Name="customerIdTextBox" 
                     Text="{x:Bind CustomerId, Mode=TwoWay}" VerticalAlignment="Center" />
            <FontIcon Grid.Column="2" FontFamily="Segoe MDL2 Assets" Glyph="" 
                      Margin="20,0" Visibility="{x:Bind IsVip, Converter={StaticResource BooleanToVisibilityConverter}}" />
            <TextBlock Text="Company Name:" Grid.Column="0" Grid.Row="1" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="1" Margin="3" Name="companyNameTextBox" 
                     Text="{x:Bind CompanyName, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Contact Name:" Grid.Column="0" Grid.Row="2" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="2" Margin="3" Name="contactNameTextBox" 
                     Text="{x:Bind ContactName, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Contact Title:" Grid.Column="0" Grid.Row="3" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="3" Margin="3" Name="contactTitleTextBox" 
                     Text="{x:Bind ContactTitle, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Address:" Grid.Column="0" Grid.Row="4" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="4" Margin="3" Name="addressTextBox" 
                     Text="{x:Bind Address, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="City:" Grid.Column="0" Grid.Row="5" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="5" Margin="3" Name="cityTextBox" 
                     Text="{x:Bind City, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Postal Code:" Grid.Column="0" Grid.Row="6" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="6" Margin="3" Name="postalCodeTextBox" 
                     Text="{x:Bind PostalCode, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Region:" Grid.Column="0" Grid.Row="7" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="7" Margin="3" Name="regionTextBox" 
                     Text="{x:Bind Region, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Country:" Grid.Column="0" Grid.Row="8" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="8" Margin="3" Name="countryTextBox" 
                     Text="{x:Bind Country, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Phone:" Grid.Column="0" Grid.Row="9" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="9" Margin="3" Name="phoneTextBox" 
                     Text="{x:Bind Phone, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Fax:" Grid.Column="0" Grid.Row="10" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="10" Margin="3" Name="faxTextBox" 
                     Text="{x:Bind Fax, Mode=TwoWay}" VerticalAlignment="Center" />
        </Grid>
    </DataTemplate>
</Page.Resources>
  • The bindings for the DataGrid columns couldn’t be replaced by x:Bind. I could do it by replacing the DataGridTextColumn with DataGridTemplateColumn like in:
<controls:DataGridTemplateColumn Header="Customer ID">
    <controls:DataGridTemplateColumn.CellTemplate>
        <DataTemplate x:DataType="customerlib:Customer">
            <TextBlock Text="{x:Bind CustomerId, Mode=TwoWay}" />
        </DataTemplate>
    </controls:DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumn> 

But I thought it didn’t worth the extra code and I left it with the bindings. If you find some way to replace the Bindings in the DataGridTextColumn for x:Bind, please leave it in the comments.

You would ask, why change the Binding for x:Bind. There are some good reasons for that:

  • x:Bind is resolved at compile time, while Binding is resolved at runtime, so it should make your code run faster
  • By being resolved at compile time, you know in advance if some binding works or not, while with Binding, you will have something that doesn’t work and you don’t know why
  • You can bind to functions in your ViewModel, thus removing the need of some converters (but not all of them)

On the other side, you have to determine explicitely the data you’re binding with x:Bind:

  • The data templates must have a DataType
  • You cannot use the DataContext property of the View, as it’s an Object and, as so, doesn’t have the properties to bind
  • You must bind to properties/fields in the View. That’s why I created this code in Mainpage.xaml.cs:
private MainViewModel _mainVm;
public MainPage()
{
    this.InitializeComponent();
    _mainVm = App.Current.MainVM;
}
  • You must declare explicitely which property/field you are binding, like in
ItemsSource="{x:Bind _mainVm.Customers}"

With that, we can run the project and this is what you get:

It’s an empty window, the customer data is not there. I tried to figure out what was happening and I set a breakpoint in CustomerRepository.GetCustomersAsync:

Everything is good, there are 91 customers, but none of them is shown in the Datagrid. Why is that? After checking and rechecking the data and the bindings, I remembered one difference between Binding and x:Bind. While Binding uses the OneWay most of the time, x:Bind uses OneTime.

It seems to be a small difference, but it bytes you when you have to deliver some code 😦. OneWay is a binding from the source (the class) to the destination (the view), but every time the class changes its value and raises the PropertyChanged event, the view reflects the change. OneTime is there for performance reasons: it’s set and forget. When the view is first rendered, it will check the binding, set the value and forget. The changes on the class won’t affect the view.

As we are setting the customer’s list in an asynchronous way, when the view is rendered, there is no data available, so it doesn’t show anything. When the data is set, all changes aren’t propagated to the view and nothing is shown. Fortunately, this is a simple change:

<controls:DataGrid AutoGenerateColumns="False" x:Name="master" Grid.Row="1" 
                   ItemsSource="{x:Bind _mainVm.Customers, Mode=OneWay}" SelectedItem="{x:Bind _mainVm.SelectedCustomer, Mode=TwoWay}">

Now, our data shows fine, but when we select a customer, we get this:

Wait a minute, where are our bindings? We just got the first one and nothing else? What is happening here? Is x:Bind broken? Let’s see. I started to change my x:Bind to Binding and see the effect:

<TextBlock Text="Company Name:" Grid.Column="0" Grid.Row="1" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="1" Margin="3" Name="companyNameTextBox" 
         Text="{Binding CompanyName, Mode=TwoWay}" VerticalAlignment="Center" />
<TextBlock Text="Contact Name:" Grid.Column="0" Grid.Row="2" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="2" Margin="3" Name="contactNameTextBox" 
         Text="{Binding ContactName, Mode=TwoWay}" VerticalAlignment="Center" />


I got these bindings working, so x:Bind must be broken, I thought. So, my idea of using x:Bind was broken and I started to change the x:Bind for Binding. That worked fine until I changed this one:

<FontIcon Grid.Column="2" FontFamily="Segoe MDL2 Assets" Glyph="" 
          Margin="20,0" Visibility="{Binding IsVip, Converter={StaticResource BooleanToVisibilityConverter}}" />

When I changed it, I got this runtime error

Wait a minute – Cannot find a Resource with the Name/Key BooleanToVisibilityConverter ? I’m sure I’ve declared it in the Resources section:

<c:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />

Oops – my bad! It’s BoolToVisibilityConverter, not BooleanToVisibilityConverter. I corrected the error and all the bindings started to work with x:Bind:

So, in some way, x:Bind has a bug, but not in the way I expected: instead of pointing the faulty converter with red squiggles, it will crash the bindings that come after it and won’t work anymore. That’s why, it was showing just the customer Id, the star for the IsVip property and nothing else. And it was showing the error at runtime, but I didn’t see it. In the Output window, in the middle of many other messages, a two line message indicated the error:

I think that, being checked as compile time, the converters should also be checked at compile time and not throw an exception in the output window at runtime and blow the rest of the bindings. But that’s just my point of view. Now, everything works and I will make sure that my converters are well defined 😃.

The source code for this project is at https://github.com/bsonnino/ConvertersInUWP

Sometime ago, I’ve written this article introducing the MVVM Community Toolkit and developing a CRUD application to show how to use the MVVM pattern with the Community toolkit.

The time has passed and version 8.0 of the MVVM Community toolkit has been released and, with it, a rewrite using incremental generators. This may seem a minor update, but it’s a huge move, as the MVVM pattern is full of boilerplate: implementing the INotifyPropertyChanged interface for the viewmodels, binding commands that implement the ICommand interface, using the RelayCommand class and implementing observable properties that raise the PropertyChanged event when changed. All that make the code cumbersome and repetitive, but that’s what we had to do to implement the MVVM pattern in our apps. Until now.

With the use of source generators, the toolkit removes a lot of the boilerplate and makes the code easier to create and read. In this article, we’ll take the project that we developed in the previous article and will change it to use the new toolkit.

You can clone the code in https://github.com/bsonnino/MvvmApp and open the CustomerApp – Mvvm app in Visual Studio 2022. As the original project is targeted to .NET 5.0, we’ll upgrade it to .NET 6.0. This is an easy task: just open CustomerApp.csproj and change the TargetFramework to .net6.0-windows:

<PropertyGroup>
	<OutputType>WinExe</OutputType>
	<TargetFramework>net6.0-windows</TargetFramework>
	<UseWPF>true</UseWPF>
	<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>

Do the same with CustomerApp.Tests.csproj. There is no need to do it on the CustomerLib project, as it’s a Net Standard project.

The next step is to update the NuGet packages. The package Microsoft.Extensions.DependencyInjection must be upgraded to version 6.0.0. If you try to upgrade the Microsoft.Toolkit.Mvvm package, you’ll see that there is no upgrade to version 8.0. That’s because the package name has changed and you must uninstall this one and install the CommunityToolkit.Mvvm package.

Now, the project is ready to build and, when you do that, you’ll see that it doesn’t work 😦. This is because the namespaces have changed and we need to update the using clauses in MainViewModel.cs. We have to remove the old using clauses and replace with the new ones:

using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

Once you do that and recompile the project, you’ll see that it compiles fine and runs in the same way the original project did. Not bad for a project upgraded from .NET 5 to .NET 6, with a new version of the MVVM framework.

But did I say that with the new toolkit you can remove the boilerplate? We can start doing that now. The first thing is to remove the properties and their getters and setters. We decorate the _selectedCustomer field with the [ObservableProperty] attribute:

[ObservableProperty]
private Customer _selectedCustomer;

When you do that, you’ll see that the class name has a red underline:

That’s because when we add the attribute, the toolkit generates a partial class, and we need to add the partial keyword to the class:

public partial class MainViewModel : ObservableObject

When we add that, SelectedCustomer is underlined:

That’s because we have declared the property in our code and the toolkit has also declared the same property in the generated code. We can now remove the property declaration from our code:

public Customer SelectedCustomer
{
    get => _selectedCustomer;
    set
    {
        SetProperty(ref _selectedCustomer, value);
        RemoveCommand.NotifyCanExecuteChanged();
    }
}

As you can see from the code we are removing, the setter notifies the RemoveCommand. To have the same effect, we add the NotifyCanExecuteChangedFor attribute to the field:

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(RemoveCommand))]
private Customer _selectedCustomer;

If you compile the code, you will see that it runs the same way it did before, and we are still using the property name (SelectedCommand) in the commands, even if it’s not explicitly defined in the code. That’s because the toolkit is generating the property in its partial part of the class.

The next steps are to remove the boilerplate from the commands in the code. For that, we must remove all declarations and leave only the command methods, changing their name to the command name (without Command at the end) and adding the [RelayCommand] attribute for the method. For the Add command, we have to change this code:

public IRelayCommand AddCommand { get; }

private void DoAdd()
{
    var customer = new Customer();
    _customerRepository.Add(customer);
    SelectedCustomer = customer;
    OnPropertyChanged("Customers");
}

To this code:

[RelayCommand]
private void Add()
{
    var customer = new Customer();
    _customerRepository.Add(customer);
    SelectedCustomer = customer;
    OnPropertyChanged("Customers");
}

We also have to remove the initialization code:

AddCommand = new RelayCommand(DoAdd);
RemoveCommand = new RelayCommand(DoRemove, () => SelectedCustomer != null);
SaveCommand = new RelayCommand(DoSave);
SearchCommand = new RelayCommand<string>(DoSearch);

If you notice the removed code, you’ll see that the RemoveCommand has a CanExecute method. To solve that, we have to have to add a parameter to RelayCommand:

[RelayCommand(CanExecute = "HasSelectedCustomer")]
private void Remove()
{

The parameter points to HasSelectedCustomer, a method that should be defined in the code:

private bool HasSelectedCustomer() => SelectedCustomer != null;

With that, we have completed our code and now the project runs in the same way it did before. The code is simpler and with no boilerplate:

public partial class MainViewModel : ObservableObject
{
    private readonly ICustomerRepository _customerRepository;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(RemoveCommand))]
    private Customer _selectedCustomer;

    public MainViewModel(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository ??
                              throw new ArgumentNullException("customerRepository");
    }

    public IEnumerable<Customer> Customers => _customerRepository.Customers;

    [RelayCommand]
    private void Add()
    {
        var customer = new Customer();
        _customerRepository.Add(customer);
        SelectedCustomer = customer;
        OnPropertyChanged("Customers");
    }

    [RelayCommand(CanExecute = "HasSelectedCustomer")]
    private void Remove()
    {
        if (SelectedCustomer != null)
        {
            _customerRepository.Remove(SelectedCustomer);
            SelectedCustomer = null;
            OnPropertyChanged("Customers");
        }
    }

    private bool HasSelectedCustomer() => SelectedCustomer != null;

    [RelayCommand]
    private void Save()
    {
        _customerRepository.Commit();
    }

    [RelayCommand]
    private void Search(string textToSearch)
    {
        var coll = CollectionViewSource.GetDefaultView(Customers);
        if (!string.IsNullOrWhiteSpace(textToSearch))
            coll.Filter = c => ((Customer)c).Country.ToLower().Contains(textToSearch.ToLower());
        else
            coll.Filter = null;
    }
}

As you can see, this new version brought a huge improvement. We can use the MVVM pattern with no issues, there is no extra code related to the pattern (except for the attributes) and the code is easier to read and follow. And all tests still run, with no change at all.

The full source code for this article is at https://github.com/bsonnino/MVVMToolkit8