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.
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.
public class DialogVisitor : ViewModel.IDialogVisitor
{
public object DynamicVisit(Object data) => Visit((dynamic)data);
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:
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.
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
Principal Software Engineer in medical device and related fields.