Click here to Skip to main content
15,888,610 members
Articles / Desktop Programming / WPF

ViewModel to View Interaction Request

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
30 Mar 2015CPOL8 min read 23.2K   451   15   3
This article describes a technique to interact with the user from view-model level.

Introduction

I construct all my WPF applications with multiple layers (assemblies). Typically, these consist of:

  • Application: the GUI implementation
  • ViewModel: the application logic
  • DataModel: the (data) model definition

The reason for this architecture is to abstract and isolate different kinds of components. The layers have a top-down dependency to each other, and cyclic library references are not permitted.

This means that objects in the top layer can use the objects in the layers below, and not the other way round. If an object wants to notify a parent object, it has to implement an event with a delegate definition to which the parent object has to register itself so that it can be notified by the child object. This is not a new topic, and many articles can be found that explain this pattern.

In this article, I describe a technique that builds on delegate and event design paradigms to provide user interaction from the application logic layer. In this case, I have code in the view-model layer that needs to interact with the user.

Example

For example, a command implementation that requests the path to some file:

C#
public class OpenFileCommand : ICommand
{
--- Code omitted ---
    public void Execute(object parameter)
    {
        OpenFileDialog openFileDialog = new OpenFileDialog();
        if (openFileDialog.ShowDialog() == true)
        {
            this.filePath = openFileDialog.FileName;
        }
    }
--- Code omitted ---
}

This example uses a GUI component, the OpenFileDialog, to interact with the user to request the path to some file. This implementation breaches the architectural concept of layering! Instead, I want to raise an event, so that the implementation that uses the OpenFileDialog is residing in the application layer.

Solution Benefits

  • Sound Architecture: clean layering, no blending of logic and GUI components
  • Clean References: no GUI references in sub layers
  • Testability: no modal dialogs, this kills any automated test
  • Environment Independency: the GUI layer can be substituted

Background

This solution is inspired by the Microsoft Prism library. This library is a part of the Microsoft Patterns for Building Composite Applications, a strong, but also large library that contains many classes and patterns for building a desktop application that can be composed of multiple units (see Domain Driven Design).

Using the Code

The sample solution contains the simplest Notepad implementation. The Interaction Request pattern is used to request the file path for loading or saving the text file when a "Load" or "Save As..." button is clicked.

Image 1

Simplified Component and Class Diagram

The next diagram shows the basic structure of the sample program. Only the load command is shown to simplify the drawing. In the next steps, I will add the Interaction Request classes as well.

The application has 2 assemblies:

  • Application: contains the GUI elements
  • ViewModel: contains the business logic and data container

Image 2

Application

  • MainWindow: the GUI definition, written in XAML
  • ShowLoadFileDialogAction: this class this will create the OpenFileDialog instance and show it to the user

ViewModel

  • MainViewModel: contains the view's commands
  • LoadCommand: is executed when the user presses the "Load..." button
  • TextContainer: singleton instance that contains the text in the view's text box

This article explains a method to programmatically get from the LoadCommand, residing in the ViewModel layer, to the ShowLoadFileDialogAction, which is part of the Application layer.

The implementation of this method can be reused, and therefore I will create a library for the implementation. I will call this library the "Small Application Framework".

Small Application Framework

The "Small Application Framework" is a library in which I will put the reusable components. I will use this library for this and future articles, and extend and modify its contents.

The "Small Application Framework" consists of two libraries:

  • SmallApplicationFramework: contains GUI independent core application logic components
  • SmallApplicaitonFramework.Wpf: contains core application logic components that use WPF

Image 3

SmallApplicationFramework

  • IInteractionRequest: interface of the interaction request, containing the Raised event
  • INotification: interface of the interaction request notification
  • InteractionRequest: generic implementation of the interaction request that takes a notification implementation as generic type argument
  • InteractionRequestEventArgs: the interaction request event arguments that are passed into the Raised event
  • CommandBase: an ICommand implementation, handling the CanExecuteChanged event handler. This class is used as the base class for the commands

SmallApplicationFramework.Wpf

  • InteractionRequestTrigger: the specialized event trigger for the interaction request
  • TriggerActionBase: base class for the interaction request actions

Complete Package Diagram

The complete package diagram looks as follows:

Image 4

The Implementation

So far, the base classes are introduced. This section explains how they work together. The goal of this exercise is to create code that can be called from a command (in this example, the LoadCommand) that will show a OpenFileDialog in the GUI layer. This means that an event is raised, which contains a notification. The notification contains data specific for the task at hand. The first step is to define the notification.

The Notification

The notification is derived from the INotification interface. It is instantiated at the time that the LoadCommand is executed. At this time, the title and the filter that will be shown in the open file dialog are set. The file path and name are result properties.

C#
public class ShowLoadFileDialogNotification : INotification
{
    public string Title { get; set; }
    public string FileName { get; set; }
    public string FilePath { get; set; }
    public string Filter { get; set; }
}

The InteractionRequest Definition

The InteractionRequestContainer is a public singleton instance, containing the interaction requests. The ShowLoadFileDialogInteractionRequest is a property of the container.

C#
public class InteractionRequestContainer
{
    private static InteractionRequestContainer instance;

    private InteractionRequestContainer()
    {
        this.ShowLoadFileDialogInteractionRequest =
           new InteractionRequest<ShowLoadFileDialogNotification>();
    }

    public static InteractionRequestContainer Instance
    {
        get
        {
            return instance ?? (instance = new InteractionRequestContainer());
        }
     }

     public InteractionRequest<ShowLoadFileDialogNotification>
         ShowLoadFileDialogInteractionRequest { get; private set; }
}

Using the InteractionRequest

The ShowLoadFileDialogInteractionRequest is used in the Execute method of the LoadCommand. The interaction request is raised with the notification instance. The resulting value is tested and if it succeeds, the file is opened and its contents are written in the TextContainer. This is also a singleton instance and contains the text string and the file path of the text file.

Please note that in this case the execution of the interaction request is a synchronous action. The control flow returns after the OpenFileDialog is closed and its result value is stored in the notification object.

C#
public class LoadCommand : CommandBase
{
    public override void Execute(object parameter)
    {
        var notification = new ShowLoadFileDialogNotification
                               {
                                   Title = "Select Text File",
                                   Filter = "Text Files|*.txt|All Files|*.*"
                               };

        InteractionRequestContainer.Instance.ShowLoadFileDialogInteractionRequest.Raise(notification);
        if (string.IsNullOrEmpty(notification.FilePath) == false
            && File.Exists(notification.FilePath))
        {
            using (var streamReader = new StreamReader(notification.FilePath))
            {
                TextContainer.Instance.Text = streamReader.ReadToEnd();
            }

            TextContainer.Instance.FilePath = notification.FilePath;
        }
    }
}

The MainViewModel

The MainViewModel does not contain any references to the interaction request. The instance is already declared in the InteractionRequestContainer which is a public singleton instance. Therefore the MainViewModel class contains only the LoadCommand as a property.

C#
public class MainViewModel
{
    public MainViewModel()
    {
        this.LoadCommand = new LoadCommand();
    }

    public ICommand LoadCommand { get; private set; }
}

The ShowLoadFileDialogAction Definition

This class contains the logic for the interaction with the user. It is derived from the TriggerActionBase class and it is instantiated with the ShowLoadFileDialogNotification generic type parameter, so that it can receive this type of notifications. The base class handles the parameter checking and calls the abstract ExecuteAction() method. This method has to be overridden and implemented as shown below. Here, the OpenFileDialog is instantiated and its result is put into the notification after the dialog is closed.

C#
public class ShowLoadFileDialogAction : TriggerActionBase<ShowLoadFileDialogNotification>
{
    protected override void ExecuteAction()
    {
        var openFileDialog = new OpenFileDialog();
        openFileDialog.Title = this.Notification.Title;
        openFileDialog.Filter = this.Notification.Filter;

        if (string.IsNullOrEmpty(this.Notification.FileName) == false)
        {
            openFileDialog.InitialDirectory = this.Notification.FileName;
        }

        this.Notification.FileName = string.Empty;
        if (openFileDialog.ShowDialog() == true)
        {
            this.Notification.FileName = openFileDialog.SafeFileName;
            this.Notification.FilePath = openFileDialog.FileName;

            var mainWindow = this.AssociatedObject as MainWindow;
            if (mainWindow != null)
            {
                mainWindow.Title = string.Format("{0} - {1}",
                                       "Simplest Notepad",
                                       Notification.FileName);
            }
        }
    }
}

Receiving the InteractionRequest

The interaction request mechanism relies on the "Windows.System.Interactivity.dll" assembly. This assembly is packed with the Expression Blend SDK and it contains many useful features. The Interactivity assembly is referenced as a Nuget package.

The next XAML definition shows the application's GUI definition. The section Interaction.Triggers holds the definition of the an InteractionRequestTrigger that is bound to the ShowLoadFileDialogInteractionRequest, defined in the InteractionRequestContainer. The ShowLoadFileDialogAction is defined as the event instance that is triggered when the InteractionReuqestContainer's ShowLoadFileDialogInteractionRequest event is raised.

XML
<Window x:Class="InteractionRequestNotepad.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:viewModel="clr-namespace:ViewModel;assembly=ViewModel"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
        xmlns:interactionRequest="clr-namespace:SmallApplicationFramework.Wpf.InteractionRequest;
                                  assembly=SmallApplicationFramework.Wpf"
        xmlns:actions="clr-namespace:Application.Actions"
        Title="Simplest Notepad" Height="350" Width="525">

    <Window.DataContext>
        <viewModel:MainViewModel/>
    </Window.DataContext>

    <i:Interaction.Triggers>
        <interactionRequest:InteractionRequestTrigger
             x:Name="ShowLoadFileDialog"
             SourceObject="{Binding Path=ShowLoadFileDialogInteractionRequest,
                          Source={x:Static viewModel:InteractionRequestContainer.Instance}}">
            <actions:ShowLoadFileDialogAction/>
        </interactionRequest:InteractionRequestTrigger>
    </i:Interaction.Triggers>

    <DockPanel LastChildFill="True">
        <ToolBar x:Name="MainToolBar" DockPanel.Dock="Top">
            <Button x:Name="LoadButton" 
            Content="Load..." Command="{Binding LoadCommand}"/>
        </ToolBar>

        <Grid>
            <TextBox x:Name="TextBox"
                     Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                     TextWrapping="Wrap"
                     AcceptsReturn="True"
                     DataContext="{x:Static viewModel:TextContainer.Instance}"
                     HorizontalScrollBarVisibility="Auto"
                     VerticalScrollBarVisibility="Auto"/>
        </Grid>
    </DockPanel>
</Window>

At this point, all actors are set up, and the system works. But how does it work?

Sequence Diagram

The next sequence diagram shows all the classes and the respective calls made to show the OpenFileDialog.

Image 5

Here is the link to the sequence diagram in the original size.

The Implementation Behind the Scenes

InteractionRequest

The InteractionRequest class takes an INotification object as its generic type parameter. It consists of an event called Raised, to which the trigger will subscribe, and the Raise(T context) method that will receive the notification instance as a parameter and packs the notification instance into the InteractionRequestEventArgs where it will be passed to the Raised event. In case of asynchronous usage, the Raise method can be called with a callback Action delegate, that can be called when the action has completed.

C#
public class InteractionRequest<T> : IInteractionRequest
    where T : INotification
{
    public event EventHandler<InteractionRequestedEventArgs> Raised;

    public void Raise(T context)
    {
        this.Raise(context, c => { });
    }

    public void Raise(T context, Action<T> callback)
    {
        EventHandler<InteractionRequestedEventArgs> handler = this.Raised;
        if (handler != null)
        {
            handler(this, new InteractionRequestedEventArgs(context, () => callback(context)));
        }
    }
}

InteractionRequestEventArgs

The event argument with the notification and the optional callback action delegate.

C#
public class InteractionRequestedEventArgs : EventArgs
{
    public InteractionRequestedEventArgs(INotification context, Action callback)
    {
        this.Context = context;
        this.Callback = callback;
    }

    public INotification Context { get; private set; }

    public Action Callback { get; private set; }
}

InteractionRequestTrigger

The specialized interaction request trigger. The GetEventName() method returns the name of the event the trigger is listening for. In this case, it is the "Raised" event.

C#
public class InteractionRequestTrigger : EventTrigger
{
    protected override string GetEventName()
    {
        return "Raised";
    }
}

TriggerActionBase

A base class for all interaction request actions. It implements the abstract Invoke(object) method, defined in the TriggerAction class which is called when the event is triggered. It retrieves the notification object and the callback action from the event arguments and calls the ExecuteAction() method if a valid notification object is received.

C#
public abstract class TriggerActionBase<T> : TriggerAction<FrameworkElement>
    where T : class, INotification
{
    private T notification;
    private Action callback;

    protected T Notification
    {
        get
        {
            return this.notification;
        }
    }

    protected Action Callback
    {
        get
        {
            return this.callback;
        }
    }

    protected override void Invoke(object parameter)
    {
        var interactionRequestedEventArgs = parameter as InteractionRequestedEventArgs;
        if (interactionRequestedEventArgs != null)
        {
            this.notification = interactionRequestedEventArgs.Context as T;
            this.callback = interactionRequestedEventArgs.Callback;
            if (this.notification != null)
            {
                this.ExecuteAction();
            }
        }
    }

    protected abstract void ExecuteAction();
}

Conclusion

As stated in the Background section: I did not invent this pattern. The basis is a part of the Microsoft Prism Library. But I found the complete library too large to use for my "simple" applications. Still, I liked the concept and therefore I use it as the code, in the way shown here.

The main reason I am writing this article is to raise the awareness that calling GUI components from the ViewModel layer must be dealt with carefully. I urge software engineers to refrain from implementing code like I presented in the introductory example. At the latest when automated testing becomes an issue, the presented dialog window will kill the test case.

I understand that the same functionality can be achieved using depencency injection (DI) and inversion of control. Meaning that an interface is defined in the ViewModel layer, which is implemented in the View layer. The interface and the implementation are both registered to the DI mechanism, and this will instantiate the interface implementation, when it is used in the ViewModel layer (i.e., called by the command).

I don't use the DI approach because of the automated application tests. Typically, I use the SpecFlow library to write the acceptance tests, and my scenarios act on the ViewModel layer classes, mimicking the Application layer. In this case, I can subscribe the interaction requests and keep the "called" state in the test context. With the DI approach, I have to implement and inject the test implementation of the interaction requests, which might influence the actual code execution, if it doesn't comply to the "real-life" implementation.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead Seven-Air Gebr. Meyer AG
Switzerland Switzerland
I am a senior Program Manager, working for Seven-Air Gebr. Meyer AG since September 2022.
I started off programming in C++ and the MFC library, and moved on to the .Net and C# world with .Net Framework 1.0 and further. My projects consist of standalone, client-server applications and web applications.

Comments and Discussions

 
QuestionSo how do you recommend creating popups? Pin
Member 1216917219-Jun-16 10:38
Member 1216917219-Jun-16 10:38 
AnswerRe: So how do you recommend creating popups? Pin
Jeroen Richters23-Jun-16 22:57
professionalJeroen Richters23-Jun-16 22:57 
QuestionExcellent Article! Pin
Your Display Name Here30-Mar-15 9:39
Your Display Name Here30-Mar-15 9:39 
Your article is right on target! Messages that indicate what needs to be done and services listening for what to do! Smile | :)

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.