Bonne ou mauvaise pratique pour Dialogs dans wpf avec MVVM?

Récemment, j’ai eu le problème de créer des boîtes de dialog d’ajout et de modification pour mon application wpf.

Tout ce que je veux faire dans mon code, c’est quelque chose comme ça. (J’utilise principalement la première approche de viewmodel avec mvvm)

ViewModel qui appelle une fenêtre de dialog:

var result = this.uiDialogService.ShowDialog("Dialogwindow Title", dialogwindowVM); // Do anything with the dialog result 

Comment ça marche?

Tout d’abord, j’ai créé un service de dialog:

 public interface IUIWindowDialogService { bool? ShowDialog(ssortingng title, object datacontext); } public class WpfUIWindowDialogService : IUIWindowDialogService { public bool? ShowDialog(ssortingng title, object datacontext) { var win = new WindowDialog(); win.Title = title; win.DataContext = datacontext; return win.ShowDialog(); } } 

WindowDialog est une fenêtre spéciale mais simple. J’en ai besoin pour contenir mon contenu:

     

Un problème avec les dialogs dans wpf est que dialogresult = true ne peut être obtenu que dans le code. C’est pourquoi j’ai créé une interface pour mon dialogviewmodel afin de l’implémenter.

 public class RequestCloseDialogEventArgs : EventArgs { public bool DialogResult { get; set; } public RequestCloseDialogEventArgs(bool dialogresult) { this.DialogResult = dialogresult; } } public interface IDialogResultVMHelper { event EventHandler RequestCloseDialog; } 

Chaque fois que mon ViewModel pense qu’il est temps pour dialogresult = true , dialogresult = true cet événement.

 public partial class DialogWindow : Window { // Note: If the window is closed, it has no DialogResult private bool _isClosed = false; public DialogWindow() { InitializeComponent(); this.DialogPresenter.DataContextChanged += DialogPresenterDataContextChanged; this.Closed += DialogWindowClosed; } void DialogWindowClosed(object sender, EventArgs e) { this._isClosed = true; } private void DialogPresenterDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { var d = e.NewValue as IDialogResultVMHelper; if (d == null) return; d.RequestCloseDialog += new EventHandler (DialogResultTrueEvent).MakeWeak( eh => d.RequestCloseDialog -= eh;); } private void DialogResultTrueEvent(object sender, RequestCloseDialogEventArgs eventargs) { // Important: Do not set DialogResult for a closed window // GC clears windows anyways and with MakeWeak it // closes out with IDialogResultVMHelper if(_isClosed) return; this.DialogResult = eventargs.DialogResult; } } 

Maintenant, au moins, je dois créer un DataTemplate dans mon fichier de ressources ( app.xaml ou autre):

    

Eh bien c’est tout, je peux maintenant appeler des dialogs à partir de mes modèles de vue:

  var result = this.uiDialogService.ShowDialog("Dialogwindow Title", dialogwindowVM); 

Maintenant ma question, voyez-vous des problèmes avec cette solution?

Modifier: pour être complet. Le ViewModel devrait implémenter IDialogResultVMHelper et ensuite il peut le OkCommand dans un OkCommand ou quelque chose comme ça:

 public class MyViewmodel : IDialogResultVMHelper { private readonly Lazy _okCommand; public MyViewmodel() { this._okCommand = new Lazy(() => new DelegateCommand(() => InvokeRequestCloseDialog( new RequestCloseDialogEventArgs(true)), () => YourConditionsGoesHere = true)); } public ICommand OkCommand { get { return this._okCommand.Value; } } public event EventHandler RequestCloseDialog; private void InvokeRequestCloseDialog(RequestCloseDialogEventArgs e) { var handler = RequestCloseDialog; if (handler != null) handler(this, e); } } 

EDIT 2: J’ai utilisé le code d’ici pour rendre mon registre EventHandler faible:
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
(Le site Web n’existe plus, WebArchive Mirror )

 public delegate void UnregisterCallback(EventHandler eventHandler) where TE : EventArgs; public interface IWeakEventHandler where TE : EventArgs { EventHandler Handler { get; } } public class WeakEventHandler : IWeakEventHandler where T : class where TE : EventArgs { private delegate void OpenEventHandler(T @this, object sender, TE e); private readonly WeakReference mTargetRef; private readonly OpenEventHandler mOpenHandler; private readonly EventHandler mHandler; private UnregisterCallback mUnregister; public WeakEventHandler(EventHandler eventHandler, UnregisterCallback unregister) { mTargetRef = new WeakReference(eventHandler.Target); mOpenHandler = (OpenEventHandler)Delegate.CreateDelegate( typeof(OpenEventHandler),null, eventHandler.Method); mHandler = Invoke; mUnregister = unregister; } public void Invoke(object sender, TE e) { T target = (T)mTargetRef.Target; if (target != null) mOpenHandler.Invoke(target, sender, e); else if (mUnregister != null) { mUnregister(mHandler); mUnregister = null; } } public EventHandler Handler { get { return mHandler; } } public static implicit operator EventHandler(WeakEventHandler weh) { return weh.mHandler; } } public static class EventHandlerUtils { public static EventHandler MakeWeak(this EventHandler eventHandler, UnregisterCallback unregister) where TE : EventArgs { if (eventHandler == null) throw new ArgumentNullException("eventHandler"); if (eventHandler.Method.IsStatic || eventHandler.Target == null) throw new ArgumentException("Only instance methods are supported.", "eventHandler"); var wehType = typeof(WeakEventHandler).MakeGenericType( eventHandler.Method.DeclaringType, typeof(TE)); var wehConstructor = wehType.GetConstructor(new Type[] { typeof(EventHandler), typeof(UnregisterCallback) }); IWeakEventHandler weh = (IWeakEventHandler)wehConstructor.Invoke( new object[] { eventHandler, unregister }); return weh.Handler; } } 

C’est une bonne approche et j’ai utilisé des approches similaires par le passé. Fonce!

Une chose mineure que je ferais certainement est de faire en sorte que l’événement reçoive un booléen lorsque vous devez définir “false” dans le DialogResult.

 event EventHandler RequestCloseDialog; 

et la classe EventArgs:

 public class RequestCloseEventArgs : EventArgs { public RequestCloseEventArgs(bool dialogResult) { this.DialogResult = dialogResult; } public bool DialogResult { get; private set; } } 

J’utilise une approche presque identique depuis plusieurs mois maintenant et j’en suis très content (c’est-à-dire que je n’ai pas encore ressenti le besoin de le réécrire complètement …)

Dans mon implémentation, j’utilise un IDialogViewModel qui expose des choses telles que le titre, les boutons standad à afficher (afin d’avoir une apparence cohérente dans toutes les boîtes de dialog), un événement RequestClose et quelques autres choses pour pouvoir contrôler la fenêtre. taille et comportement

Si vous parlez de fenêtres de dialog et pas seulement des boîtes de message contextuelles, veuillez considérer mon approche ci-dessous. Les points clés sont:

  1. Je passe une référence à Module Controller dans le constructeur de chaque ViewModel (vous pouvez utiliser l’injection).
  2. Ce Module Controller a des méthodes publiques / internes pour créer des fenêtres de dialog (il suffit de créer, sans renvoyer de résultat). Par conséquent, pour ouvrir une fenêtre de dialog dans ViewModel j’écris: controller.OpenDialogEntity(bla, bla...)
  3. Chaque fenêtre de dialog informe de son résultat (comme OK , Enregistrer , Annuler , etc.) via Evénements faibles . Si vous utilisez PRISM, il est plus facile de publier des notifications à l’aide de cet EventAggregator .
  4. Pour gérer les résultats du dialog, j’utilise l’abonnement aux notifications (encore une fois Weak Events et EventAggregator dans le cas de PRISM). Pour réduire la dépendance à de telles notifications, utilisez des classes indépendantes avec des notifications standard.

Avantages:

  • Moins de code. Cela ne me dérange pas d’utiliser des interfaces, mais j’ai vu trop de projets où la trop grande utilisation des interfaces et des couches d’abstraction causait plus de problèmes que d’aide.
  • Ouvrir des fenêtres de dialog via Module Controller est un moyen simple d’éviter des références fortes et d’utiliser des maquettes pour les tests.
  • La notification des événements faibles réduit le nombre de memory leaks potentielles.

Les inconvénients:

  • Pas facile de distinguer les notifications requirejses des autres utilisateurs du gestionnaire. Deux solutions:
    • envoyer un jeton unique à l’ouverture d’une fenêtre de dialog et vérifier ce jeton dans l’abonnement
    • utiliser des classes de notification génériques T est l’énumération des entités (ou pour simplifier, il peut s’agir du type de ViewModel).
  • Pour un projet, il devrait s’agir d’un accord sur l’utilisation des classes de notification pour éviter leur duplication.
  • Pour les projets de très grande taille, le Module Controller peut être dépassé par des méthodes de création de fenêtres. Dans ce cas, il est préférable de le diviser en plusieurs modules.

PS J’utilise cette approche depuis assez longtemps maintenant et je suis prêt à défendre son éligibilité dans les commentaires et à fournir des exemples si nécessaire.