Manejo del evento de cierre de la ventana con WPF / MVVM Light Toolkit

145

Me gustaría manejar el Closingevento (cuando un usuario hace clic en el botón 'X' superior derecho) de mi ventana para eventualmente mostrar un mensaje de confirmación o cancelar el cierre.

Sé cómo hacer esto en el código subyacente: suscríbase al Closingevento de la ventana y luego use la CancelEventArgs.Cancelpropiedad.

Pero estoy usando MVVM, así que no estoy seguro de que sea un buen enfoque.

Creo que el buen enfoque sería vincular el Closingevento a un Commanden mi ViewModel.

Intenté eso:

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

Con un asociado RelayCommanden mi ViewModel pero no funciona (el código del comando no se ejecuta).

Olivier Payen
fuente
3
También estoy interesado en una buena respuesta para responder a esto.
Sekhat
3
Descargué el código de codeplex y la depuración reveló: "No se puede convertir el objeto del tipo 'System.ComponentModel.CancelEventArgs' para escribir 'System.Windows.RoutedEventArgs'". Funciona bien si no quieres el CancelEventArgs pero eso no responde a tu pregunta ...
David Hollinshead
Supongo que su código no funciona porque el control al que adjuntó su disparador no tiene un evento de cierre. Su contexto de datos no es una ventana ... Probablemente sea una plantilla de datos con una cuadrícula o algo, que no tiene evento de cierre. Entonces la respuesta de dbkk es la mejor respuesta en este caso. Sin embargo, prefiero el enfoque Interaction / EventTrigger cuando el evento está disponible.
NielW
El código que tiene funcionará bien en un evento cargado, por ejemplo.
NielW

Respuestas:

126

Simplemente asociaría el controlador en el constructor Ver:

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

Luego agregue el controlador a ViewModel:

using System.ComponentModel;

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

En este caso, no gana exactamente nada excepto la complejidad al usar un patrón más elaborado con más indirección (5 líneas adicionales de Commandpatrón XAML plus ).

El mantra "código cero detrás" no es el objetivo en sí mismo, el punto es desacoplar ViewModel de la Vista . Incluso cuando el evento está vinculado en el código subyacente de la Vista, ViewModelno depende de la Vista y la lógica de cierre puede ser probada por unidad .

dbkk
fuente
44
Me gusta esta solución: solo enganchar en un botón oculto :)
Benjol
3
Para los principiantes de mvvm que no usan MVVMLight y buscan cómo informar al ViewModel sobre el evento de Cierre, los enlaces sobre cómo configurar correctamente el dataContext y cómo obtener el objeto viewModel en la Vista pueden ser interesantes. ¿Cómo obtener una referencia al ViewModel en la vista? y ¿Cómo configuro un ViewModel en una ventana en xaml usando la propiedad datacontext ... Me tomó varias horas, cómo se podía manejar un simple evento de cierre de ventana en ViewModel.
MarkusEgle
18
Esta solución es irrelevante en el entorno MVVM. El código subyacente no debería saber sobre ViewModel.
Jacob
2
@Jacob Creo que el problema es más que obtienes un controlador de eventos de formulario en tu ViewModel, que combina el ViewModel con una implementación de interfaz de usuario específica. Si van a usar código detrás, deben verificar CanExecute y luego llamar a Execute () en una propiedad ICommand.
Evil Pigeon
14
@Jacob El código subyacente puede conocer bien a los miembros de ViewModel, al igual que el código XAML. ¿O qué crees que estás haciendo cuando creas un enlace a una propiedad ViewModel? Esta solución es perfectamente adecuada para MVVM, siempre y cuando no maneje la lógica de cierre en el código subyacente, pero en ViewModel (aunque usar un ICommand, como sugiere EvilPigeon, podría ser una buena idea ya que también puede vincular a él)
almulo
81

Este código funciona bien:

ViewModel.cs:

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

y en XAML:

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

asumiendo que:

  • ViewModel se asigna a uno DataContextdel contenedor principal.
  • xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.SL5"
  • xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Stas
fuente
1
Olvidé: para obtener argumentos de eventos en el comando use PassEventArgsToCommand = "True"
Stas
2
+1 enfoque simple y convencional. Sería aún mejor dirigirse al PRISM.
Tri Q Tran
16
Este es un escenario que destaca los huecos en WPF y MVVM.
Damien
1
Sería realmente útil mencionar lo que hay identro <i:Interaction.Triggers>y cómo obtenerlo.
Andrii Muzychuk
1
@Chiz, es un espacio de nombres que debe declarar en el elemento raíz de esta manera: xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Stas
34

Esta opción es aún más fácil y quizás sea adecuada para usted. En su constructor de modelo de vista, puede suscribirse al evento de cierre de la ventana principal de esta manera:

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

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

Todo lo mejor.

PILAces
fuente
Esta es la mejor solución entre las otras mencionadas en este número. Gracias !
Jacob
Esto es lo que estaba buscando. ¡Gracias!
Nikki Punjabi
20
... y esto crea un acoplamiento estrecho entre ViewModel y View. -1.
PiotrK
66
Esta no es la mejor respuesta. Rompe MVVM.
Safiron
1
@Craig Requiere una referencia estricta a la ventana principal, o la ventana para la que se está utilizando. Es mucho más fácil, pero significa que el modelo de vista no está desacoplado. No se trata de satisfacer a los nerds MVVM o no, pero si el patrón MVVM tiene que romperse para que funcione, no tiene sentido usarlo.
Alex
16

Aquí hay una respuesta de acuerdo con el patrón MVVM si no desea conocer la Ventana (o cualquiera de sus eventos) en ViewModel.

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

En ViewModel agregue la interfaz y la implementación

public bool OnClosing()
{
    bool close = true;

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

    return close;
}

En la ventana agrego el evento de cierre. Este código detrás no rompe el patrón MVVM. ¡La Vista puede conocer el modelo de vista!

void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    IClosing context = DataContext as IClosing;
    if (context != null)
    {
        e.Cancel = !context.OnClosing();
    }
}
AxdorphCoder
fuente
Simple, claro y limpio. ViewModel no necesita conocer detalles específicos de la vista, por lo tanto, las preocupaciones permanecen separadas.
Bernhard Hiller
¡el contexto es siempre nulo!
Shahid Od
@ShahidOd Su ViewModel necesita implementar la IClosinginterfaz, no solo implementar el OnClosingmétodo. De lo contrario, el DataContext as IClosingelenco fallará y volveránull
Erik White
10

Geez, parece que hay mucho código aquí para esto. Stas arriba tenía el enfoque correcto para un esfuerzo mínimo. Aquí está mi adaptación (usando MVVMLight pero debería ser reconocible) ... Ah, y el PassEventArgsToCommand = "True" es definitivamente necesario como se indicó anteriormente.

(crédito a 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> 

En el modelo de vista:

///<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);

en el 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 se parece a lo siguiente, pero básicamente RequestShutdown o como se llame decide si se cierra la aplicación o no (lo cual cerrará la ventana de todos modos):

...
...
...
    /// <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);
    }
    }
AllenM
fuente
8

El autor de la pregunta debería usar la respuesta STAS, pero para los lectores que usan prisma y no galasoft / mvvmlight, es posible que quieran probar lo que usé:

En la definición en la parte superior para ventana o control de usuario, etc., defina el espacio de nombres:

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

Y justo debajo de esa definición:

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

Propiedad en su modelo de vista:

public ICommand WindowClosing { get; private set; }

Adjunte delegatecommand en su constructor de modelo de vista:

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

Finalmente, su código que desea alcanzar al cerrar el control / ventana / lo que sea:

private void OnWindowClosing(object obj)
        {
            //put code here
        }
Chris
fuente
3
Esto no da acceso a CancelEventArgs, que es necesario para cancelar el evento de cierre. El objeto pasado es el modelo de vista, que técnicamente es el mismo modelo de vista desde el que se ejecuta el comando WindowClosing.
stephenbayer
4

Me sentiría tentado a usar un controlador de eventos dentro de su archivo App.xaml.cs que le permitirá decidir si cierra la aplicación o no.

Por ejemplo, podría tener algo como el siguiente código en su archivo App.xaml.cs:

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();

}

Luego, dentro de su código MainWindowViewModel, podría tener lo siguiente:

#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]
ChrisBD
fuente
1
Gracias por la respuesta detallada. Sin embargo, no creo que eso resuelva mi problema: necesito manejar el cierre de la ventana cuando el usuario hace clic en el botón 'X' superior derecho. Sería fácil hacer esto en el código subyacente (simplemente vincularía el evento de cierre y establecería CancelEventArgs.Cancel en verdadero o falso), pero me gustaría hacerlo en estilo MVVM. Perdón por la confusión
Olivier Payen
1

Básicamente, el evento de ventana no puede asignarse a MVVM. En general, el botón Cerrar muestra un cuadro de diálogo para preguntarle al usuario "guardar: sí / no / cancelar", y MVVM no puede lograrlo.

Puede mantener el controlador de eventos OnClosing, donde llama al Model.Close.CanExecute () y establece el resultado booleano en la propiedad del evento. Entonces, después de la llamada CanExecute () si es verdadero, O en el evento OnClosed, llame al Model.Close.Execute ()

Echtelion
fuente
1

No he hecho muchas pruebas con esto, pero parece funcionar. Esto es lo que se me ocurrió:

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);
        }
    }
}
Brian Ortiz
fuente
1
¿Qué pasará aquí en el escenario donde la VM desea cancelar el cierre?
Tri Q Tran
1

Usando MVVM Light Toolkit:

Suponiendo que hay un comando Salir en el modelo de vista:

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);
}

Esto se recibe en la vista:

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

Por otro lado, manejo el Closingevento en MainWindow, usando la instancia de ViewModel:

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

CancelBeforeClose comprueba el estado actual del modelo de vista y devuelve verdadero si se debe detener el cierre.

Espero que ayude a alguien.

Ron
fuente
-2
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        MessageBox.Show("closing");
    }
Mattias Sturebrand
fuente
Hola, agregue un poco de explicación junto con el código, ya que ayuda a comprender su código. Código solo las respuestas están mal vistas
Bhargav Rao
El operador declaró explícitamente que no estaba interesado en usar el código de evento de código subyacente para esto.
Fer García