使用IObservable(Rx)作为MVVM的INotifyCollectionChanged替换?

时间:2011-01-18 14:28:00

标签: c# silverlight mvvm system.reactive

我一直在研究在MVVM框架中使用Rx。我们的想法是对内存数据集使用“实时”LINQ查询,将数据投影到View Models中以进行绑定。

以前,可以使用INotifyPropertyChanged / INotifyCollectionChanged和一个名为CLINQ的开源库。 Rx和IObservable的潜力是使用Subject类将更改的事件从源模型传播到View,从而转移到更具声明性的ViewModel。最后一步需要从IObservable到常规数据绑定接口的转换。

问题是Rx似乎不支持已从流中删除实体的通知。示例如下。
该代码显示了一个POCO,它使用BehaviorSubject类作为字段状态。代码继续创建这些实体的集合,并使用Concat将过滤器流合并在一起。这意味着对POCO的任何更改都会报告给单个流。

此流的过滤器设置为过滤评级== 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;
        }
    };
}

5 个答案:

答案 0 :(得分:6)

实际上我发现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);
        }
    }
}

答案 1 :(得分:2)

使用ObservableCollection<T>有什么问题? Rx是一个非常容易过度使用的框架;我发现如果你发现自己正在反对异步流的基本前提,那么你可能不应该使用Rx来解决这个问题。

答案 2 :(得分:1)

我见过的所有INPC实现都可以最好地标记为快捷方式或黑客。但是,由于.NET创建者选择支持的INPC机制非常糟糕,因此我无法真正对开发人员造成错误。话虽如此,我最近发现,在我看来,INPC的最佳实现,以及对任何MVVM框架的最佳恭维。除了提供数十种极其有用的功能和扩展外,它还具有我见过的最优雅的INPC模式。它有点类似于ReactiveUI框架,但它并不是一个全面的MVVM平台。要创建支持INPC的ViewModel,它不需要基类或接口,是的仍然能够支持完整的更改通知和双向绑定,最重要的是,所有属性都可以自动生成!

它不使用PostSharp或NotifyPropertyWeaver等实用程序,而是围绕Reactive Extensions框架构建。此新框架的名称为 ReactiveProperty 。我建议访问项目网站(在codeplex上),然后下拉NuGet包。另外,查看源代码,因为它确实是一种享受。

我与开发人员没有任何联系,项目仍然相当新。我对它提供的功能非常热衷。

答案 3 :(得分:0)

在我看来,这不适合使用Rx。 Rx Observable是您可以订阅的“事件”流。您可以在视图模型中对这些事件做出反应,例如将它们添加到绑定到视图的ObservableCollection。但是,Observable不能用于表示您添加/删除项目的固定项目集。

答案 4 :(得分:0)

问题是您正在查看TestEntity列表中的通知,而不是来自TestEntity本身。所以你看到了添加,但没有任何TestEntity的变化。要查看此评论:

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

你会看到该程序运行相同!您在TestEntity中的通知没有连接到任何东西。如其他人所述,使用ObservableCollection将为您添加此连线。