2011-01-18 30 views
7

He estado buscando el uso de Rx en un marco MVVM. La idea es usar consultas LINQ "en vivo" sobre conjuntos de datos en memoria para proyectar datos en Modelos de visualización para vincular.¿Cómo utilizar IObservable (Rx) como reemplazo de INotifyCollectionChanged para MVVM?

Anteriormente esto ha sido posible con el uso de INotifyPropertyChanged/INotifyCollectionChanged y una biblioteca de código abierto llamada CLINQ. El potencial con Rx e IObservable es pasar a un modelo de vista mucho más declarativo utilizando clases de materias para propagar eventos modificados desde el modelo de origen hasta la vista. Para el último paso, se necesitaría una conversión de IObservable a las interfaces de enlace de datos normales.

El problema es que Rx no parece ser compatible con la notificación de que una entidad se ha eliminado de la transmisión. Ejemplo a continuación.
El código muestra un POCO que utiliza la clase BehaviorSubject para el estado del campo. El código continúa para crear una colección de estas entidades y utiliza Concat para combinar las secuencias de filtro. Esto significa que cualquier cambio en las POCO se informa a una sola transmisión.

Un filtro para esta secuencia está configurado para filtrar para Rating == 0. La suscripción simplemente envía el resultado a la ventana de depuración cuando se produce una actualización.

La configuración Rating = 0 en cualquier elemento activará el evento. Pero al volver a establecer la clasificación en 5, no se verán eventos.

En el caso de CLINQ, la salida de la consulta admitirá INotifyCollectionChanged, de modo que los elementos agregados y eliminados del resultado de la consulta activarán el evento correcto para indicar que el resultado de la consulta ha cambiado (un elemento agregado o eliminado).

La única forma en que se me ocurre abordar esto es configurar dos flujos con consultas de oposición (doble). Un elemento agregado a la secuencia opuesta implica la eliminación del conjunto de resultados. De lo contrario, podría usar FromEvent y no hacer que ninguno de los modelos de entidades sean observables, lo que hace que Rx sea más un simple agregador de eventos. ¿Alguna sugerencia?

using System; 
using System.ComponentModel; 
using System.Linq; 
using System.Collections.Generic; 

namespace RxTest 
{ 

    public class TestEntity : Subject<TestEntity>, INotifyPropertyChanged 
    { 
     public IObservable<string> FileObservable { get; set; } 
     public IObservable<int> RatingObservable { get; set; } 

     public string File 
     { 
      get { return FileObservable.First(); } 
      set { (FileObservable as IObserver<string>).OnNext(value); } 
     } 

     public int Rating 
     { 
      get { return RatingObservable.First(); } 
      set { (RatingObservable as IObserver<int>).OnNext(value); } 
     } 

     public event PropertyChangedEventHandler PropertyChanged; 

     public TestEntity() 
     { 
      this.FileObservable = new BehaviorSubject<string>(string.Empty); 
      this.RatingObservable = new BehaviorSubject<int>(0); 
      this.FileObservable.Subscribe(f => { OnNotifyPropertyChanged("File"); }); 
      this.RatingObservable.Subscribe(f => { OnNotifyPropertyChanged("Rating"); }); 
     } 

     private void OnNotifyPropertyChanged(string property) 
     { 
      if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(property)); 
      // update the class Observable 
      OnNext(this); 
     } 

    } 

    public class TestModel 
    { 
     private List<TestEntity> collection { get; set; } 
     private IDisposable sub; 

     public TestModel() 
     { 
      this.collection = new List<TestEntity>() { 
      new TestEntity() { File = "MySong.mp3", Rating = 5 }, 
      new TestEntity() { File = "Heart.mp3", Rating = 5 }, 
      new TestEntity() { File = "KarmaPolice.mp3", Rating = 5 }}; 

      var observableCollection = Observable.Concat<TestEntity>(this.collection.Cast<IObservable<TestEntity>>()); 
      var filteredCollection = from entity in observableCollection 
            where entity.Rating==0 
            select entity; 
      this.sub = filteredCollection.Subscribe(entity => 
       { 
        System.Diagnostics.Debug.WriteLine("Added :" + entity.File); 
       } 
      ); 
      this.collection[0].Rating = 0; 
      this.collection[0].Rating = 5; 
     } 
    }; 
} 
+4

"El problema es que Rx no parece ser compatible con la notificación de que una entidad se ha eliminado de la secuencia", esto se debe a que IObservable no representa una colección persistente, solo una secuencia de valores asincrónica. –

Respuesta

5

En realidad, encontré la biblioteca Reactive-UI útil para esto (disponible en NuGet). Esta biblioteca incluye temas especiales IObservable para colecciones y la posibilidad de crear uno de estos 'ReactiveCollections' sobre una tradicional colección INCC. A través de esto tengo transmisiones para artículos nuevos y eliminados y artículos cambiantes en la colección.Luego uso un Zip para combinar las secuencias y modificar una colección observable de ViewModel de destino. Esto proporciona una proyección en vivo basada en una consulta sobre el modelo de origen.

El siguiente código solucionó el problema (este código sería incluso más simple, pero hay algunos problemas con la versión de Silverlight de la IU reactiva que necesitaba soluciones). La colección de fuegos código cambiado eventos, simplemente ajustando el valor de 'Rating' en uno de los elementos de la colección:

using System; 
using System.ComponentModel; 
using System.Linq; 
using System.Collections.Generic; 
using System.Collections.ObjectModel; 
using System.Collections.Specialized; 
using ReactiveUI; 

namespace RxTest 
{ 

    public class TestEntity : ReactiveObject, INotifyPropertyChanged, INotifyPropertyChanging 
    { 
     public string _File; 
     public int _Rating = 0; 
     public string File 
     { 
      get { return _File; } 
      set { this.RaiseAndSetIfChanged(x => x.File, value); } 
     } 

     public int Rating 
     { 
      get { return this._Rating; } 
      set { this.RaiseAndSetIfChanged(x => x.Rating, value); } 
     } 

     public TestEntity() 
     { 
     } 
    } 

    public class TestModel 
    { 
     private IEnumerable<TestEntity> collection { get; set; } 
     private IDisposable sub; 

     public TestModel() 
     { 
      this.collection = new ObservableCollection<TestEntity>() { 
      new TestEntity() { File = "MySong.mp3", Rating = 5 }, 
      new TestEntity() { File = "Heart.mp3", Rating = 5 }, 
      new TestEntity() { File = "KarmaPolice.mp3", Rating = 5 }}; 

      var filter = new Func<int, bool>(Rating => (Rating == 0)); 

      var target = new ObservableCollection<TestEntity>(); 
      target.CollectionChanged += new NotifyCollectionChangedEventHandler(target_CollectionChanged); 
      var react = new ReactiveCollection<TestEntity>(this.collection); 
      react.ChangeTrackingEnabled = true; 

      // update the target projection collection if an item is added 
      react.ItemsAdded.Subscribe(v => { if (filter.Invoke(v.Rating)) target.Add(v); }); 
      // update the target projection collection if an item is removed (and it was in the target) 
      react.ItemsRemoved.Subscribe(v => { if (filter.Invoke(v.Rating) && target.Contains(v)) target.Remove(v); }); 

      // track items changed in the collection. Filter only if the property "Rating" changes 
      var ratingChangingStream = react.ItemChanging.Where(i => i.PropertyName == "Rating").Select(i => new { Rating = i.Sender.Rating, Entity = i.Sender }); 
      var ratingChangedStream = react.ItemChanged.Where(i => i.PropertyName == "Rating").Select(i => new { Rating = i.Sender.Rating, Entity = i.Sender }); 
      // pair the two streams together for before and after the entity has changed. Make changes to the target 
      Observable.Zip(ratingChangingStream,ratingChangedStream, 
       (changingItem, changedItem) => new { ChangingRating=(int)changingItem.Rating, ChangedRating=(int)changedItem.Rating, Entity=changedItem.Entity}) 
       .Subscribe(v => { 
        if (filter.Invoke(v.ChangingRating) && (!filter.Invoke(v.ChangedRating))) target.Remove(v.Entity); 
        if ((!filter.Invoke(v.ChangingRating)) && filter.Invoke(v.ChangedRating)) target.Add(v.Entity); 
       }); 

      // should fire CollectionChanged Add in the target view model collection 
      this.collection.ElementAt(0).Rating = 0; 
      // should fire CollectionChanged Remove in the target view model collection 
      this.collection.ElementAt(0).Rating = 5; 
     } 

     void target_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 
     { 
      System.Diagnostics.Debug.WriteLine(e.Action); 
     } 
    } 
} 
+0

¡Buen uso de RxUI! Una cosa que noté es que ReactiveCollection no siempre es una colección derivada, es una subclase de ObservableCollection, así que puedes usarla directamente. –

+0

Gracias Paul. Noté un par de errores, que supongo que son específicos de Silverlight. La propiedad '.Value' no se completa desde un ReactiveObject para ItemChanging/Changed (se establece en NULL).También tuve problemas para hacer que ReactiveCollection rastreara los cambios en objetos INPC normales, usando un ReactiveObject arreglado eso. –

+0

Esto es por razones de rendimiento: ItemChanging.Value() le dará una secuencia de valores –

2

¿Qué pasa con el uso de un ObservableCollection<T>? Rx es un marco muy fácil de usar en exceso; Encuentro que si te encuentras luchando contra la premisa básica de una transmisión asíncrona, probablemente no deberías usar Rx para ese problema en particular.

+0

Rx es ideal para propagar cambios desde un modelo al ViewModel hasta la Vista. Las características en Rx como clasificación de hilos, fusión etc. lo hacen ideal. –

+0

Basado en la experiencia (he usado Rx en una aplicación WPF de producción), recomendaría tratar (INotifyPropertyChanged) las propiedades de ViewModel como "UI", ya que eso no debería cambiarse de una cadena de fondo. –

+0

Las características en Rx como clasificación de hilos, fusión, temas etc. lo hacen ideal. El solo uso de Rx para los eventos mismos limita este uso y significa que admite dos paradigmas en su código. Creo que el problema fundamental aquí es que IObservable no es adecuado para colecciones, solo los eventos de una colección. Todavía creo que es posible una solución genérica, si el flujo de eventos de la colección está 'comprimido' con la secuencia concat de los contenidos de la colección. –

0

En mi opinión, no es un uso adecuado de Rx. Un Rx Observable es una secuencia de 'eventos' a los que puede suscribirse. Puede reaccionar a estos eventos en su Modelo de Vista, por ejemplo, agregarlos a un ObservableCollection que esté vinculado a su vista. Sin embargo, un Observable no se puede usar para representar un conjunto fijo de elementos a los que agrega/quita elementos.

+0

No, pero el objetivo de ObservableCollection es que expone varios Sujetos que representan las operaciones que puede realizar en la colección. Es una solución muy elegante. – DanH

0

El problema es que está viendo las notificaciones de una Lista de TestEntitys, no de TestEntity. Entonces ves adiciones, pero no cambios en TestEntity. Para ver este comentario:

 if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(property)); 

y verá que el programa funciona igual. Sus notificaciones en TestEntity no están conectadas a nada. Según lo manifestado por otros, el uso de un ObservableCollection agregará este cableado para usted.

+0

Para tu información, siempre debes asignar un evento a una variable local antes de subirlo. De lo contrario, puede encontrarse con una condición de carrera que podría arrojar una NullReferenceException –

+0

de acuerdo, simplemente tratando de mantener el código simple (aunque el soporte de INPC no es realmente necesario). –

1

Todas las implementaciones INPC que he visto nunca puede ser mejor etiquetado como accesos directos o hacks. Sin embargo, realmente no puedo culpar a los desarrolladores ya que el mecanismo INPC que los creadores de .NET eligen soportar es terrible. Dicho esto, recientemente descubrí, en mi opinión, la mejor implementación de INPC, y el mejor complemento para cualquier marco de MVVM. Además de proporcionar docenas de funciones y extensiones extremadamente útiles, también presenta el patrón de INPC más elegante que he visto. Se parece un poco al marco de ReactiveUI, pero no fue diseñado para ser una plataforma completa de MVVM. Para crear un ViewModel que admita INPC, no requiere una clase base o interfaces, sí puede admitir la notificación de cambio completa y el enlace bidireccional, y lo mejor de todo es que todas sus propiedades pueden ser automáticas.

NO utiliza una utilidad como PostSharp o NotifyPropertyWeaver, pero se basa en el marco de las extensiones reactivas. El nombre de este nuevo marco es ReactiveProperty. Sugiero visitar el sitio del proyecto (en el codeplex) y bajar el paquete NuGet. Además, mira a través del código fuente, porque es realmente un placer.

No estoy de ninguna manera asociado con el desarrollador, y el proyecto todavía es bastante nuevo. Estoy realmente entusiasmado con las características que ofrece.

+0

ReactiveProperty se ve bien, pero ninguna de las muestras incluidas utiliza una colección de ViewModels o una vista de detalles maestros, por lo que no está claro cómo se aplica esta biblioteca a esta pregunta (o si es útil en el mundo real, donde a menudo necesitamos una IU para editar una colección de objetos). – Qwertie

+0

La biblioteca es algo nueva, por lo que falta algo de documentación es comprensible. Es cierto que cuando publiqué mi respuesta aquí, solo estaba recogiendo la pila de Silverlight/Xaml. Después de mucha investigación en otros métodos, terminé regresando a la biblioteca, y todavía estoy de acuerdo con mi publicación original. Lo que la mayoría de las veces criticaba la implementación de INPC, pero aún dentro del alcance de esta discusión. Al mirar el código fuente, existe un tipo llamado ReactiveCollection que debe relacionarse directamente y solidificar mis pensamientos con respecto al OP. –

Cuestiones relacionadas