【问题标题】:Using IObservable (Rx) as a INotifyCollectionChanged Replacement for MVVM?使用 IObservable (Rx) 作为 MVVM 的 INotifyCollectionChanged 替代品?
【发布时间】:2011-01-18 14:27:59
【问题描述】:

我一直在研究在 MVVM 框架中使用 Rx。这个想法是对内存数据集使用“实时”LINQ 查询,将数据投影到要绑定的视图模型中。

以前,这可以通过使用 INotifyPropertyChanged/INotifyCollectionChanged 和名为 CLINQ 的开源库来实现。 Rx 和 IObservable 的潜力在于使用主题类将更改的事件从源模型传播到视图,从而转移到更具声明性的 ViewModel。最后一步需要从 IObservable 转换为常规数据绑定接口。

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

为此流设置了一个过滤器以过滤 Rating==0。当偶数发生时,订阅只是将结果输出到调试窗口。

在任何元素上设置 Rating=0 都会触发该事件。但是将 Rating 设置回 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;
        }
    };
}

【问题讨论】:

  • "问题在于 Rx 似乎不支持实体已从流中移除的通知" - 这是因为 IObservable 不代表持久集合,仅代表异步值流.

标签: c# silverlight mvvm system.reactive


【解决方案1】:

实际上,我发现 Reactive-UI 库对此很有帮助(在 NuGet 中可用)。 该库包括用于收藏的特殊 IObservable 主题以及在传统 INCC 收藏上创建这些“ReactiveCollections”之一的设施。 通过这个,我有新的、已删除的项目和集合中更改项目的流。然后我使用 Zip 将流合并在一起并修改目标 ViewModel 可观察集合。这提供了基于源模型查询的实时投影。

下面的代码解决了这个问题(这个代码会更简单,但是 Reactive-UI 的 Silverlight 版本存在一些问题,需要解决方法)。代码通过简单地调整集合元素之一上的“评级”值来触发集合更改事件:

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);
        }
    }
}

【讨论】:

  • RxUI 的酷用!我注意到的一件事是,ReactiveCollection 并不总是派生集合,它是 ObservableCollection 的子类,因此您可以直接使用它。
  • 谢谢保罗。注意到几个错误,我猜这是 Silverlight 特有的。 '.Value' 属性不是从 ReactiveObject 中填写的 ItemChanging/Changed(它设置为 NULL)。我也很难让 ReactiveCollection 跟踪常规 INPC 对象的更改 - 使用 ReactiveObject 修复了该问题。
  • 这是出于性能原因 - ItemChanging.Value() 将为您提供值流
【解决方案2】:

使用ObservableCollection&lt;T&gt; 有什么问题? Rx 是一个非常容易过度使用的框架;我发现如果你发现自己与异步流的基本前提作斗争,你可能不应该使用 Rx 来解决这个特定问题。

【讨论】:

  • Rx 非常适合将更改从模型传播到 ViewModel 再到 View。 Rx 中的特性(如线程编组、合并等)使其非常理想。
  • 根据经验(我在生产 WPF 应用程序中使用过 Rx),我建议将 (INotifyPropertyChanged) ViewModel 属性视为“UI”,因为不应从后台线程更改.
  • Rx 中的线程编组、合并、主题等特性使其非常理想。仅将 Rx 用于事件本身会限制这种使用,并意味着在您的代码中支持两种范式。我认为这里的根本问题是 IObservable 不适合集合,只适合集合中的事件。如果集合中的事件流与集合内容中的 concat 流“压缩”,我仍然认为通用解决方案是可能的。
  • 理查德 - 在大多数情况下我会同意。但不幸的是,这在进行大型或频繁的模型更改时无法扩展。 Dispatcher 溢出,UI 变得无响应。这就是 Rx 和一些事件流函数的吸引力。
【解决方案3】:

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

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

我与开发者没有任何关系,而且这个项目还是相当新的。我只是对它提供的功能非常感兴趣。

【讨论】:

  • ReactiveProperty 看起来很酷,但是所包含的示例都没有使用 ViewModel 集合或主从视图,所以不清楚这个库如何应用于这个问题(或者它在实际中是否有用)世界,我们经常需要一个 UI 来编辑对象集合)。
  • 这个库有点新,所以缺少一些文档是可以理解的。诚然,当我在这里发布我的回复时,我只是在挑选 Silverlight/Xaml 堆栈。在对其他方法进行了大量研究之后,我最终回到了图书馆,仍然同意我原来的帖子。其中大部分时间批评了 INPC 的实施,但仍在本次讨论的范围内。查看源代码,存在一种称为 ReactiveCollection 的类型,它应该直接关联并巩固我对 OP 的想法。
【解决方案4】:

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

【讨论】:

  • 不,但 ObservableCollection 的重点在于它公开了多个主题,这些主题代表您可以对集合执行的操作。这是一个非常优雅的解决方案。
【解决方案5】:

问题是您正在查看来自 TestEntity 列表的通知,而不是来自 TestEntity 本身的通知。因此,您会看到添加,但没有任何 TestEntity 中的更改。要查看此评论:

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

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

【讨论】:

  • 仅供参考,您应该始终在引发事件之前将事件分配给局部变量。否则,您可能会遇到可能引发 NullReferenceException 的竞争条件
  • 同意,只是尽量保持代码简单(尽管实际上并不需要 INPC 支持)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-14
相关资源
最近更新 更多