MVVM: enlace al modelo mientras se mantiene el modelo sincronizado con una versión del servidor

He pasado bastante tiempo tratando de encontrar una solución elegante para el siguiente desafío. No he podido encontrar una solución que sea más que un truco para solucionar el problema.

Tengo una configuración simple de View, ViewModel y Model. Lo mantendré muy simple por el bien de la explicación.

  • La Model tiene una sola propiedad llamada Title de tipo String.
  • La Model es el DataContext para el View.
  • La View tiene un TextBlock eso está enlazado a datos Title en el Modelo.
  • La ViewModel tiene un método llamado Save() que salvará el Model a una Server
  • La Server puede impulsar los cambios realizados en el Model

Hasta ahora tan bueno. Ahora hay dos ajustes que debo hacer para mantener el Modelo sincronizado con un Server. El tipo de servidor no es importante. Solo sé que necesito llamar Save() para empujar el Modelo a la Server.

Ajuste 1:

  • La Model.Title la propiedad tendrá que llamar RaisePropertyChanged() para traducir los cambios realizados en el Model para Server de las personas acusadas injustamente llamadas View. Esto funciona bien desde el Model es el DataContext para el View

No está mal.

Ajuste 2:

  • El siguiente paso es llamar Save() para guardar los cambios realizados desde el View de las personas acusadas injustamente llamadas Model al Server. Aquí es donde me quedo atascado. puedo manejar el Model.PropertyChanged evento en el ViewModel que llama a Save() cuando se cambia el modelo, pero esto hace que se hagan eco de los cambios realizados por el servidor.

Estoy buscando una solución elegante y lógica y estoy dispuesto a cambiar mi arquitectura si tiene sentido.

preguntado el 03 de mayo de 12 a las 19:05

Algo es raro... ¿Usar el modelo como contexto de datos? eso no es MVVM real. en primer lugar, simplemente pruebe ViewModel como si "no existiera View". -

La aplicación es pesada en la interfaz de usuario y he optado por probar un enfoque para traducir el Modelo directamente a la Vista. Sin embargo, ViewModel maneja algunas propiedades. Mira esto: stackoverflow.com/a/10324065/1120175 -

@ndsc Eche un vistazo más profundo a su pregunta a la que se hace referencia, especialmente a mi respuesta. Un modelo como DataContext viola drásticamente el patrón MVVM. -

5 Respuestas

En el pasado, escribí una aplicación que admite la edición "en vivo" de objetos de datos desde múltiples ubicaciones: muchas instancias de la aplicación pueden editar el mismo objeto al mismo tiempo, y cuando alguien envía cambios al servidor, todos los demás reciben una notificación y (en el escenario más simple) ve esos cambios inmediatamente. Aquí hay un resumen de cómo fue diseñado.

Preparar

  1. Vistas siempre vincular a ViewModels. Sé que es un montón de repeticiones, pero la vinculación directa a Modelos no es aceptable excepto en los escenarios más simples; tampoco está en el espíritu de MVVM.

  2. Los modelos de vista tienen sol responsabilidad de impulsar los cambios. Obviamente, esto incluye enviar cambios al servidor, pero también podría incluir cambios a otros componentes de la aplicación.

    Para hacer esto, ViewModels podría querer clonar los Modelos que envuelven para que puedan proporcionar semántica de transacciones al resto de la aplicación como lo hacen con el servidor (es decir, puede elegir cuándo enviar cambios al resto de la aplicación, lo que no puede hacer si todos se vinculan directamente a la misma instancia del modelo). Aislar cambios como este requiere posible más trabajo, pero también abre poderosas posibilidades (por ejemplo, deshacer cambios es trivial: simplemente no los presione).

  3. ViewModels tiene una dependencia de algún tipo de servicio de datos. El servicio de datos es un componente de aplicación que se ubica entre el almacén de datos y los consumidores y maneja toda la comunicación entre ellos. Cada vez que un ViewModel clona su modelo, también se suscribe a los eventos apropiados de "almacén de datos modificado" que expone el servicio de datos.

    Esto permite que los ViewModels sean notificados de los cambios en "su" modelo que otros ViewModels han enviado al almacén de datos y reaccionen de manera adecuada. Con la abstracción adecuada, el almacén de datos también puede ser cualquier cosa (por ejemplo, un servicio WCF en esa aplicación específica).

Flujo de Trabajo

  1. Se crea un modelo de vista y se le asigna la propiedad de un modelo. Inmediatamente clona el Modelo y expone este clon a la Vista. Al tener una dependencia en el Servicio de Datos, le dice al DS que quiere suscribirse a las notificaciones para actualizaciones de este Modelo específico. ViewModel no sabe qué es lo que identifica su modelo (la "clave principal"), pero no necesita saberlo porque es responsabilidad del DS.

  2. Cuando el usuario termina de editar, interactúa con la Vista que invoca un Comando en la VM. Luego, la máquina virtual llama al DS y envía los cambios realizados a su modelo clonado.

  3. El DS conserva los cambios y, además, genera un evento que notifica a todas las demás máquinas virtuales interesadas que se han realizado cambios en Model X; la nueva versión del modelo se proporciona como parte de los argumentos del evento.

  4. Otras máquinas virtuales a las que se les ha asignado la propiedad del mismo modelo ahora saben que han llegado cambios externos. Ahora pueden decidir cómo actualizar la Vista teniendo a mano todas las piezas del rompecabezas (la versión "anterior" del Modelo, que fue clonada; la versión "sucia", que es el clon; y la versión "actual", que fue empujado como parte de los argumentos del evento).

Notas

  • Los modelos INotifyPropertyChanged es utilizado solo por la Vista; si ViewModel quiere saber si el modelo está "sucio", siempre puede comparar el clon con la versión original (si se ha mantenido, lo cual recomiendo si es posible).
  • ViewModel envía los cambios al servidor de forma atómica, lo cual es bueno porque garantiza que el almacén de datos esté siempre en un estado coherente. Esta es una elección de diseño, y si desea hacer las cosas de manera diferente, otro diseño sería más apropiado.
  • El servidor puede optar por no generar el evento "Modelo cambiado" para el modelo de vista que fue responsable de este cambio si el modelo de vista pasa this como un parámetro para la llamada "empujar cambios". Incluso si no lo hace, ViewModel puede optar por no hacer nada si ve que la versión "actual" del modelo es idéntica a su propio clon.
  • Con suficiente abstracción, los cambios se pueden enviar a otros procesos que se ejecutan en otras máquinas tan fácilmente como se pueden enviar a otras vistas en su shell.

Espero que esto ayude; Puedo ofrecer más aclaraciones si es necesario.

Respondido el 20 de junio de 20 a las 10:06

+1 por establecer los fundamentos de MVVM en términos breves y claros. - PVitt

Creé una prueba de concepto de esto y creo que es justo lo que estaba buscando. También me convenciste de usar modelos de vista y, lo que es más importante, lo dejaste claro el porqué Debería usarlos. - ndsc

¿Cómo manejaría los cambios realizados por la Vista que no pueden vincularse a datos en este caso? ¿Exponer métodos en ViewModel para que View los use? - ndsc

Por cierto: traté de trabajar con su sugerencia de usar la clonación. Simplemente corrí contra cierta complejidad que considero que hace que no valga la pena usar la clonación. Mis modelos obtuvieron una relación padre/hijo y obtuvieron propiedades de navegación que deben conservarse. ¿Sugeriría usar la reflexión para hacer las copias o hacer que ViewModel implemente algo como ModelChanged() y hacer que DS proporcione una lista de propiedades que han cambiado (ya que DS tiene acceso a una copia anterior cuando recibe el nuevo modelo). - ndsc

@SteffenWinkler profundo frente a superficial no se traduce directamente en grande frente a pequeño. En cuanto a compartir la instancia del modelo, claro, eso es una obviedad cuando solo los usas para leer. Para escribir, obviamente, las consideraciones comerciales pueden ser más importantes que lo que sugieren los libros al enseñar MVVM 101. Cuando presiona cambiar el nombre en un archivo en su aplicación de exploración del sistema de archivos, ¿le gustaría ver el nombre del archivo en cualquier otro lugar del sistema operativo actualizado automáticamente? para reflejar sus pulsaciones de teclas aún no confirmadas? - Jon

Sugeriría agregar controladores a la combinación de MVVM (¿MVCVM?) para simplificar el patrón de actualización.

El controlador escucha los cambios en un nivel superior y propaga los cambios entre Model y ViewModel.

Las reglas básicas para mantener las cosas limpias son:

  • ViewModels son solo contenedores tontos que contienen una cierta forma de datos. No saben de dónde provienen los datos ni dónde se muestran.
  • Las vistas muestran una cierta forma de datos (a través de enlaces a un modelo de vista). No saben de dónde provienen los datos, solo cómo mostrarlos.
  • Los modelos proporcionan datos reales. No saben dónde se consume.
  • Los controladores implementan la lógica. Cosas como proporcionar el código para ICommands en máquinas virtuales, escuchar cambios en los datos, etc. Llenan las máquinas virtuales desde modelos. Tiene sentido que escuchen los cambios de VM y actualicen el modelo.

Como se menciona en otra respuesta, su DataContext debe ser la VM (o propiedad de ella), no el modelo. Señalar un modelo de datos hace que sea difícil separar las preocupaciones (por ejemplo, para el desarrollo basado en pruebas).

La mayoría de las otras soluciones ponen la lógica en ViewModels que "no es correcta", pero veo que los beneficios de los controladores se pasan por alto todo el tiempo. ¡Maldita sea ese acrónimo MVVM! :)

Respondido 03 ago 12, 10:08

En esta arquitectura, ¿los controladores escuchan los cambios en las propiedades del modelo (y los cambios en la colección) y actualizan las máquinas virtuales en consecuencia? - curry rojo

@redcurry: Sí, toda la lógica está en el controlador en este patrón. Toma todas las decisiones sobre qué hacer con los cambios de datos. - Codificación ido

Gracias. He estado usando este patrón MVCVM o MVVMC (al menos mi versión) y ha sido genial. Mis modelos de vista no tienen tantas dependencias y responsabilidades, y es más fácil implementar la funcionalidad de deshacer, registro, informe de errores, etc. Sin embargo, no hay muchos (¿alguno?) Ejemplos de este patrón donde uno pueda ver cómo otros implementan eso. ¿Conoces algún código disponible en línea (o que puedas compartir) usando este patrón? Gracias de nuevo. - curry rojo

@redcurry: El único lugar donde lo vimos en producción fue un generador de framework de terceros Sculture: escultura.codeplex.com pero se siente bien. - Codificación ido

vincular el modelo para ver directamente solo funciona si el modelo implementa la interfaz INotifyPropertyChanged. (por ejemplo, su modelo generado por Entity Framework)

Modelo implementar INotifyPropertyChanged

Puedes hacerlo.

public interface IModel : INotifyPropertyChanged //just sample model
{
    public string Title { get; set; }
}

public class ViewModel : NotificationObject //prism's ViewModel
{
    private IModel model;

    //construct
    public ViewModel(IModel model)
    {
        this.model = model;
        this.model.PropertyChanged += new PropertyChangedEventHandler(model_PropertyChanged);
    }

    private void model_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Title")
        {
            //Do something if model has changed by external service.
            RaisePropertyChanged(e.PropertyName);
        }
    }
    //....more properties
}

Ver modelo como DTO

si Model implementa INotifyPropertyChanged (depende), puede usarlo como DataContext en la mayoría de los casos. pero en DDD, la mayoría de los modelos MVVM se considerarán como EntityObject, no como un modelo de dominio real.

una forma más eficiente es usar ViewModel como DTO

//Option 1.ViewModel act as DTO / expose some Model's property and responsible for UI logic.
public string Title
{
    get 
    {
        // some getter logic
        return string.Format("{0}", this.model.Title); 
    }
    set
    {
        // if(Validate(value)) add some setter logic
        this.model.Title = value;
        RaisePropertyChanged(() => Title);
    }
}

//Option 2.expose the Model (have self validation and implement INotifyPropertyChanged).
public IModel Model
{
    get { return this.model; }
    set
    {
        this.model = value;
        RaisePropertyChanged(() => Model);
    }
}

las dos propiedades de ViewModel anteriores se pueden usar para vincular sin romper el patrón MVVM (patrón! = regla), realmente depende.

Una cosa más... ViewModel depende de Model. si el modelo puede ser cambiado por un servicio/entorno externo. es el "estado global" lo que complica las cosas.

contestado el 04 de mayo de 12 a las 07:05

Si su único problema es que los cambios del servidor se vuelven a guardar inmediatamente, ¿por qué no hacer algo como lo siguiente?

//WARNING: typed in SO window
public class ViewModel
{
    private string _title;
    public string Title
    {
        get { return _title; }
        set
        {
            if (value != _title) 
            {
                _title = value;
                this.OnPropertyChanged("Title");
                this.BeginSaveToServer();
            }
        }
    }

    public void UpdateTitleFromServer(string newTitle)
    {
        _title = newTitle;
        this.OnPropertyChanged("Title"); //alert the view of the change
    }
}

Este código alerta manualmente a la vista del cambio de propiedad desde el servidor sin pasar por el establecedor de propiedades y, por lo tanto, sin invocar el código "guardar en el servidor".

contestado el 03 de mayo de 12 a las 19:05

Sin embargo, el contexto de datos para la vista es el modelo. ¿Sugiere mantener propiedades separadas en ViewModel? Elegí enlazar directamente con el modelo, por lo que no necesito duplicar las propiedades. - ndsc

Dado que desea un comportamiento diferente y específico de la vista para el Title propiedad (guardar en el servidor), entonces sugeriría que pertenezca al modelo de vista. Podría implementarlo de modo que, en lugar de usar un string para almacenar su valor, solo usa la propiedad del modelo (por ejemplo, get { return _model.Title; } etc.) - steve granrex

Sin embargo, necesito que Model notifique a ViewModel sobre los cambios. Pasar directamente los valores de las propiedades al modelo hará que repita la vista. - ndsc

¿Qué causará los cambios en el modelo de los que se deberá notificar a la vista? ¿Solo el servidor como se describe arriba? - steve granrex

Tanto el servidor como la interacción del usuario con la Vista. - ndsc

La razón por la que tiene este problema es porque su modelo no sabe si está sucio o no.

string Title {
  set {
    this._title = value;
    this._isDirty = true; // ??!!
  }
}}

La solución es copiar los cambios del servidor a través de un método separado:

public void CopyFromServer(Model serverCopy)
{
  this._title = serverCopy.Title;
}

contestado el 04 de mayo de 12 a las 06:05

El modelo no debe saber si está sucio o no. Es por eso que ViewModel está ahí. - PVitt

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.