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

148

Ich hatte kürzlich das Problem, Dialogfelder zum Hinzufügen und Bearbeiten für meine wpf-App zu erstellen.

Alles, was ich in meinem Code tun möchte, war so etwas. (Ich verwende meistens den ersten Ansatz von viewmodel 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 Dialogdienst 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();
    }
}

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

<Window x:Class="WindowDialog"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    Title="WindowDialog" 
    WindowStyle="SingleBorderWindow" 
    WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight">
    <ContentPresenter x:Name="DialogPresenter" Content="{Binding .}">

    </ContentPresenter>
</Window>

Ein Problem mit Dialogen in wpf ist, dass dialogresult = truedies nur im Code erreicht werden kann. Deshalb habe ich eine Schnittstelle erstellt, über die ich dialogviewmodelsie implementieren kann.

public class RequestCloseDialogEventArgs : EventArgs
{
    public bool DialogResult { get; set; }
    public RequestCloseDialogEventArgs(bool dialogresult)
    {
        this.DialogResult = dialogresult;
    }
}

public interface IDialogResultVMHelper
{
    event EventHandler<RequestCloseDialogEventArgs> RequestCloseDialog;
}

Wenn mein ViewModel glaubt, dass es Zeit für ist dialogresult = true, lösen Sie dieses Ereignis aus.

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<RequestCloseDialogEventArgs>
                                    (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 eine DataTemplatein meiner Ressourcendatei erstellen ( app.xamloder so):

<DataTemplate DataType="{x:Type DialogViewModel:EditOrNewAuswahlItemVM}" >
        <DialogView:EditOrNewAuswahlItem/>
</DataTemplate>

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

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

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

Bearbeiten: der Vollständigkeit halber. Das ViewModel sollte implementiert werden IDialogResultVMHelperund kann es dann in einem OkCommandoder einem ähnlichen Bereich auslösen:

public class MyViewmodel : IDialogResultVMHelper
{
    private readonly Lazy<DelegateCommand> _okCommand;

    public MyViewmodel()
    {
         this._okCommand = new Lazy<DelegateCommand>(() => 
             new DelegateCommand(() => 
                 InvokeRequestCloseDialog(
                     new RequestCloseDialogEventArgs(true)), () => 
                         YourConditionsGoesHere = true));
    }

    public ICommand OkCommand
    { 
        get { return this._okCommand.Value; } 
    }

    public event EventHandler<RequestCloseDialogEventArgs> RequestCloseDialog;
    private void InvokeRequestCloseDialog(RequestCloseDialogEventArgs e)
    {
        var handler = RequestCloseDialog;
        if (handler != null) 
            handler(this, e);
    }
 }

BEARBEITEN 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<TE>(EventHandler<TE> eventHandler) 
    where TE : EventArgs;

public interface IWeakEventHandler<TE> 
    where TE : EventArgs
{
    EventHandler<TE> Handler { get; }
}

public class WeakEventHandler<T, TE> : IWeakEventHandler<TE> 
    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<TE> mHandler;
    private UnregisterCallback<TE> mUnregister;

    public WeakEventHandler(EventHandler<TE> eventHandler,
                                UnregisterCallback<TE> 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<TE> Handler
    {
        get { return mHandler; }
    }

    public static implicit operator EventHandler<TE>(WeakEventHandler<T, TE> weh)
    {
        return weh.mHandler;
    }
}

public static class EventHandlerUtils
{
    public static EventHandler<TE> MakeWeak<TE>(this EventHandler<TE> eventHandler, 
                                                    UnregisterCallback<TE> 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<TE>), typeof(UnregisterCallback<TE>) 
                             });

        IWeakEventHandler<TE> weh = (IWeakEventHandler<TE>)wehConstructor.Invoke(
                                        new object[] { eventHandler, unregister });

        return weh.Handler;
    }
}
blindmeis
quelle
1
Wahrscheinlich fehlt Ihnen die Referenz xmlns : x = " schemas.microsoft.com/winfx/2006/xaml " in Ihrer WindowDialog XAML.
Adiel Yaacov
Eigentlich ist der Namespace xmlns: x = "[http: //] schemas.microsoft.com/winfx/2006/xaml" ohne die Klammern
reggaeguitar
1
Hallo! Nachzügler hier. Ich verstehe nicht, wie Ihr Dienst auf den WindowDialog verweist. Wie ist die Hierarchie Ihrer Modelle? In meinen Augen enthält die Ansicht einen Verweis auf die Viewmodel-Assembly und das Viewmodel auf die Service- und Model-Assemblys. Dadurch hätte die Service-Schicht keine Kenntnis von der WindowDialog-Ansicht. Was vermisse ich?
Moe45673
2
Hallo @blindmeis, ich versuche nur, mich mit diesem Konzept zu beschäftigen. Ich nehme nicht an, dass es ein Online-Beispielprojekt gibt, das ich auswählen kann. Es gibt eine Reihe von Dingen, über die ich verwirrt bin.
Hank

Antworten:

48

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

Eine Kleinigkeit, die ich definitiv tun würde, ist, das Ereignis einen Booleschen Wert zu erhalten, wenn Sie im DialogResult "false" setzen müssen.

event EventHandler<RequestCloseEventArgs> RequestCloseDialog;

und die EventArgs-Klasse:

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

    public bool DialogResult { get; private set; }
}
Julian Dominguez
quelle
Was ist, wenn anstelle von Diensten eine Art Rückruf verwendet wird, um die Interaktion mit dem ViewModel und der Ansicht zu erleichtern? Beispielsweise führt View einen Befehl im ViewModel aus. Wenn alles gesagt und getan ist, löst das ViewModel einen Rückruf für die View aus, um die Ergebnisse des Befehls anzuzeigen. Ich kann mein Team immer noch nicht dazu bringen, Dienste für die Verarbeitung von Dialoginteraktionen im ViewModel zu verwenden.
Matthew S
15

Ich benutze seit einigen Monaten einen fast identischen Ansatz und bin sehr zufrieden damit (dh ich habe noch nicht den Drang verspürt, ihn komplett neu zu schreiben ...)

In meiner Implementierung verwende ich a IDialogViewModel, das Dinge wie den Titel, die anzuzeigenden Standad-Schaltflächen (um über alle Dialoge hinweg eine einheitliche Darstellung zu erhalten), ein RequestCloseEreignis und einige andere Dinge zur Steuerung der Fenstergröße und anzeigt Verhalten

Thomas Levesque
quelle
thx, der titel sollte wirklich in mein IDialogViewModel passen. Die anderen Eigenschaften wie Größe, Standardschaltfläche werde ich verlassen, da dies alles zumindest aus der Datenvorlage stammt.
Blindmeis
1
Das habe ich auch zuerst getan. Verwenden Sie einfach SizeToContent, um die Größe des Fensters zu steuern. Aber in einem Fall musste ich die Größe des Fensters ändern, also musste ich es ein wenig optimieren ...
Thomas Levesque
@ThomasLevesque die in Ihrem ViewModel enthaltenen Schaltflächen, sind sie tatsächlich UI-Schaltflächenobjekte oder Objekte, die Schaltflächen darstellen?
Thomas
3
@Thomas, Objekte, die Schaltflächen darstellen. Sie sollten im ViewModel niemals auf UI-Objekte verweisen.
Thomas Levesque
2

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 Controllerden Konstruktor eines jeden ViewModel(Sie können die Injektion verwenden).
  2. Das Module Controllerhat öffentliche / interne Methoden zum Erstellen von Dialogfenstern (nur erstellen, ohne ein Ergebnis zurückzugeben). Um ein Dialogfenster in zu öffnen, ViewModelschreibe ich:controller.OpenDialogEntity(bla, bla...)
  3. Jedes Dialogfenster benachrichtigt über schwache Ereignisse über sein Ergebnis (wie OK , Speichern , Abbrechen usw.) . Wenn Sie PRISM verwenden, ist es einfacher, Benachrichtigungen mit diesem EventAggregator zu veröffentlichen .
  4. Um die Dialogergebnisse zu verarbeiten, verwende ich ein Abonnement für Benachrichtigungen (erneut schwache Ereignisse und EventAggregator bei PRISM). Verwenden Sie unabhängige Klassen mit Standardbenachrichtigungen, um die Abhängigkeit von solchen Benachrichtigungen zu verringern.

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 Abstraktionsebenen mehr Probleme als Hilfe verursacht.
  • Das Öffnen von Dialogfenstern Module Controllerist ein einfacher Weg, um starke Referenzen zu vermeiden, und ermöglicht es dennoch, Modelle zum Testen zu verwenden.
  • Benachrichtigungen durch schwache Ereignisse reduzieren die Anzahl potenzieller Speicherverluste.

Nachteile:

  • Es ist nicht einfach, die erforderliche Benachrichtigung von anderen im Handler zu unterscheiden. Zwei Lösungen:
    • Senden Sie beim Öffnen eines Dialogfensters ein eindeutiges Token und überprüfen Sie dieses Token im Abonnement
    • Verwenden Sie generische Benachrichtigungsklassen, in <T>denen TEntitäten aufgelistet sind (oder der Einfachheit halber kann es sich um einen ViewModel-Typ handeln).
  • Für ein Projekt sollte eine Vereinbarung über die Verwendung von Benachrichtigungsklassen getroffen werden, um zu verhindern, dass diese dupliziert werden.
  • Bei enorm großen Projekten Module Controllerkann dies durch Methoden zum Erstellen von Fenstern überfordert sein. In diesem Fall ist es besser, es in mehrere Module aufzuteilen.

PS Ich verwende diesen Ansatz bereits seit geraumer Zeit und bin bereit, seine Berechtigung in Kommentaren zu verteidigen und bei Bedarf einige Beispiele anzugeben.

Alex Klaus
quelle