Peregrine's View

Yet another C# / WPF / MVVM blog …

MVVM

MVVM – Message Dialogs

A Message to You Rudy

In this post I’m going to continue my look at some ‘hard’ MVVM issues, this time how to display a message dialog from the ViewModel layer, but in a MVVM pure manner.

Remember that the key principle of MVVM is separation of the user interface from business logic, so we shouldn’t just write something like this in the ViewModel …

MessageBox.Show("Here's a message ...");

In the last post I introduced the concept of using a service as a means of indirectly accessing the U.I. layer from the ViewModel. We can do the same for message dialogs – but this time it is a little more complicated. The Dialog Service within the Peregrine.WPF.View library is split into three distinct segments.

A. perDialogServiceRegistration.Register

This part provides the (indirect) connection between the ViewModel and the user interface layer. It is effectively the View saying “whatever is my DataContext (i.e. its ViewModel) may want to display a message dialog – when it does, it can use my UI context to do so”. This technique was introduced by a great WPF user interface toolkit – Mahapps Metro, in particular their DialogParticipation class. I was inspired by that concept and have used it as the basis for my own code. To register with the dialog service, just set the attached property value to true …

<vctrl:perViewBase
    ...
    dlg:perDialogServiceRegistration.Register="{Binding}">

One important point to note is that the dialog registration class persists for the lifetime of the application, but we don’t want to be blocking any garbage collection operations once a View or ViewModel object is no longer otherwise required. To counter this, the ViewModel & View links are held using WeakReferences.

B. perDialogServiceRegistration.DialogContent

This optional part allows you to display a dialog containing any UIElement rather than just a simple text string. It can either be set as a single item, or (as in the case of the demo application for this post) as an array of items which are identified using their Tag property. The data context of the selected U.I. component will be set to the calling ViewModel when it is displayed (in ShowContentDialogAsync()), allowing the dialog contents to bound to any of its properties. Again, the registration dictionary just contains weak references to the contained items. For example …

<vctrl:perViewBase
    ...
    dlg:perDialogServiceRegistration.DialogContent="{DynamicResource WindowDialogContentArray}">

    <vctrl:perViewBase.Resources>
        <x:Array
            x:Key="WindowDialogContentArray"
            Type="FrameworkElement">
            <Grid Tag="{x:Static local:MainViewModel.RED_CIRCLE}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="*" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>

                <Ellipse
                    Grid.Row="0"
                    Width="100"
                    Height="100"
                    Fill="Red"
                    Stroke="DarkRed"
                    StrokeThickness="5" />

                <TextBlock
                    Grid.Row="1"
                    HorizontalAlignment="Stretch"
                    Text="{Binding RedCircleDescription, Mode=OneTime}"
                    TextAlignment="Center" />
            </Grid>

            <Grid Tag="{x:Static local:MainViewModel.GREEN_SQUARE}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="*" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>

                <Rectangle
                    Grid.Row="0"
                    Width="100"
                    Height="100"
                    Fill="LightGreen"
                    Stroke="DarkGreen"
                    StrokeThickness="5" />

                <TextBlock
                    Grid.Row="1"
                    HorizontalAlignment="Stretch"
                    Text="{Binding GreenSquareDescription, Mode=OneTime}"
                    TextAlignment="Center" />
            </Grid>

        </x:Array>
    </vctrl:perViewBase.Resources>

C. perDialogService

This part follows the service pattern I’ve introduced previously – the ViewModel constructor has an interface parameter which is injected by the IoC container, the implementation of which is defined in the View layer. To ensure that the dialog service implementation is correctly registered, you must call perWpfViewBootstrapper.Run() as part of your application initialisation. The interface contains three methods for displaying dialogs to the user…

public interface IperDialogService
{
    Task ShowMessageAsync(object viewModel, 
        string body, 
        perDialogIcon dialogIcon = perDialogIcon.Information, 
        string title = "");

    Task<perDialogButton> ShowDialogAsync(object viewModel, 
        perDialogButton buttons, 
        string body, 
        perDialogIcon dialogIcon = perDialogIcon.Information, 
        string title = "" );

    Task<perDialogButton> ShowContentDialogAsync(object viewModel, 
        perDialogButton buttons, 
        perDialogIcon dialogIcon = perDialogIcon.Information, 
        string title = "", 
        string tag = "");
}

I’ve defined my own icon (a direct clone of System.Windows.MessageBoxImage) and button enumeration types too, so that the ViewModel layer has no dependencies on any View assemblies …

public enum perDialogIcon
{
    Asterisk,
    Error,
    Exclamation,
    Hand,
    Information,
    None,
    Question,
    Stop,
    Warning
}

[Flags]
public enum perDialogButton
{
    None = 0,
    Ok = 1,
    Yes = 1 << 1,
    No = 1 << 2,
    Cancel = 1 << 3,
    Retry = 1 << 4,
    Ignore = 1 << 5,
    Abort = 1 << 6,
    Save = 1 << 7,
    YesNo = Yes | No,
    YesNoCancel = Yes | No | Cancel,
    OkCancel = Ok | Cancel,
    RetryCancel = Retry | Cancel,
    AbortRetryIgnore = Abort | Retry | Ignore,
    SaveCancel = Save | Cancel
}

perDialogButton is defined with the Flags attribute so that we can build any combination of buttons we need in our application.

To aid with our drive towards clean-code, the dialog is displayed in an async manner, with the caller (the ViewModel) waiting for the result, rather than using some other kind of callback mechanism. To display a dialog from the ViewModel, just call the appropriate sevice method …

await _dialogService.ShowMessageAsync(this, 
    "Hello from the dialog service.", 
    perDialogIcon.Information, 
    "Mvvm Dialog Service").ConfigureAwait(false);
var response = await _dialogService.ShowDialogAsync(this, 
    perDialogButton.YesNo, 
    "Do you want to continue?", 
    perDialogIcon.Question, 
    "Mvvm Dialog Service").ConfigureAwait(false);
await _dialogService.ShowContentDialogAsync(this, 
    perDialogButton.Ok, 
    perDialogIcon.None, 
    "Xaml Content Dialog", 
    RED_CIRCLE).ConfigureAwait(false);

The implementation of the interface creates a perDialog (a chromeless window) and populates the title, icon, content and buttons properties as required. The display context for this window is requested from perDialogServiceRegistration.GetAssociatedControl(), using the passed ViewModel instance as a parameter.

To allow ViewModel classes using the dialog service to be unit tested (without any kind of U.I. interaction), there is also a series of mock implementations that only return the specified result value. To use these, set the implementation of the interface in the IoC container to the required mock for each specific unit test.

In my next post, I’ll continue with this theme by looking at another ‘hard’ MVVM problem – data validation.

Don’t forget that all of the code samples for this blog are available on Github, along with my own personal C# / WPF library.

If you find this article useful, or you have any other feedback, please leave a comment below.

Leave a Reply

Your email address will not be published. Required fields are marked *