¿Buenas o malas prácticas para diálogos en wpf con MVVM?


Últimamente tuve el problema de crear diálogos de adición y edición para mi aplicación wpf.

Todo lo que quiero hacer en mi código es algo como esto. (Principalmente uso el primer enfoque de viewmodel con mvvm)

ViewModel que llama a una ventana de diálogo:

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

¿Cómo funciona?

Primero, creé un servicio de diálogo:

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 es una ventana especial pero simple. Lo necesito para mantener mi contenido:

<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 los diálogos en wpf es el dialogresult = true puede solo se logrará en código. Es por eso que creé una interfaz para mi dialogviewmodel para implementarla.

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

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

Cuando mi ViewModel crea que es hora de dialogresult = true, entonces plantea este 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;
    }
 }

Ahora al menos tengo que crear un DataTemplate en mi archivo de recursos (app.xaml o algo así):

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

Bueno, eso es todo, ahora puedo llamar a diálogos desde mis viewmodels:{[18]]}

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

Ahora mi pregunta, ¿ves algún problema con esta solución?

Editar: para completar. El ViewModel debe implementar IDialogResultVMHelper y luego puede elevarlo dentro de un OkCommand o algo así:

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: Usé el código de aquí para hacer mi registro de EventHandler weak:
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
(El sitio web ya no existe, 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;
    }
}
Author: AndyUK, 2010-09-27

3 answers

Este es un buen enfoque y usé otros similares en el pasado. ¡Adelante!

Una cosa menor que definitivamente haría es hacer que el evento reciba un booleano para cuando necesite establecer "false" en el DialogResult.

event EventHandler<RequestCloseEventArgs> RequestCloseDialog;

Y la clase EventArgs:

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

    public bool DialogResult { get; private set; }
}
 45
Author: Julian Dominguez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-02-10 17:57:19

He estado usando un enfoque casi idéntico durante varios meses, y estoy muy contento con él (es decir, todavía no he sentido la necesidad de reescribirlo completamente...)

En mi implementación, uso un IDialogViewModel que expone cosas como el título, los botones standad para mostrar (para tener una apariencia consistente en todos los diálogos), un evento RequestClose y algunas otras cosas para poder controlar el tamaño y el comportamiento de la ventana

 15
Author: Thomas Levesque,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-02-09 22:56:18

Si está hablando de ventanas de diálogo y no solo de los cuadros de mensajes emergentes, considere mi enfoque a continuación. Los puntos clave son:

  1. Paso una referencia a Module Controller en el constructor de cada ViewModel (puede usar inyección).
  2. Que Module Controller tiene métodos públicos/internos para crear ventanas de diálogo (simplemente creando, sin devolver un resultado). Por lo tanto, para abrir una ventana de diálogo en ViewModel escribo: controller.OpenDialogEntity(bla, bla...)
  3. Cada ventana de diálogo notifica sobre su resultado (como OK, Guardar, Cancelar, etc.) vía Eventos débiles. Si usa PRISM, entonces es más fácil publicar notificaciones usando este EventAggregator.
  4. Para manejar los resultados del diálogo, estoy usando suscripción a notificaciones (nuevamente Eventos débiles y EventAggregator en el caso de PRISM). Para reducir la dependencia de dichas notificaciones, utilice clases independientes con notificación.

Ventajas:

  • Menos código. No me importa usar interfaces, pero he visto demasiados proyectos donde el exceso de uso de interfaces y capas de abstracción causan más problemas que ayuda.
  • Abrir ventanas de diálogo a través de Module Controller es una forma sencilla de evitar referencias fuertes y aún así permite usar maquetas para probar.
  • La notificación a través de eventos débiles reduce el número de memoria potencial fuga.

Contras:

  • No es fácil distinguir la notificación requerida de otras en el controlador. Dos soluciones:
    • envíe un token único al abrir una ventana de diálogo y verifique ese token en la suscripción
    • use clases de notificación genéricas <T> donde T es enumeración de entidades (o por simplicidad puede ser tipo de ViewModel).
  • Para un proyecto debe ser un acuerdo sobre el uso de clases de notificación para evita duplicarlos.
  • Para proyectos enormemente grandes, Module Controller puede ser abrumado por métodos para crear ventanas. En este caso es mejor dividirlo en varios módulos.

P.D. He estado usando este enfoque durante bastante tiempo y estoy listo para defender su elegibilidad en comentarios y proporcionar algunos ejemplos si es necesario.

 2
Author: Alex Klaus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-03 08:05:08