My blog about application development on the .NET platform.

Implement a MVVM loading dialog in WPF

Continuing from my last post about how to display dialogs to the user in a MVVM WPF application using Prism without breaking the pattern, this one is about how you can extend the built-in functionality to implement a loading dialog to be shown to the user while running a background operation.

The previous post covering the basics of interaction requests in Prism is available here: “Implement a confirmation dialog in WPF using MVVM and Prism

To be able to close the dialog when a background operation has completed without requiring any action from the user, you can extend the IInteractionRequest interface by defining a new one that inherits from the former and adds another event called “Close”.

using Microsoft.Practices.Prism.Interactivity.InteractionRequest;
using System;

namespace Mm.LoadingDialog.Dialog
{
    public interface IInteractionLoadingRequest : IInteractionRequest
    {
        event EventHandler<InteractionRequestedEventArgs> Closed;
    }
}

For the dialog to be able to support cancellation, add a new class deriving from Notification to represent the context of the actual loading dialog. The below sample implementation extends the base class with a Boolean property named “SupportCancellation” to decide whether the dialog should actually provide support for cancellation, another Boolean property named “Cancelled” to signify if the user actually cancelled the operation, equivalent to the “Confirmed” property in the pre-defined Confirmation class, and an identifier to uniquely identify each object in cases where you have several background operations with corresponding dialogs running at the same time.

using System;
using Microsoft.Practices.Prism.Interactivity.InteractionRequest;

namespace Mm.LoadingDialog.Dialog
{
    public class Cancellation : Notification
    {
        public Cancellation()
            : base()
        {
            this.Id = Guid.NewGuid();
        }

        public bool SupportCancellation { get; set; }
        public bool Cancelled { get; set; }
        public Guid Id { get; private set; }
    }
}

When implementing the IInteractionLoadingRequest interface you inherit from the generic Prism InteractionRequest<T> class specifying the custom Cancellation class as its type argument and adding a public method to publish the “Close” event.

using Microsoft.Practices.Prism.Interactivity.InteractionRequest;
using System;

namespace Mm.LoadingDialog.Dialog
{
    public class InteractionLoadingRequest : InteractionRequest<Cancellation>, IInteractionLoadingRequest
    {
        public event EventHandler<InteractionRequestedEventArgs> Closed;

        public void Close(Cancellation context)
        {
            if (this.Closed != null)
            {
                this.Closed(this,
                    new InteractionRequestedEventArgs(context, null)
                    );
            }
        }
    }
}

Next the view model must expose this new type of interaction for the view to be able to respond to the events. If you plan to reuse the loading dialog functionality across several of your modules and view models you should define an abstract class to hold the property and have your specific view model classes inheriting from this one.

namespace Mm.LoadingDialog.Dialog
{
    public abstract class LoadingDialogViewModel
    {
        protected readonly InteractionLoadingRequest _interactionLoadingRequest;

        protected LoadingDialogViewModel()
        {
            _interactionLoadingRequest = new InteractionLoadingRequest();
        }

        public IInteractionLoadingRequest InteractionLoadingRequest
        {
            get { return _interactionLoadingRequest; }
        }
    }
}

In the Invoke method of the action to be run when the events are raised from a view model, the value of a dependency property “EventName” is used to distinguish between the “Raised” and “Closed” events. When a request is initialized a window named LoadingWindow, specifying the appearance of the loading dialog in XAML, is shown and added to a static list of open windows and when an operation has finished and a “Closed” event is published from the view model the corresponding window is closed and removed from the static list. The “Id” property of the Cancellation class, passed as an argument when the event is raised through the InteractionLoadingRequest class from the view model, is used to identify each window.

using System;
using System.Collections.Generic;
using System.Windows.Interactivity;
using System.Windows;
using Microsoft.Practices.Prism.Interactivity.InteractionRequest;

namespace Mm.LoadingDialog.Dialog
{
    public class LoadingWindowAction : TriggerAction<FrameworkElement>
    {
        private static readonly Dictionary<Guid, LoadingWindow> _openWindows
            = new Dictionary<Guid, LoadingWindow>();

        public static readonly DependencyProperty EventNameProperty =
            DependencyProperty.Register("EventName", typeof(string),
            typeof(LoadingWindowAction), new PropertyMetadata(null));

        public string EventName
        {
            get { return (string)this.GetValue(EventNameProperty); }
            set { this.SetValue(EventNameProperty, value); }
        }

        protected override void Invoke(object parameter)
        {
            InteractionRequestedEventArgs args = parameter as InteractionRequestedEventArgs;
            if (args != null)
            {
                Cancellation cancellation = args.Context as Cancellation;
                if (cancellation != null)
                {
                    switch (this.EventName)
                    {
                        case "Raised":
                            LoadingWindow cancWindow = new LoadingWindow(cancellation);
                            EventHandler closeHandler = null;
                            closeHandler = (sender, e) =>
                            {
                                cancWindow.Closed -= closeHandler;
                                args.Callback();
                            };
                            cancWindow.Closed += closeHandler;
                            _openWindows.Add(cancellation.Id, cancWindow);
                            cancWindow.Show();
                            break;
                        case "Closed":
                            if (_openWindows.ContainsKey(cancellation.Id))
                            {
                                _openWindows[cancellation.Id].Close();
                                _openWindows.Remove(cancellation.Id);
                            }
                            break;
                        default:
                            break;
                    }
                }
            }
        }
    }
}

Note that the view must now define two EventTriggers to act on the “Raised” and “Closed” events issued from the view model respectively.

<UserControl x:Class="Mm.LoadingDialog.Module.View"
             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:i="http://schemas.microsoft.com/expression/2010/interactivity"
             xmlns:cancellation="clr-namespace:Mm.LoadingDialog.Dialog;assembly=Mm.LoadingDialog.Dialog"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <i:Interaction.Triggers>
        <!-- Trigger listening for the "Raised" event on the source object -->
        <i:EventTrigger EventName="Raised" SourceObject="{Binding InteractionLoadingRequest}">
            <i:EventTrigger.Actions>
                <cancellation:LoadingWindowAction EventName="Raised" />
            </i:EventTrigger.Actions>
        </i:EventTrigger>
        <!-- Add another trigger listening for the "Closed" event -->
        <i:EventTrigger EventName="Closed" SourceObject="{Binding InteractionLoadingRequest}">
            <i:EventTrigger.Actions>
                <cancellation:LoadingWindowAction EventName="Closed" />
            </i:EventTrigger.Actions>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid>
        ....
    </Grid>
</UserControl>

Now, for a method to support cancellation in cases where a user may decide that the execution of some long running work is taking too long to be worth waiting for the .NET Framework 4.0 introduced a new cancellation model based on an object called CancellationToken. It’s important to be aware of the fact that this model doesn’t come with any guarantees.

Above all, cancellation cannot really occur without the explicit support of the operation to be cancelled. This means that the operation to be canceled needs to take a CancellationToken as an argument and decide when to honour a request for cancellation from the user, typically by polling the token to find out if a cancellation request was made or by registering for a callback to be invoked should the token be cancelled. There is also a third option; the token can provide a WaitHandle to be signaled when cancelation is requested.

A lot of API:s won’t support cancellation as there is no way to stop the work once it has been started. For example, consider a call to some third-party service located on a remote computer. Once the request to the service has been sent from the client and all the bytes have been transmitted on to the network, there is no way of cancelling it. It may take several seconds for a response to come back though and in situations like this you need to decide whether your UI dialog should provide a cancel button at all and if so what action to take when it is clicked. Anyway, the only thing you can actually cancel in the mentioned scenario is the job of waiting for the response to come back from the remote server. You should make sure to always keep your application in a consistent state.

The sample XAML markup defining the appearance of the loading dialog below hides or displays a “Cancel” button depending of the value of the “SupportCancellation” property of the Cancellation context passed to the window’s constructor and set as its DataContext. It uses a BooleanToVisibilityConverter to convert the Boolean value to a valid System.Windows.Visibility enumeration value.

<Window x:Class="Mm.LoadingDialog.Dialog.LoadingWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        Title="{Binding Title}"
        Width="460"
        MinHeight="185"
        ResizeMode="NoResize"
        ShowInTaskbar="False"
        SizeToContent="Height"
        WindowStartupLocation="CenterOwner"
        x:Name="cancellationWindow">
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="booleanToVisibilityConverter"/>
    </Window.Resources>
    <Grid Margin="4">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <ContentPresenter Content="{Binding Content}" Margin="10,10,10,10" Grid.Row="0"/>

        <Button Content="Cancel" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,0,0"
                Grid.Row="1" Visibility="{Binding SupportCancellation, 
                Converter={StaticResource booleanToVisibilityConverter}}">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <ei:ChangePropertyAction PropertyName="Cancelled" TargetObject="{Binding}" Value="True"/>
                    <ei:CallMethodAction TargetObject="{Binding ElementName=cancellationWindow}"
                                         MethodName="Close"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
    </Grid>
</Window>

Below is a sample implementation of a view model that derives from the abstract LoadingDialogViewModel class and uses its functionality to display a loading dialog to the user while running a cancelable service operation on a background thread. It uses the new async feature introduced in C# 5/.NET Framework 4.5 and Visual Studio 2012 to call the service method asynchronously without defining continuations or splitting the code across multiple methods or lambda expressions.

using Microsoft.Practices.Prism.Commands;
using Mm.LoadingDialog.Dialog;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;

namespace Mm.LoadingDialog.Module
{
    public class ViewModel : LoadingDialogViewModel, INotifyPropertyChanged
    {
        private readonly ISampleService _service;
        private readonly ICommand _callServiceCommand;
        private string _result;

        public ViewModel(ISampleService service)
            : base()
        {
            _service = service;
            _callServiceCommand = new DelegateCommand(CallServiceAsync);
        }

        private async void CallServiceAsync()
        {
            //1. Create a dialog context
            Cancellation cancellation = new Cancellation
            {
                Title = "Loading dialog",
                Content = "Processing your request, please wait...",
                SupportCancellation = true
            };
            //2. Create a CancellationTokenSource to be passed to the cancelable service operation
            CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
            //3. Define an action callback to be executed when a cancelation request has been made
            Action<Cancellation> onCancel = (c) =>
            {
                if (c.Cancelled && cancellationTokenSource != null)
                {
                    cancellationTokenSource.Cancel();
                    _interactionLoadingRequest.Close(cancellation);
                }
            };
            //4. Display the loading dialog
            _interactionLoadingRequest.Raise(cancellation, onCancel); //show dialog
            double result;
            try
            {
                //5. Run the service operation on a background thread
                result = await Task<double>.Run(() =>
                {
                    return _service.DoSomeCalculation(cancellationTokenSource.Token,
                        10, 56, 60, 76, 84, 13);
                });
                Result = result.ToString("N2"); //6. Present the return value to the user
            }
            catch (OperationCanceledException)
            {
                Result = "The operation was cancelled.";
            }
            catch (Exception ex)
            {
                Result = string.Format("The operation failed:{0}{1}",
                    Environment.NewLine, ex.Message);
            }
            finally
            {
                _interactionLoadingRequest.Close(cancellation); //7. Close the dialog
                cancellationTokenSource.Dispose();
            }
        }

        #region Properties
        public ICommand CallServiceCommand
        {
            get { return _callServiceCommand; }
        }

        public string Result
        {
            get { return _result; }
            private set { _result = value; OnPropertyChanged(); }
        }
        #endregion

        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] string name = "")
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
        #endregion
    }
}

The sample service method supports cancellation by polling the token through its ThrowIfCancellationRequested() method. It will throw a System.CanceledOperationException if the token has had cancellation requested, i.e. if the Cancel() method on the CancellationTokenSource has been called from the onCancel callback passed to the InteractionLoadingRequest’s Raise method. The callback is executed when the “Cancel” button in the LoadingWindow is clicked and when the window is closed.

You should note that a certain amount of time may elapse between the user asking to cancel the operation and the System.CanceledOperationException being thrown. For example, if a cancellation was requested just after the first call to ThrowIfCancellationRequested() returned in the below sample, the operation will continue to execute until the second call to the same method.

using System;
using System.Threading;

namespace Mm.LoadingDialog.Module
{
    public class SampleService : ISampleService
    {
        public double DoSomeCalculation(CancellationToken token, params int[] numbers)
        {
            double total = 0;
            foreach (double d in numbers)
            {
                //Poll #1: throw a CanceledOperationException if a cancellation request has been made
                token.ThrowIfCancellationRequested();
                
                //simulate some long running operation by sleeping for a second
                //NOTE: If a cancellation is requested at this point, it will be ignored...
                Thread.Sleep(1000);

                //...until the token is polled again
                //Poll #2: throw a CanceledOperationException if a cancellation request has been made
                token.ThrowIfCancellationRequested();
                
                total += Math.Pow(d, 2) + Math.Sqrt(d*3);
            }
            return total;
        }
    }
}

The cancelable operation inspects the token from time to time to see if it should abandon the work. It’s the responsibility of the developer to decide on the balance between checking for cancellation to in-frequently and getting a slow response to the user’s cancellation request and checking to frequently and slowing down the process.



Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s