Click here to Skip to main content
15,868,164 members
Articles / Desktop Programming / WPF

Using the Visitor Pattern to Maintain MVVM Layering While Implementing Dialogs in WPF

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
2 Jan 2020CPOL4 min read 9.8K   288   5   3
A simplified approach to maintaining the WPF MVVM layers when the ViewModel needs to show a form dialog

Introduction

Maintaining the layering of MVVM (Model-View-ViewModel) when displaying a dialog form is challenging, because the command handler in the ViewModel layer needs to somehow access the dialog window without breaking the layering. The ViewModel should not have a compile-time dependency on any View (Windows UI) object, including the View that contains the dialog window, while the dialog window needs to be given a reference to the data it will manipulate, which is not known until runtime. This article describes a simplified approach using a modified Visitor Pattern (Gamma, et al, Design Patterns) and dependency injection (DI) to maintain the separation of layers.

Background

The Visitor Pattern separates data from the operations to be performed on it. In this case, a dialog form needs to update or populate the fields of an object. The Visitor achieves this by having a class with overloaded methods, each accepting a specific object type (class) argument. Thus, when a "Visit" method is invoked with a specific argument type, the correct method is automatically chosen. These Visit methods are responsible for creating the correct dialog window, assigning the argument object as the DataContext of the dialog, and showing the dialog (we're assuming a modal dialog here). The Visitor is injected into the ViewModel (VM) objects in the MainWindow-loaded event handler by property injection (the ViewModel object(s) have a public Visitor property field to hold the reference to the Visitor). I believe this is simpler than using a mediator, since there is no need for events to pass between the layers.

This example does not require a Dependency Injection container, although one could be applied with little difficulty. It does not reference Prism Behaviors or other external frameworks. It can be added to an existing code base with no disruption.

Using the Code

The ViewModel uses the Relay Command (Hall, Pro WPF and Silverlight MVVM, Apress) to provide the command behavior. The VM objects are created in XAML as static resources. The MainWindow-Loaded event handler retrieves these objects, instantiates the View's Visitor class, and injects it into the VM objects by setting their Visitor property. The main window buttons are bound to the commands in XAML. When invoked, the command handler either creates a new data object, or retrieves the one currently selected, and invokes the Visitor's DynamicVisitor method, which invokes the overloaded Visit method matching the argument.

The main point of this article is the DialogVisitor. The ViewModel layer defines an interface, IDialogVisitor, with one method, DynamicVisit. The ViewModel classes contain a public reference to the interface class. Thus, the ViewModel classes (ViewPersons, ViewVehicles) have a compile-time dependency only on the ViewModel and Model layers. It is important to note that the interface does not define any of the methods for showing the dialog windows.

C#
 // The interface is defined in the ViewModel
public interface DialogVisitor
 {
    object DynamicVisit(Object data);
 }

The View layer defines a derived DialogVisitor that overrides the DynamicVisit method, supplying it with the method which calls the correct Visit method, based on the Visit method's signature, and defines the private Visit methods. The Visit methods handle instantiating and showing the dialog window that handles the object in their arguments. The MainWindow-loaded event handler instantiates the DynamicVisit class, and injects it (by property injection) into the ViewModel classes.

C#
/// <summary>
/// Modified Visitor. Using Dynamic to simplify the pattern.
/// See "Albahari, C# 7.0 in a Nutshell"
/// Daniel Ziegelmiller, author
/// </summary>
public class DialogVisitor : ViewModel.IDialogVisitor
{
    /// <summary>
    /// The method which is called by ViewModel classes to instantiate and show the dialog 
    /// windows. By dynamic member resolution, the correct private Visit method will
    /// be invoked based on the method signature.
    /// </summary>
    /// <param name="data">The object which the dialog window
    /// will manipulate.</param>
    /// <returns>The object argument as modified.</returns>
public object DynamicVisit(Object data) => Visit((dynamic)data);

    // create overloaded Visit methods. The correct one will
    // be called based on the method signature, when the DynamicVisit delegate 
    // is invoked.
    //
    // This decouples the data (argument) from the action (dialog) performed
    // on it.

private Person Visit(Person p)
  {
    var dlg = new PersonDialog();
    dlg.DataContext = p;
    dlg.ShowDialog();
    return p;
  }

private Vehicle Visit(Vehicle v)
  {
    var dlg = new VehicleDialog();
    dlg.DataContext = v;
    dlg.ShowDialog();
    return v;
  }
}

After the Visitor is injected into the ViewModel class, the DynamicVisit method is invoked to show the dialog, for example:

C#
public void NewPerson()
{
   if (Visitor == null) return;

   Person p = new Person();
   Visitor.DynamicVisit(p);
   PersonList.Add(p);
}

Most of the code in the example is scaffolding to support and demonstrate the Visitor class. The data argument could potentially contain information to control more complex behavior in the dialog handler. The DialogVisitor can be easily extended with more Visit methods, as long as each one has a distinct signature based on the type of the argument.

After compilation, run the program and click the "Add" buttons raise the dialogs to create some rows of data; highlight a row and observe that the "Update" button becomes enabled. Click an "Update" button to raise the dialog with the row data in the dialog. Modify it, and, when the dialog is closed, the row data will reflect the updates.

Image 1

Unit Testing

Unit testing is not demonstrated in this example. Because there is no dependency on any View or UI objects in the ViewModel layer, unit testing can be done by instantiating test versions of the DynamicVisitor class and injecting those into the ViewModel classes under test.

Limitations

A DynamicView class can have only one method per dialog data type. Depending on the complexity of the application, there could be more than one DynamicView class, potentially with Visit methods having more than one argument.

This example uses the MainWindow-loaded event handler to create the DynamicView and inject it into the ViewModel classes, so that the ViewModel classes can have parameterless constructors. One could refactor the ViewModel classes to accept the DynamicView class as a constructor argument, and use the ObjectDataProvider mechanisms in XAML to create the DynamicView and ViewModel classes, injecting the DynamicView. This is a matter of preference. In my view, the example mechanism makes it a bit clearer what is going on, and is similar to how a unit test would be set up.

History

  • 2nd January, 2020: Initial version

License

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


Written By
Software Developer (Senior)
United States United States
Principal Software Engineer in medical device and related fields.

Comments and Discussions

 
Questioncoupling of classes Person , Vehicle and DialogVisitor Pin
Digitalbeach5-Jan-20 11:25
Digitalbeach5-Jan-20 11:25 
A limitation of the suggested approach is the class DialogVisitor depends on (and must be recompiled) for each new Dialog class needed. Unit testing may be difficult. This limitation may be acceptable in the context of a specific application.

For alternatives, try searching for MVVM [Dialog] Service Pattern.

Sorry don't have specific pointer to summary article on hand for alternate approach. Not sure if MVVM Light is still a supported library, but it had simple message pattern that would display the correct view (which could be a dialog or whatever was needed). Can also look for Caliburn.Micro library.
Answersource code do not compile. ViewModel.IDialogVisitor - not exist in source code. Pin
Member 110262314-Jan-20 20:01
Member 110262314-Jan-20 20:01 
PraiseVisitor Pattern Pin
Clouded Leopard3-Jan-20 18:50
Clouded Leopard3-Jan-20 18:50 

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.