153 votes

Bonne ou mauvaise pratique pour les boîtes de dialogue dans wpf avec MVVM ?

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

Tout ce que je voulais faire dans mon code était quelque chose comme ceci. (J'utilise principalement l'approche viewmodel first 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 cela fonctionne-t-il ?

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 boîtes de dialogue dans wpf est le suivant dialogresult = true ne peut être réalisée que dans le code. C'est pourquoi j'ai créé une interface pour mon programme dialogviewmodel pour le mettre en œuvre.

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 de dialogresult = true puis déclencher 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, je dois au moins créer un DataTemplate dans mon fichier de ressources( app.xaml ou autre) :

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

C'est tout, 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 et il peut ensuite l'augmenter dans un OkCommand ou 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 que mon EventHandler s'enregistre faiblement :
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
(Le site web n'existe plus, Miroir WebArchive )

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 votes

Il vous manque probablement le xmlns:x=" schémas.microsoft.com/winfx/2006/xaml "dans votre XAML WindowDialog.

0 votes

En fait, l'espace de noms est xmlns:x="[http://\]schemas.microsoft.com/winfx/2006/xaml" sans les parenthèses

0 votes

50voto

Julian Dominguez Points 1521

C'est une bonne approche et j'en ai utilisé des similaires dans le passé. Allez-y !

Une petite chose que je ferais certainement est de faire en sorte que l'événement reçoive un booléen pour le cas où vous auriez besoin de mettre "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; }
}

0 votes

Et si au lieu d'utiliser des services, on utilisait une sorte de Callback pour faciliter l'interaction avec le ViewModel et le View ? Par exemple, la vue exécute une commande dans le ViewModel, puis lorsque tout est dit et fait, le ViewModel déclenche un Callback pour que la vue affiche les résultats de la commande. Je n'arrive toujours pas à convaincre mon équipe d'utiliser les services pour gérer les interactions de dialogue dans le ViewModel.

16voto

Thomas Levesque Points 141081

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

Dans mon implémentation, j'utilise un IDialogViewModel qui expose des éléments tels que le titre, les boutons standad à afficher (afin d'avoir une apparence cohérente dans toutes les boîtes de dialogue), une icône RequestClose et quelques autres éléments permettant de contrôler la taille et le comportement de la fenêtre.

0 votes

Thx, le titre devrait vraiment aller dans mon IDialogViewModel. les autres propriétés comme la taille, le bouton standard je vais laisser, parce que tout cela vient du modèle de données au moins.

1 votes

C'est ce que j'ai fait au début aussi, il suffit d'utiliser SizeToContent pour contrôler la taille de la fenêtre. Mais dans un cas, j'ai eu besoin de rendre la fenêtre redimensionnable, alors j'ai dû le modifier un peu...

0 votes

@ThomasLevesque les boutons contenus dans votre ViewModel, sont-ils réellement des objets boutons UI ou des objets représentant des boutons ?

3voto

Klaus Points 1023

Si vous parlez des 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 les suivants :

  1. Je passe une référence à Module Controller dans le constructeur de chaque ViewModel (vous pouvez utiliser l'injection).
  2. Ce Module Controller possède des méthodes publiques/internes pour créer des fenêtres de dialogue (juste créer, sans retourner un résultat). Ainsi, pour ouvrir une fenêtre de dialogue dans ViewModel J'écris : controller.OpenDialogEntity(bla, bla...)
  3. Chaque fenêtre de dialogue notifie son résultat (comme OK , Sauvez , Annuler etc.) via Événements faibles . Si vous utilisez PRISM, il est plus facile de publier des notifications à l'aide de la fonction Cet agrégateur d'événements .
  4. Pour gérer les résultats du dialogue, j'utilise l'abonnement aux notifications (à nouveau Événements faibles y Agrégateur d'événements dans le cas de PRISM). Pour réduire la dépendance à l'égard de ces notifications, utilisez des classes indépendantes avec des notifications standard.

Pour :

  • Moins de code. L'utilisation d'interfaces ne me dérange pas, 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'avantages.
  • Ouvrir des fenêtres de dialogue par Module Controller est un moyen simple d'éviter les références fortes tout en permettant d'utiliser des maquettes pour les tests.
  • La notification par le biais d'événements faibles réduit le nombre de fuites de mémoire potentielles.

Cons :

  • Il n'est 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
    • utiliser des classes de notification génériques <T> donde T est une énumération d'entités (ou, pour simplifier, un type de 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, la Module Controller peuvent être submergés par les méthodes de création de Windows. Dans ce cas, il est préférable de le diviser en plusieurs modules.

P.S. J'utilise cette approche depuis assez longtemps maintenant et je suis prêt à défendre son admissibilité dans les commentaires et à fournir quelques exemples si nécessaire.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X