Tuesday, June 18, 2019

MVVM Message Boxes through Dependency Injection

This blog is going to combine subjects from several recent blogs in a concrete example.

Pure MVVM does not allow the view-model to interact with the UI in any way. This also applies to message boxes. We often need to alert the user or get confirmation from them, but when automatically testing view-models we don't want those message boxes displayed because they cause the test to wait for user interaction. One approach, which unfortunately needs to be built in from the start, is to use dependency injection to specify which mechanism to use for interacting with the user.

You start by defining a MessageBox interface, then write a class that wraps the standard message box and another class that provides the same functionality without using a UI. Each view-model receives an instance of one of these two classes via its constructor (or a property or event) and executes the Show method when it needs to.

In this example, I have split the view-model away from the view's code-behind because it is purer than my usual technique of putting the view-model in the code-behind. Unfortunately it makes the example a lot more complex.

Start a new WPF C# project called "Message Box With Dependency Injection" and target framework 4.0 or later. I'm using Visual Studio 2019.

MainWindow contains two tabs. The first launches a user control. This is the user control we are going to test. The second tab contains a user control that tests the first user control. When we click the button on the first user control it does something and displays a confirmation and then an alert. When we click the button on the second user control it does the same thing, but we don't see the confirmation or alert. Instead the confirmation returns the default button and the alert is written to the output window. The difference is in the MessageBox class that we pass to the constructor (inject into the view-model).

Here is MainWindow.xaml. You do't need to modify MainWindow.xaml.cs

<Window x:Class="Message_Box_with_Dependency_Injection.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:Message_Box_with_Dependency_Injection"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:DoSomething x:Key="DoSomething"/>
        <local:TestSomething x:Key="TestSomething"/>
    </Window.Resources>
      <TabControl>
        <TabItem Header="A Window" Content="{StaticResource DoSomething}"/>
        <TabItem Header="Test the other Window" Content="{StaticResource TestSomething}"/>
    </TabControl>
</Window>


Before we go any further we need to define RelayCommand. This is needed because the CanExecute and Executed event handlers are in a separate class instead of the code-behind. This also means we have to define the relay commands in code, rather than defining RoutedCommands in XAML.

Add a class and call it RelayCommand. You can find this code in all WPF MVVM projects.

using System;
using System.Windows.Input;

namespace Message_Box_with_Dependency_Injection
{
    public class RelayCommand<T> : ICommand
    {
        readonly Action<T> _execute = null;
        readonly Predicate<T> _canExecute = null;

        public RelayCommand(Action<T> execute, object p)
            : this(execute, null)
        {
        }

        public RelayCommand(Action<T> execute, Predicate<T> canExecute)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");

            _execute = execute;
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null ? true : _canExecute((T)parameter);
        }

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

        public void Execute(object parameter)
        {
            _execute((T)parameter);
        }
    }
}

Now we need to define our MessageBox interface and two implementations of it. Add a file called IMessageBox. It defines a single method called Show. I am not supporting the MessageBox icon parameter.

using System.Windows;

namespace Message_Box_with_Dependency_Injection
{
    interface IMessageBox
    {
         MessageBoxResult Show(string Text, string Title = "", MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxResult defaultResult = MessageBoxResult.OK);
    }
}

Add a class called MyMessageBox which contains an implementation of IMessageBox that will act like a regular MessageBox.

using System.Windows;

namespace Message_Box_with_Dependency_Injection
{
    class MyMessageBox : IMessageBox
    {
        public  MessageBoxResult Show(string Text, string Title = "", MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxResult defaultResult = MessageBoxResult.OK)
        {
            return MessageBox.Show(Text, Title, buttons, new MessageBoxImage(), defaultResult);
        }
    }
}

Also add a class called TestMessageBox which will contain an implementation of IMessageBox that is suitable for testing. It logs the text to the console and returns the default button.

using System;
using System.Windows;

namespace Message_Box_with_Dependency_Injection
{
    class TestMessageBox:IMessageBox
    {
        public MessageBoxResult Show(string Text, string Title = "", MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxResult defaultResult = MessageBoxResult.OK)
        {
            Console.WriteLine(Text);
            return defaultResult;
        }
    }
}

Now we have to write the two user controls referenced by MainWindow. Let's start with DoSomething. This represents a complete Window, Page, or UserControl. In this example it's a huge button that executes a relay command. That command displays a confirmation and an alert. Add a new user control and call it DoSomething. The XAML looks like this. You don't have to modify the code-behind.

<UserControl x:Class="Message_Box_with_Dependency_Injection.DoSomething"
        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:Message_Box_with_Dependency_Injection"
        mc:Ignorable="d">
    <UserControl.DataContext>
        <local:DoSomethingVM/>
    </UserControl.DataContext>
    <Grid>
        <Button Content="Do Something"  Command="{Binding DoSomethingCommand}"/>
    </Grid>
</UserControl>

Add a new class called DoSomethingVM. This is where we put the view-model for DoSomething. Note it has two constructors. The default constructor, which is what the framework will call, will use MyMessageBox to handle confirmations and alerts. The other constructor takes an explicit implementation of IMessageBox. The test page will use this second constructor to inject a reference to the TestMessageBox class.

using System.Windows;
using System.Windows.Controls;

namespace Message_Box_with_Dependency_Injection
{
    class DoSomethingVM : UserControl
    {
        IMessageBox theMessageBox;
        public DoSomethingVM()
        {
            theMessageBox = new MyMessageBox();
            DoInitialization();
        }

        public DoSomethingVM(IMessageBox MessageBox)
        {
            theMessageBox = MessageBox;
            DoInitialization();
        }

        private void DoInitialization()
        {
            DoSomethingCommand = new RelayCommand<object>(ExecuteDoSomething, true);
        }

        public RelayCommand<object> DoSomethingCommand
        {
            get;
            private set;
        }

        private void ExecuteDoSomething(object o)
        {
            MessageBoxResult result;
            result = theMessageBox.Show("Are you sure you want to do something?", "Confirm", MessageBoxButton.YesNo, MessageBoxResult.Yes);
            theMessageBox.Show("You clicked " + result.ToString());
        }
    }
}

Now we need to create the Test user control. This instantiates the view-model above using the second constructor, then executes the DoSomething command. The XAML looks like this. Note I'm being lazy here and putting the view-model in the code-behind.

<UserControl x:Class="Message_Box_with_Dependency_Injection.TestSomething"
        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:Message_Box_with_Dependency_Injection"
        mc:Ignorable="d">
    <UserControl.Resources>
        <RoutedCommand x:Key="TestSomethingCommand"/>
    </UserControl.Resources>
    <UserControl.CommandBindings>
        <CommandBinding Command="{StaticResource TestSomethingCommand}" Executed="CommandBinding_Executed"/>
    </UserControl.CommandBindings>
    <Grid>
        <Button Content="Test Something" Command="{StaticResource TestSomethingCommand}"/>
    </Grid>
</UserControl>

The code-behind looks like this.

using System.Windows.Controls;
using System.Windows.Input;

namespace Message_Box_with_Dependency_Injection
{
    public partial class TestSomething : UserControl
    {
        IMessageBox MessageBox = new TestMessageBox();

        public TestSomething()
        {
            InitializeComponent();
        }

        private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            DoSomethingVM MV = new DoSomethingVM(MessageBox);
            MV.DoSomethingCommand.Execute(null);
        }
    }
}

If you click on the "Do Something" button you see the following message boxes.




If you move to the Test tab and click "Test Something" you don't see any message boxes but the Output window looks like this.


There's lots of fiddly bits in this project so I zipped it up here.

No comments:

Post a Comment