Handling the window closing event with WPF / MVVM Light Toolkit
Asked Answered
I

14

159

I'd like to handle the Closing event (when a user clicks the upper right 'X' button) of my window in order to eventually display a confirm message or/and cancel the closing.

I know how to do this in the code-behind: subscribe to the Closing event of the window then use the CancelEventArgs.Cancel property.

But I'm using MVVM so I'm not sure it's the good approach.

I think the good approach would be to bind the Closing event to a Command in my ViewModel.

I tried that:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <cmd:EventToCommand Command="{Binding CloseCommand}" />
    </i:EventTrigger>
</i:Interaction.Triggers>

With an associated RelayCommand in my ViewModel but it doesn't work (the command's code is not executed).

Inwardly answered 10/9, 2010 at 9:18 Comment(4)
Also interested in nice answer to answer to this.Rataplan
I downloaded the code from codeplex and debugging it revealed: "Unable to cast object of type 'System.ComponentModel.CancelEventArgs' to type 'System.Windows.RoutedEventArgs'." It works fine if you don't want the CancelEventArgs but that doesn't answer your question...Catadromous
I'm guessing your code doesn't work because the control you attached your trigger to doesn't have a Closing event. Your data context is not a window...It's probably a data template with a grid or something, which has no Closing event. So dbkk's answer is the best answer in this case. However, I prefer the Interaction/EventTrigger approach when the event is available.Newfeld
The code you have will work fine on a Loaded event, for example.Newfeld
A
151

I would simply associate the handler in the View constructor:

MyWindow() 
{
    // Set up ViewModel, assign to DataContext etc.
    Closing += viewModel.OnWindowClosing;
}

Then add the handler to the ViewModel:

using System.ComponentModel;

public void OnWindowClosing(object sender, CancelEventArgs e) 
{
   // Handle closing logic, set e.Cancel as needed
}

In this case, you gain exactly nothing except complexity by using a more elaborate pattern with more indirection (5 extra lines of XAML plus Command pattern).

The "zero code-behind" mantra is not the goal in itself, the point is to decouple ViewModel from the View. Even when the event is bound in code-behind of the View, the ViewModel does not depend on the View and the closing logic can be unit-tested.

Austreng answered 24/5, 2012 at 12:4 Comment(9)
I like this solution: just hook into a hidden button :)Rootstock
For mvvm beginners not using MVVMLight and searching for how to inform the ViewModel about the Closing event, the links how to set up the dataContext correctly and how to get the viewModel object in the View may be interesting. How to get a reference to the ViewModel in the View? and How do I set a ViewModel on a window in xaml using datacontext property ...It took me several hours, how a simple window closing event could be handled in the ViewModel.Monastery
I love this elegant solution. It worked fabulously for me. Thanks!Tamaratamarack
This solution is irrelevant in MVVM environment. The code behind shouldn't know about the ViewModel.Electromotive
@Electromotive I think the problem is more that you get a form event handler in your ViewModel, which couples the ViewModel to a specific UI implementation. If they're going to use code behind, they should check CanExecute and then call Execute() on an ICommand property instead.Barely
@Electromotive The code-behind can know about the ViewModel members just fine, just ike the XAML code does. Or what do you think you are doing when you create a Binding to a ViewModel property? This solution is perfectly fine for MVVM, as long as you don't handle the closing logic in the code-behind itself, but in the ViewModel (though using an ICommand, like EvilPigeon suggests, could be a good idea since you can also bind to it)Haddington
This is a fairly clean solution however it isn't as clean as the author suggests. It's not 5 extra lines for the same end result. With using DataBinding you gain the ability to change the ViewModel on the fly. This solution does not handle that scenario. If you wanted to expand this solution to handle that, you would want to add a property change handler to the DataContextProperty or alternatively add a DataContextChanged handler. Either way, In that handler you would need to unassign the event handler and assign it to the DataContext.Sp
This is simplest solution that does not violate MVVM pattern and does not makes things unnecessarily complicated.Goosefoot
This answer got me pointed in the right direction. Perhaps this may make it a bit clearer for someone new to C# (as I): Window statsWindow = new Window(); statsWindow.Closing += OnStatsWindowClosing; Then add the handler: public void OnStatsWindowClosing(object sender, CancelEventArgs e) { Console.WriteLine("Here in closing event"); }Basilica
J
84

This code works just fine:

ViewModel.cs:

public ICommand WindowClosing
{
    get
    {
        return new RelayCommand<CancelEventArgs>(
            (args) =>{
                     });
    }
}

and in XAML:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <command:EventToCommand Command="{Binding WindowClosing}" PassEventArgsToCommand="True" />
    </i:EventTrigger>
</i:Interaction.Triggers>

assuming that:

  • ViewModel is assigned to a DataContext of the main container.
  • xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.SL5"
  • xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Jens answered 4/11, 2010 at 5:14 Comment(10)
Forgot: to get event arguments in command use PassEventArgsToCommand="True"Jens
+1 simple and conventional approach. Would be even better to head over to PRISM.Yorktown
This is one scenario that highlights gaping holes in WPF and MVVM.Mackenzie
Is there any way to do it without Expression Blend?Mackenzie
@Damien, if it isn't too late, you can create the command using attached propertyCoseismal
It would be really helpful to mention what is i in <i:Interaction.Triggers> and how to get it.Viewpoint
@Chiz, it's a namespace you should declare in the root element like this: xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"Jens
In my opinion this should be the accepted answer to this question. He wanted to use the MVVM and with that in mind this is the most "MVVM way" to do it.Cheddar
And which implementation of EventToCommand is referenced here? An answer should be complete, or at the VERY least instruct users how to find the relevant bits. Do you use MVVM Light, Glue, Blend or one of the other implementations? What is the command namespace? This answer is half finished.Cracow
@Alex, it's about MvvmLight since it is listed in Tags. And the command namespace is actually xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.SL5"Jens
V
39

This option is even easier, and maybe is suitable for you. In your View Model constructor, you can subscribe the Main Window closing event like this:

Application.Current.MainWindow.Closing += new CancelEventHandler(MainWindow_Closing);

void MainWindow_Closing(object sender, CancelEventArgs e)
{
            //Your code to handle the event
}

All the best.

Vesting answered 6/12, 2011 at 10:11 Comment(9)
This is the best solution among the other mentioned in this issue. Thank you !Electromotive
This is what i was looking for. Thanks!Scarcely
Why then people use more cumbersome methods while there is such a straightforward approach? Thanks.Brigantine
... and this creates tight coupling between ViewModel and View. -1.Sternutatory
best answer for this!Fm
This is not the best answer. It breaks MVVM.Antung
Excellent, simple and elegant. Screw the MVVM nerds, this works without adding a ton of code to do one simple thing.Archiearchiepiscopacy
@Archiearchiepiscopacy It requires a hard reference to the main window, or whichever window it's being used for. It's much easier, but it does mean the view model is not decoupled. It's not a question of satisfying the MVVM nerds or not, but if the MVVM pattern has to be broken to make it work, there's no point in using it at all.Cracow
Creating a new delegate here is redundant, you can pass the function directly.Adjoint
E
25

Here is an answer according to the MVVM-pattern if you don't want to know about the Window (or any of its event) in the ViewModel.

public interface IClosing
{
    /// <summary>
    /// Executes when window is closing
    /// </summary>
    /// <returns>Whether the windows should be closed by the caller</returns>
    bool OnClosing();
}

In the ViewModel add the interface and implementation

public bool OnClosing()
{
    bool close = true;

    //Ask whether to save changes och cancel etc
    //close = false; //If you want to cancel close

    return close;
}

In the Window I add the Closing event. This code behind doesn't break the MVVM pattern. The View can know about the viewmodel!

void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    IClosing context = DataContext as IClosing;
    if (context != null)
    {
        e.Cancel = !context.OnClosing();
    }
}
Estrada answered 25/9, 2014 at 7:18 Comment(3)
Simple, clear, and clean. The ViewModel need not know specifics of the view, hence concerns stay separated.Monition
the context is always null !Delectable
@ShahidOd Your ViewModel needs to implement the IClosing interface, not just implement the OnClosing method. Otherwise the DataContext as IClosing cast will fail and return nullBanal
D
11

Geez, seems like a lot of code going on here for this. Stas above had the right approach for minimal effort. Here is my adaptation (using MVVMLight but should be recognizable)... Oh and the PassEventArgsToCommand="True" is definitely needed as indicated above.

(credit to Laurent Bugnion http://blog.galasoft.ch/archive/2009/10/18/clean-shutdown-in-silverlight-and-wpf-applications.aspx)

   ... MainWindow Xaml
   ...
   WindowStyle="ThreeDBorderWindow" 
    WindowStartupLocation="Manual">



<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <cmd:EventToCommand Command="{Binding WindowClosingCommand}" PassEventArgsToCommand="True" />
    </i:EventTrigger>
</i:Interaction.Triggers> 

In the view model:

///<summary>
///  public RelayCommand<CancelEventArgs> WindowClosingCommand
///</summary>
public RelayCommand<CancelEventArgs> WindowClosingCommand { get; private set; }
 ...
 ...
 ...
        // Window Closing
        WindowClosingCommand = new RelayCommand<CancelEventArgs>((args) =>
                                                                      {
                                                                          ShutdownService.MainWindowClosing(args);
                                                                      },
                                                                      (args) => CanShutdown);

in the ShutdownService

    /// <summary>
    ///   ask the application to shutdown
    /// </summary>
    public static void MainWindowClosing(CancelEventArgs e)
    {
        e.Cancel = true;  /// CANCEL THE CLOSE - let the shutdown service decide what to do with the shutdown request
        RequestShutdown();
    }

RequestShutdown looks something like the following but basicallyRequestShutdown or whatever it is named decides whether to shutdown the application or not (which will merrily close the window anyway):

...
...
...
    /// <summary>
    ///   ask the application to shutdown
    /// </summary>
    public static void RequestShutdown()
    {

        // Unless one of the listeners aborted the shutdown, we proceed.  If they abort the shutdown, they are responsible for restarting it too.

        var shouldAbortShutdown = false;
        Logger.InfoFormat("Application starting shutdown at {0}...", DateTime.Now);
        var msg = new NotificationMessageAction<bool>(
            Notifications.ConfirmShutdown,
            shouldAbort => shouldAbortShutdown |= shouldAbort);

        // recipients should answer either true or false with msg.execute(true) etc.

        Messenger.Default.Send(msg, Notifications.ConfirmShutdown);

        if (!shouldAbortShutdown)
        {
            // This time it is for real
            Messenger.Default.Send(new NotificationMessage(Notifications.NotifyShutdown),
                                   Notifications.NotifyShutdown);
            Logger.InfoFormat("Application has shutdown at {0}", DateTime.Now);
            Application.Current.Shutdown();
        }
        else
            Logger.InfoFormat("Application shutdown aborted at {0}", DateTime.Now);
    }
    }
Darrickdarrill answered 29/8, 2011 at 3:42 Comment(0)
D
10

The asker should use STAS answer, but for readers who use prism and no galasoft/mvvmlight, they may want to try what I used:

In the definition at the top for window or usercontrol, etc define namespace:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

And just below that definition:

<i:Interaction.Triggers>
        <i:EventTrigger EventName="Closing">
            <i:InvokeCommandAction Command="{Binding WindowClosing}" CommandParameter="{Binding}" />
        </i:EventTrigger>
</i:Interaction.Triggers>

Property in your viewmodel:

public ICommand WindowClosing { get; private set; }

Attach delegatecommand in your viewmodel constructor:

this.WindowClosing = new DelegateCommand<object>(this.OnWindowClosing);

Finally, your code you want to reach on close of the control/window/whatever:

private void OnWindowClosing(object obj)
        {
            //put code here
        }
Delphina answered 6/11, 2014 at 17:56 Comment(1)
This does not give access to the CancelEventArgs which is necessary to cancel the closing event. The object passed is the view model, which technically is the same view model that the WindowClosing command is being executed from.Coryza
C
4

I would be tempted to use an event handler within your App.xaml.cs file that will allow you to decide on whether to close the application or not.

For example you could then have something like the following code in your App.xaml.cs file:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    // Create the ViewModel to attach the window to
    MainWindow window = new MainWindow();
    var viewModel = new MainWindowViewModel();

    // Create the handler that will allow the window to close when the viewModel asks.
    EventHandler handler = null;
    handler = delegate
    {
        //***Code here to decide on closing the application****
        //***returns resultClose which is true if we want to close***
        if(resultClose == true)
        {
            viewModel.RequestClose -= handler;
            window.Close();
        }
    }
    viewModel.RequestClose += handler;

    window.DataContaxt = viewModel;

    window.Show();

}

Then within your MainWindowViewModel code you could have the following:

#region Fields
RelayCommand closeCommand;
#endregion

#region CloseCommand
/// <summary>
/// Returns the command that, when invoked, attempts
/// to remove this workspace from the user interface.
/// </summary>
public ICommand CloseCommand
{
    get
    {
        if (closeCommand == null)
            closeCommand = new RelayCommand(param => this.OnRequestClose());

        return closeCommand;
    }
}
#endregion // CloseCommand

#region RequestClose [event]

/// <summary>
/// Raised when this workspace should be removed from the UI.
/// </summary>
public event EventHandler RequestClose;

/// <summary>
/// If requested to close and a RequestClose delegate has been set then call it.
/// </summary>
void OnRequestClose()
{
    EventHandler handler = this.RequestClose;
    if (handler != null)
    {
        handler(this, EventArgs.Empty);
    }
}

#endregion // RequestClose [event]
Carrier answered 10/9, 2010 at 9:42 Comment(1)
Thanks for the detailed answer. However, I don't think that solves my problem: I need to handle the window closing when the user clicks the upper right 'X' button. It would be easy to do this in the code-behind (i'd just link the Closing event and set the CancelEventArgs.Cancel to true of false) but I'd like to do this in MVVM style. Sorry for the confusionInwardly
N
1

Basically, window event may not be assigned to MVVM. In general, the Close button show a Dialog box to ask the user "save : yes/no/cancel", and this may not be achieved by the MVVM.

You may keep the OnClosing event handler, where you call the Model.Close.CanExecute() and set the boolean result in the event property. So after the CanExecute() call if true, OR in the OnClosed event, call the Model.Close.Execute()

Nobility answered 28/9, 2010 at 7:59 Comment(0)
E
1

I haven't done much testing with this but it seems to work. Here's what I came up with:

namespace OrtzIRC.WPF
{
    using System;
    using System.Windows;
    using OrtzIRC.WPF.ViewModels;

    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        private MainViewModel viewModel = new MainViewModel();
        private MainWindow window = new MainWindow();

        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            viewModel.RequestClose += ViewModelRequestClose;

            window.DataContext = viewModel;
            window.Closing += Window_Closing;
            window.Show();
        }

        private void ViewModelRequestClose(object sender, EventArgs e)
        {
            viewModel.RequestClose -= ViewModelRequestClose;
            window.Close();
        }

        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            window.Closing -= Window_Closing;
            viewModel.RequestClose -= ViewModelRequestClose; //Otherwise Close gets called again
            viewModel.CloseCommand.Execute(null);
        }
    }
}
Em answered 6/10, 2010 at 2:54 Comment(1)
What will happen here in the scenario where the V-M wishes to cancel the closing?Yorktown
I
1

We use AttachedCommandBehavior for this. You can attach any event to a command on your view model avoiding any code behind.

We use it throughout our solution and have almost zero code behind

http://marlongrech.wordpress.com/2008/12/13/attachedcommandbehavior-v2-aka-acb/

Insensitive answered 7/10, 2010 at 11:59 Comment(0)
Y
1

Using MVVM Light Toolkit:

Assuming that there is an Exit command in view model:

ICommand _exitCommand;
public ICommand ExitCommand
{
    get
    {
        if (_exitCommand == null)
            _exitCommand = new RelayCommand<object>(call => OnExit());
        return _exitCommand;
    }
}

void OnExit()
{
     var msg = new NotificationMessageAction<object>(this, "ExitApplication", (o) =>{});
     Messenger.Default.Send(msg);
}

This is received in the view:

Messenger.Default.Register<NotificationMessageAction<object>>(this, (m) => if (m.Notification == "ExitApplication")
{
     Application.Current.Shutdown();
});

On the other hand, I handle Closing event in MainWindow, using the instance of ViewModel:

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{ 
    if (((ViewModel.MainViewModel)DataContext).CancelBeforeClose())
        e.Cancel = true;
}

CancelBeforeClose checks the current state of view model and returns true if closing should be stopped.

Hope it helps someone.

Yare answered 19/3, 2017 at 16:7 Comment(0)
S
1

I took inspiration from this post, and have adapted it into a library I'm building for my own use (but will be public located here: https://github.com/RFBCodeWorks/MvvmControls

While my approach does somewhat expose the View to the ViewModel via the 'sender' and 'eventargs' being passed to the handler, I used this approach just in case its needed for some other sort of handling. For example, if the handler was not the ViewModel, but was instead some service that recorded when windows were opened/closed, then that service may want to know about the sender. If the VM doesn't want to know about the View, then it simply doesn't examine the sender or args.

Here is the relevant code I've come up with, which eliminates the Code-Behind, and allows binding within xaml:

Behaviors:WindowBehaviors.IWindowClosingHandler="{Binding ElementName=ThisWindow, Path=DataContext}"
    /// <summary>
    /// Interface that can be used to send a signal from the View to the ViewModel that the window is closing
    /// </summary>
    public interface IWindowClosingHandler
    {
        /// <summary>
        /// Executes when window is closing
        /// </summary>
        void OnWindowClosing(object sender, System.ComponentModel.CancelEventArgs e);

        /// <summary>
        /// Occurs when the window has closed
        /// </summary>
        void OnWindowClosed(object sender, EventArgs e);

    }

    /// <summary>
    /// Attached Properties for Windows that allow MVVM to react to a window Loading/Activating/Deactivating/Closing
    /// </summary>
    public static class WindowBehaviors
    {
        #region < IWindowClosing >

        /// <summary>
        /// Assigns an <see cref="IWindowClosingHandler"/> handler to a <see cref="Window"/>
        /// </summary>
        public static readonly DependencyProperty IWindowClosingHandlerProperty =
            DependencyProperty.RegisterAttached(nameof(IWindowClosingHandler),
                typeof(IWindowClosingHandler),
                typeof(WindowBehaviors),
                new PropertyMetadata(null, IWindowClosingHandlerPropertyChanged)
                );

        /// <summary>
        /// Gets the assigned <see cref="IWindowLoadingHandler"/> from a <see cref="Window"/>
        /// </summary>
        public static IWindowClosingHandler GetIWindowClosingHandler(DependencyObject obj) => (IWindowClosingHandler)obj.GetValue(IWindowClosingHandlerProperty);

        /// <summary>
        /// Assigns an <see cref="IWindowClosingHandler"/> to a <see cref="Window"/>
        /// </summary>
        public static void SetIWindowClosingHandler(DependencyObject obj, IWindowClosingHandler value)
        {
            if (obj is not null and not Window) throw new ArgumentException($"{nameof(IWindowClosingHandler)} property can only be bound to a {nameof(Window)}");
            obj.SetValue(IWindowClosingHandlerProperty, value);
        }

        private static void IWindowClosingHandlerPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Window w = d as Window;
            if (w is null) return;
            if (e.NewValue != null)
            {
                w.Closing += W_Closing;
                w.Closed += W_Closed;
            }
            else
            {
                w.Closing -= W_Closing;
                w.Closed -= W_Closed;
            }
        }

        private static void W_Closed(object sender, EventArgs e)
        {
            GetIWindowClosingHandler(sender as DependencyObject)?.OnWindowClosed(sender, e);
        }

        private static void W_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            GetIWindowClosingHandler(sender as DependencyObject)?.OnWindowClosing(sender, e);
        }

        #endregion
    }
Steady answered 8/11, 2022 at 13:58 Comment(1)
Quite interesting. Provocatively, visuals "exposed via EventArgs" aren't a big deal, since the second most popular answer does that too.Saguenay
A
0

You could easily do it with some code behind; In Main.xaml set: Closing="Window_Closing"

In Main.cs:

 public MainViewModel dataContext { get; set; }
 public ICommand CloseApp 
   {
      get { return (ICommand)GetValue(CloseAppProperty); }
      set { SetValue(CloseAppProperty, value); }
   }
public static readonly DependencyProperty CloseAppProperty =
DependencyProperty.Register("CloseApp", typeof(ICommand), typeof(MainWindow), new PropertyMetadata(null));

In Main.OnLoading:

dataContext = DataContext as MainViewModel;

In Main.Window_Closing:

   if (CloseApp != null)
      CloseApp .Execute(this);

In MainWindowModel:

    public ICommand CloseApp => new CloseApp (this);

And finally:

class CloseApp : ICommand { public event EventHandler CanExecuteChanged;

    private MainViewModel _viewModel;

    public CloseApp (MainViewModel viewModel)
    {
        _viewModel = viewModel;
    }


    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        Console.WriteLine();
    }
}
Aklog answered 25/8, 2021 at 11:14 Comment(0)
R
-2
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        MessageBox.Show("closing");
    }
Reinhart answered 17/9, 2016 at 19:25 Comment(2)
Hi, do add a bit of explanation along with the code as it helps to understand your code. Code only answers are frowned uponClitoris
The op explicitly stated that he wasn't interested in using code-behind event code for this.Parson

© 2022 - 2024 — McMap. All rights reserved.