¿Buena o mala práctica para los cuadros de diálogo en wpf con MVVM?

148

Últimamente tuve el problema de crear cuadros de diálogo para agregar y editar 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

¿Como 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();
    }
}

WindowDialogEs una ventana especial pero simple. Lo necesito para guardar 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 dialogresult = trueque solo se puede lograr en el código. Es por eso que creé una interfaz para dialogviewmodelque la implemente.

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

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

Cada vez que mi ViewModel piense que es hora de hacerlo dialogresult = true, entonces genere 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 DataTemplateen mi archivo de recursos ( app.xamlo algo así):

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

Bueno, eso es todo, ahora puedo llamar diálogos desde mis modelos de vista:

 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 implementarse IDialogResultVMHelpery luego puede elevarlo dentro de OkCommando 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: utilicé el código desde aquí para hacer que mi EventHandler se registre débil:
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;
    }
}
blindmeis
fuente
1
probablemente le falte la referencia xmlns: x = " schemas.microsoft.com/winfx/2006/xaml " en su WindowDialog XAML.
Adiel Yaacov
En realidad, el espacio de nombres es xmlns: x = "[http: //] schemas.microsoft.com/winfx/2006/xaml" sin los corchetes
reggaeguitar
1
¡Hola! Llegado tarde aquí. No entiendo cómo su Servicio tiene una referencia al WindowDialog. ¿Cuál es la jerarquía de tus modelos? En mi opinión, la vista contiene una referencia al ensamblaje del modelo de vista y el modelo de la vista a los ensamblajes de servicio y modelo. De este modo, la capa de Servicio no tendría conocimiento de la vista WindowDialog. ¿Qué me estoy perdiendo?
Moe45673
2
Hola @blindmeis, solo tratando de entender este concepto, ¿no creo que haya algún proyecto de ejemplo en línea que pueda elegir? Hay varias cosas sobre las que estoy confundido.
Hank

Respuestas:

48

Este es un buen enfoque y utilicé otros similares en el pasado. ¡Ve a por ello!

Una cosa menor que definitivamente haría es hacer que el evento reciba un valor booleano para cuando necesite establecer "falso" en 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; }
}
Julián Domínguez
fuente
¿Qué pasa si en lugar de usar servicios, uno usa una especie de Devolución de llamada para facilitar la interacción con ViewModel y View? Por ejemplo, View ejecuta un comando en ViewModel, luego, cuando todo está dicho y hecho, ViewModel activa una devolución de llamada para que la vista muestre los resultados del comando. Todavía no puedo lograr que mi equipo use los Servicios para manejar las interacciones de Diálogo en ViewModel.
Matthew S
15

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 volver a escribirlo por completo ...)

En mi implementación, utilizo un IDialogViewModelque expone cosas como el título, los botones de standad para mostrar (para tener una apariencia consistente en todos los cuadros de diálogo), un RequestCloseevento y algunas otras cosas para poder controlar el tamaño de la ventana y comportamiento

Thomas Levesque
fuente
Gracias, el título realmente debería ir en mi IDialogViewModel. las otras propiedades como tamaño, botón estándar que dejaré, porque todo esto proviene de la plantilla de datos al menos.
blindmeis
1
Eso es lo que hice al principio también, solo use SizeToContent para controlar el tamaño de la ventana. Pero en un caso necesitaba hacer que la ventana fuera redimensionable, así que tuve que ajustarla un poco ...
Thomas Levesque
@ThomasLevesque los botones contenidos en su ViewModel, ¿son realmente objetos UI Button u objetos que representan botones?
Thomas
3
@Thomas, objetos que representan botones. Nunca debe hacer referencia a objetos de IU en ViewModel.
Thomas Levesque
2

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. Le paso una referencia al Module Controllerconstructor de cada uno ViewModel(puede usar inyección).
  2. Que Module Controllertiene métodos públicos / internos para la creación de ventanas de diálogo (sólo la creación, sin devolver un resultado). Por lo tanto, para abrir una ventana de diálogo en ViewModelescribo:controller.OpenDialogEntity(bla, bla...)
  3. Cada ventana de diálogo notifica sobre su resultado (como OK , Guardar , Cancelar , etc.) a través de 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 la suscripción a notificaciones (nuevamente Eventos débiles y EventAggregator en caso de PRISM). Para reducir la dependencia de tales notificaciones, use clases independientes con notificaciones estándar.

Pros:

  • Menos código No me importa usar interfaces, pero he visto demasiados proyectos donde el uso excesivo de interfaces y capas de abstracción causan más problemas que ayuda.
  • Abrir ventanas de diálogo Module Controlleres una forma simple de evitar referencias fuertes y aún permite usar maquetas para realizar pruebas.
  • La notificación a través de eventos débiles reduce el número de posibles pérdidas de memoria.

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 Tes la enumeración de entidades (o por simplicidad puede ser un tipo de ViewModel).
  • Para un proyecto debe haber un acuerdo sobre el uso de clases de notificación para evitar duplicarlas.
  • Para proyectos enormemente grandes, los Module Controllermétodos para crear ventanas pueden abrumarlo. En este caso, es mejor dividirlo en varios módulos.

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

Alex Klaus
fuente