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>
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());
}
}
}
<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>
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 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