2011-01-18 12 views
7

Ho cercato di utilizzare Rx in un framework MVVM. L'idea è di utilizzare query LINQ "live" su dataset in-memory per proiettare i dati in View Models da associare.Utilizzo di IObservable (Rx) come sostituzione di INotifyCollectionChanged per MVVM?

In precedenza questo era possibile con l'uso di INotifyPropertyChanged/INotifyCollectionChanged e una libreria open source denominata CLINQ. Il potenziale con Rx e IObservable è quello di passare a un ViewModel molto più dichiarativo usando le classi Subject per propagare gli eventi modificati dal modello sorgente alla View. Per l'ultimo passaggio sarebbe necessaria una conversione da IObservable alle normali interfacce di associazione dati.

Il problema è che Rx non sembra supportare la notifica che un'entità è stata rimossa dal flusso. Esempio di seguito.
Il codice mostra un POCO che utilizza la classe BehaviorSubject per lo stato del campo. Il codice va avanti per creare una raccolta di queste entità e utilizzare Concat per unire i flussi di filtro insieme. Ciò significa che qualsiasi modifica ai POCO viene segnalata a un singolo flusso.

Un filtro per questo flusso è impostato per filtrare per Rating == 0. L'abbonamento emette semplicemente il risultato nella finestra di debug quando si verifica un evento pari.

Impostazioni Valutazione = 0 su qualsiasi elemento attiverà l'evento. Tuttavia, l'impostazione di Valutazione su 5 non vedrà alcun evento.

Nel caso di CLINQ l'output della query supporterà INotifyCollectionChanged - in modo che gli elementi aggiunti e rimossi dal risultato della query generino l'evento corretto per indicare che il risultato della query è stato modificato (un elemento aggiunto o rimosso).

L'unico modo in cui posso pensare a questo indirizzo è impostare due flussi con query oppossite (doppie). Un elemento aggiunto al flusso opposto implica la rimozione dal set di risultati. In caso contrario, potrei semplicemente utilizzare FromEvent e non rendere nessuno dei modelli di entità osservabili, il che rende Rx più di un semplice Aggregatore di eventi. Qualche indicazione?

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

"Il problema è che Rx non sembra supportare la notifica che un'entità è stata rimossa dal flusso" - questo perché IObservable non rappresenta una raccolta persistente, ma solo un flusso asincrono di valori. –

risposta

5

In realtà ho trovato utile la libreria Reactive-UI (disponibile in NuGet). Questa libreria include oggetti IO specifici per le collezioni e la possibilità di creare una di queste "ReactiveCollections" su una tradizionale raccolta INCC. Attraverso questo ho flussi per nuovi, articoli rimossi e cambiando oggetti nella collezione.Quindi utilizzo uno Zip per unire insieme gli stream e modificare una raccolta osservabile ViewModel di destinazione. Questo fornisce una proiezione dal vivo basata su una query sul modello sorgente.

Il seguente codice risolveva il problema (questo codice sarebbe ancora più semplice, ma ci sono alcuni problemi con la versione di Silverlight di Reactive-UI che aveva bisogno di soluzioni alternative). La collezione incendi codice modificato eventi semplicemente regolando il valore di 'Valutazione' su uno degli elementi della collezione:

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

Uso razionale di RxUI! Una cosa che ho notato è che ReactiveCollection non è sempre una collezione derivata, è una sottoclasse di ObservableCollection, quindi puoi semplicemente usarla direttamente. –

+0

Grazie Paul. Ho notato un paio di bug, che credo siano specifici di Silverlight. La proprietà '.Value' non è compilata da ReactiveObject per ItemChanging/Changed (è impostata su NULL).Ho anche avuto problemi a ottenere ReactiveCollection per tenere traccia delle modifiche sugli oggetti INPC regolari, usando un oggetto ReactiveObject che lo ha risolto. –

+0

Questo è per motivi perf - ItemChanging.Value() ti darà un flusso di valori –

2

Cosa c'è di sbagliato con l'utilizzo di un ObservableCollection<T>? Rx è una struttura molto semplice da utilizzare eccessivamente; Trovo che se ti trovi a combattere contro la premessa di base di un flusso asincrono, probabilmente non dovresti usare Rx per quel particolare problema.

+0

Rx è ideale per propagare le modifiche da un modello a ViewModel fino alla vista. Le caratteristiche in Rx come il thread marshaling, la conversione ecc. Lo rendono ideale. –

+0

Sulla base dell'esperienza (ho usato Rx in un'applicazione WPF di produzione), consiglierei di trattare (INotifyPropertyChanged) le proprietà ViewModel come "UI", in quanto ciò non dovrebbe essere modificato da un thread in background. –

+0

Le funzionalità in Rx come il threading del marshalling, la conversione, i soggetti ecc. Lo rendono ideale. Il solo utilizzo di Rx per gli eventi stessi limita questo uso e significa supportare due paradigmi nel codice. Penso che il problema fondamentale qui sia che IObservable non è adatto per le collezioni, solo gli eventi di una raccolta. Continuo a pensare che sia possibile una soluzione generica, se il flusso di eventi dalla raccolta viene "compresso" con il flusso concat dai contenuti della raccolta. –

0

A mio parere non è un utilizzo appropriato di Rx. Un Rx Observable è un flusso di "eventi" a cui puoi iscriverti. Puoi reagire a questi eventi nel tuo Modello di vista, ad esempio aggiungendoli a una ObservableCollection associata alla tua vista. Tuttavia, un Osservabile non può essere usato per rappresentare un insieme fisso di elementi da cui aggiungi/rimuovi oggetti.

+0

No, ma il punto di ObservableCollection è che espone più oggetti che rappresentano le operazioni che è possibile eseguire sulla raccolta. È una soluzione molto elegante. – DanH

0

Il problema è che si stanno visualizzando le notifiche da un elenco di TestEntitys, non dallo stesso TestEntity. Quindi vedi aggiunge, ma non le modifiche in qualsiasi TestEntity. Per vedere questo commento:

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

e vedrai che il programma funziona allo stesso modo! Le tue notifiche nel tuo TestEntity non sono collegate a nulla. Come affermato da altri, l'uso di ObservableCollection aggiungerà questo cablaggio per te.

+0

FYI, dovresti sempre assegnare un evento a una variabile locale prima di aumentarlo. In caso contrario, è possibile imbattersi in una condizione di competizione che potrebbe generare una NullReferenceException –

+0

Concordata, semplicemente cercando di mantenere il codice semplice (sebbene il supporto INPC non sia realmente richiesto). –

1

Tutte le implementazioni INPC che io abbia mai visto può essere meglio etichettata come scorciatoie o hack. Tuttavia, non posso davvero criticare gli sviluppatori dal momento che il meccanismo INPC che i creatori di .NET scelgono di supportare è terribile. Detto questo, ho scoperto di recente, a mio parere, la migliore implementazione di INPC, e il miglior complimento a qualsiasi framework MVVM in circolazione. Oltre a fornire dozzine di funzioni ed estensioni estremamente utili, offre anche il modello INPC più elegante che abbia mai visto. Assomiglia in qualche modo al framework ReactiveUI, ma non è stato progettato per essere una piattaforma MVVM completa. Per creare un ViewModel che supporti INPC, non richiede alcuna classe di base o interfacce, sì è ancora in grado di supportare la notifica completa delle modifiche e il binding bidirezionale e, soprattutto, tutte le proprietà possono essere automatiche!

NON utilizza un'utilità come PostSharp o NotifyPropertyWeaver, ma è costruito attorno al framework Reactive Extensions. Il nome di questo nuovo framework è ReactiveProperty. Suggerisco di visitare il sito del progetto (codeplex) e di rimuovere il pacchetto NuGet. Inoltre, controlla il codice sorgente, perché è davvero un piacere.

Non sono in alcun modo associato allo sviluppatore e il progetto è ancora abbastanza nuovo. Sono davvero entusiasta delle funzionalità che offre.

+0

ReactiveProperty sembra interessante, ma nessuno degli esempi inclusi utilizza una raccolta di ViewModels o una vista dettagli master, quindi non è chiaro come si applica questa libreria a questa domanda (o se è utile nel mondo reale, dove spesso abbiamo bisogno di un'interfaccia utente per modificare una collezione di oggetti). – Qwertie

+0

La libreria è in qualche modo nuova, quindi mancare qualche documentazione è comprensibile. Certo, quando ho postato la mia risposta, stavo solo raccogliendo lo stack Silverlight/Xaml. Dopo molte ricerche su altri metodi, ho finito per tornare in biblioteca, e sono ancora d'accordo con il mio post originale. La maggior parte delle volte ha criticato l'implementazione dell'INPC, ma ancora nell'ambito di questa discussione. Osservando il codice sorgente, esiste un tipo chiamato ReactiveCollection che dovrebbe direttamente correlare e consolidare i miei pensieri riguardo all'OP. –

Problemi correlati