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

He dedicado 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 hack alrededor del problema.

Tengo una configuración simple de View, 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 Cadena.
  • El Model es el DataContext para la View .
  • La View tiene un bloque de texto que está enlazado a Title en el modelo.
  • ViewModel tiene un método llamado Save() que guardará el Model en un Server
  • El Server puede impulsar los cambios realizados en el Model

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

Ajuste 1:

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

No está mal.

Ajuste 2:

  • El siguiente paso es llamar a Save() para guardar los cambios realizados desde la 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 haga eco de los cambios realizados por el Servidor.

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

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) ve esos cambios de inmediato. Aquí hay un resumen de cómo fue diseñado.

Preparar

  1. Las vistas siempre se vinculan a ViewModels. Sé que es una gran repetición, pero vincular directamente a los Modelos no es aceptable en los escenarios más simples; tampoco está en el espíritu de MVVM.

  2. ViewModels tiene la responsabilidad exclusiva de impulsar los cambios. Obviamente, esto incluye impulsar cambios en el servidor, pero también podría incluir cambios a otros componentes de la aplicación.

    Para hacer esto, es posible que ViewModels quiera clonar los Modelos que envuelven para que puedan proporcionar semánticas de transacción al rest de la aplicación tal como lo proporcionan al servidor (es decir, puede elegir cuándo insertar cambios en el rest de la aplicación, lo que no se puede hacer si todos se vinculan directamente a la misma instancia de modelo). Aislar cambios como este requiere aún más trabajo, pero también abre poderosas posibilidades (por ejemplo, deshacer cambios es trivial: simplemente no los presione).

  3. ViewModels tiene una dependencia en 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 todas las comunicaciones entre ellos. Cada vez que un modelo de vista clona su modelo, también se suscribe a los eventos apropiados “almacén de datos modificados” que expone el servicio de datos.

    Esto permite que se notifique a ViewModels de los cambios en “su” modelo que otros ViewModels han enviado al almacén de datos y reactjsn de forma 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 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 en el servicio de datos, le dice al DS que desea suscribirse a las notificaciones de actualizaciones de este modelo específico. ViewModel no sabe qué identifica su Modelo (la “clave principal”), pero no es necesario porque eso es responsabilidad del DS.

  2. Cuando el usuario finaliza la edición, interactúa con la Vista que invoca un Comando en la VM. A continuación, la máquina virtual llama al DS, impulsando los cambios realizados en su modelo clonado.

  3. El DS persiste en los cambios y, además, plantea un evento que notifica a todas las demás VM interesadas que se han realizado cambios al Modelo X; la nueva versión del Modelo se suministra 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 que tiene 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 empujado como parte de los argumentos del evento).

Notas

  • El INotifyPropertyChanged del Modelo 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).
  • El ViewModel empuja los cambios al servidor atómicamente, lo cual es bueno porque asegura que el data store esté siempre en un estado consistente. Esta es una opción de diseño, y si quiere hacer las cosas de manera diferente, otro diseño sería más apropiado.
  • El Servidor puede optar por no generar el evento “Modelo modificado” para ViewModel que fue responsable de este cambio si ViewModel lo pasa como un parámetro a la llamada “push changes”. Incluso si no lo hace, ViewModel puede elegir 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 pasar a otros procesos que se ejecutan en otras máquinas con la misma facilidad con la que pueden enviarse a otras Vistas en su caparazón.

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

Sugeriría agregar Controladores a la mezcla 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 el Modelo y el Modelo de Vista.

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

  • ViewModels son simplemente 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 suministrar el código para ICommands en máquinas virtuales, escuchar cambios en los datos, etc. Llenan máquinas virtuales desde modelos. Tiene sentido hacer que escuchen los cambios de VM y actualicen el Modelo.

Como se menciona en otra respuesta, su DataContext debería ser la VM (o propiedad de ella), no el modelo. Apuntando a un DataModel hace que sea difícil separar las preocupaciones (por ejemplo, para el Desarrollo controlado por prueba).

La mayoría de las otras soluciones ponen lógica en ViewModels que “no está bien”, pero veo que los beneficios de los controladores se pasan por alto todo el tiempo. ¡Maldito ese acrónimo de MVVM! 🙂

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

Modelo implementado 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 } 

ViewModel como DTO

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

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 anteriores de ViewModel se pueden usar para vincular sin romper el patrón MVVM (pattern! = rule) de lo que realmente depende.

Una cosa más … ViewModel tiene dependencia en el Modelo. si el modelo puede ser cambiado por un servicio / entorno externo. es el “estado global” que complica las cosas.

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 establecimiento de la propiedad y, por lo tanto, sin invocar el código “guardar en el servidor”.

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 diferente:

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