MVVM en WPF – Cómo alertar a ViewModel de los cambios en el Modelo … o debería?

Estoy revisando algunos artículos de MVVM, principalmente esto y esto .

Mi pregunta específica es: ¿Cómo comunico los cambios del Modelo desde el Modelo al Modelo de Vista?

En el artículo de Josh, no veo que él haga esto. ViewModel siempre le pregunta al Modelo por las propiedades. En el ejemplo de Rachel, ella tiene el modelo implementado INotifyPropertyChanged , y plantea eventos del modelo, pero son para el consumo de la vista en sí (ver su artículo / código para más detalles sobre por qué lo hace).

En ninguna parte veo ejemplos en los que el modelo advierta a ViewModel de cambios en las propiedades del modelo. Esto me tiene preocupado de que quizás no se haga por alguna razón. ¿Existe un patrón para alertar al Modelo de Vista de los cambios en el Modelo? Parecería ser necesario ya que (1) es concebible que haya más de 1 modelo de vista para cada modelo, y (2) incluso si solo hay un modelo de vista, alguna acción en el modelo podría dar como resultado la modificación de otras propiedades.

Sospecho que podría haber respuestas / comentarios del formulario “¿Por qué querrías hacer eso?” comentarios, así que aquí hay una descripción de mi progtwig. Soy nuevo en MVVM, así que quizás todo mi diseño sea defectuoso. Lo describiré brevemente.

Estoy progtwigndo algo que es más interesante (¡al menos para mí!) Que clases de “Cliente” o “Producto”. Estoy progtwigndo BlackJack.

Tengo una vista que no tiene ningún código detrás y solo confía en el enlace a las propiedades y comandos en ViewModel (ver el artículo de Josh Smith).

Para bien o para mal, tomé la actitud de que el Modelo debe contener no solo clases como PlayingCard , Deck , sino también la clase BlackJackGame que mantiene el estado de todo el juego, y sabe cuándo el jugador ha fracasado, el dealer tiene que dibujar cartas, y cuál es el puntaje actual del jugador y el distribuidor (menos de 21, 21, quiebre, etc.).

Desde BlackJackGame expongo métodos como “DrawCard” y se me ocurrió que cuando se extrae una tarjeta, las propiedades como CardScore e IsBust deben actualizarse y estos nuevos valores deben comunicarse al ViewModel. Tal vez eso es un pensamiento defectuoso?

Uno podría adoptar la actitud de que ViewModel llamaba al método DrawCard() por lo que debería saber para pedir un puntaje actualizado y averiguar si está en quiebra o no. Opiniones?

En mi ViewModel, tengo la lógica para tomar una imagen real de una carta de juego (basada en palo, rango) y ponerla a disposición para la vista. El modelo no debería preocuparse por esto (quizás otro ViewModel simplemente use números en lugar de imágenes de naipes). Por supuesto, tal vez algunos me digan que el Modelo ni siquiera debería tener el concepto de un juego BlackJack y que debería manejarse en el ViewModel.

Si desea que sus Modelos le avisen a ViewModels de los cambios, deberían implementar INotifyPropertyChanged , y los ViewModels deberían suscribirse para recibir las notificaciones de PropertyChange.

Tu código podría verse más o menos así:

 // Attach EventHandler PlayerModel.PropertyChanged += PlayerModel_PropertyChanged; ... // When property gets changed in the Model, raise the PropertyChanged // event of the ViewModel copy of the property PlayerModel_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "SomeProperty") RaisePropertyChanged("ViewModelCopyOfSomeProperty"); } 

Pero, por lo general, esto solo es necesario si más de un objeto hará cambios en los datos del Modelo, lo cual no suele ser el caso.

Si alguna vez tiene un caso en el que no tiene una referencia a su propiedad de modelo para adjuntarle el evento PropertyChanged, puede usar un sistema de mensajería como Prism’s EventAggregator o MVVM Light’s Messenger .

Tengo una breve descripción de los sistemas de mensajería en mi blog, sin embargo, para resumirlo, cualquier objeto puede transmitir un mensaje y cualquier objeto puede suscribirse para escuchar mensajes específicos. Por lo tanto, puede transmitir un PlayerScoreHasChangedMessage desde un objeto, y otro objeto puede suscribirse para escuchar esos tipos de mensajes y actualizar su propiedad PlayerScore cuando oye uno.

Pero no creo que esto sea necesario para el sistema que ha descrito.

En un mundo ideal de MVVM, su aplicación se compone de sus ViewModels, y sus Modelos son solo los bloques utilizados para construir su aplicación. Por lo general, solo contienen datos, por lo que no tienen métodos como DrawCard() (que estaría en un ViewModel)

Entonces probablemente tendrías objetos de datos de modelo simples como estos:

 class CardModel { int Score; SuitEnum Suit; CardEnum CardValue; } class PlayerModel { ObservableCollection FaceUpCards; ObservableCollection FaceDownCards; int CurrentScore; bool IsBust { get { return Score > 21; } } } 

y tendrías un objeto ViewModel como

 public class GameViewModel { ObservableCollection Deck; PlayerModel Dealer; PlayerModel Player; ICommand DrawCardCommand; void DrawCard(Player currentPlayer) { var nextCard = Deck.First(); currentPlayer.FaceUpCards.Add(nextCard); if (currentPlayer.IsBust) // Process next player turn Deck.Remove(nextCard); } } 

(Todos los objetos anteriores deberían implementar INotifyPropertyChanged , pero lo dejé por simplicidad)

Respuesta corta: depende de los detalles.

En su ejemplo, los modelos se están actualizando “por sí solos” y estos cambios, por supuesto, deben propagarse de alguna manera a las vistas. Como las vistas solo pueden acceder directamente a viewmodels, significa que el modelo debe comunicar estos cambios al modelo de vista correspondiente. El mecanismo establecido para hacerlo es, por supuesto, INotifyPropertyChanged , lo que significa que obtendrá un flujo de trabajo como este:

  1. Viewmodel se crea y se ajusta al modelo
  2. Viewmodel se suscribe al evento PropertyChanged del modelo
  3. Viewmodel se establece como DataContext la vista, las propiedades están vinculadas, etc.
  4. Ver desencadena acción en viewmodel
  5. Método de llamadas de Viewmodel en modelo
  6. El modelo se actualiza
  7. Viewmodel maneja el PropertyChanged del modelo y levanta su propio PropertyChanged en respuesta
  8. La vista refleja los cambios en sus enlaces, cerrando el ciclo de retroalimentación

Por otro lado, si sus modelos contenían poca (o ninguna) lógica comercial, o si por alguna otra razón (como ganar capacidad transaccional) decidieron dejar que cada modelo de vista “apropie” su modelo envuelto, todas las modificaciones al modelo pasarían el modelo de vista entonces tal arreglo no sería necesario.

Describo dicho diseño en otra pregunta de MVVM aquí .

Tus opciones:

  • Implementar INotifyPropertyChanged
  • Eventos
  • POCO con manipulador Proxy

Como lo veo, INotifyPropertyChanged es una parte fundamental de .Net. es decir, está en System.dll . Implementarlo en su “Modelo” es similar a implementar una estructura de eventos.

Si desea POCO puro, debe manipular efectivamente sus objetos a través de proxies / services y luego su ViewModel recibe notificaciones de los cambios al escuchar el proxy.

Personalmente, simplemente implementé INotifyPropertyChanged y luego uso FODY para hacer el trabajo sucio por mí. Se ve y se siente POCO.

Un ejemplo (usando FODY a IL Weave the PropertyChanged raise):

 public class NearlyPOCO: INotifyPropertyChanged { public string ValueA {get;set;} public string ValueB {get;set;} public event PropertyChangedEventHandler PropertyChanged; } 

luego puede hacer que su ViewModel escuche PropertyChanged para cualquier cambio; o cambios específicos a la propiedad.

La belleza de la ruta INotifyPropertyChanged, es que lo encadena con una Extended ObservableCollection . Así que viertes tus objetos near poco en una colección, y escuchas la colección … si algo cambia, en cualquier parte, aprendes sobre ello.

Seré honesto, esto podría unirse al debate “¿Por qué no se trató INotifyPropertyChanged manipulado automáticamente por el comstackdor?”, Que se refiere a: cada objeto en c # debería tener la capacidad de notificar si alguna parte de él se modificó; es decir, implementar INotifyPropertyChanged de forma predeterminada. Pero no es así y la mejor ruta, que requiere la menor cantidad de esfuerzo, es usar IL Weaving (específicamente FODY ).

Hilo bastante viejo pero después de mucha búsqueda, se me ocurrió mi propia solución: un PropertyChangedProxy

Con esta clase, puede registrarse fácilmente en NotifyPropertyChanged de otra persona y tomar las medidas adecuadas si se dispara para la propiedad registrada.

Aquí hay una muestra de cómo podría ser esto cuando tenga un “estado” de propiedad modelo que puede cambiar por sí mismo y luego debería notificar automáticamente al modelo de vista para que active su PropertyChanged en su propiedad “Estado” para que también se notifique la vista: )

 public class MyModel : INotifyPropertyChanged { private string _status; public string Status { get { return _status; } set { _status = value; OnPropertyChanged(); } } // Default INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { var handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } } public class MyViewModel : INotifyPropertyChanged { public string Status { get { return _model.Status; } } private PropertyChangedProxy _statusPropertyChangedProxy; private MyModel _model; public MyViewModel(MyModel model) { _model = model; _statusPropertyChangedProxy = new PropertyChangedProxy( _model, myModel => myModel.Status, s => OnPropertyChanged("Status") ); } // Default INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { var handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } } 

y aquí está la clase en sí:

 ///  /// Proxy class to easily take actions when a specific property in the "source" changed ///  /// Last updated: 20.01.2015 /// Type of the source /// Type of the property public class PropertyChangedProxy where TSource : INotifyPropertyChanged { private readonly Func _getValueFunc; private readonly TSource _source; private readonly Action _onPropertyChanged; private readonly string _modelPropertyname; ///  /// Constructor for a property changed proxy ///  /// The source object to listen for property changes /// Expression to the property of the source /// Action to take when a property changed was fired public PropertyChangedProxy(TSource source, Expression> selectorExpression, Action onPropertyChanged) { _source = source; _onPropertyChanged = onPropertyChanged; // Property "getter" to get the value _getValueFunc = selectorExpression.Compile(); // Name of the property var body = (MemberExpression)selectorExpression.Body; _modelPropertyname = body.Member.Name; // Changed event _source.PropertyChanged += SourcePropertyChanged; } private void SourcePropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == _modelPropertyname) { _onPropertyChanged(_getValueFunc(_source)); } } } 

Encontré útil este artículo: http://social.msdn.microsoft.com/Forums/vstudio/en-US/3eb70678-c216-414f-a4a5-e1e3e557bb95/mvvm-businesslogic-is-part-of-the-?forum = wpf

Mi resumen:

La idea detrás de la organización MVVM es permitir una reutilización más fácil de vistas y modelos y también permitir pruebas desacopladas. Su modelo de vista es un modelo que representa las entidades de vista, su modelo representa las entidades de negocio.

¿Qué pasaría si quisieras hacer un juego de póquer más tarde? Gran parte de la IU debe ser reutilizable. Si la lógica de su juego está vinculada en su modelo de vista, sería muy difícil reutilizar esos elementos sin tener que reprogtwigr el modelo de vista. ¿Qué pasa si quieres cambiar tu interfaz de usuario? Si su lógica de juego está acoplada a su lógica de modelo de vista, necesitaría volver a verificar que su juego todavía funciona. ¿Qué sucede si quieres crear un escritorio y una aplicación web? Si su modelo de vista contiene la lógica del juego, sería complicado tratar de mantener estas dos aplicaciones una al lado de la otra ya que la lógica de la aplicación estaría inevitablemente ligada a la lógica de negocios en el modelo de vista.

Las notificaciones de cambio de datos y la validación de datos ocurren en cada capa (la vista, el modelo de vista y el modelo).

El modelo contiene sus representaciones de datos (entidades) y lógica de negocios específica para esas entidades. Una baraja de cartas es una “cosa” lógica con propiedades inherentes. Un buen mazo no puede tener cartas duplicadas. Necesita exponer una forma de obtener la (s) carta (s) superior (es). Necesita saber que no debe dar más tarjetas de las que le quedan. Tales comportamientos de mazo son parte del modelo porque son inherentes a un mazo de cartas. También habrá modelos de distribuidor, modelos de jugador, modelos de mano, etc. Estos modelos pueden interactuar y lo harán.

El modelo de vista consistiría en la presentación y la lógica de la aplicación. Todo el trabajo asociado con mostrar el juego está separado de la lógica del juego. Esto podría incluir la visualización de las manos como imágenes, las solicitudes de tarjetas al modelo del distribuidor, la configuración de visualización del usuario, etc.

Las agallas del artículo:

Básicamente, la forma en que me gusta explicar esto es que la lógica de su negocio y las entidades conforman el modelo. Esto es lo que su aplicación específica está usando, pero podría ser compartida en muchas aplicaciones.

La Vista es la capa de presentación, todo lo relacionado con la interacción directa con el usuario.

El ViewModel es básicamente el “pegamento” que es específico de su aplicación y que los vincula a los dos.

Tengo un buen diagtwig aquí que muestra cómo interactúan:

http://reedcopsey.com/2010/01/06/better-user-and-developer-experiences-from-windows-forms-to-wpf-with-mvvm-part-7-mvvm/

En tu caso, vamos a abordar algunos de los detalles …

Validación: esto generalmente viene en 2 formas. La validación relacionada con la entrada del usuario ocurriría en el modelo de vista (principalmente) y la vista (es decir: el cuadro de texto “numérico” que impide que se ingrese texto se maneja para usted en la vista, etc.). Como tal, la validación de la entrada del usuario suele ser una preocupación VM. Dicho esto, a menudo hay una segunda “capa” de validación: esta es la validación de que los datos que se utilizan coinciden con las reglas comerciales. Esto a menudo es parte del modelo en sí mismo: cuando inserta datos en su Modelo, puede causar errores de validación. La máquina virtual tendrá que volver a asignar esta información a la vista.

Operaciones “detrás de escena sin vistas, como escribir en DB, enviar correos electrónicos, etc.”: Esto es realmente parte de las “Operaciones específicas de dominio” en mi diagtwig, y ​​es realmente parte pura del Modelo. Esto es lo que intentas exponer a través de la aplicación. ViewModel actúa como un puente para exponer esta información, pero las operaciones son de puro modelo.

Operaciones para ViewModel: The ViewModel necesita más que solo INPC: también necesita cualquier operación que sea específica para su aplicación (no su lógica comercial), como guardar las preferencias y el estado del usuario, etc. Esto va a variar la aplicación. por aplicación, incluso cuando interactúan con el mismo “modelo”.

Una buena manera de pensar sobre ello: supongamos que quiere hacer 2 versiones de su sistema de pedidos. El primero está en WPF, y el segundo es una interfaz web.

La lógica compartida que se ocupa de los pedidos mismos (envío de correos electrónicos, ingreso a DB, etc.) es el Modelo. Su aplicación expone estas operaciones y datos al usuario, pero lo hace de 2 maneras.

En la aplicación WPF, la interfaz de usuario (con qué interactúa el espectador) es la “vista”: en la aplicación web, este es básicamente el código que (al menos eventualmente) se convierte en javascript + html + css en el cliente.

ViewModel es el rest del “pegamento” que se necesita para adaptar su modelo (estas operaciones relacionadas con el pedido) a fin de que funcione con la tecnología / capa de visualización específica que está utilizando.

La notificación basada en INotifyPropertyChanged e INotifyCollectionChanged es exactamente lo que necesita. Para simplificar su vida con la suscripción a cambios de propiedad, validación en tiempo de comstackción del nombre de la propiedad, evitando memory leaks, le aconsejo que use PropertyObserver de la Fundación MVVM de Josh Smith . Como este proyecto es de código abierto, puede agregar solo esa clase a su proyecto desde las fonts.

Para comprender cómo usar PropertyObserver, lea este artículo .

Además, eche un vistazo más profundo en Reactive Extensions (Rx) . Puede exponer IObserver de su modelo y suscribirse a este en el modelo de visualización.

He defendido el Modelo direccional -> Ver modelo -> Ver el flujo de cambios desde hace mucho tiempo, como puede ver en la sección Flujo de cambios de mi artículo de MVVM de 2008. Esto requiere implementar INotifyPropertyChanged en el modelo. Por lo que puedo decir, se ha convertido en una práctica común.

Como mencionó a Josh Smith, eche un vistazo a su clase PropertyChanged . Es una clase de ayuda para suscribirse al evento INotifyPropertyChanged.PropertyChanged del modelo.

De hecho, puedes llevar este enfoque mucho más lejos, ya que recientemente he creado mi clase PropertiesUpdater . Las propiedades en el modelo de vista se calculan como expresiones complejas que incluyen una o más propiedades en el modelo.

Puede subir eventos desde el modelo, al que el modelo de vista debería suscribirse.

Por ejemplo, recientemente trabajé en un proyecto para el cual tuve que generar una vista en árbol (naturalmente, el modelo tenía una naturaleza jerárquica). En el modelo, tuve una ChildElements observable llamada ChildElements .

En el modelo de vista, había almacenado una referencia al objeto en el modelo y me suscribí al evento CollectionChanged la CollectionChanged observable, así: ModelObject.ChildElements.CollectionChanged += new CollectionChangedEventHandler(insert function reference here)

Luego su modelo de vista se notifica automáticamente una vez que ocurre un cambio en el modelo. Puedes seguir el mismo concepto usando PropertyChanged , pero necesitarás plantear explícitamente eventos de cambio de propiedad de tu modelo para que funcionen.

Los chicos hicieron un trabajo increíble respondiendo a esto, pero en situaciones como esta realmente siento que el patrón MVVM es un dolor, así que usaría un control de supervisión o un enfoque de visión pasiva y dejaría el sistema de enlace al menos para objetos modelo que generan cambios por sí mismos.

Esto me parece una pregunta realmente importante, incluso cuando no hay presión para hacerlo. Estoy trabajando en un proyecto de prueba, que implica un TreeView. Hay elementos de menú y tales que se asignan a comandos, por ejemplo, Eliminar. Actualmente, estoy actualizando tanto el modelo como el modelo de vista desde el modelo de vista.

Por ejemplo,

 public void DeleteItemExecute () { DesignObjectViewModel node = this.SelectedNode; // Action is on selected item DocStructureManagement.DeleteNode(node.DesignObject); // Remove from application node.Remove(); // Remove from view model Controller.UpdateDocument(); // Signal document has changed } 

Esto es simple, pero parece tener un defecto muy básico. Una prueba unitaria típica ejecutaría el comando, luego verificaría el resultado en el modelo de vista. Pero esto no prueba que la actualización del modelo fue correcta, ya que los dos se actualizan simultáneamente.

Entonces quizás sea mejor usar técnicas como PropertyObserver para permitir que la actualización del modelo active una actualización del modelo de vista. La misma prueba unitaria ahora solo funcionaría si ambas acciones fueran exitosas.

Esta no es una respuesta potencial, me doy cuenta, pero parece que merece la pena publicarla.

No hay nada de malo en implementar INotifyPropertyChanged dentro de Model y escucharlo dentro de ViewModel. De hecho, incluso puede puntear en la propiedad del modelo directamente en XAML: {Binding Model.ModelProperty}

En cuanto a las propiedades dependientes / calculadas de solo lectura, de lejos no he visto nada mejor y más simple que esto: https://github.com/StephenCleary/CalculatedProperties . Es muy simple pero increíblemente útil, es realmente “fórmulas de Excel para MVVM”, simplemente funciona de la misma manera que Excel propaga los cambios a las celdas de fórmula sin un esfuerzo extra de su parte.