Click here to Skip to main content
14,981,770 members
Articles / Desktop Programming / WPF
Article
Posted 2 Feb 2016

Stats

22.8K views
500 downloads
17 bookmarked

Behavior to support Window Closing in WPF

Rate me:
Please Sign up or sign in to vote.
5.00/5 (8 votes)
2 Feb 2016CPOL7 min read
A problem with the WPF MVVM design pattern is closing a window from the ViewModel. This article presents a method that allows the use of dependency properties to be used to control the closing of a window

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:

XML
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:

C#
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)
       { //if did not find Window, then DataContext has changed
             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:

C#
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.

C#
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:

C#
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 =>
// so command has been set back to null
       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:

XML
<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

Image 1

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

License

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

Share

About the Author

Clifford Nelson
Software Developer (Senior) Clifford Nelson Consulting
United States United States
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last eight years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with BioNano Genomics in San Diego, CA redesigning their UI for their camera system. he can be reached at qck1@hotmail.com.

Comments and Discussions

 
Question5 - Nicely Done Pin
Kevin Marois3-Feb-16 10:07
professionalKevin Marois3-Feb-16 10:07 
QuestionWell written Pin
Bob Ranck3-Feb-16 9:03
MemberBob Ranck3-Feb-16 9:03 

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.