Introduction
This article provides a simple solution for opening and closing a dialog that displays the implicit DataTemplate
of a ViewModel
. Opening dialogs when you have structured your solution using a Model-View-Presenter/Model-View-ViewModel pattern can be awkward. When you have button click handlers all over in the code-behind files you don't have this problem, but when you are striving to make your application driven only through commands and bindings, this becomes an issue. Because you want loose coupled layers, separation of concerns, etc. So code like this in the ViewModel
would look really ugly:
private ICommand displayInvoice;
public ICommand DisplayInvoice
{
get
{
return displayInvoice ?? (displayInvoice = new DelegateCommand(
a =>
{
var dialog = new Window();
var invoiceViewModel = new InvoiceViewModel();
dialog.DataContext = invoiceViewModel;
dialog.Content = invoiceViewModel;
dialog.ShowDialog();
if (invoiceViewModel.IsApproved)
{
}
}));
}
}
The UtilityDialog
There are a lot of ways of doing things in WPF. Simplifying a little bit, there are two main approaches for the UI creation: you can build your entire visual tree using tight coupled controls, and after that, set DataContext
s for all the nodes in the tree that require so; at the other end, you only set a DataContext
for your root control (usually the main Window
) and rely on implicit DataTemplate
s from there to work their magic. I tend to use the latter approach and this article is better suited for this. In this way, you can have a single window for all your 'dialog needs', as long as what you need to display is a View for a specific ViewModel
. I use a UtilityDialog
control, which is actually a Window
with some properties set, so it is styled like a ToolWindow
(but you could change this, or even have one for each type of dialog you need). When I want to display a dialog, I simply create a new instance of this window and set its DataContext
to a ViewModel
which has an implicit DataTemplate
defined somewhere in the resource scope. The Content
of the UtilityDialog
binds to the DataContext
so the template will be loaded and displayed. This eliminates having a Window
for every ViewModel
that needs one. The only thing left to figure out is how to open/close this dialog.
A First Approach
I used to handle this by raising a plain C# event, combined with an attached behaviour. All the ViewModel
s that have to open a dialog would need implement a simple interface:
public interface IDialogOwner
{
event EventHandler<RequestOpenDialogEventArgs> RequestOpenDialog;
}
public sealed class RequestOpenDialogEventArgs : EventArgs
{
public RequestOpenDialogEventArgs(object dialogDataContext)
{
DialogDataContext = dialogDataContext;
}
public object DialogDataContext
{
get;
private set;
}
}
Suppose we want to display an invoice form to a customer and let him agree or disagree with the payment terms, and we want to do this using a modal dialog. We have a nice dedicated ViewModel
for this, with commands for approve and reject:
public sealed class InvoiceViewModel : IRemovable
{
public bool IsApproved
{
get;
private set;
}
private ICommand approve;
public ICommand Approve
{
get
{
return approve ?? (approve = new DelegateCommand(
a =>
{
IsApproved = true;
RequestRemove(this, EventArgs.Empty);
}));
}
}
private ICommand reject;
public ICommand Reject
{
get
{
return reject ?? (reject = new DelegateCommand(
a =>
{
IsApproved = false;
RequestRemove(this, EventArgs.Empty);
}));
}
}
public event EventHandler RequestRemove = delegate { };
}
The approving or rejecting is done through ICommand
s, so we would bind these to ICommandSource
objects (like Button
s) in order to approve/reject the invoice. Once one of these commands is invoked, we also want to close the dialog. To avoid code-behind we have to introduce yet another interface, for ViewModel
s that are displayed using dialogs and which need to close:
public interface IRemovable
{
event EventHandler RequestRemove;
}
Displaying the invoice requires raising the RequestOpenDialog
event:
public sealed class MainViewModel : IDialogOwner
{
private ICommand displayInvoice;
public ICommand DisplayInvoice
{
get
{
return displayInvoice ?? (displayInvoice = new DelegateCommand(
a =>
{
var invoice = new InvoiceViewModel();
RequestOpenDialog(this, new RequestOpenDialogEventArgs(invoice));
if (invoice.IsApproved)
{
}
}));
}
}
public event EventHandler RequestOpenDialog = delegate { };
}
The two events used in our ViewModel
s - RequestRemove
and RequestOpenDialog
are monitored by two attached behaviours (since it is such a nice-to-have feature these days). They are implemented by taking advantage of the Blend's System.Windows.Interactivity
assembly, but you can do it as well in the 'traditional' way as advertised by the original creator of the pattern, John Gossman. These behaviours must be set on the views associated with the ViewModel
s that raise the events (in this case, on the main window and on the UtilityDialog
).
public sealed class CloseWindowBehavior : Behavior<Window>
{
protected override void OnAttached()
{
base.OnAttached();
EventHandler closeWindow = (a, b) => AssociatedObject.Close();
Action<object> hookRequestRemove = (a) =>
{
var removable = a as IRemovable;
if (removable != null)
{
removable.RequestRemove += closeWindow;
}
};
Action<object> unhookRequestRemove = (a) =>
{
var removable = a as IRemovable;
if (removable != null)
{
removable.RequestRemove -= closeWindow;
}
};
hookRequestRemove(AssociatedObject.DataContext);
AssociatedObject.DataContextChanged += (a, b) =>
{
unhookRequestRemove(b.OldValue);
hookRequestRemove(b.NewValue);
};
}
}
CloseWindowBehavior
handles the DataContextChanged
event for the associated window. When this is raised, if the new DataContext
is an IRemovable
, a handler is added for the RequestRemove
event. This handler closes the associated window.
public sealed class OpenDialogBehavior : Behavior<Window>
{
protected override void OnAttached()
{
base.OnAttached();
EventHandler<RequestOpenDialogEventArgs> openDialog = (a, b) =>
{
var dialog = new UtilityDialog();
dialog.DataContext = b.DialogDataContext;
dialog.Owner = AssociatedObject;
dialog.ShowDialog();
};
Action<object> hookRequestOpenDialog = (a) =>
{
var dialogOwner = a as IDialogOwner;
if (dialogOwner != null)
{
dialogOwner.RequestOpenDialog += openDialog;
}
};
Action<object> unhookRequestOpenDialog = (a) =>
{
var dialogOwner = a as IDialogOwner;
if (dialogOwner != null)
{
dialogOwner.RequestOpenDialog -= openDialog;
}
};
hookRequestOpenDialog(AssociatedObject.DataContext);
AssociatedObject.DataContextChanged += (a, b) =>
{
unhookRequestOpenDialog(b.OldValue);
hookRequestOpenDialog(b.NewValue);
};
}
}
OpenDialogBehavior
also monitors the DataContextChanged
event of its associated window. When this is raised, a handler is added for the RequestOpenDialog
event (provided that the DataContext
is a valid IDialogOwner
entity). This handler creates a new instance of the UtilityDialog
control, sets its DataContext
to be the ViewModel
received through the event arguments (an InvoiceViewModel
object in this case) and displays the dialog.
Have you spotted the problems already? We're going through all this pain and create interfaces and attached behaviors and raise events in order to decouple the ViewModel
from the View. But we haven't achieved this. We simply moved the code from one place to another (and added additional code along the way) but the ViewModel
is still 100% coupled to the View. It may not instantiate a Window
object inside the command's execute delegate, but:
- It knows that it opens a dialog, which is a View detail and shouldn't concern the
ViewModel
: RequestOpenDialog
- the name says it all.
- In this particular case, it abuses the fact that it knows that the dialog will be opened as modal (and this is a mistake I made throughout an entire project, because I never needed non-modal dialogs) so the code to handle the state of the
invoice
object is written immediately after the open request, because since the dialog will be displayed as modal, the method will halt execution and wait.
- What if in another View for the same
ViewModel
(maybe another skin), we don't want to open a dialog? Maybe we want a custom control to come into view bouncing around in the same window. This means we will need to modify the ViewModel
to handle that additional logic.
The Hidden Gem
At first sight, the FrameworkElement
does not make very much sense in a XAML file. We use it in code for polymorphism mainly, but in XAML it doesn't seem much helpful since it does not have a visual appearance. Still, some of the most interesting tricks in XAML-only scenarios I know of are based on the FrameworkElement
, and the fact that it does not actually have a visual template is exactly what makes it valuable.
- Take a look at these examples by Charles Petzold: AllXamlClock.xaml (example, article), AnimatedSpiral1.xaml (example, article) and WindDownPendulum.xaml (example, article). These require no code and you can load them right away in an XAML-ready app (like IE or Kaxaml) to see them in action. In the first example, he uses a
FrameworkElement
in order to store in its Tag
property the current time (the system time at the point when the XAML is loaded). Then he applies some clever transforms on this initial value in order to make the clock spin. In the other two examples, the FrameworkElement
hosts some compound transforms which are used to define a spiral-like movement of an object and, respectively, to mimic a pendulum's friction with air, which slows it down gradually. Pretty slick!
- Josh Smith's VirtualBranch takes advantage of the
FrameworkElement
in order to provide data to objects that are not part of the visual tree (and therefore cannot use bindings because they do not inherit the DataContext
).
- Dr. WPF couldn't miss this list. Among the plenitude of WPF advices the good doctor gives to his patients there is the 'attached dependency property theft'... sorry I meant borrowing scheme, when you use an attached dependency property from a built-in framework type in an XAML-only scenario, for testing a proof of concept of some sort and you need one or two extra properties (ok, so there are not that many useful
FrameworkElement
attached properties you can borrow, but the technique is worth being remembered).
With all these in mind, the solution to our dialog problem becomes stupid simple. Take a look at this piece of XAML:
<FrameworkElement DataContext="{Binding Path=CurrentInvoice}">
<i:Interaction.Behaviors>
<w:DialogBehavior DisplayModal="True"/>
</i:Interaction.Behaviors>
</FrameworkElement>
Instead of using an attached behaviour that monitors events raised by specific interface implementors, we could monitor instead the DataContextChanged
event of the invisible FrameworkElement
. When this is set to a non-null
object, we display a dialog with the content set to this object. When the DataContext
becomes null
, we close the dialog.
public sealed class DialogBehavior : Behavior<FrameworkElement>
{
private static Dictionary<object, UtilityDialog> mapping =
new Dictionary<object, UtilityDialog>();
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.DataContextChanged += (a, b) =>
{
if (b.NewValue != null)
{
var dialog = new UtilityDialog();
mapping.Add(b.NewValue, dialog);
dialog.DataContext = b.NewValue;
dialog.Owner = AssociatedObject as Window ??
Application.Current.MainWindow;
if (DisplayModal)
{
dialog.ShowDialog();
}
else
{
dialog.Show();
}
}
else if (mapping.ContainsKey(b.OldValue))
{
var dialog = mapping[b.OldValue];
mapping.Remove(b.OldValue);
dialog.Close();
dialog.DataContext = null;
dialog.Owner = null;
}
};
}
}
The ViewModel
need not implement any interface, or care that it opens a dialog. Its only responsibility in this case is to manage the lifetime of its logically subordinated ViewModel
s - once brought into the ecosystem, it is the UI's concern what to do with the associated View of a specific ViewModel
and in what way (if any) to display it.
public sealed class MainViewModel : INotifyPropertyChanged
{
private InvoiceViewModel currentInvoice;
public InvoiceViewModel CurrentInvoice
{
get
{
return currentInvoice;
}
set
{
if (currentInvoice != value)
{
currentInvoice = value;
PropertyChanged(this, new PropertyChangedEventArgs("CurrentInvoice"));
}
}
}
private ICommand displayInvoice;
public ICommand DisplayInvoice
{
get
{
return displayInvoice ?? (displayInvoice = new DelegateCommand(
a => CurrentInvoice = new InvoiceViewModel()));
}
}
}
So the DisplayInvoice
command simply instantiates the CurrentInvoice
property (the property that our invisible FrameworkElement
binds to). And it stops there. We don't know how the view will be displayed, as a dialog or not, modal or modeless. This enforces the processing logic to be placed somewhere else, which is a good thing. Now, we need a mechanism for the ViewModel
s to communicate - the invoice has to tell the main ViewModel
that the user has finished with it. You can rely on plain old CLR events again, but as your system will grow and every ViewModel
needs to know about a lot of other ViewModel
s, it becomes very hard to maintain the entire relationship structure. So you might consider some sort of Mediator (like Josh Smith's from the MVVM Foundation, or Prism's EventAggregator
). The sample uses a very simple custom made EventAggregator
, that you should never use in production code.
public sealed class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
EventAggregator<InvoiceReviewedEvent>.Subscribe(ProcessInvoice);
}
public void ProcessInvoice()
{
if (CurrentInvoice.IsApproved)
{
}
CurrentInvoice = null;
}
}
public sealed class InvoiceViewModel
{
public ICommand Approve
{
get
{
return approve ?? (approve = new DelegateCommand(
a =>
{
IsApproved = true;
EventAggregator<InvoiceReviewedEvent>.Broadcast();
}));
}
}
}
When the invoice ViewModel
is done, it signals this by broadcasting on a specific channel. The main ViewModel
subscribes to such messages, and handles them in the ProcessInvoice
method. If the invoice has been accepted, it does something significant with it. At the end, it sets CurrentInvoice
to null
- this object did its job, it is no longer useful. The attached behavior captures this change and closes the dialog. With this setup in place, if in another skin we don't want a dialog but the invoice to be displayed on the same window, docked to the left, we simply do:
<ContentControl Content="{Binding Path=CurrentInvoice}" DockPanel.Dock="Left"/>
without having to modify anything in the
ViewModel
. That's pretty much it. Let the View decide!
History
- 15th March, 2010 - Original article