Behandeln des Fensterschließereignisses mit WPF / MVVM Light Toolkit

145

Ich würde gerne damit umgehen Closing Ereignis (wenn ein Benutzer auf die Schaltfläche "X" oben rechts klickt) meines Fensters behandeln, um eventuell eine Bestätigungsmeldung anzuzeigen oder / und das Schließen abzubrechen.

Ich weiß, wie das im Code-Behind geht: Abonnieren Sie das ClosingEreignis des Fensters und verwenden Sie dann die CancelEventArgs.CancelEigenschaft.

Aber ich benutze MVVM und bin mir nicht sicher, ob es der gute Ansatz ist.

Ich denke, der gute Ansatz wäre, das ClosingEreignis an ein Commandin meinem ViewModel zu binden .

Ich habe das versucht:

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

Mit einem RelayCommandin meinem ViewModel verknüpften, aber es funktioniert nicht (der Code des Befehls wird nicht ausgeführt).

Olivier Payen
quelle
3
Auch interessiert an netter Antwort, um darauf zu antworten.
Sekhat
3
Ich habe den Code von codeplex heruntergeladen und beim Debuggen festgestellt: "Objekt vom Typ 'System.ComponentModel.CancelEventArgs' kann nicht in 'System.Windows.RoutedEventArgs' umgewandelt werden." Es funktioniert gut, wenn Sie die CancelEventArgs nicht wollen, aber das beantwortet Ihre Frage nicht ...
David Hollinshead
Ich vermute, Ihr Code funktioniert nicht, weil das Steuerelement, an das Sie Ihren Trigger angehängt haben, kein Closing-Ereignis hat. Ihr Datenkontext ist kein Fenster ... Es ist wahrscheinlich eine Datenvorlage mit einem Raster oder etwas anderem, das kein Abschlussereignis hat. Die Antwort von dbkk ist in diesem Fall die beste Antwort. Ich bevorzuge jedoch den Interaction / EventTrigger-Ansatz, wenn das Ereignis verfügbar ist.
NielW
Der Code, den Sie haben, funktioniert beispielsweise bei einem geladenen Ereignis einwandfrei.
NielW

Antworten:

126

Ich würde den Handler einfach im View-Konstruktor zuordnen:

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

Fügen Sie dann den Handler zu ViewModel:

using System.ComponentModel;

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

In diesem Fall erhalten Sie genau etwas anderes als Komplexität, wenn Sie ein ausgefeilteres Muster mit mehr Indirektion verwenden (5 zusätzliche Zeilen XAML plus CommandMuster).

Das "Zero Code-Behind" -Mantra ist nicht das eigentliche Ziel, es geht darum, ViewModel von der Ansicht zu entkoppeln . Selbst wenn das Ereignis im Code-Behind der Ansicht gebunden ist, ViewModelhängt dies nicht von der Ansicht ab und die Abschlusslogik kann Unit-getestet werden .

dbkk
quelle
4
Ich mag diese Lösung: einfach in einen versteckten Knopf
einhaken
3
Für mvvm-Anfänger, die MVVMLight nicht verwenden und suchen, wie sie das ViewModel über das Closing-Ereignis informieren können, sind die Links zum korrekten Einrichten des dataContext und zum Abrufen des viewModel-Objekts in der Ansicht möglicherweise interessant. Wie erhalte ich einen Verweis auf das ViewModel in der Ansicht? und Wie setze ich ein ViewModel für ein Fenster in xaml mithilfe der datacontext-Eigenschaft? Es dauerte mehrere Stunden, wie ein einfaches Fensterschließereignis im ViewModel behandelt werden konnte.
MarkusEgle
18
Diese Lösung ist in der MVVM-Umgebung irrelevant. Der Code dahinter sollte nichts über das ViewModel wissen.
Jacob
2
@Jacob Ich denke, das Problem besteht eher darin, dass Sie in Ihrem ViewModel einen Formularereignishandler erhalten, der das ViewModel mit einer bestimmten UI-Implementierung koppelt. Wenn sie Code dahinter verwenden möchten, sollten sie CanExecute überprüfen und stattdessen Execute () für eine ICommand-Eigenschaft aufrufen.
Evil Pigeon
14
@Jacob Der Code-Behind kann die ViewModel-Mitglieder genau kennen, genau wie der XAML-Code. Oder was machen Sie Ihrer Meinung nach, wenn Sie eine Bindung an eine ViewModel-Eigenschaft erstellen? Diese Lösung ist für MVVM vollkommen in Ordnung, solange Sie die Abschlusslogik nicht im Code-Behind selbst, sondern im ViewModel behandeln (obwohl die Verwendung eines ICommands, wie EvilPigeon vorschlägt, eine gute Idee sein könnte, da Sie auch binden können dazu)
Almulo
81

Dieser Code funktioniert einwandfrei:

ViewModel.cs:

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

und in XAML:

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

vorausgesetzt, dass:

  • ViewModel ist einem DataContextder Hauptcontainer zugeordnet .
  • xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.SL5"
  • xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Stas
quelle
1
Vergessen: Um Ereignisargumente im Befehl abzurufen, verwenden Sie PassEventArgsToCommand = "True"
Stas
2
+1 einfacher und konventioneller Ansatz. Wäre noch besser, zu PRISM zu gehen.
Tri Q Tran
16
Dies ist ein Szenario, das klaffende Löcher in WPF und MVVM hervorhebt.
Damien
1
Es wäre wirklich hilfreich sein , zu erwähnen , was ist iin , <i:Interaction.Triggers>und wie es zu bekommen.
Andrii Muzychuk
1
@Chiz, es ist ein Namespace, den Sie im root-Element wie folgt deklarieren sollten: xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Stas
34

Diese Option ist noch einfacher und möglicherweise für Sie geeignet. In Ihrem View Model-Konstruktor können Sie das Abschlussereignis des Hauptfensters wie folgt abonnieren:

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

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

Alles Gute.

PILuaces
quelle
Dies ist die beste Lösung unter den anderen in dieser Ausgabe genannten. Danke !
Jacob
Das habe ich gesucht. Vielen Dank!
Nikki Punjabi
20
... und dies schafft eine enge Kopplung zwischen ViewModel und View. -1.
PiotrK
6
Dies ist nicht die beste Antwort. Es bricht MVVM.
Safiron
1
@Craig Es erfordert einen harten Verweis auf das Hauptfenster oder das Fenster, für das es verwendet wird. Es ist viel einfacher, aber es bedeutet, dass das Ansichtsmodell nicht entkoppelt ist. Es geht nicht darum, die MVVM-Nerds zufrieden zu stellen oder nicht, aber wenn das MVVM-Muster gebrochen werden muss, damit es funktioniert, macht es keinen Sinn, es überhaupt zu verwenden.
Alex
16

Hier ist eine Antwort gemäß dem MVVM-Muster, wenn Sie nichts über das Fenster (oder eines seiner Ereignisse) im ViewModel wissen möchten.

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

Fügen Sie im ViewModel die Schnittstelle und die Implementierung hinzu

public bool OnClosing()
{
    bool close = true;

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

    return close;
}

Im Fenster füge ich das Closing-Ereignis hinzu. Dieser Code dahinter bricht das MVVM-Muster nicht. Die Ansicht kann über das Ansichtsmodell Bescheid wissen!

void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    IClosing context = DataContext as IClosing;
    if (context != null)
    {
        e.Cancel = !context.OnClosing();
    }
}
AxdorphCoder
quelle
Einfach, klar und sauber. Das ViewModel muss keine Besonderheiten der Ansicht kennen, daher bleiben Bedenken getrennt.
Bernhard Hiller
Der Kontext ist immer null!
Shahid Od
@ShahidOd Ihr ViewModel muss die IClosingSchnittstelle implementieren , nicht nur die OnClosingMethode. Andernfalls wird die DataContext as IClosingBesetzung scheitern und zurückkehrennull
Erik White
10

Meine Güte, hier scheint eine Menge Code dafür zu laufen. Stas oben hatte den richtigen Ansatz für minimalen Aufwand. Hier ist meine Anpassung (mit MVVMLight, sollte aber erkennbar sein) ... Oh und das PassEventArgsToCommand = "True" ist definitiv benötigt, wie oben angegeben.

(Dank an 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> 

Im Ansichtsmodell:

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

im 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 sieht ungefähr so ​​aus, aber im Grunde entscheidet RequestShutdown oder wie auch immer es heißt, ob die Anwendung heruntergefahren werden soll oder nicht (wodurch das Fenster ohnehin fröhlich geschlossen wird):

...
...
...
    /// <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
quelle
8

Der Fragesteller sollte die STAS-Antwort verwenden, aber für Leser, die Prisma und kein galasoft / mvvmlight verwenden, möchten sie möglicherweise versuchen, was ich verwendet habe:

Definieren Sie in der Definition oben für Fenster- oder Benutzersteuerung usw. den Namespace:

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

Und genau unter dieser Definition:

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

Eigenschaft in Ihrem Ansichtsmodell:

public ICommand WindowClosing { get; private set; }

Fügen Sie delegatecommand in Ihren viewmodel-Konstruktor ein:

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

Schließlich Ihren Code, den Sie beim Schließen des Steuerelements / Fensters / was auch immer erreichen möchten:

private void OnWindowClosing(object obj)
        {
            //put code here
        }
Chris
quelle
3
Dies gewährt keinen Zugriff auf die CancelEventArgs, die zum Abbrechen des Abschlussereignisses erforderlich sind. Das übergebene Objekt ist das Ansichtsmodell, das technisch das gleiche Ansichtsmodell ist, von dem aus der WindowClosing-Befehl ausgeführt wird.
Stephenbayer
4

Ich wäre versucht, einen Ereignishandler in Ihrer App.xaml.cs-Datei zu verwenden, mit dem Sie entscheiden können, ob Sie die Anwendung schließen möchten oder nicht.

Zum Beispiel könnten Sie dann so etwas wie den folgenden Code in Ihrer App.xaml.cs-Datei haben:

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

}

Dann könnten Sie in Ihrem MainWindowViewModel-Code Folgendes haben:

#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
quelle
1
Danke für die ausführliche Antwort. Ich glaube jedoch nicht, dass dies mein Problem löst: Ich muss das Schließen des Fensters behandeln, wenn der Benutzer auf die obere rechte Schaltfläche "X" klickt. Es wäre einfach, dies im Code-Behind zu tun (ich würde nur das Closing-Ereignis verknüpfen und CancelEventArgs.Cancel auf true oder false setzen), aber ich würde dies gerne im MVVM-Stil tun. Entschuldigung für die Verwirrung
Olivier Payen
1

Grundsätzlich kann das Fensterereignis MVVM nicht zugewiesen werden. Im Allgemeinen wird auf der Schaltfläche Schließen ein Dialogfeld angezeigt, in dem der Benutzer aufgefordert wird, "Speichern: Ja / Nein / Abbrechen" zu senden. Dies wird möglicherweise von der MVVM nicht erreicht.

Sie können den OnClosing-Ereignishandler beibehalten, in dem Sie Model.Close.CanExecute () aufrufen und das boolesche Ergebnis in der Ereigniseigenschaft festlegen. Rufen Sie nach dem Aufruf von CanExecute (), falls true, ODER im Ereignis OnClosed, Model.Close.Execute () auf.

Echtelion
quelle
1

Ich habe nicht viel damit getestet, aber es scheint zu funktionieren. Folgendes habe ich mir ausgedacht:

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
quelle
1
Was passiert hier in dem Szenario, in dem die VM das Schließen abbrechen möchte?
Tri Q Tran
1

Verwenden des MVVM Light Toolkit:

Angenommen, das Ansichtsmodell enthält einen Befehl Beenden :

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

Dies wird in der Ansicht empfangen:

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

Auf der anderen Seite behandle ich ClosingEreignisse in MainWindowmithilfe der Instanz von ViewModel:

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

CancelBeforeClose Überprüft den aktuellen Status des Ansichtsmodells und gibt true zurück, wenn das Schließen gestoppt werden soll.

Hoffe es hilft jemandem.

Ron
quelle
-2
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        MessageBox.Show("closing");
    }
Mattias Sturebrand
quelle
Hallo, fügen Sie zusammen mit dem Code eine Erklärung hinzu, da dies zum Verständnis Ihres Codes beiträgt. Nur-Code-Antworten sind verpönt
Bhargav Rao
Die Operation erklärte ausdrücklich, dass er nicht daran interessiert sei, Code-Behind-Ereigniscode dafür zu verwenden.
Fer García