MVVM: Enlace al Modelo mientras mantiene el Modelo sincronizado con una versión de servidor


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

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

  • El Model tiene una sola propiedad llamada Title de tipo String.
  • El Model es el DataContext para el View.
  • El View tiene un TextBlock que es databound a Title en el Modelo.
  • El ViewModel tiene un método llamado Save() que guardará el Model a un Server
  • El Server puede empujar los cambios realizados en el Model

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

Ajuste 1:

  • La propiedad Model.Title necesita llamar a RaisePropertyChanged() para traducir los cambios realizados a Model por Server a View. Esto funciona muy bien ya que el Model es el DataContext para el View

No está tan mal.

Ajuste 2:

  • El siguiente paso es llamar a Save() para guardar los cambios realizados desde el View al Model en el Server. Aquí es donde me quedo atascado. Puedo manejar el evento Model.PropertyChanged 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.

Author: ndsc, 2012-05-03

5 answers

En el pasado he escrito 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 notificaciones y (en el escenario más simple) ven esos cambios inmediatamente. Aquí hay un resumen de cómo fue diseñado.

Configuración

  1. Views siempre enlaza a ViewModels. Sé que es un montón de repeticiones, pero vinculante directamente a los Modelos no es aceptable en cualquiera de los escenarios más simples; tampoco está en el espíritu de MVVM.

  2. Los modelos de vista tienen la única responsabilidad de empujar cambios. Esto obviamente incluye enviar cambios al servidor, pero también podría incluir enviar cambios a otros componentes de la aplicación.

    Para hacer esto, ViewModels podría querer clonar los Modelos que empaquetan para que puedan proporcionar semántica de transacciones al resto de la aplicación como proporcionan al 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 todavía más trabajo, pero también abre poderosas posibilidades (por ejemplo, deshacer cambios es trivial: simplemente no los empuje).

  3. Los modelos de vista tienen una dependencia de algún tipo de Servicio de Datos. El Servicio de datos es un componente de aplicación que se encuentra 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 "data store changed" apropiados que expone el Servicio de Datos.

    Esto permite que ViewModels sea notificado de los cambios en "su" modelo que otros ViewModels hayan enviado al almacén de datos y reaccione apropiadamente. Con la abstracción adecuada, el almacén de datos también puede ser cualquier cosa (por ejemplo, un servicio WCF en ese aplicación).

Flujo de trabajo

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

  2. Cuando el usuario termina de editar, interactúa con la vista que invoca un comando en la máquina virtual. La VM luego llama al DS, empujando los cambios realizados a su Modelo clonado.

  3. El DS mantiene 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 el Modelo X; la nueva versión del modelo se proporciona como parte de los argumentos de evento.

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

Notas

  • El INotifyPropertyChanged del Modelo es utilizado solo por la Vista; si el ViewModel quiere saber si el Modelo está "sucio", siempre puede comparar clonar a la versión original (si se ha mantenido alrededor, que recomiendo si es posible).
  • El ViewModel envía los cambios al servidor atómicamente, lo cual es bueno porque asegura que el almacén de datos esté siempre en un estado consistente. Esta es una opción de diseño, y si quieres hacer las cosas de manera diferente, otro diseño sería más apropiado.
  • El servidor puede optar por no generar el evento "Model changed" para el ViewModel que fue responsable de este cambio si el ViewModel pasa this como parámetro de la llamada "push changes". Incluso si no lo hace, el 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 pueden ser empujados a otros procesos que se ejecutan en otras máquinas tan fácilmente como pueden ser empujados a otras vistas en su shell.

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

 63
Author: Jon,
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
2012-05-04 10:04:54

Sugeriría agregar Controladores a la mezcla MVVM (MVCVM?) para simplificar el patrón de actualización.

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

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

  • Los ViewModels son contenedores tontos que contienen cierta forma de datos. No saben de dónde vienen 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 vienen 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 suministrar el código para ICommands en máquinas virtuales, escuchar cambios en los datos, etc. Rellenan las máquinas virtuales a partir de Modelos. Tiene sentido que escuchen los cambios de VM y actualicen el Modelo.

Como se mencionó en otra respuesta, su DataContext debe ser el VM (o propiedad de la misma), no el modelo. Apuntar a un modelo de datos hace que sea difícil separar las preocupaciones (por ejemplo, para el Desarrollo Impulsado por Pruebas).

La mayoría de las otras soluciones ponen lógica en ViewModels lo cual "no es correcto", pero veo los beneficios de los controladores pasados por alto todo el tiempo. Maldito acrónimo MVVM! :)

 6
Author: Gone Coding,
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
2012-08-03 09:34:50

El enlace del modelo a ver directamente solo funciona si el modelo implementa la interfaz INotifyPropertyChanged. (eg. su modelo generado por Entity Framework)

Modelo implementar INotifyPropertyChanged

Puedes hacer esto.

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
}

ViewModel como DTO

Si el modelo implementa INotifyPropertyChanged(depende), puede usarlo como DataContext en la mayoría de los casos. pero en DDD la mayoría del modelo MVVM será considerado como EntityObject no como un Modelo de Dominio real.

La forma más eficiente es para 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);
    }
}

Ambas propiedades de ViewModel anteriores se pueden usar para enlazar sin romper el patrón MVVM (pattern != regla) realmente depende.

Una cosa más.. ViewModel tiene dependencia del Modelo. si el modelo puede ser cambiado por el servicio/entorno externo. es el "estado global" lo que complica las cosas.

 1
Author: aifarfa,
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
2012-05-04 06:49:09

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 la vista del cambio de propiedad desde el servidor sin pasar por el configurador de propiedades y, por lo tanto, sin invocar el código "guardar en el servidor".

 0
Author: Steve Greatrex,
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
2012-05-03 18:29:34

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;
}
 0
Author: Chui Tey,
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
2012-05-04 05:52:35