Comment gérer la fermeture d’une application WPF avec Prism
Dans certains scénarios, on peut être amené à vouloir empêcher l’utilisateur de fermer une application (quand il y a des modifications en attente par exemple). Autant il est simple de le faire dans notre application en grisant des boutons etc… Autant l’utilisateur a un petit bouton magique situé en haut à droite de notre fenêtre (oui oui la petite croix à la con !) sur lequel on a pas forcément la main. Comment peut-on faire ça proprement quand on est en WPF et qu’on utilise Prism ?
Le soucis avec Prism / le pattern MVVM
Quand vous adoptez le pattern MVVM (avec Prism ou un autre Framework Mvvm), vous séparez votre Vue de votre ViewModel. Cette séparation est une bonne chose, puisqu’elle permet de découpler les différentes couches de votre application. Cependant, il arrive que votre code soit plus ou moins « tiré » par une fonctionnalité qui se trouve dans une autre couche. Et c’est donc parfois un vrai casse-tête pour trouver une implémentation correcte qui ne vienne pas faire tâche au milieu du reste.
Un exemple courant est la besoin de traiter la fermeture « forcée » de l’application. Ce cas se présente lorsque l’utilisateur décide de cliquer sur la petite croix pour fermer l’application (oui à côté des boutons minimiser et mode plein écran/fenêtré). En effet, lorsque l’utilisateur clique sur ce bouton, nous n’avons pas spécialement la main sur le code qui va être exécuté. Et en fonction de ce qui se passe dans notre application, il n’est pas possible de laisser l’utilisateur partir comme ça. Il est peut être nécessaire de sauvegarder le travail en cours, de poser une question… etc…avant de fermer le programme.
Le soucis c’est que dans la plupart des cas, on associe une Vue à un ViewModel. Que pour gérer notre cas, c’est notre MainWindow qui possède la logique pour gérer ce cas précis. Se pose alors la question de savoir comment on peut faire pour que notre ou nos ViewModel(s) interviennent proprement dans ce processus et que le tout reste propre et testable.
Il y a plusieurs possibilités pour gérer ce cas. Je vais vous exposer celle que je préfère et qui est un mécanisme de Prism : La CompositeCommand.
La CompositeCommand
La solution
La CompositeCommand c’est quoi ? C’est l’implémentation du pattern ICommand (logique oui…) mais avec une approche qui peut bien nous aider !
Le principe c’est quoi ? C’est d’avoir une commande qui sera une agrégation de plusieurs commandes. Tout simplement. Si on regarde la définition de cette classe, on peut y trouver certaines méthodes intéressantes.
1 2 3 4 5 6 7 8 9 10 |
public class CompositeCommand : ICommand { // Other Methods public IList<ICommand> RegisteredCommands { get; } public virtual void RegisterCommand(ICommand command); public virtual void UnregisterCommand(ICommand command); } |
Nous pouvons bien inscrire et désinscrire des commandes via (Un)RegisterCommand(). Lorsqu’on va exécuter (via la méthode Execute()) cette CompositeCommand, elle va donc exécuter les différentes commandes inscrites. De plus, nous ne pourrons éxecuter cette commande que si toutes les commandes inscrites sont d’accord pour s’exécuter. Le CanExecute() doit donc être à « true » partout.
L’implémentation
Ok, on utilise une CompositeCommand. Mais comment qu’on fait pour la mettre en place dans notre code ?
Vous avez plusieurs moyens de le faire (comme d’habitude !).
Certains utiliseront une classe static pour partager cette commande au sein de l’application. Je n’aime pas trop cette solution puisqu’elle rend « visible » notre commande. Ce qui pose un soucis d’encapsulation.
Je préfère donc la cacher dans un service de la manière suivante
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
public class WindowService { private readonly CompositeCommand _shutdownCommand; /// <summary> /// Initialize a default <see cref="WindowService"/> /// </summary> public WindowService() { _shutdownCommand = new CompositeCommand(); App.Current.MainWindow.Closing += OnWindowClosing; } /// <summary> /// Register a command to be execute at shutdown /// </summary> /// <param name="commandToRegister">The command to register</param> public void RegisterCommandOnShutdown(ICommand commandToRegister) { _shutdownCommand.RegisterCommand(commandToRegister); } /// <summary> /// Unregister a command. It will be not execute at shutdown anymore. /// </summary> /// <param name="commandToUnregister"></param> public void UnRegisterCommandOnShutdown(ICommand commandToUnregister) { _shutdownCommand.UnregisterCommand(commandToUnregister); } /// <summary> /// Occurs when the window is closing /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void OnWindowClosing(object sender, CancelEventArgs e) { if (_shutdownCommand.CanExecute(null)) { _shutdownCommand.Execute(null); } else { e.Cancel = false; } } } |
En gros je fait pas grand chose, je wrap juste la commande dans un service et je gère le OnClosing dedans.
A noter qu’il faudrait une interface qui expose l’event OnClosing et la passer dans le constructor du service pour ne pas avoir à utiliser notre App.Current.MainWindow.
Après on fait quoi ? Et bien il suffit de résoudre le WindowService dans votre ViewModel. Et pour gérer l’inscription/désinscription, vous pouvez utiliser l’interface INavigationAware.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
public class MyViewModel : INavigationAware { /// <summary> /// The service who manage current open window /// </summary> private readonly WindowService _windowService; /// <summary> /// Initialize a default <see cref="MyViewModel "/> /// </summary> public MyViewModel (WindowService windowService) { _windowService = windowService; _deactiveSyncCommand = new DelegateCommand(() => { // Work to be done }); } private readonly DelegateCommand _deactiveSyncCommand; /// <summary> /// Gets the command to deactivate sync /// </summary> public ICommand DeactivateCommand { get { return _deactiveSyncCommand; } } /// <summary> /// Occurs when the user navigate out from this screen /// </summary> /// <param name="navigationContext"></param> public void OnNavigatedFrom(NavigationContext navigationContext) { _windowService.UnRegisterCommandOnShutdown(_deactiveSyncCommand); } /// <summary> /// Occurs when the user navigate to this screen /// </summary> /// <param name="navigationContext"></param> public void OnNavigatedTo(NavigationContext navigationContext) { _windowService.RegisterCommandOnShutdown(_deactiveSyncCommand); } } |
Nous avons maintenant un mécanisme simple et efficace pour gérer la fermeture de notre application !