2011-01-18 31 views
7

Tôi đã xem xét sử dụng Rx trong một khung MVVM. Ý tưởng là sử dụng các truy vấn LINQ 'trực tiếp' trên bộ dữ liệu trong bộ nhớ để đưa dữ liệu vào Mô hình Xem để liên kết với.Sử dụng IObservable (Rx) làm thay thế INotifyCollectionChanged cho MVVM?

Trước đây, bạn có thể sử dụng INotifyPropertyChanged/INotifyCollectionChanged và thư viện nguồn mở được gọi là CLINQ. Tiềm năng với Rx và IObservable là di chuyển đến một ViewModel khai báo nhiều hơn bằng cách sử dụng các lớp Subject để truyền các sự kiện đã thay đổi từ mô hình nguồn thông qua View. Một chuyển đổi từ IObservable cho các giao diện databinding thường xuyên sẽ là cần thiết cho bước cuối cùng.

Vấn đề là Rx dường như không hỗ trợ thông báo rằng một thực thể đã bị xóa khỏi luồng. Ví dụ bên dưới.
Mã này hiển thị một POCO sử dụng lớp BehaviorSubject cho trạng thái trường. Mã đi vào để tạo ra một tập hợp các thực thể này và sử dụng Concat để hợp nhất các luồng bộ lọc với nhau. Điều này có nghĩa là bất kỳ thay đổi nào đối với POCO được báo cáo cho một luồng đơn lẻ.

Bộ lọc cho luồng này được thiết lập để lọc cho Xếp hạng == 0. Đăng ký chỉ đơn giản là kết quả đầu ra cho cửa sổ gỡ lỗi khi một thậm chí xảy ra.

Cài đặt Xếp hạng = 0 trên bất kỳ phần tử nào sẽ kích hoạt sự kiện. Nhưng thiết lập Xếp hạng trở lại 5 sẽ không thấy bất kỳ sự kiện nào.

Trong trường hợp CLINQ kết quả của truy vấn sẽ hỗ trợ INotifyCollectionChanged - để các mục được thêm và xóa khỏi kết quả truy vấn sẽ kích hoạt sự kiện chính xác để cho biết kết quả truy vấn đã thay đổi (mục được thêm hoặc xóa).

Cách duy nhất tôi có thể nghĩ đến địa chỉ này là thiết lập hai luồng với truy vấn ngược (kép). Một mục được thêm vào luồng ngược lại ngụ ý xóa khỏi resultset. Nếu không, tôi chỉ có thể sử dụng FromEvent và không làm cho bất kỳ mô hình thực thể nào quan sát được - điều này làm cho Rx chỉ là một Event Aggregator. Bất kỳ con trỏ?

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

"Vấn đề là Rx dường như không hỗ trợ thông báo rằng một thực thể đã bị xóa khỏi luồng" - điều này là do IObservable không đại diện cho một bộ sưu tập liên tục, chỉ là một luồng không đồng bộ các giá trị. –

Trả lời

5

Thực ra tôi thấy thư viện Reactive-UI hữu ích cho điều này (có sẵn trong NuGet). Thư viện này bao gồm các đối tượng IObservable đặc biệt cho các bộ sưu tập và cơ sở để tạo ra một trong những 'Phản ứng thu thập' này qua một bộ sưu tập INCC truyền thống. Thông qua điều này, tôi có các luồng mới, các mục đã xóa và thay đổi các mục trong bộ sưu tập.Sau đó tôi sử dụng một Zip để hợp nhất các luồng với nhau và sửa đổi một bộ sưu tập ViewModel quan sát đích. Điều này cung cấp một phép chiếu trực tiếp dựa trên truy vấn trên mô hình nguồn.

Mã sau giải quyết được vấn đề (mã này thậm chí còn đơn giản hơn, nhưng có một số vấn đề với phiên bản Silverlight của Reactive-UI cần giải pháp). Bộ sưu tập hoả hoạn đang thay đổi các sự kiện bằng cách đơn giản điều chỉnh giá trị của 'Đánh giá' trên một trong những yếu tố thu:

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

Sử dụng mát mẻ RxUI! Một điều tôi nhận thấy là, ReactiveCollection không phải luôn luôn là một bộ sưu tập có nguồn gốc, nó là một phân lớp của ObservableCollection, vì vậy bạn chỉ có thể sử dụng nó trực tiếp. –

+0

Cảm ơn Paul. Nhận thấy một vài lỗi, mà tôi đoán là Silverlight cụ thể. Thuộc tính '.Value' không được điền từ một ReactiveObject cho ItemChanging/Changed (nó được đặt thành NULL).Tôi cũng gặp khó khăn khi nhận ReactiveCollection để theo dõi các thay đổi đối với các đối tượng INPC thông thường - bằng cách sử dụng ReactiveObject đã sửa lỗi đó. –

+0

Đây là lý do hoàn hảo - ItemChanging.Value() sẽ cung cấp cho bạn luồng giá trị –

2

Có gì sai khi sử dụng ObservableCollection<T>? Rx là một khuôn khổ rất dễ sử dụng; Tôi thấy rằng nếu bạn thấy mình chiến đấu chống lại tiền đề cơ bản của một luồng không đồng bộ, có thể bạn không nên sử dụng Rx cho vấn đề cụ thể đó.

+0

Rx lý tưởng cho việc truyền các thay đổi từ một mô hình sang ViewModel đến View. Các tính năng trong Rx như chủ đề marshaling, conflation vv làm cho nó lý tưởng. –

+0

Dựa trên kinh nghiệm (tôi đã sử dụng Rx trong một ứng dụng WPF sản xuất), tôi khuyên bạn nên xử lý (INotifyPropertyChanged) thuộc tính ViewModel là "Giao diện người dùng", trong đó không nên thay đổi từ một chủ đề nền. –

+0

Các tính năng trong Rx như chủ đề marshaling, conflation, Subjects vv làm cho nó lý tưởng. Chỉ cần sử dụng Rx cho các sự kiện tự giới hạn việc sử dụng này và có nghĩa là hỗ trợ hai mô hình trong mã của bạn. Tôi nghĩ rằng vấn đề cơ bản ở đây là IObservable không thích hợp cho các bộ sưu tập, chỉ các sự kiện từ một bộ sưu tập. Tôi vẫn nghĩ rằng một giải pháp chung là có thể, nếu dòng sự kiện từ bộ sưu tập được 'nén' với luồng concat từ nội dung của bộ sưu tập. –

0

Theo tôi, đó không phải là cách sử dụng Rx phù hợp. Một Rx Observable là một dòng 'sự kiện' mà bạn có thể đăng ký. Bạn có thể phản ứng với những sự kiện này trong Chế độ xem của bạn, ví dụ: thêm chúng vào ObservableCollection được gắn với chế độ xem của bạn. Tuy nhiên, một Observable không thể được sử dụng để đại diện cho một tập hợp các mục cố định mà bạn thêm/xóa các mục từ đó.

+0

Không, điểm quan sát của ObservableCollection là nó cho thấy nhiều đối tượng đại diện cho các hoạt động bạn có thể thực hiện trên bộ sưu tập. Đó là một giải pháp rất thanh lịch. – DanH

0

Vấn đề là bạn đang xem thông báo từ Danh sách TestEntity, chứ không phải từ chính TestEntity. Vì vậy, bạn thấy thêm, nhưng không thay đổi trong bất kỳ TestEntity nào. Để xem nhận xét này:

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

và bạn sẽ thấy chương trình chạy giống nhau! Thông báo của bạn trong TestEntity không được kết nối với bất kỳ thứ gì. Như đã nêu bởi những người khác, sử dụng ObservableCollection sẽ thêm dây này cho bạn.

+0

FYI, bạn nên luôn gán một sự kiện cho biến cục bộ trước khi tăng nó. Nếu không, bạn có thể chạy vào một điều kiện chủng tộc có thể ném một NullReferenceException –

+0

Đồng ý, chỉ cần cố giữ mã đơn giản (mặc dù hỗ trợ INPC không thực sự cần thiết). –

1

Tất cả các triển khai INPC mà tôi đã từng nhìn thấy có thể được dán nhãn tốt nhất như các phím tắt hoặc hacks. Tuy nhiên, tôi không thể thực sự gây lỗi cho các nhà phát triển vì cơ chế INPC mà những người sáng tạo .NET chọn hỗ trợ là khủng khiếp. Như đã nói, gần đây tôi đã phát hiện ra, theo ý kiến ​​của tôi, việc thực hiện tốt nhất INPC và lời khen tốt nhất cho bất kỳ khung MVVM nào xung quanh. Ngoài việc cung cấp hàng chục chức năng và tiện ích vô cùng hữu ích, nó cũng thể hiện mẫu INPC thanh lịch nhất mà tôi từng thấy. Nó hơi giống khuôn khổ ReactiveUI, nhưng nó không được thiết kế để trở thành một nền tảng MVVM toàn diện. Để tạo một ViewModel hỗ trợ INPC, nó không yêu cầu lớp cơ sở hoặc giao diện, có vẫn có thể hỗ trợ thông báo thay đổi hoàn toàn và ràng buộc hai chiều, và tốt nhất là tất cả các thuộc tính của bạn có thể tự động!

Nó KHÔNG sử dụng một tiện ích như PostSharp hoặc NotifyPropertyWeaver, nhưng được xây dựng xung quanh khung Hoạt động mở rộng phản ứng. Tên của khung công tác mới này là ReactiveProperty. Tôi khuyên bạn nên truy cập trang web dự án (trên codeplex) và kéo gói NuGet xuống. Ngoài ra, xem xét mã nguồn, bởi vì nó thực sự là một điều trị.

Tôi không liên kết với nhà phát triển và dự án vẫn còn khá mới. Tôi thực sự rất nhiệt tình về các tính năng mà nó cung cấp.

+0

ReactiveProperty trông thật tuyệt, nhưng không có mẫu nào được bao gồm sử dụng bộ sưu tập Chế độ xem hoặc chế độ xem chi tiết chính, vì vậy không rõ cách thư viện này áp dụng cho câu hỏi này (hoặc liệu nó có hữu ích trong thế giới thực hay không, nơi mà chúng ta thường cần một giao diện người dùng để chỉnh sửa một tập hợp các đối tượng). – Qwertie

+0

Thư viện có phần mới, vì vậy thiếu một số tài liệu có thể hiểu được. Phải thừa nhận rằng khi tôi đăng câu trả lời của tôi ở đây, tôi đã chọn lên ngăn xếp Silverlight/Xaml. Sau nhiều nghiên cứu trong các phương pháp khác, tôi đã trở lại thư viện và vẫn đồng ý với bài viết gốc của mình. Hầu hết thời gian chỉ trích việc triển khai INPC, nhưng vẫn nằm trong phạm vi của cuộc thảo luận này. Nhìn vào mã nguồn, một loại tồn tại được gọi là ReactiveCollection nên liên quan trực tiếp và củng cố suy nghĩ của tôi về OP. –

Các vấn đề liên quan