Prism WPF + MahApps modal window the MVVM way - part 1

Tags: wpf, prism, mvvm, mahapps

WPF development can be at the same time very fun and rewarding but on the other side, can also be VERY VERY frustrating. Sometimes this whole MVVM stuff you want (or need) to follow, can really be annoying since doing some basic stuff in WPF/MVVM can be quite cumbersome when compared to WinForms or even C++, I mean Qt (let's forget about MFC - anyone still uses it?).

And don't get me even started on Prism. I did some projects with it a few years ago, it was a mix of pain and pleasure, but recently a quite interesting opportunity arose to get back to it for the development of (hopefully) several projects.

So I was very pleased to see that Prism is still very alive and kicking. Which is awesome because, at the bottom of my heart, I really like it. Maybe someday it will be a true love?

Anyway, the reason I like WPF is that it can look good. Obviously, it's not always the case, but often it is and there are some nice tools that can help people like me with absolutely no drawing skills to make their apps looks cool. BTW since I started to be interested in men's classic fashion, I think I got much better with colors. Maybe I'll write something about it.

MahApps

One of these great tools is the MahApps library. It's a really cool and if you're doing any WPF development, you should definitely try it. There's also very nice icon packs library that leverages one of the biggest WPF features which is vectors drawing. It's like Fontawesome but for WPF.

The MahApps library will make your WPF app much nicer. The installation and basic usage are very straightforward, it's all on their website and github.

One of the things I like in MahApps is the Windows 8/10 style modal windows or modal dialogs. They're fresh and modern. And I wanted to have this freshness in my application when displaying busy indicator.

So we will build something like this: but in a MVVM and Prism friendly way.

Disclaimer: sorry I won't publish the whole sample project, but bootstrapping and setting up Prism can be time-consuming and I'm just too lazy to do it.

Let's get our hands dirty

The main idea is that when the main window (shell) receives IsBusy event then it means that, well, there's some important stuff going on and the user should not be allowed to do anything (cancel the operation maybe but leave it for now) and the modal with busy indicator should be displayed.

Prism has a concept for displaying (not only) modals and it's called the User interaction request pattern, so we're going to use this mechanism.

So let's start with our main window's view model (ShellVM.cs):

public class ShellVM : IViewModel
{
    private IEventAggregator _eventAggregator;
    private BusyIndicatorNotification _busyNotification;

    public InteractionRequest<BusyIndicatorNotification> AppBusyRequest { get; private set; }

    public ShellVM(IEventAggregator eventAggregator)
    {
        _eventAggregator = eventAggregator;

        AppBusyRequest = new InteractionRequest<BusyIndicatorNotification>();
        subscribeToEvents();
    }

    private void subscribeToEvents()
    {
        _eventAggregator.GetEvent<Events.IsBusy>().Subscribe((isBusy) =>
        {
            if(isBusy)
            {
                // ensure that we're not showing indicator already
                if(_busyNotification != null)
                    return;

                _busyNotification = new BusyIndicatorNotification();
                AppBusyRequest.Raise(_busyNotification);
            }
            else
            {
                if(_busyNotification == null)
                    return;
                _busyNotification.RequestClose();
                _busyNotification = null;
            }

        }, ThreadOption.UIThread);
    }
}

where BusyIndicatorNotification is just a standard Prism Notification but with the ability to be closed from the outside word by invoking RequestClose():

public class BusyIndicatorNotification : Notification
{
    public event EventHandler CloseRequested;

    public BusyIndicatorNotification()
    {
        Title = "Please wait...";
    }

    public void RequestClose()
    {
        CloseRequested?.Invoke(this, EventArgs.Empty);
    }
}

Interaction requests requires InteractionRequestTrigger to placed in the view (Shell.xaml):

<Controls:MetroWindow x:Class="MySuperApp.View.Shell"
    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:MySuperApp.View"
    xmlns:prism="http://prismlibrary.com/"
    xmlns:Controls="http://metro.mahapps.com/winfx/xaml/controls"
    xmlns:common="http://mysuperapp/"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
    mc:Ignorable="d"
    Title="Shell" WindowState="Maximized"
    prism:ViewModelLocator.AutoWireViewModel="True">
<i:Interaction.Triggers>
    <prism:InteractionRequestTrigger SourceObject="{Binding AppBusyRequest, Mode=OneWay}">
        <common:MahAppsPopupWindowAction IsModal="True" SuppressUserClosing="True">
            <common:MahAppsPopupWindowAction.WindowContent>
                <local:BusyIndicator />
            </common:MahAppsPopupWindowAction.WindowContent>
        </common:MahAppsPopupWindowAction>
    </prism:InteractionRequestTrigger>
</i:Interaction.Triggers>
...

There are two custom things here: common:MahAppsPopupWindowAction and local:BusyIndicator. The local:BusyIndicator is just a UserControl view with MahApps ProgressRing in it (BusyIndicator.xaml):

<UserControl x:Class="MySuperApp.View.BusyIndicator"
             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:local="clr-namespace:MySuperApp.View"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:MahControls="http://metro.mahapps.com/winfx/xaml/controls"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
              prism:ViewModelLocator.AutoWireViewModel="True">
    <Grid>
        <StackPanel Orientation="Vertical" VerticalAlignment="Center">
            <MahControls:ProgressRing/>
            <TextBlock Text="{Binding Notification.Title}" TextAlignment="Center" Margin="0 10 0 0" FontSize="15"/>
        </StackPanel>
    </Grid>
</UserControl>

The view model for it just needs to implement IInteractionRequestAware that handles passed notification (BusyIndicatorVM.cs):

public class BusyIndicatorVM : BindableBase, IViewModel, IInteractionRequestAware
{
    private BusyIndicatorNotification _notification;

    public Action FinishInteraction
    {
        get;
        set;
    }

    public INotification Notification
    {
        get { return _notification; }

        set
        {
            if( value is BusyIndicatorNotification )
            {
                _notification = (BusyIndicatorNotification)value;
                _notification.CloseRequested += _notification_CloseRequested;
                RaisePropertyChanged(nameof(Notification));
            }
        }
    }

    private void _notification_CloseRequested(object sender, EventArgs e)
    {
        FinishInteraction();
        _notification.CloseRequested -= _notification_CloseRequested;
    }
}

As you can see, all of this is rather basic Prism stuff with maybe the exception of handling CloseRequested event that just closes the interaction (much like in standard Confirmation class).

The real stuff is going in common:MahAppsPopupWindowAction class and that I'll show you in another blog post, because this post is getting too long and it's really late right now so I just want to go to bed.

For the final word: if you're into the CNC business, then check out my other website: cnctailor.com and Scout - real-time CNC machine monitoring system. If you're using or manufacturing CNC machines, then I'd love to talk to you about integrating our software with your software, so that together we will create an uber-cool-cnc-software (and hopefully make some money along the way).

Read also

Templating in .NET

Hardcoding strings is one of the biggest crimes in programming. Seriously, you should never do this. Instead, use one of the templating engines for an easy and flexible content generation.

Data export tools for .NET

How to export data from .NET to PDFs, Word/Exel documents or CSVs.

ASP.NET MVC pretty URLs on steroids

As you may know, there's a little problem with the attribute routing in ASP.NET MVC. But now there's an easy way to fix it.

Comments