Gestion de l'événement de fermeture de fenêtre avec WPF / MVVM Light Toolkit

145

Je voudrais gérer l' Closingévénement (lorsqu'un utilisateur clique sur le bouton 'X' en haut à droite) de ma fenêtre afin d'afficher éventuellement un message de confirmation ou / et d'annuler la fermeture.

Je sais comment faire cela dans le code-behind: abonnez-vous à l' Closingévénement de la fenêtre puis utilisez la CancelEventArgs.Cancelpropriété.

Mais j'utilise MVVM donc je ne suis pas sûr que ce soit la bonne approche.

Je pense que la bonne approche serait de lier l' Closingévénement à un Commanddans mon ViewModel.

J'ai essayé ça:

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

Avec un associé RelayCommanddans mon ViewModel mais cela ne fonctionne pas (le code de la commande n'est pas exécuté).

Olivier Payen
la source
3
Également intéressé par une belle réponse pour répondre à cela.
Sekhat
3
J'ai téléchargé le code de codeplex et le débogage a révélé: "Impossible de convertir un objet de type 'System.ComponentModel.CancelEventArgs' pour taper 'System.Windows.RoutedEventArgs'." Cela fonctionne bien si vous ne voulez pas le CancelEventArgs mais cela ne répond pas à votre question ...
David Hollinshead
J'imagine que votre code ne fonctionne pas car le contrôle auquel vous avez associé votre déclencheur n'a pas d'événement Closing. Votre contexte de données n'est pas une fenêtre ... C'est probablement un modèle de données avec une grille ou quelque chose, qui n'a pas d'événement de clôture. La réponse de dbkk est donc la meilleure réponse dans ce cas. Cependant, je préfère l'approche Interaction / EventTrigger lorsque l'événement est disponible.
NielW
Le code que vous avez fonctionnera correctement sur un événement Loaded, par exemple.
NielW

Réponses:

126

J'associerais simplement le gestionnaire dans le constructeur View:

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

Ajoutez ensuite le gestionnaire au ViewModel:

using System.ComponentModel;

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

Dans ce cas, vous ne gagnez exactement rien sauf la complexité en utilisant un modèle plus élaboré avec plus d'indirection (5 lignes supplémentaires de XAML plus Commandmodèle).

Le mantra «zéro code derrière» n'est pas le but en soi, il s'agit de découpler ViewModel de la vue . Même lorsque l'événement est lié dans le code-behind de la vue, le ViewModelne dépend pas de la vue et la logique de fermeture peut être testée unitaire .

dbkk
la source
4
J'aime cette solution: accrochez-vous simplement à un bouton caché :)
Benjol
3
Pour les débutants mvvm n'utilisant pas MVVMLight et cherchant comment informer le ViewModel de l'événement Closing, les liens comment configurer correctement le dataContext et comment obtenir l'objet viewModel dans la vue peuvent être intéressants. Comment obtenir une référence au ViewModel dans la vue? et Comment définir un ViewModel sur une fenêtre en xaml en utilisant la propriété datacontext ... Cela m'a pris plusieurs heures, comment un simple événement de fermeture de fenêtre pouvait être géré dans le ViewModel.
MarkusEgle
18
Cette solution n'est pas pertinente dans l'environnement MVVM. Le code derrière ne devrait pas connaître le ViewModel.
Jacob
2
@Jacob Je pense que le problème est plus que vous obtenez un gestionnaire d'événements de formulaire dans votre ViewModel, qui couple le ViewModel à une implémentation d'interface utilisateur spécifique. S'ils vont utiliser du code derrière, ils doivent vérifier CanExecute, puis appeler Execute () sur une propriété ICommand à la place.
Evil Pigeon
14
@Jacob Le code-behind peut très bien connaître les membres de ViewModel, tout comme le code XAML. Ou que pensez-vous faire lorsque vous créez une liaison à une propriété ViewModel? Cette solution convient parfaitement à MVVM, tant que vous ne gérez pas la logique de fermeture dans le code-behind lui-même, mais dans le ViewModel (bien que l'utilisation d'une ICommand, comme EvilPigeon le suggère, pourrait être une bonne idée car vous pouvez également lier à lui)
almulo
81

Ce code fonctionne très bien:

ViewModel.cs:

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

et en XAML:

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

en admettant que:

  • ViewModel est affecté à l'un DataContextdes conteneurs principaux.
  • xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.SL5"
  • xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Stas
la source
1
Oublié: pour obtenir des arguments d'événement dans la commande, utilisez PassEventArgsToCommand = "True"
Stas
2
+1 approche simple et conventionnelle. Ce serait encore mieux de se diriger vers PRISM.
Tri Q Tran
16
C'est un scénario qui met en évidence des trous béants dans WPF et MVVM.
Damien
1
Il serait vraiment utile de mentionner ce qui est idans <i:Interaction.Triggers>et comment l'obtenir.
Andrii Muzychuk
1
@Chiz, c'est un espace de noms que vous devez déclarer dans l'élément racine comme ceci: xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Stas
34

Cette option est encore plus simple et vous convient peut-être. Dans votre constructeur de modèle de vue, vous pouvez vous abonner à l'événement de fermeture de la fenêtre principale comme ceci:

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

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

Bonne chance.

PILuaces
la source
C'est la meilleure solution parmi les autres mentionnées dans ce numéro. Je vous remercie !
Jacob
C'est ce que je cherchais. Merci!
Nikki Punjabi
20
... et cela crée un couplage étroit entre ViewModel et View. -1.
PiotrK
6
Ce n'est pas la meilleure réponse. Cela casse MVVM.
Safiron
1
@Craig Il nécessite une référence ferme à la fenêtre principale, ou à la fenêtre pour laquelle elle est utilisée. C'est beaucoup plus facile, mais cela signifie que le modèle de vue n'est pas découplé. Il ne s'agit pas de satisfaire les nerds MVVM ou non, mais si le modèle MVVM doit être rompu pour le faire fonctionner, il ne sert à rien de l'utiliser.
Alex
16

Voici une réponse selon le modèle MVVM si vous ne voulez pas connaître la fenêtre (ou l'un de ses événements) dans le ViewModel.

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

Dans le ViewModel, ajoutez l'interface et l'implémentation

public bool OnClosing()
{
    bool close = true;

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

    return close;
}

Dans la fenêtre, j'ajoute l'événement de clôture. Ce code derrière ne rompt pas le modèle MVVM. La vue peut connaître le modèle de vue!

void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    IClosing context = DataContext as IClosing;
    if (context != null)
    {
        e.Cancel = !context.OnClosing();
    }
}
AxdorphCoder
la source
Simple, clair et propre. Le ViewModel n'a pas besoin de connaître les spécificités de la vue, donc les préoccupations restent séparées.
Bernhard Hiller
le contexte est toujours nul!
Shahid Od
@ShahidOd Votre ViewModel doit implémenter l' IClosinginterface, pas seulement implémenter la OnClosingméthode. Sinon, le DataContext as IClosingcasting échouera et reviendranull
Erik White
10

Décidément, il semble que beaucoup de code se passe ici pour cela. Stas ci-dessus avait la bonne approche pour un effort minimal. Voici mon adaptation (en utilisant MVVMLight mais devrait être reconnaissable) ... Oh et le PassEventArgsToCommand = "True" est définitivement nécessaire comme indiqué ci-dessus.

(crédit à 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> 

Dans le modèle de vue:

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

dans le 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 ressemble à ce qui suit, mais en gros, RequestShutdown ou quel que soit son nom décide d'arrêter l'application ou non (ce qui fermera joyeusement la fenêtre de toute façon):

...
...
...
    /// <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
la source
8

Le demandeur doit utiliser la réponse STAS, mais pour les lecteurs qui utilisent prism et pas de galasoft / mvvmlight, ils voudront peut-être essayer ce que j'ai utilisé:

Dans la définition en haut de window ou usercontrol, etc. définissez l'espace de noms:

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

Et juste en dessous de cette définition:

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

Propriété dans votre modèle de vue:

public ICommand WindowClosing { get; private set; }

Attachez delegatecommand dans votre constructeur viewmodel:

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

Enfin, votre code que vous souhaitez atteindre à la fermeture du champ / fenêtre / peu importe:

private void OnWindowClosing(object obj)
        {
            //put code here
        }
Chris
la source
3
Cela ne donne pas accès à CancelEventArgs qui est nécessaire pour annuler l'événement de clôture. L'objet passé est le modèle de vue, qui est techniquement le même modèle de vue à partir duquel la commande WindowClosing est exécutée.
stephenbayer
4

Je serais tenté d'utiliser un gestionnaire d'événements dans votre fichier App.xaml.cs qui vous permettra de décider de fermer ou non l'application.

Par exemple, vous pourriez avoir quelque chose comme le code suivant dans votre fichier 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();

}

Ensuite, dans votre code MainWindowViewModel, vous pouvez avoir les éléments suivants:

#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
la source
1
Merci pour la réponse détaillée. Cependant, je ne pense pas que cela résout mon problème: je dois gérer la fermeture de la fenêtre lorsque l'utilisateur clique sur le bouton «X» en haut à droite. Ce serait facile de le faire dans le code-behind (je lierais simplement l'événement Closing et définirais CancelEventArgs.Cancel sur true ou false) mais j'aimerais le faire dans le style MVVM. Désolé pour la confusion
Olivier Payen
1

Fondamentalement, l'événement de fenêtre ne peut pas être affecté à MVVM. En général, le bouton Fermer affiche une boîte de dialogue pour demander à l'utilisateur "enregistrer: oui / non / annuler", et cela peut ne pas être réalisé par le MVVM.

Vous pouvez conserver le gestionnaire d'événements OnClosing, où vous appelez Model.Close.CanExecute () et définissez le résultat booléen dans la propriété event. Donc, après l'appel CanExecute () si true, OU dans l'événement OnClosed, appelez Model.Close.Execute ()

Echtelion
la source
1

Je n'ai pas fait beaucoup de tests avec cela, mais cela semble fonctionner. Voici ce que j'ai trouvé:

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
la source
1
Que se passera-t-il ici dans le scénario où la VM souhaite annuler la fermeture?
Tri Q Tran du
1

Utilisation de MVVM Light Toolkit:

En supposant qu'il existe une commande Quitter dans le modèle de vue:

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

Ceci est reçu dans la vue:

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

D'autre part, je gère les Closingévénements dans MainWindow, en utilisant l'instance de ViewModel:

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

CancelBeforeClose vérifie l'état actuel du modèle de vue et renvoie true si la fermeture doit être arrêtée.

J'espère que ça aide quelqu'un.

Ron
la source
-2
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        MessageBox.Show("closing");
    }
Mattias Sturebrand
la source
Salut, ajoutez un peu d'explication avec le code car cela aide à comprendre votre code. Les réponses au code uniquement sont mal
vues
L'op a explicitement déclaré qu'il n'était pas intéressé par l'utilisation du code d'événement code-behind pour cela.
Fer García