Gute oder schlechte Praxis für Dialoge in WPF mit MVVM?

Ich hatte kürzlich das Problem, Hinzufügen und Bearbeiten von Dialogen für meine WPF-App zu erstellen.

Alles, was ich in meinem Code machen wollte, war so etwas. (Ich benutze meist Viewmodel ersten Ansatz mit MVVM)

ViewModel, das ein Dialogfenster aufruft:

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

Wie funktioniert es?

Zuerst habe ich einen Dialog-Service erstellt:

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

WindowDialog ist ein spezielles, aber einfaches Fenster. Ich brauche es, um meinen Inhalt zu halten:

     

Ein Problem mit Dialogen in WPF ist, dass das dialogresult = true nur im Code erreicht werden kann. Deshalb habe ich eine Schnittstelle für mein dialogviewmodel , um es zu implementieren.

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

Immer wenn mein ViewModel denkt, es ist Zeit für dialogresult = true , dann hebe dieses Event an.

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

Jetzt muss ich zumindest ein DataTemplate in meiner Ressourcendatei ( app.xaml oder so) erstellen:

    

Nun, das ist alles, ich kann jetzt Dialoge aus meinen Viewmodels aufrufen:

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

Nun meine Frage, sehen Sie irgendwelche Probleme mit dieser Lösung?

Bearbeiten: zur Vollständigkeit. Das ViewModel sollte IDialogResultVMHelper implementieren und dann kann es in einem OkCommand oder ähnlichem OkCommand :

 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: Ich habe den Code von hier verwendet, um mein EventHandler-Register schwach zu machen:
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
(Website existiert nicht mehr, 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; } } 

Dies ist ein guter Ansatz und ich habe ähnliche in der Vergangenheit verwendet. Tue es!

Eine kleine Sache, die ich definitiv tun würde, ist, dass das Ereignis einen booleschen Wert erhält, wenn Sie im DialogResult “false” setzen müssen.

 event EventHandler RequestCloseDialog; 

und die EventArgs-class:

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

Ich verwende seit einigen Monaten einen fast identischen Ansatz, und ich bin sehr glücklich damit (dh ich habe noch nicht den Drang verspürt, es komplett neu zu schreiben …)

In meiner Implementierung verwende ich ein IDialogViewModel , das Dinge wie den Titel, die Standardschaltflächen (um eine konsistente Erscheinung über alle Dialoge zu haben), ein RequestClose Ereignis und einige andere Dinge, die das Fenster steuern können, RequestClose Größe und Verhalten

Wenn Sie über Dialogfenster und nicht nur über die Popup-Meldungsfelder sprechen, beachten Sie bitte meinen Ansatz unten. Die wichtigsten Punkte sind:

  1. Ich übergebe einen Verweis auf Module Controller in den Konstruktor jedes ViewModel (Sie können injection verwenden).
  2. Dieser Module Controller verfügt über öffentliche / interne Methoden zum Erstellen von Dialogfenstern (einfach erstellen, ohne ein Ergebnis zurückzuliefern). Um also ein Dialogfenster in ViewModel zu öffnen, schreibe ich: controller.OpenDialogEntity(bla, bla...)
  3. Jedes Dialogfenster meldet über schwache Ereignisse (wie OK , Speichern , Abbrechen usw.). Wenn Sie PRISM verwenden, ist es einfacher, Benachrichtigungen mit diesem EventAggregator zu veröffentlichen .
  4. Um mit den Dialogergebnissen umgehen zu können, verwende ich ein Abonnement für Benachrichtigungen (im Falle von PRISM wiederum Weak Events und EventAggregator ). Um die Abhängigkeit von solchen Benachrichtigungen zu verringern, verwenden Sie unabhängige classn mit Standardbenachrichtigungen.

Vorteile:

  • Weniger Code Es macht mir nichts aus, Schnittstellen zu verwenden, aber ich habe zu viele Projekte gesehen, bei denen die übermäßige Verwendung von Schnittstellen und Abstraktionsschichten mehr Probleme als Hilfe bereitet.
  • Das Öffnen von Dialogfenstern über den Module Controller ist eine einfache Möglichkeit, starke Referenzen zu vermeiden und dennoch das Verwenden von Modellen zum Testen zu ermöglichen.
  • Benachrichtigungen durch schwache Ereignisse verringern die Anzahl möglicher Speicherlecks.

Nachteile:

  • Nicht leicht zu unterscheiden erforderliche Benachrichtigung von anderen im Handler. Zwei Lösungen:
    • Senden Sie beim Öffnen eines Dialogfensters ein eindeutiges Token und prüfen Sie das Token im Abonnement
    • Verwenden Sie generische Benachrichtigungsklassen wobei T Enumeration von Entitäten ist (oder zur Vereinfachung kann es sich um ViewModel-Typen handeln).
  • Für ein Projekt sollte eine Vereinbarung über die Verwendung von Benachrichtigungsklassen getroffen werden, um deren Duplizierung zu verhindern.
  • Bei enorm großen Projekten kann der Module Controller durch Methoden zur Erstellung von Fenstern überfordert sein. In diesem Fall ist es besser, es in mehrere Module aufzuteilen.

PS Ich verwende diesen Ansatz schon seit längerer Zeit und bin bereit, seine Eignung in Kommentaren zu verteidigen und bei Bedarf einige Beispiele zu nennen.