web-dev-qa-db-fra.com

Bonne ou mauvaise pratique pour les dialogues dans wpf avec MVVM?

J'ai récemment eu le problème de créer des dialogues d'ajout et d'édition pour mon application wpf.

Tout ce que je veux faire dans mon code était quelque chose comme ça. (J'utilise principalement viewmodel d'abord avec mvvm)

ViewModel qui appelle une fenêtre de dialogue:

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

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 est une fenêtre spéciale mais simple. J'en ai besoin pour contenir mon contenu:

<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 problème avec les dialogues dans wpf est le dialogresult = true ne peut être réalisé qu'en code. C'est pourquoi j'ai créé une interface pour que mon dialogviewmodel l'implémente.

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

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

Chaque fois que mon ViewModel pense qu'il est temps pour dialogresult = true, puis relance 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<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;
    }
 }

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

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

Bien, je peux maintenant appeler des dialogues à 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?

Edit: pour être complet. Le ViewModel doit implémenter IDialogResultVMHelper pour ensuite le relever dans un OkCommand ou quelque chose comme ceci:

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: J'ai utilisé le code d'ici pour affaiblir mon registre EventHandler:
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
(Le site Web n'existe plus, 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;
    }
}
141
blindmeis

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

Une chose mineure que je ferais assurément serait que l'événement reçoive un booléen lorsque vous devez définir "false" dans DialogResult.

event EventHandler<RequestCloseEventArgs> RequestCloseDialog;

et la classe EventArgs:

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

    public bool DialogResult { get; private set; }
}
46
Julian Dominguez

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

Dans mon implémentation, j’utilise IDialogViewModel qui expose des éléments tels que le titre, les boutons à afficher (pour conserver une apparence cohérente dans tous les dialogues), un événement RequestClose et un peu d'autres choses pour pouvoir contrôler la taille et le comportement de la fenêtre

15
Thomas Levesque

Si vous parlez de fenêtres de dialogue et pas seulement de 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 dialogue (juste pour créer, sans renvoyer de résultat). Par conséquent, pour ouvrir une fenêtre de dialogue dans ViewModel j’écris: controller.OpenDialogEntity(bla, bla...)
  3. Chaque fenêtre de dialogue informe de son résultat (comme [~ # ~] ok [~ # ~], Enregistrer, Annuler =, etc.) via événements faibles . Si vous utilisez PRISM, il est alors plus facile de publier des notifications à l'aide de this EventAggregator .
  4. Pour gérer les résultats du dialogue, j'utilise un abonnement aux notifications (à nouveau Événements faibles et EventAggregator dans le cas de PRISM). Pour réduire la dépendance à l'égard 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ù l'utilisation excessive d'interfaces et de couches d'abstraction pose plus de problèmes que d'aide.
  • Ouvrir les fenêtres de dialogue via Module Controller Est un moyen simple d’éviter les fortes références et permet toujours d’utiliser des maquettes pour les tests.
  • La notification via des événements faibles réduit le nombre de fuites de mémoire potentielles.

Inconvénients:

  • Pas facile de distinguer la notification requise des autres dans le gestionnaire. Deux solutions:
    • envoyer un jeton unique à l'ouverture d'une fenêtre de dialogue et vérifier ce jeton dans l'abonnement
    • utilisez les classes de notification génériques <T>T représente une énumération d'entités (ou, pour simplifier, ce peut être le type de ViewModel).
  • Pour un projet, un accord sur l’utilisation des classes de notification est nécessaire pour éviter leur duplication.
  • Pour des projets extrêmement volumineux, Module Controller Peut être submergé par les méthodes de création de fenêtres. Dans ce cas, il est préférable de le scinder en plusieurs modules.

P.S. J'utilise cette approche depuis assez longtemps et je suis prête à défendre son éligibilité dans des commentaires et à donner quelques exemples si nécessaire.

2
Alex Klaus