Joe*_*ood 7 c# silverlight mvvm system.reactive
我一直在研究在MVVM框架中使用Rx.我们的想法是对内存数据集使用"实时"LINQ查询,将数据投影到View Models中进行绑定.
以前,使用INotifyPropertyChanged/INotifyCollectionChanged和一个名为CLINQ的开源库已经成为可能.Rx和IObservable的潜力是使用Subject类将更改的事件从源模型传播到View,从而转移到更具声明性的ViewModel.最后一步需要从IObservable到常规数据绑定接口的转换.
问题是Rx似乎不支持已从流中删除实体的通知.以下示例.
该代码显示了一个POCO,它使用BehaviorSubject类作为字段状态.代码继续创建这些实体的集合,并使用Concat将过滤器流合并在一起.这意味着对POCO的任何更改都会报告给单个流.
设置此流的过滤器以过滤Rating == 0.订阅只会在偶数发生时将结果输出到调试窗口.
任何元素上的设置评级= 0将触发事件.但将评级设置回5将不会发现任何事件.
在CLINQ的情况下,查询的输出将支持INotifyCollectionChanged - 以便从查询结果中添加和删除的项目将触发正确的事件以指示查询结果已更改(添加或删除项目).
我能想到解决这个问题的唯一方法是使用反对(双)查询来设置两个流.添加到相反流的项目意味着从结果集中删除.如果做不到这一点,我可以使用FromEvent并且不会使任何实体模型可观察 - 这使得Rx更像是一个事件聚合器.有什么指针吗?
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;
}
};
}
Run Code Online (Sandbox Code Playgroud)
实际上我发现Reactive-UI库对此有帮助(在NuGet中可用).该库包括用于集合的特殊IObservable主题,以及通过传统INCC集合创建其中一个"ReactiveCollections"的工具.通过这个我有新的,删除的项目和更改集合中的项目的流.然后我使用Zip将流合并在一起并修改目标ViewModel可观察集合.这提供了基于源模型查询的实时投影.
下面的代码解决了这个问题(这段代码甚至更简单,但是需要解决方法的Silverlight版本的Reactive-UI存在一些问题).该代码通过简单地调整其中一个集合元素上的"Rating"值来触发集合更改事件:
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);
}
}
}
Run Code Online (Sandbox Code Playgroud)