Gestire l'evento di chiusura della finestra con WPF / MVVM Light Toolkit


145

Vorrei gestire l' Closingevento (quando un utente fa clic sul pulsante "X" in alto a destra) della mia finestra per poter eventualmente visualizzare un messaggio di conferma o / e annullare la chiusura.

So come farlo nel code-behind: iscriviti Closingall'evento della finestra quindi usa la CancelEventArgs.Cancelproprietà.

Ma sto usando MVVM, quindi non sono sicuro che sia il buon approccio.

Penso che il buon approccio sarebbe quello di associare l' Closingevento a a Commandnel mio ViewModel.

Ho provato che:

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

Con un associato RelayCommandnel mio ViewModel ma non funziona (il codice del comando non viene eseguito).


3
Interessato anche a una bella risposta per rispondere a questo.
Sekhat,

3
Ho scaricato il codice da codeplex e il debug ha rivelato: "Impossibile eseguire il cast dell'oggetto di tipo 'System.ComponentModel.CancelEventArgs' per digitare 'System.Windows.RoutedEventArgs'." Funziona bene se non vuoi CancelEventArgs ma questo non risponde alla tua domanda ...
David Hollinshead,

Immagino che il tuo codice non funzioni perché il controllo a cui hai collegato il trigger non ha un evento Closing. Il tuo contesto di dati non è una finestra ... Probabilmente è un modello di dati con una griglia o qualcosa del genere, che non ha eventi di chiusura. Quindi la risposta di dbkk è la migliore risposta in questo caso. Tuttavia, preferisco l'approccio Interaction / EventTrigger quando l'evento è disponibile.
NielW

Il codice che hai funzionerà bene su un evento Loaded, per esempio.
NielW

Risposte:


126

Vorrei semplicemente associare il gestore nel costruttore Visualizza:

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

Quindi aggiungere il gestore a ViewModel:

using System.ComponentModel;

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

In questo caso, non si ottiene esattamente nulla se non la complessità utilizzando uno schema più elaborato con maggiore riferimento indiretto (5 righe extra di XAML più Commandschema).

Il mantra "zero code-behind" non è l'obiettivo in sé, il punto è disaccoppiare ViewModel dalla vista . Anche quando l'evento è associato nel code-behind della vista, ViewModelciò non dipende dalla vista e la logica di chiusura può essere testata dall'unità .


4
Mi piace questa soluzione: basta agganciare un pulsante nascosto :)
Benjol

3
Per i principianti di mvvm che non usano MVVMLight e cercano come informare ViewModel sull'evento di chiusura, i collegamenti su come impostare correttamente dataContext e come ottenere l'oggetto viewModel nella vista possono essere interessanti. Come ottenere un riferimento a ViewModel nella vista? e come posso impostare un ViewModel su una finestra in xaml usando la proprietà datacontext ... Mi ci sono volute diverse ore, come un semplice evento di chiusura della finestra poteva essere gestito in ViewModel.
Markus Eagle

18
Questa soluzione è irrilevante nell'ambiente MVVM. Il codice dietro non dovrebbe conoscere ViewModel.
Jacob,

2
@Jacob Penso che il problema sia più che si ottiene un gestore di eventi modulo nel ViewModel, che abbina il ViewModel a un'implementazione dell'interfaccia utente specifica. Se useranno il codice dietro, dovrebbero controllare CanExecute e quindi chiamare Execute () su una proprietà ICommand.
Evil Pigeon,

14
@Jacob Il code-behind può conoscere bene i membri di ViewModel, proprio come fa il codice XAML. O cosa pensi di fare quando crei un Binding a una proprietà ViewModel? Questa soluzione è perfetta per MVVM, purché non si gestisca la logica di chiusura nel code-behind stesso, ma nel ViewModel (sebbene l'utilizzo di un comando IC, come EvilPigeon suggerisce, potrebbe essere una buona idea poiché è anche possibile eseguire il binding ad esso)
almulo

81

Questo codice funziona perfettamente:

ViewModel.cs:

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

e in XAML:

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

supponendo che:

  • ViewModel è assegnato a un DataContextcontenitore principale.
  • xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.SL5"
  • xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

1
Dimenticato: per ottenere gli argomenti evento nel comando utilizzare PassEventArgsToCommand = "True"
Stas

2
+1 approccio semplice e convenzionale. Sarebbe anche meglio andare al PRISM.
Tri Q Tran,

16
Questo è uno scenario che evidenzia buchi spalancati in WPF e MVVM.
Damien,

1
Sarebbe davvero utile menzionare cosa c'è identro <i:Interaction.Triggers>e come ottenerlo.
Andrii Muzychuk,

1
@Chiz, è uno spazio dei nomi che dovresti dichiarare nell'elemento radice in questo modo: xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Stas

34

Questa opzione è ancora più semplice e forse è adatta a te. Nel costruttore del modello di visualizzazione, puoi iscriverti all'evento di chiusura della finestra principale in questo modo:

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

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

Ti auguro il meglio.


Questa è la soluzione migliore tra le altre menzionate in questo numero. Grazie !
Jacob,

Questo è quello che stavo cercando. Grazie!
Nikki Punjabi,

20
... e questo crea uno stretto accoppiamento tra ViewModel e View. -1.
PiotrK,

6
Questa non è la risposta migliore. Rompe MVVM.
Safiron

1
@Craig Richiede un duro riferimento alla finestra principale o alla finestra per cui viene utilizzata. È molto più semplice, ma significa che il modello di visualizzazione non è disaccoppiato. Non si tratta di soddisfare i nerd MVVM o meno, ma se il modello MVVM deve essere rotto per farlo funzionare, non ha senso usarlo affatto.
Alex

16

Ecco una risposta secondo il modello MVVM se non vuoi conoscere la finestra (o uno dei suoi eventi) nel ViewModel.

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

Nel ViewModel aggiungere l'interfaccia e l'implementazione

public bool OnClosing()
{
    bool close = true;

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

    return close;
}

Nella finestra aggiungo l'evento di chiusura. Questo codice dietro non rompe il modello MVVM. Il View può conoscere il modello di visualizzazione!

void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    IClosing context = DataContext as IClosing;
    if (context != null)
    {
        e.Cancel = !context.OnClosing();
    }
}

Semplice, chiaro e pulito. ViewModel non ha bisogno di conoscere i dettagli della vista, quindi le preoccupazioni rimangono separate.
Bernhard Hiller,

il contesto è sempre nullo!
Shahid Od

@ShahidOd ViewModel deve implementare l' IClosinginterfaccia, non solo implementare il OnClosingmetodo. Altrimenti il DataContext as IClosingcast fallirà e tornerànull
Erik White

10

Accidenti, sembra che ci sia molto codice qui per questo. Stas sopra aveva l'approccio giusto per il minimo sforzo. Ecco il mio adattamento (usando MVVMLight ma dovrebbe essere riconoscibile) ... Oh e PassEventArgsToCommand = "True" è sicuramente necessario come indicato sopra.

(credito 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> 

Nel modello di visualizzazione:

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

nel servizio di spegnimento

    /// <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 è simile al seguente ma in sostanzaRequestShutdown o come viene chiamato decide se chiudere l'applicazione (che chiuderà comunque allegramente la finestra):

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

8

Il richiedente dovrebbe usare la risposta STAS, ma per i lettori che usano il prisma e nessun galasoft / mvvmlight, potrebbero voler provare quello che ho usato:

Nella definizione in alto per window o usercontrol, etc definisci lo spazio dei nomi:

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

E proprio sotto quella definizione:

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

Proprietà nel tuo modello di visualizzazione:

public ICommand WindowClosing { get; private set; }

Collega delegatecommand al costruttore del modello di visualizzazione:

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

Infine, il tuo codice che vuoi raggiungere alla chiusura del controllo / finestra / qualunque cosa:

private void OnWindowClosing(object obj)
        {
            //put code here
        }

3
Ciò non consente l'accesso a CancelEventArgs, necessario per annullare l'evento di chiusura. L'oggetto passato è il modello di vista, che tecnicamente è lo stesso modello di vista da cui viene eseguito il comando WindowClosing.
stephenbayer,

4

Sarei tentato di utilizzare un gestore eventi all'interno del tuo file App.xaml.cs che ti consentirà di decidere se chiudere l'applicazione o meno.

Ad esempio, potresti avere qualcosa di simile al seguente codice nel tuo file 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();

}

Quindi nel tuo codice MainWindowViewModel potresti avere quanto segue:

#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]

1
Grazie per la risposta dettagliata Tuttavia, non penso che risolva il mio problema: devo gestire la chiusura della finestra quando l'utente fa clic sul pulsante "X" in alto a destra. Sarebbe facile farlo nel code-behind (collegherei semplicemente l'evento Closing e imposti CancelEventArgs.Cancel su true of false) ma mi piacerebbe farlo in stile MVVM. Ci scusiamo per la confusione
Olivier Payen,

1

Fondamentalmente, l'evento finestra potrebbe non essere assegnato a MVVM. In generale, il pulsante Chiudi mostra una finestra di dialogo per chiedere all'utente "salva: sì / no / annulla", e questo non può essere realizzato da MVVM.

È possibile mantenere il gestore eventi OnClosing, dove si chiama Model.Close.CanExecute () e si imposta il risultato booleano nella proprietà dell'evento. Quindi dopo la chiamata CanExecute () se vero, O nell'evento OnClosed, chiama Model.Close.Execute ()


1

Non ho fatto molti test con questo, ma sembra funzionare. Ecco cosa mi è venuto in mente:

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

1
Cosa succederà qui nello scenario in cui la VM desidera annullare la chiusura?
Tri Q Tran,


1

Utilizzando MVVM Light Toolkit:

Supponendo che esista un comando Exit nel modello di visualizzazione:

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

Questo è ricevuto nella vista:

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

D'altra parte, gestisco l' Closingevento in MainWindow, usando l'istanza di ViewModel:

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

CancelBeforeClose controlla l'attuale modello di visualizzazione e restituisce true se la chiusura deve essere arrestata.

Spero che aiuti qualcuno.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.