Buone o cattive pratiche per i dialoghi in wpf con MVVM?


148

Di recente ho avuto il problema di creare finestre di dialogo di aggiunta e modifica per la mia app wpf.

Tutto quello che voglio fare nel mio codice è stato qualcosa del genere. (Uso principalmente viewmodel primo approccio con mvvm)

ViewModel che chiama una finestra di dialogo:

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

Come funziona?

Innanzitutto, ho creato un servizio di dialogo:

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è una finestra speciale ma semplice. Ne ho bisogno per contenere i miei contenuti:

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

Un problema con le finestre di dialogo in wpf è che dialogresult = truepuò essere raggiunto solo nel codice. Ecco perché ho creato un'interfaccia per dialogviewmodelimplementarla.

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

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

Ogni volta che il mio ViewModel pensa che sia il momento per dialogresult = true, quindi sollevare questo evento.

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

Ora almeno devo creare un DataTemplatenel mio file di risorse ( app.xamlo qualcosa del genere):

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

Bene, tutto qui, ora posso chiamare dialoghi dai miei modelli di visualizzazione:

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

Ora la mia domanda, vedi qualche problema con questa soluzione?

Modifica: per completezza. ViewModel dovrebbe implementarsi IDialogResultVMHelpere quindi può sollevarlo all'interno di OkCommandqualcosa del genere:

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

EDIT 2: Ho usato il codice da qui per rendere il mio registro EventHandler debole:
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
(Il sito Web non esiste più, 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;
    }
}

1
probabilmente ti manca il riferimento xmlns: x = " schemas.microsoft.com/winfx/2006/xaml " nel tuo XAML WindowDialog.
Adiel Yaacov,

In realtà lo spazio dei nomi è xmlns: x = "[http: //] schemas.microsoft.com/winfx/2006/xaml" senza parentesi
reggaeguitar


1
Ciao! Ritardatario qui. Non capisco come il tuo Servizio abbia un riferimento a WindowDialog. Qual è la gerarchia dei tuoi modelli? Nella mia mente, la View contiene un riferimento all'assembly Viewmodel e Viewmodel agli assembly Service e Model. Pertanto, il livello di servizio non avrebbe alcuna conoscenza della vista WindowDialog. Cosa mi sto perdendo?
Moe45673,

2
Ciao @blindmeis, sto solo cercando di avvolgere la mia testa attorno a questo concetto, non credo che ci sia qualche progetto di esempio online che posso prendere? Ci sono molte cose di cui sono confuso.
Hank

Risposte:


48

Questo è un buon approccio e ne ho usati di simili in passato. Fallo!

Una cosa minore che sicuramente farei è rendere l'evento booleano per quando è necessario impostare "false" in DialogResult.

event EventHandler<RequestCloseEventArgs> RequestCloseDialog;

e la classe EventArgs:

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

    public bool DialogResult { get; private set; }
}

Che cosa succede se invece di utilizzare i servizi, si utilizza una sorta di callback per facilitare l'interazione con ViewModel e View? Ad esempio, View esegue un comando in ViewModel, quindi quando tutto è detto e fatto, ViewModel genera un callback per la visualizzazione per visualizzare i risultati del comando. Non riesco ancora a coinvolgere il mio team con l'utilizzo dei servizi per gestire le interazioni dei dialoghi nel ViewModel.
Matthew S,

15

Sto usando un approccio quasi identico da diversi mesi e ne sono molto contento (cioè non ho ancora sentito l'impulso di riscriverlo completamente ...)

Nella mia implementazione, utilizzo un oggetto IDialogViewModelche espone elementi come il titolo, i pulsanti standard da mostrare (al fine di avere un'apparenza coerente in tutte le finestre di dialogo), un RequestCloseevento e poche altre cose per essere in grado di controllare le dimensioni della finestra e comportamento


grazie, il titolo dovrebbe davvero andare nel mio IDialogViewModel. le altre proprietà come dimensione, pulsante standard lascerò, perché almeno tutto questo deriva dalla piastra dati.
blindmeis,

1
Questo è quello che ho fatto anche all'inizio, basta usare SizeToContent per controllare le dimensioni della finestra. Ma in un caso dovevo rendere la finestra ridimensionabile, quindi ho dovuto modificarla un po '...
Thomas Levesque,

@ThomasLevesque i pulsanti contenuti nel ViewModel, sono in realtà oggetti UI Button o oggetti che rappresentano pulsanti?
Thomas,

3
@Thomas, oggetti che rappresentano i pulsanti. Non si dovrebbe mai fare riferimento a oggetti dell'interfaccia utente in ViewModel.
Thomas Levesque,

2

Se stai parlando di finestre di dialogo e non solo di finestre a comparsa, ti preghiamo di considerare il mio approccio di seguito. I punti chiave sono:

  1. Passo un riferimento al Module Controllercostruttore di ciascuno ViewModel(puoi usare l'iniezione).
  2. Questo Module Controllerha metodi pubblici / interni per la creazione di finestre di dialogo (solo per creare, senza restituire un risultato). Quindi per aprire una finestra di dialogo in ViewModelscrivo:controller.OpenDialogEntity(bla, bla...)
  3. Ogni finestra di dialogo avvisa del risultato (come OK , Salva , Annulla , ecc.) Tramite Eventi deboli . Se usi PRISM, è più semplice pubblicare notifiche usando questo EventAggregator .
  4. Per gestire i risultati del dialogo, sto usando l'abbonamento alle notifiche (di nuovo Eventi deboli ed EventAggregator in caso di PRISM). Per ridurre la dipendenza da tali notifiche, utilizzare classi indipendenti con notifiche standard.

Professionisti:

  • Meno codice. Non mi dispiace usare le interfacce, ma ho visto troppi progetti in cui l'eccessiva utilizzazione delle interfacce e dei livelli di astrazione causano più problemi che aiuti.
  • Aprire le finestre di dialogo Module Controllerè un modo semplice per evitare riferimenti forti e consente comunque di utilizzare i modelli per i test.
  • Le notifiche tramite eventi deboli riducono il numero di potenziali perdite di memoria.

Contro:

  • Non è facile distinguere la notifica richiesta dagli altri nel gestore. Due soluzioni:
    • invia un token univoco all'apertura di una finestra di dialogo e controlla quel token nell'abbonamento
    • utilizzare classi di notifica generiche in <T>cui Tè presente l'enumerazione di entità (o per semplicità può essere un tipo di ViewModel).
  • Per un progetto dovrebbe essere un accordo sull'uso delle classi di notifica per evitare di duplicarle.
  • Per progetti di grandi dimensioni Module Controllerpuò essere sopraffatto dai metodi per creare finestre. In questo caso è meglio suddividerlo in più moduli.

PS Sto usando questo approccio da molto tempo ormai e sono pronto a difendere la sua ammissibilità nei commenti e fornire alcuni esempi, se necessario.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.