Introduction
What always seems to be an issue when trying to stay as much as possible with the MVVM pattern when creating WPF applications is closing a window, and communicating the closing of a window to the ViewModel
. In a previous project we used MVVMLite Messenger
to do functions like this (I feel that we overused the Messenger)
. When starting a new project (not really starting since there was a lot of code base already created), I had the issue again. After some thought, I decided to investigate using a behavior to support this interface between the View
and the ViewModel
. I have always rolled my own behaviors, and have not used any of the support for behaviors provided by any of the frameworks, including Expression Blend. I just have never found a case where I thought that the infrastructures provided anything that was really needed for the problem that I needed to solve with a behavior. In most cases, all you need is a single DependencyProperty
, and use of specialized behaviors limits the use to projects that have the framework.
The first part is actually pretty easy—the communication of the window closing to the ViewModel
. This is required only if the user can close a window without interaction with the ViewModel
and the ViewModel
needs to know that the Window
has closed. This is easily enough handled using the ICommand
interface. The behavior has a DependencyProperty
of type ICommnad
. The nice thing about using the ICommand
interface is that it is possible to check the CanExecute
method of the ICommand
, and cancel the close if it is false
.
The second part, and most important, is communicating to the View
from the ViewModel
that the Window
Close
method should be executed. There is no direct way to do this, but a property can be used to signal that the Close
method of the Window
should be executed. This can be done with a bool
type. What is needed is reset this flag afterwards in case the same ViewModel
is used again with another window. At least this should be the case for this behavior since it the flexibility of the behavior should not be limited without good reason.
The behavior also supports the Nullable<bool>
DialogResult
. The reason I do this is because sometimes it is convenient to return a flag to indicate if a change was saved whe the dialog is something like an edit dialog, or it is UI window has a yes/no or OK/cancel result. This way the method that causes the display of the dialog can be put in an if statement and so special flag has to be checked later. To support this, the behavior uses a Nullable<bool>
to communicate to the behavior a request to close the window—null
is the initial state, and the bound value is changed to either true
or false
to set the DialogResult
of the window. After the behavior process the change of the close request, it sets the dialog result to that value, and sets the close request flag back to null so that another close request can be made.
Since this behavior can also be used for a window that is not opened with the ShowDialog
method, there is a problem since the DialogResult
cannot be set when the window is opened with the Show
method—It causes an exception. One easy way to check for this is check if the Owner
property is not null
before setting the DialogResult
.
An unusual feature that this behavior does not have to be associated with a Window
, but can be applied to a FrameworkElement
contained within a Window
, including a UserControl
. If the FrameworkElement
is not a Window
, the VisualTreeHelper
is used to find the parent Window
. The reason this was done was to improve visibility in what is happening. In my project there are some standard windows that are used as containers for UserContorls that provide the specific functionality. My thought was that if the behavior was specified in the window, a future programmer may have more difficulty working with the code than if the behavior was UserControl
.
The Close Request
The DependencyProperty
for the signal to close the window and set the DialogResult
is of type object:
public static readonly DependencyProperty CloseRequestProperty =
DependencyProperty.RegisterAttached("CloseRequest", typeof(object),
typeof(WindowCloseCommandBehaviour),
new FrameworkPropertyMetadata(new object(),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnCloseRequestChanged));
The reason for this is to have an initial value that can check that the ViewModel
property binding to this is initially null
. It is not, then can force it to null
. This is because if the ViewModel
is used multiple times for a different window, the property will be the last set value of true
or false
, and not null
. It would be possible to make the ViewModel
responsible, but butter this behavior handle the.
Also, needed to have two way binding to allow the behavior to force the bound property to an initial value of null, so the DependencyProperty
is set to have a default of two way binding.
The following is the callback code for the DependencyProperty
:
private static void OnCloseRequestChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
Debug.Assert(e.NewValue == null || e.NewValue is bool?,
"Close Request must be nullable bool");
if (e.OldValue == null && e.NewValue != null)
{
var newValue = (bool?)e.NewValue;
sender.SetValue(CommandProperty, null);
var window = sender as Window ?? sender.FindParent<Window>();
if (window?.Owner != null && window?.DialogResult != newValue)
window.DialogResult = newValue;
window?.Close();
}
else if (e.OldValue != null && e.NewValue != null)
{ sender.SetValue(CloseRequestProperty, null);
}
}
Initially there is a Debug
Assert
that forces the user to bind with a Nullable<bool>
value. First the CloseRequest
is reset back to null
before the Window
is closed so that the bound value is reset back to null
in case the ViewModel
is reused. Next the old and new values are checked to be null
and not null
respectively (the new value will have to be Boolean true
or false
because of the Assert
). If this is the case, then the DialogResult
forced to be set to the same value as the CloseRequestProperty
if the Window
Owner
is not null
(to execute the ShowDialog
method, the Owner
property has to be set). Then if the new value is not null
and the Close
was not executed, the DependencyProperty
is set to null. I did not get the immediate changing of this property to null to work, and that may be because the window has been closed. The initial forcing to null
may be best since this requires less of the ViewModel
.
The Close Command
The CloseCommand
uses a pretty standard DependencyProperty
of type ICommand
with a callback:
private static void OnCommandChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
if (sender is Window)
{
SetOnClosing(sender, e, (Window) sender);
}
else
{
var frameworkElement = sender as FrameworkElement;
Debug.Assert(frameworkElement != null,
"Did not find FrameworkElement for control " +
sender.GetType().Name);
frameworkElement.Loaded += (s, arg) =>
{
var window = sender as Window ?? sender.FindParent<Window>();
Debug.Assert(window != null,
"Did not find window for control " + sender.GetType().Name);
SetOnClosing(sender, e, window);
};
}
}
One of the things that can be seen is that if the sender
is a Window
, can immediately call the SetOnClosing
method, an attempt will be made to find the Window
after the FrameworkElement
is loaded and then the SetOnClosing
method can be called.
The SetOnCLosing
method sets a method to observe the Closing
event, and also set a DependencyProperty
for the Window
control to maintain a reference to the Control
with the behavior so that the Window
has access to the DependencyProperties
of the Control
that has the behavior. This is required to get the ICommand
to execute.
private static void SetOnClosing(DependencyObject sender,
DependencyPropertyChangedEventArgs e, Window window)
{
SetOriginalControl(window, sender);
window.Closing -= OnClosing;
if (e.NewValue is ICommand) window.Closing += OnClosing;
}
The handler for the Closing
event then has to get a reference to the Control
with the behavior to get the ICommand reference. If the ICommand reference is null, then that would mean that the window has been closed, which causes the DependencyProperty
to been reset to null
. If the ICommand
is not null then can check if the ICommand
is enabled before continuing:
private static void OnClosing(object sender, System.ComponentModel.CancelEventArgs e)
{
var window = (Window)sender;
var originalControl = GetOriginalControl(window) as DependencyObject;
var command = GetCommand(originalControl);
if (command == null) return; since the window has been closed =>
if (command.CanExecute(closeRequest)) command.Execute(closeRequest);
else e.Cancel = true;
}
If the ICommand
is disabled, then the CancelEventArgs
Cancel
property is set to false
, otherwise, execute the ICommand
and the window will automatically close.
Using the Behavior
The behavior can probably be applied to any control, but I have only used it with a Window
and a UserControl
. Also either the CloseRequest
or the CloseRequest
and CloseCommand
can be set. I suspect that the CloseCommand
can be used alone, but again, I have not tested this case. Here is an example of both being set:
<UserControl x:Class="CloseBehaviorWPF.MainUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:closeBehaviorWpf="clr-namespace:CloseBehaviorWPF"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
closeBehaviorWpf:WindowCloseCommandBehaviour.CloseRequest="{Binding CloseWindow}"
closeBehaviorWpf:WindowCloseCommandBehaviour.Command="{Binding ClosingCommand}"
mc:Ignorable="d">
<!—Content-->
</UserControl>
Sample
The sample has a main window that has two buttons to open two different windows: one with a View
and ViewModel
that uses the behavior’s CloseCommand
and the CloseRequest
, and one that only behavior’s CloseRequest
. The child window with the binding to the CloseCommand
has a CheckBox
that needs to be checked to allow the child window to be closed. The buttons labeled with the X and Close both use an event that triggers the Window
Close
method to close the window. In addition, the main window also uses the behavior to close the main window. The main window also is able to know that the child windows are closed, and if the DialogResult
when the window is closed. A MessageBox
is displayed by the main window in response to this DialogResult
.
History
02/02/2016: Initial version