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

148

J'ai récemment eu le problème de créer des boîtes de dialogue 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 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();
    }
}

WindowDialogest 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 dialogresult = trueque cela ne peut être réalisé que dans le code. C'est pourquoi j'ai créé une interface pour l' dialogviewmodelimplémenter.

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 dialogresult = true, déclenchez 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 DataTemplatedans mon fichier de ressources ( app.xamlou quelque chose):

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

Eh bien c'est tout, je peux maintenant appeler des boîtes de dialogue à 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: par souci d'exhaustivité. Le ViewModel devrait implémenter IDialogResultVMHelperet ensuite il peut le lever dans un OkCommandou quelque chose comme ça:

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 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<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;
    }
}
aveugle
la source
1
il vous manque probablement le refernece xmlns: x = " schemas.microsoft.com/winfx/2006/xaml " dans votre WindowDialog XAML.
Adiel Yaacov
En fait, l'espace de noms est xmlns: x = "[http: //] schemas.microsoft.com/winfx/2006/xaml" sans les crochets
reggaeguitar
1
Salut! Retard ici. Je ne comprends pas comment votre service fait référence à WindowDialog. Quelle est la hiérarchie de vos modèles? Dans mon esprit, la vue contient une référence à l'assembly Viewmodel et le Viewmodel aux assemblys Service et Model. Ainsi, la couche Service n'aurait aucune connaissance de la vue WindowDialog. Qu'est-ce que je rate?
Moe45673
2
Salut @blindmeis, j'essaie juste de comprendre ce concept, je ne suppose pas qu'il y ait un exemple de projet en ligne que je puisse choisir? Il y a un certain nombre de choses sur lesquelles je suis confus.
Hank

Réponses:

48

C'est une bonne approche et j'ai utilisé des approches similaires dans 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<RequestCloseEventArgs> RequestCloseDialog;

et la classe EventArgs:

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

    public bool DialogResult { get; private set; }
}
Julian Dominguez
la source
Et si au lieu d'utiliser des services, on utilisait une sorte de Callback pour faciliter l'interaction avec le ViewModel et la View? Par exemple, View exécute une commande dans le ViewModel, puis lorsque tout est dit et fait, le ViewModel déclenche un rappel pour que la vue affiche les résultats de la commande. Je ne parviens toujours pas à intégrer mon équipe à l'utilisation des services pour gérer les interactions de dialogue dans le ViewModel.
Matthew S
15

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 la réécrire complètement ...)

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

Thomas Levesque
la source
thx, le titre devrait vraiment aller dans mon IDialogViewModel. les autres propriétés comme la taille, le bouton standard que je laisserai, car tout cela provient au moins du modèle de données.
blindmeis
1
C'est ce que j'ai fait au début aussi, utilisez simplement SizeToContent pour contrôler la taille de la fenêtre. Mais dans un cas, je devais rendre la fenêtre redimensionnable, alors j'ai dû la peaufiner un peu ...
Thomas Levesque
@ThomasLevesque les boutons contenus dans votre ViewModel, sont-ils réellement des objets UI Button ou des objets représentant des boutons?
Thomas
3
@Thomas, objets représentant des boutons. Vous ne devez jamais référencer des objets d'interface utilisateur dans ViewModel.
Thomas Levesque
2

Si vous parlez de fenêtres de dialogue 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 Controllerdans le constructeur de chacun ViewModel(vous pouvez utiliser l'injection).
  2. Cela Module Controllera des méthodes publiques / internes pour créer des fenêtres de dialogue (simplement créer, sans renvoyer de résultat). Par conséquent pour ouvrir une fenêtre de dialogue dans ViewModelj'écris:controller.OpenDialogEntity(bla, bla...)
  3. Chaque fenêtre de dialogue informe de son résultat (comme OK , Enregistrer , Annuler , etc.) via des évé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 dialogue, j'utilise l'abonnement aux notifications (encore une fois Weak Events et EventAggregator en cas de PRISM). Pour réduire la dépendance à ces 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 causait plus de problèmes que d'aide.
  • Ouvrir les fenêtres de dialogue via Module Controller est un moyen simple d'éviter les références fortes 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.

Les 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 des classes de notification génériques <T>où se Ttrouve l'énumération des entités (ou pour plus de simplicité, il peut s'agir du type ViewModel).
  • Pour un projet, il devrait y avoir un accord sur l'utilisation des classes de notification pour éviter de les dupliquer.
  • Pour les projets de très grande envergure, les Module Controllerméthodes de création de fenêtres peuvent déborder. 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 à donner quelques exemples si nécessaire.

Alex Klaus
la source