In this article, I examine the problems encountered when trying to display dialog boxes in a WPF/MVVM application and provide a solution that avoids many of the problems exhibited by other techniques.
Introduction
As any MVVM purist will tell you, dialog boxes are particularly problematic when it comes to maintaining good separation of concerns. With historical roots dating back to the earliest versions of Windows, dialog boxes are the complete antithesis of good architectural design. Unlike WPF, with its powerful data binding mechanism and ability to achieve complete separation of view model and view, modal dialog boxes are invoked via direct calls to the operating system at which point on-going communication back to the business layers of your application becomes messy and unit testing virtually impossible.
Despite their problems, dialog boxes are still a valuable addition to the modern programmer's arsenal. They provide us with a convenient method to display pop-up notifications, provide encapsulated implementations of common application tasks and expose a standardized set of interfaces to ease user interaction with our applications.
There are multiple resources on the net showing partial solutions to the MVVM/dialog-box problem. Some employ Inversion of Control to expose interfaces to service providers, others resort to custom WPF popup windows and a few discard separation-of-concerns altogether and simply hack a solution into the code-behind. In this article, I will examine the problems encountered when trying to display dialog boxes in a WPF/MVVM application and provide a solution that avoids many of the problems exhibited by other techniques.
Any comprehensive solution to MVVM dialog boxes must, at a bare minimum, achieve the following goals:
- Good separation of concerns. More specifically, dialog boxes must adhere strictly to the MVVM architectural pattern and consist of a view and corresponding view model bound together via the WPF data binding mechanism. Views and view models must also be able to reside in separate projects, should that be required by the project.
- Must support proper top-level windowed dialog boxes, as opposed to relying on custom windowing solutions that attempt to mimic true dialog box behaviour.
- Must provide the ability to completely control all aspects of a custom dialog box’s visual appearance insofar as is possible with a top-level WPF window.
- Must provide support for both modal and modeless dialog boxes.
- Must support standard OS dialogs (i.e.,
MessageBox
) as well as common dialog boxes like OpenFile
/SaveFile
, etc. Must also support 3rd party dialog box frameworks. - Must support an arbitrary number of dialogs visible on-screen at once, including any and all combinations of modal, modeless, system/common and 3rd party. Dialogs must also be allowed to invoke the creation of other dialogs without breaking SOC, including the creation of confirmation/validation dialogs as part of their normal shutdown process.
- Must allow for full unit testing of the view models, including support for related technologies such as dependency injection and mocking, etc.
- Must be very easy to use at the application level.
The majority of this article argues the justification for implementing dialog boxes in the manner chosen in order to achieve all of these goals whilst circumventing the various technical challenges that arise along the way. While it may be considered overkill for a feature as simple as dialog boxes, it is relatively straightforward to implement and very easy to use once in place. A comprehensive understanding of the underlying mechanism is helpful but not actually required to add dialog box support to your project; if your primary interest is in the practical implementation then I encourage you to skip directly to the last section in this article (“Practical Application of MVVM Dialog Boxes”) and look at the specific examples of using dialog boxes in an actual MVVM application.
Opening Dialog Boxes
Let’s start with the most basic example possible: displaying a single custom dialog box. MVVM is a heavily data-driven architecture and if we wish to adhere to its architectural philosophy, then our first attempt might involve a single Boolean property with change notification added to our main view model:
public class MainViewModel : ViewModelBase
{
private bool _DialogVisible = false;
public bool DialogVisible
{
get { return this._DialogVisible; }
set
{
if (this._DialogVisible != value)
{
this._DialogVisible = value;
RaisePropertyChanged(() => this.DialogVisible);
}
}
}
When set to true
, this property should invoke the creation of a dialog box on-screen. Implementing such functionality isn’t too difficult: we can simply create an attached behaviour for our main window and bind it to this member. When we detect that the property has been set to true
, we create our dialog box:
public static class DialogBehavior
{
public static readonly DependencyProperty DialogVisibleProperty =
DependencyProperty.RegisterAttached(
"DialogVisible", typeof(bool), typeof(DialogBehavior),
new PropertyMetadata(false, OnDialogVisibleChange));
public static void SetDialogVisible(DependencyObject source, bool value)
{
source.SetValue(DialogVisibleProperty, value);
}
public static bool GetDialogVisible(DependencyObject source)
{
return (bool)source.GetValue(DialogVisibleProperty);
}
private static void OnDialogVisibleChange
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var parent = d as Window;
if (parent == null)
return;
if ((bool)e.NewValue == true)
{
new CustomDialogWindow {DataContext = parent.DataContext}.ShowDialog();
}
}
}
To use this behaviour, we simply bind the DialogVisible
attached property to the DialogVisible
property in our main view model:
<Window x:Class="MyDialogApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="clr-namespace:MyDialogApp.Behaviors"
behaviors:DialogBehavior.DialogVisible="{Binding DialogVisible}"
Setting our main view model’s DialogVisible
property to true
will now cause an instance of CustomDialogWindow
to instantly appear as a modal dialog box.
Implementing dialog boxes in this manner is, of course, somewhat limited. The most obvious problem is that it only allows for the creation of a single type of dialog box; for every additional type, we will need to create a separate Boolean flag. There’s also the issue of how to display dynamic content; in the absence of any other architectural features, our dialog box view would need to bind directly to properties in our main view model.
For the sake of argument, let us assume that we only need to display a single dialog box at a time. If we can guarantee this, at least for the moment, then it would be more useful to create a separate view model for each type of dialog box we wish to display and to replace our Boolean property with a generic property of type “object
”. In order for this to work, our attached behaviour must now be provided with a mapping between each of our dialog box view model types and the actual view class that must be created when it has been set. Within the framework of WPF, such mappings are typically achieved by way of data templates.
Data Templates: Almost a Solution
At first glance, data templates provide us with an obvious mechanism through which to achieve our mapping between dialog view models and their corresponding views, were it not for one slight problem: technically at least, top-level windows cannot form the content of a data template. Let’s say we decide to achieve our mapping by doing something like this:
<DataTemplate DataType="{x:Type local:DialogBoxViewModel}">
<local:DialogBoxWindow />
</DataTemplate>
If DialogBoxWindow
is a top-level window class, then this code will result in a compile-time error. Since it’s a XAML error, it won’t actually break the compile, and attempting to create an instance of this class at runtime by calling the DataTemplate
’s LoadContent()
function will, in fact, work correctly. That said, the error is there none-the-less because we’re using data templates in a manner in which they were not designed. Templates are meant to represent child content; top-level windows are containers, not content.
One possibility is to use a common top-level window for all of our dialog boxes and to use data templates to populate the content for that window based on the type of view model. The problem with doing this is that we don’t necessarily want all of our dialog box windows to be styled the same. Some may need to be resized, some we may want to appear relative to the parent and almost all will require different initial sizes and captions. We could use regular data binding to provide the required flexibility in our views but this would require additional complexity in our view models for setting style parameters that we really should be able to set directly in XAML.
Instead of trying to declare our dialog box windows as template content, it makes more sense to view them as the container itself, replacing data templates altogether. To see how this can work, it helps to take a closer look at how data templates achieve what they do. The DataTemplate
class is one of several that can be declared without an x:Key
parameter: what they instead provide is a DataType
property which is mapped to the corresponding view model and the DictionaryKeyAttribute
attribute is used to set that property as the key. In other words, the view model class type becomes its own key for the DataTemplate
specifying the content for its corresponding view.
Alas, this mechanism cannot be used in our case due to a bug in the WPF framework which prevents DictionaryKeyAttribute
working with custom classes. This bug, first reported in 2008, was subsequently confirmed by Microsoft and remains within the code base at the time of this writing. However, the purpose of DictionaryKeyAttribute
with respect to DataTemplate
is simply to provide syntactic sugar for specifying the resource key: there is nothing to stop us from simply specifying the key ourselves explicitly in XAML. In this scenario, we create our dialog box as we would any other WPF window and then declare an instance of it in our application XAML using the view model type as the key:
<local:DialogBoxWindow x:Key="{x:Type local:DialogBoxViewModel}" x:Shared="False" />
Note that this code also sets the “x:Shared
” property to “False
”. By default, the resources indexed from a ResourceDictionary
are not unique, every time you request an instance of a particular resource, you are given a reference to the same object. Setting the “x:Shared
“ property to False
overrides this behaviour and causes a new instance of our dialog box class to be instantiated each time we request that resource.
At this point, our behaviour class has everything it needs to take a view model and use it to index the global resource dictionary to instantiate a new instance of the corresponding dialog box window class, setting itself as the view’s DataContext
. First, we change the name of the attached property to “DialogBox
” and set its type to “object
”. When set to an instance of a view model, we have it look up that key in the global resources and create a window of the associated type:
public static class DialogBehavior
{
public static readonly DependencyProperty DialogBoxProperty =
DependencyProperty.RegisterAttached(
"DialogBox",
typeof(object),
typeof(DialogBehavior),
new PropertyMetadata(null, OnDialogBoxChange));
public static void SetDialogBox(DependencyObject source, object value)
{
source.SetValue(DialogBoxProperty, value);
}
public static object GetDialogBox(DependencyObject source)
{
return (object)source.GetValue(DialogBoxProperty);
}
private static void OnDialogBoxChange
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var parent = d as Window;
if (parent == null)
return;
if (e.NewValue == null)
return;
var resource = Application.Current.TryFindResource(e.NewValue.GetType());
if (resource == null)
return;
if (resource is Window)
{
(resource as Window).DataContext = e.NewValue;
(resource as Window).ShowDialog();
}
}
}
Our view model also needs to be updated with a new property of type object
:
private object _DialogBox = null;
public object DialogBox
{
get { return this._DialogBox; }
set
{
if (this._DialogBox != value)
{
this._DialogBox = value;
RaisePropertyChanged(() => this.DialogBox);
}
}
}
And of course, our attached property binding in the XAML needs to be updated to reflect the new property names:
behaviors:DialogBehavior.DialogBox="{Binding DialogBox}"
Creating a dialog box now involves creating an instance of the view model and assigning it to the main view model’s property:
this.DialogBox = new CustomDialogBoxViewModel();
The next obvious issue to address is the requirement for displaying multiple dialog boxes at a time, and for that we need to switch our view model property to a collection.
Multiple Dialog Boxes
While I haven’t yet addressed the issue of closing dialog boxes, it should be obvious that doing so could be achieved as it is for regular data templated child content, i.e., set the relevant view model property back to null
. An obvious consequence of this is that attempting to open a second dialog box while one is already open will cause the first to close. In order to display multiple dialog boxes simultaneously, we need to maintain a list of view models that we can monitor for changes. Fortunately, WPF gives us the perfect tool for doing this: the ObservableCollection
.
ObservableCollection
is a collection of items with collection change notification support, which means we can have our attached behaviour hook in to change events and respond accordingly. If a view model is added to the collection, then our attached behaviour can create a dialog box view for it. Similarly, if that view model is removed, then its dialog box can be automatically closed.
A real application would in all likelihood need to create dialog boxes all throughout the application, but dialog boxes can only be “owned” by top-level windows like our main window. In a real-world application, you would typically create a single collection and expose it, or some type of interface to it, to the rest of the application, possibly via the use of a dependency injection framework. Such architectural issues are beyond the scope of this article and I leave it as an exercise for the reader, in the mean time, I simply add the ObservableCollection
as an exposed property in my demo applications main view model:
private ObservableCollection<IDialogViewModel> _Dialogs =
new ObservableCollection<IDialogViewModel>();
public ObservableCollection<IDialogViewModel> Dialogs { get { return _Dialogs; } }
Next, we need to create a corresponding dependency property in our attached behaviour which will bind to this collection:
public static readonly DependencyProperty DialogViewModelsProperty =
DependencyProperty.RegisterAttached(
"DialogViewModels",
typeof(object),
typeof(DialogBehavior),
new PropertyMetadata(null, OnDialogViewModelsChange));
public static void SetDialogViewModels(DependencyObject source, object value)
{
source.SetValue(DialogViewModelsProperty, value);
}
public static object GetDialogViewModels(DependencyObject source)
{
return source.GetValue(DialogViewModelsProperty);
}
And now, we create the binding itself by adding the appropriate code to our main application window’s XAML:
xmlns:dlgs="clr-namespace:MvvmDialogs.Behaviors;assembly=MvvmDialogs"
dlgs:DialogBehavior.DialogViewModels="{Binding Dialogs}"
Finally, we need to add code to our attached behaviour to create a new dialog box whenever a new view model is added to our collection. The full code to do this (i.e., the OnDialogViewModelsChange
method in DialogBehavior
) is somewhat non-trivial due to additional functionality described further in this article but the basic behaviour is to subscribe to collection change notifications so as to create a new dialog window whenever a view model instance is added to the collection:
if (!ChangeNotificationHandlers.ContainsKey(parent))
ChangeNotificationHandlers[parent] = (sender, args) =>
{
var collection = sender as ObservableCollection<IDialogViewModel>;
if (collection != null)
{
if (args.Action == NotifyCollectionChangedAction.Add)
{
if (args.NewItems != null)
foreach (IDialogViewModel viewModel in args.NewItems)
{
AddDialog(viewModel, collection, d as Window);
}
}
}
};
We now have everything we need to easily display new dialog boxes, both modal and modeless, by simply creating the relevant view model and adding it to the dialog box collection property:
this.Dialogs.Add(new CustomDialogBoxViewModel());
So long as we have also added the mapping between view model and view as discussed earlier in this article, the correct window class will be created and bound to the view model.
Closing Dialog Boxes
Once dialog boxes are open, we need a way of closing them but the best way to achieve this isn’t quite as straightforward as it initially seems. The problem is that in a Windows application, there are several different ways in which dialog boxes get shut down.
In the case of modeless dialog boxes, the method should be obvious: simply remove them from the main view models dialogs collection. Our attached behaviour initially created the dialog box in response to the view model being added to the dialog collection, it seems perfectly reasonable that it should also be responsible for closing it if and when the view model is subsequently removed.
Modal dialog boxes, however, are a little different due to the fact that they usually have one or more buttons that are used to close them, i.e., Ok, Cancel, etc. In MVVM, these buttons will typically be bound to commands in the view model, so the first question to address is whether the view model should be given a reference to the collection so that it can remove itself from the list in response to those commands. Alternatively, should the view model always notify its parent when it wishes to close and rely on that parent to do the actual removal? Or should the view model instead somehow notify the behaviour that it wishes to close and rely on it to do the dirty work, thus relieving that task from both itself and its parent? The behaviour does, after all, have knowledge of the collection to which the view model was added.
Further complicating the matter is the fact that there are times in which the behaviour needs to initiate the closing action, the most common example of which is the user pressing the cancel button on the dialog box caption bar. Ordinarily, we could add handlers to our code-behind to detect these situations but since we’re doing things the MVVM way, we instead need to attach notification handlers to the window in the behaviour and notify the view model when a close has been requested by the OS so that it can determine whether or not it wants to allow it.
In order to add flexible closing behaviour to our dialog boxes, we need to add two additional members to the base user dialog view model:
void RequestClose();
event EventHandler DialogClosing;
The RequestClose
method is used to signal to the dialog that we wish for it to shut down. Anything can call this member, including other view models, but our attached behaviour uses it to signal that the caption bar close button has been pressed.
The default implementation of RequestClose()
in our dialog box view models should be to raise the DialogClosing
event which will in turn signal to everything else, including other view models, that the dialog box is shutting down. Our attached behaviour can subscribe to this event and, when detected, close the corresponding view. In the special case of the caption bar close button being pressed, the behaviour checks to see if this event was raised in response to the call to RequestClose
; if it wasn’t, then the behaviour can signal back to the operating system to cancel the window close action. This allows the view model to provide custom behaviour for determining whether or not to allow itself to be closed, e.g., a confirmation dialog box or a cancel in the event of data validation failure.
This functionality also handles the case of dialog boxes wanting to close themselves. If a dialog box has an OK button and the command handler raises the DialogClosing
event, then the behaviour will automatically detect that as a close request and respond by removing it from the collection. This action will result in the same behaviour as if the main view model or anything else had removed it: the attached behaviour will detect the CollectionChanged
event and close the dialog box view window.
Execution Flow Control
Let’s take a short detour for a moment and examine what happens to the execution tree when working with dialogs using the techniques described in this article.
In “regular” Windows code, creating a dialog box effectively halts the execution of the code that invokes it. In practice, what actually happens is that Windows internally kicks off a separate message pump so that application messages continue being delivered without the entire application locking up. In our case, we’re using an attached behaviour to create the dialog box in response to a collection change notification, but in the case of modal dialog boxes, the effect on the execution of the calling function is the same:
this.Dialogs.Add(new CustomDialogBoxViewModel());
Fortunately, WPF data binding is multi-threaded, so if this behaviour is a problem, then there is nothing to prevent you from doing the assignment in a separate thread to avoid blocking the calling function:
Task.Run(() =>
{
lock (this.Dialogs)
this.Dialogs.Add(new CustomDialogBoxViewModel());
});
Another important consequence of this behaviour is that under normal circumstances, a modal dialog box does not receive notification when its view becomes visible on-screen. A modal dialog can perform any initialization it likes in its constructor, and it can expose any additional functions as needed for the parent class to call, but it does not actually become visible until it has been added to the Dialogs
collection at which point execution effectively halts until the dialog box closes. What this means in practice is that any worker threads that need to be created for background tasks or progress updates, etc. must be created before the assignment of the view model to the dialog box collection. In practice, this is rarely a problem, after all the view is simply a loosely-bound representation of the view model, but if you absolutely must know when the dialog window itself is visible on-screen, then you will need to obtain this notification via some other means. In MVVM, this can be achieved by way of an interaction trigger added to the window class to invoke a command in the view model in response to the appropriate window event:
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<cmd:EventToCommand Command="{Binding LoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
Integration with System Dialogs and 3rd Party Frameworks
Any generic dialog box framework must have the ability to support dialog boxes that don’t adhere to the MVVM architectural pattern. This includes system dialog boxes like MessageBox
, common dialog boxes like OpenFileDialog
/PrintDialog
, etc. and dialog boxes supplied by 3rd party libraries such as the exceptionally good Ookii Dialogs package.
Like all other dialog box types in this architecture, adding support for external dialog boxes must begin by creating a view model. In the case of common dialog boxes, it’s theoretically possible to use the WPF dialog classes as their own view models, but doing so would invariably mean breaking separation of concerns and requiring the view model to reference Windows libraries that it would otherwise not require. The way I have chosen to solve this is by way of the IDialogPresenter<>
generic.
IDialogPresenter<>
inherits the IDialogViewModel
interface and forms the “glue”, if you like, between the view model and the actual dialog box that is to be displayed. Its single method, Show()
, accepts an instance of the typed view model and is responsible for displaying the dialog box with the appropriate parameters. It is also responsible for populating the view model with any results that the dialog box may have returned. How the mapping between view model and dialog box properties is actually achieved in practice is an implementation issue I leave to the reader, the examples in the sample project simply copy each field explicitly:
public class OpenFileDialogPresenter : IDialogBoxPresenter<OpenFileDialogViewModel>
{
public void Show(OpenFileDialogViewModel vm)
{
var dlg = new OpenFileDialog();
dlg.Multiselect = vm.Multiselect;
dlg.ReadOnlyChecked = vm.ReadOnlyChecked;
dlg.ShowReadOnly = vm.ShowReadOnly;
var result = dlg.ShowDialog();
vm.Result = (result != null) && result.Value;
vm.Multiselect = dlg.Multiselect;
vm.ReadOnlyChecked = dlg.ReadOnlyChecked;
vm.ShowReadOnly = dlg.ShowReadOnly;
}
}
It could be argued that the use of IDialogPresenter<>
breaks separation of concerns by relying on both the view model and what is effectively the view, a fact to which I am inclined to agree. However, I would also argue that this is somewhat inevitable given that the dialog box implementations themselves do not adhere to the MVVM pattern; in the absence of some type of binding mechanism used by the external dialog code itself, there is probably no satisfactory way to circumvent this entirely. What IDialogPresenter
does attempt to do, however, is to isolate this anti-pattern and minimize any potential effect on the rest of the code base.
As with user dialog boxes, dialog presenters need a mapping between view model and the dialog presenter responsible for displaying the view, and as before, we can achieve that in our XAML resources by declaring instances of the presenters keyed to the type of their corresponding view models:
<local:MessageBoxPresenter x:Key="{x:Type vm:MessageBoxViewModel}" />
<local:OpenFileDialogPresenter x:Key="{x:Type vm:OpenFileDialogViewModel}" />
Creating a 3rd party dialog is now no different than it is for creating modeless and modal dialog boxes: simply create an instance of the relevant view model and add it to the Dialogs
collection.
Unit Testing
One of the major advantages of MVVM is the ease with which it allows us to unit test not just our business logic but also our front-end behaviour without actually having to create the views themselves. Implementing commercial-quality unit testing is beyond the scope of this article but the demo project provided with this article does include basic tests for the behaviour of both the modeless and modal dialog boxes created in the main application.
To see how unit-testing works with this system, it will help to pick a single test and walk through it line by line. Unit tests are typically designed to test a single aspect of behaviour per test, hence why they’re called “unit” tests. The test I have chosen has been designed to test the behaviour of a dialog box that appears when the user tries to close a modeless dialog box by pressing the Cancel button. When this occurs, we want the modeless dialog box to pop up a Message Box asking the user for confirmation that they really do want to close the modeless dialog box, and it is this behaviour that we will test. We begin by declaring the test function:
[TestMethod]
[Description("Tests that clicking the 'Cancel' button on the modeless dialog
requests confirmation from the user that they actually want to close the dialog.")]
public void Modeless_Cancel_Requests_Confirmation()
{
}
The set-up code for this test needs to create an instance of our main view model along with a modeless dialog box:
var mainViewModel = new MainViewModel();
mainViewModel.NewModelessDialogCommand.Execute(null);
var dlg = mainViewModel.Dialogs[0] as CustomDialogViewModel;
In real-world unit-testing, we generally wouldn’t create a full-blown instance of our MainViewModel
class like this. What we would most likely do instead is create a mocked object that provides only the functionality needed to cause the modeless dialog to appear and we would obtain an instance to this mocked object using a dependency injection framework or something similar. The view models in this demo, however, are very simplistic so we can work with them directly.
The code above should be fairly self-explanatory: we create a view model and then execute the NewModelessDialogCommand
by calling its Execute
function directly, thus simulating what happens when the user clicks on a button whose Command
property has been bound to it. Our MainViewModel
should respond by creating an instance of CustomDialolgViewModel
and adding it to its Dialogs
collection, but another test in the project tests for that so in this test, we can assume it’s there.
The next part of the test contains our action. In this test, we’re testing what happens when the user presses the Cancel button, so we simulate that with a direct call to the CancelCommand
’s Execute
method:
dlg.CancelCommand.Execute(null);
If our view model code is working correctly, then we should now have created two dialog boxes: the first should be the modeless dialog that we created in the setup code while the second should be a MessageBox
asking the user for confirmation. We won’t actually see these dialogs of course because our unit testing framework doesn’t create views, but the view models representing those dialogs should be present in the main view model’s dialog collection. All that remains is for us to check that an appropriate MessageBoxViewModel
has in fact been added with the expected property values set:
mainViewModel.Dialogs.Count.ShouldEqual(2);
var msg_dlg = mainViewModel.Dialogs[1] as MessageBoxViewModel;
msg_dlg.ShouldNotBeNull();
msg_dlg.Caption.ShouldEqual("Closing");
msg_dlg.Message.ShouldEqual("Are you sure you want to close this window?");
msg_dlg.Buttons.ShouldEqual(MessageBoxButton.YesNo);
msg_dlg.Image.ShouldEqual(MessageBoxImage.Question);
Practical Application of MVVM Dialog Boxes
In this section, I will summarize everything discussed in the article and show how to add dialog box support to any MVVM application.
First, you will need to add the MvvmDialogs
project or its files to your own solution in order to get the various interfaces along with the behaviour class. Next, you will need to add an ObservableCollection<IDialogViewModel>
to your main view model which will be responsible for maintaining a collection of dialog box view models:
private ObservableCollection<IDialogViewModel> _Dialogs =
new ObservableCollection<IDialogViewModel>();
public ObservableCollection<IDialogViewModel> Dialogs { get { return _Dialogs; } }
Finally, you will need to add the DialogBehaviour.DialogViewModels
attached behaviour to your main window class and bind it to your Dialogs
collection:
dlgs:DialogBehavior.DialogViewModels="{Binding Dialogs}"
We are now ready to start creating and displaying dialog boxes. The most minimalistic custom dialog box we can create is a class that inherits IUserDialogViewModel
:
public class MinimalDialogViewModel : IUserDialogViewModel
{
public virtual bool IsModal { get { return true; } }
public virtual void RequestClose() { this.DialogClosing(this, null); }
public virtual event EventHandler DialogClosing;
}
In this minimal example, we want the dialog itself to be modal, and if anything requests it to close (i.e., the user clicking the Close button on the caption bar) then we raise the DialogClosing
event to actually cause the window to be closed. Making these properties virtual isn’t a requirement but it is useful if we want this class to form the base for all other dialogs in our application and to allow any derived classes to override this default behaviour.
With the view model logic out of the way, we need to create a corresponding Window for the view that will display the dialog box on screen:
<Window x:Class="MvvmDialogs.Demo.View.MinimalDialogBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Minimal Dialog Box" Height="160" Width="300">
</Window>
Last of all, we need some way of mapping the view model to this view. When working with child controls, we typically use Data Templates to do this but since we’re dealing with a top level window, we instead create a resource of the Window
type, decorate it with the x:Shared
attached property and use the view model type as the key:
<view:MinimalDialogBox x:Key="{x:Type vm:MinimalDialogViewModel}" x:Shared="False" />
We have now created a minimalist dialog box that can be displayed at any time by simply adding an instance of the view model to the Dialogs
collection in our main view model:
this.Dialogs.Add(new MinimalDialogViewModel());
Displaying system dialog boxes (MessageBox
, etc.) involves a similar mechanism but requires the creation of a dialog presenter instead of a window. This class needs to inherit the IDialogPresenter<>
generic and implement the “Show
” property to invoke the dialog box using the parameters in the view model. The demo application provided with this article shows an example of using an IDialogPresenter
to display message boxes:
public class MessageBoxPresenter : IDialogBoxPresenter<MessageBoxViewModel>
{
public void Show(MessageBoxViewModel vm)
{
vm.Result = MessageBox.Show(vm.Message, vm.Caption, vm.Buttons, vm.Image);
}
}
As per regular dialog boxes, we need some way to associate this class with its corresponding view model, and we again achieve that with a global resource that uses the view model type for the key:
<local:MessageBoxPresenter x:Key="{x:Type vm:MessageBoxViewModel}" />
(Note that the x:Shared
property is not required for dialog presenters). With this mapping in place, we can now easily invoke message boxes from anywhere in our application:
this.Dialogs.Add(new MessageBoxViewModel { Message = "Hello World!" });
We can make this design slightly more amenable to method cascading with the addition of a Show()
method. The following example demonstrates this being used to display a File Open dialog box and to report the result back to the user with a message box:
var dlg = new OpenFileDialogViewModel {
Title = "Select a file (I won't actually do anything with it)",
Filter="All files (*.*)|*.*",
Multiselect=false
};
if (dlg.Show(this.Dialogs))
new MessageBoxViewModel {
Message = "You selected the following file: " + dlg.FileName + "."
}.Show(this.Dialogs);
else
new MessageBoxViewModel { Message = "You didn't select a file." }.Show(this.Dialogs);
In a real world application, you would probably want to replace the new operator with a call to a dependency injection framework to create the instance for you, the framework would then also be responsible for injecting the dialog collection into the view models so that you don’t have to pass it as a parameter into the Show
methods yourself.
The final example in this article shows different dialog boxes working in conjunction with each other. The behaviour class in the MVVM dialog library has been designed to allow new dialogs to be created whilst others are in the process of being closed. This snippet utilizes that feature to display a modal dialog box that pops up a confirmation message box if the user tries to close it via the close button on the caption bar. The parent modeless dialog box will only close if the user selects “OK” in the confirmation box:
new CustomDialogViewModel {
Message = "Hello World!",
Caption = "Modal Dialog Box",
OnCloseRequest = (sender) =>
{
if (new MessageBoxViewModel {
Caption = "Closing",
Message = "Are you sure you want to close this window?",
Buttons = MessageBoxButton.YesNo,
Image = MessageBoxImage.Question
}.Show(this.Dialogs) == MessageBoxResult.Yes)
sender.Close();
}
}.Show(this.Dialogs);
History
- 18th September, 2014: Initial version