ReactiveUIの抂芁コレクション

こんにちは、Habr



パヌト1ReactiveUIの抂芁ViewModelのプロパティをポンピングする



前の蚘事で、ViewModelのプロパティず、ReactiveUIを䜿甚しおそれらを䜿甚しおできるこずに぀いお説明したした。 プロパティ間の䟝存関係を敎理し、ビュヌモデルの1か所に、プロパティ間の䟝存関係が含たれおいる情報を収集したした。

今回は、プロパティに぀いおもう少し話しおから、コレクションに移りたしょう。 通垞のコレクションにはどんな問題があるのか​​、なぜ倉曎を通知しお新しいコレクションを䜜成する必芁があったのかを理解しおみたしょう。 そしお、もちろん、それらを䜿甚しおみおください。



プロパティに関するいく぀かの蚀葉



メむントピックに移る前に、プロパティに぀いおいく぀か説明したす。 前回、次の構文に到達したした。

private string _firstName; public string FirstName { get { return _firstName; } set { this.RaiseAndSetIfChanged(ref _firstName, value); } }
      
      





各プロパティに6行のコヌドを費やすこずは、特にそのようなプロパティが倚く、実装が垞に同じ堎合は、かなり無駄です。 Cでは、この問題を解決するために、自動プロパティが䞀床に远加され、より簡単に生掻できるようになりたした。 私たちのケヌスでは䜕ができたすか

コメントでは、 Fodyが蚀及されたした-プロゞェクトのビルド埌にILコヌドを倉曎できるツヌルです。 たずえば、倉曎通知を自動プロパティの実装に远加したす。 たた、ReactiveUIには、察応する拡匵機胜であるReactiveUI.Fodyもありたす。 䜿っおみよう。 ちなみに、埓来の実装の拡匵機胜であるPropertyChangedもありたすが、 RaiseAndSetIfChangedを呌び出す必芁があるため、私たちには適しおいたせん。



NuGet> Install-Package ReactiveUI.Fodyからむンストヌルしたす

FodyWeavers.xmlがプロゞェクトに衚瀺されたす。 むンストヌル枈みの拡匵機胜を远加したす。

 <?xml version="1.0" encoding="utf-8" ?> <Weavers> <ReactiveUI /> </Weavers>
      
      





そしお、プロパティを倉曎したす。

 [Reactive] public string FirstName { get; set; }
      
      





このツヌルを䜿甚するず、ObservableAsPropertyHelper <>に基づいおFullNameプロパティを実装するこずもできたす。 この方法はGitHubのドキュメントで説明されおいたすが、ここでは省略したす。 2行は完党に受け入れられるオプションだず思いたす。RePropertyUI.Fodyがこのプロパティを正しく実装できるようにするToPropertyの代わりに、サヌドパヌティのメ゜ッドを䜿甚したくありたせん。



䜕も壊れおいないこずを確認したす。 どうやっお 単䜓テストで確認したす。 ReactiveUIはそれらに友奜的であり、 MVVMフレヌムワヌク<...>が゚レガントでテスト可胜なナヌザヌむンタヌフェヌスを䜜成するのは、䜕のためでもありたせん.... むベントのトリガヌを確認するには、手動でサむンアップし、トリガヌされたずきにデヌタを保存しおから凊理する必芁はありたせん。

Observable.FromEventPatternは、むベントトリガヌをすべおの必芁な情報ずずもにIObservable <>に倉換するのに圹立ちたす。 IObservable <>をむベントのリストに倉換し、正確性を確認するには、拡匵メ゜ッド.CreateCollectionを䜿甚するず䟿利です。 圌は、゜ヌスがOnCompleteを呌び出すか、Disposeを呌び出すたで、IObservable <>を経由した芁玠が远加されるコレクションを䜜成したす。この堎合、トリガヌされたむベントに関する情報が远加されたす。

コレクションはすぐに返され、埌で非同期的に芁玠が远加されるこずに泚意しおください。 この動䜜は、たずえば.ToListずは異なりたす。.ToListは制埡を返さないため、コレクション自䜓はOnCompleteに返されたす。OnCompleteは、通垞のむベントサブスクリプションの堎合は氞遠に埅機したす。

 [Test] public void FirstName_WhenChanged_RaisesPropertyChangedEventForFirstNameAndFullNameProperties() { var vm = new PersonViewModel("FirstName", "LastName"); var evendObservable = Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>( a => vm.PropertyChanged += a, a => vm.PropertyChanged -= a); var raisedEvents = evendObservable.CreateCollection(); using (raisedEvents) { vm.FirstName = "NewFirstName"; } Assert.That(vm.FullName, Is.EqualTo("NewFirstName LastName")); Assert.That(raisedEvents, Has.Count.EqualTo(2)); Assert.That(raisedEvents[0].Sender, Is.SameAs(vm)); Assert.That(raisedEvents[0].EventArgs.PropertyName, Is.EqualTo(nameof(PersonViewModel.FirstName))); Assert.That(raisedEvents[1].Sender, Is.SameAs(vm)); Assert.That(raisedEvents[1].EventArgs.PropertyName, Is.EqualTo(nameof(PersonViewModel.FullName))); }
      
      





テストスクリプト自䜓プロパティに新しい倀を蚭定するはusingの内郚で実行され、その埌でチェックが行われたす。 これは、チェック䞭に䞀郚のむベントが誀っお機胜せず、コレクションが損なわれないようにするために必芁です。 もちろん、これはしばしば必芁ではありたせんが、時には重芁になる堎合がありたす。



IObservable <> Changedが同じものを返すこずを確認したしょう。

 [Test] public void FirstName_WhenChanged_PushesToPropertyChangedObservableForFirstNameAndFullNameProperties() { var vm = new PersonViewModel("FirstName", "LastName"); var notifications = vm.Changed.CreateCollection(); using (notifications) { vm.FirstName = "NewFirstName"; } Assert.That(vm.FullName, Is.EqualTo("NewFirstName LastName")); Assert.That(notifications, Has.Count.EqualTo(2)); Assert.That(notifications[0].Sender, Is.SameAs(vm)); Assert.That(notifications[0].PropertyName, Is.EqualTo(nameof(PersonViewModel.FirstName))); Assert.That(notifications[1].Sender, Is.SameAs(vm)); Assert.That(notifications[1].PropertyName, Is.EqualTo(nameof(PersonViewModel.FullName))); }
      
      





そしお...テストは萜ちたした。 しかし、プロパティの倉曎に関する情報の゜ヌスを倉曎したした テストが倱敗した理由を理解しおみたしょう。

 vm.Changed.Subscribe(n => Console.WriteLine(n.PropertyName)); vm.FirstName = "OMG";
      
      





そしお、我々は埗る

氏名

名



行くぞ これはフレヌムワヌクのバグではなく、実装の詳现のようです。 これは理解できたす。䞡方のプロパティはすでに倉曎されおおり、通知の順序は重芁ではありたせん。 䞀方で、むベントの順序ず䞀臎せず、期埅を満たしおいないため、混乱する可胜性がありたす。 もちろん、通知の順序に基づいおアプリケヌションロゞックを構築するこずは明らかに悪い考えです。 しかし、たずえば、アプリケヌションログを読み取るず、䟝存関係プロパティが䟝存関係の倉曎前に倉曎に぀いお通知しおいるこずがわかりたす。 したがっお、この機胜を忘れないでください。

そのため、ReactiveUI.Fodyが適切に動䜜し、コヌドの量を倧幅に削枛するようにしたした。 次にそれを䜿甚したす。






それでは、コレクションに移りたしょう



INotifyPropertyChangedむンタヌフェむスは、既知のように、ビュヌモデルのプロパティを倉曎する堎合に䜿甚されたす。たずえば、䜕かが倉曎され、むンタヌフェむスを再描画する必芁があるこずを芖芚芁玠に通知するために䜿甚されたす。 しかし、ビュヌモデルに倚くの芁玠のコレクションニュヌスフィヌドなどがあり、既に衚瀺されおいるものに新しい゚ントリを远加する必芁がある堎合はどうすればよいでしょうか。 コレクションが存圚するプロパティが倉曎されたこずを通知したすか 可胜ですが、むンタヌフェむス内のリスト党䜓が再配眮されるこずになり、特にモバむルデバむスの堎合は、操䜜が遅くなる可胜性がありたす。 いいえ、それは機胜したせん。 コレクション自䜓が䜕かが倉曎されたこずを通知する必芁がありたす。 幞いなこずに、玠晎らしいむンタヌフェヌスがありたす

 public interface INotifyCollectionChanged { /// <summary>Occurs when the collection changes.</summary> event NotifyCollectionChangedEventHandler CollectionChanged; }
      
      





コレクションがそれを実装する堎合、远加/削陀/眮換などの堎合 CollectionChangedによっおトリガヌされるむベント。 たた、ニュヌスリストを再床䜜成し盎しおレコヌドのコレクション党䜓を確認する必芁はありたせん。むベントを通じお远加された新しい芁玠を远加するだけです。 .NETにはそれを実装するコレクションがありたすが、ReactiveUIに぀いお話しおいたす。 䜕が入っおいるの

むンタヌフェむスのセット党䜓IReactiveList <T>、IReadOnlyReactiveList <T>、IReadOnlyReactiveCollection <T>、IReactiveCollection <T>、IReactiveNotifyCollectionChanged <T>、IReactiveNotifyCollectionItemChanged <T>。 ここではそれぞれの説明はしたせんが、それらが䜕であるかずいう名前から明確にすべきだず思いたす。

しかし、実装をさらに詳しく芋おみたしょう。 䌚うReactiveList <T>。 圌はそれらすべおを実装しおいたす。 コレクションの倉曎を远跡するこずに興味があるので、このクラスの察応するプロパティを芋おみたしょう。

IReactiveListlt; Tgt;の倉曎を远跡するためのプロパティ

かなりたくさん 芁玠の远加、削陀、移動、芁玠の数、コレクションの空、リセットの必芁性を監芖したす。 これをすべお詳现に怜蚎しおください。 もちろん、INotifyCollectionChanged、INotifyPropertyChanged、およびそれらのペアになった* Changindからのむベントも実装されたすが、それらに぀いおは説明したせん。これらは、図に瀺されおいる「芳枬可胜な」プロパティず䞊行しお動䜜し、そこにはナニヌクなものはありたせん。

手始めに、簡単な䟋。 通知のいく぀かの゜ヌスをサブスクラむブし、コレクションを少し操䜜したす。

 var list = new ReactiveList<string>(); list.BeforeItemsAdded.Subscribe(e => Console.WriteLine($"Before added: {e}")); list.ItemsAdded.Subscribe(e => Console.WriteLine($"Added: {e}")); list.BeforeItemsRemoved.Subscribe(e => Console.WriteLine($"Before removed: {e}")); list.ItemsRemoved.Subscribe(e => Console.WriteLine($"Removed: {e}")); list.CountChanging.Subscribe(e => Console.WriteLine($"Count changing: {e}")); list.CountChanged.Subscribe(e => Console.WriteLine($"Count changed: {e}")); list.IsEmptyChanged.Subscribe(e => Console.WriteLine($"IsEmpty changed: {e}")); Console.WriteLine("# Add 'first'"); list.Add("first"); Console.WriteLine("\n# Add 'second'"); list.Add("second"); Console.WriteLine("\n# Remove 'first'"); list.Remove("first");
      
      







結果が埗られたす。

「最初」を远加

カりント倉曎0

远加前最初

倉曎されたカりント1

倉曎されたIsEmptyFalse

远加最初



「秒」を远加

カりント倉曎1

远加前2番目

倉曎されたカりント2

远加2番目



「最初」を削陀

カりント倉曎2

削陀する前最初

倉曎されたカりント1

削陀枈み最初



远加たたは削陀されたもの、芁玠の数の倉曎、コレクションの空の兆候が通知されたす。

さらに

-ItemsAdded / ItemsRemoved / BeforeItemsAdded / BeforeItemsRemovedは、远加たたは削陀されたアむテム自䜓を返したす

-CountChanging / CountChangedは、倉曎前埌の芁玠の数を返したす

-IsEmptyChangedは、コレクションの空の属性の新しい倀を返したす



埮劙な点が1぀ありたす



これたでのずころ、すべおが予枬可胜です。 ここで、コレクション内のレコヌドの数をカりントするのは、远加ず削陀の通知に基づいおのみであるず想像しおください。 䜕がもっず簡単だろうか

 var count = 0; var list = new ReactiveList<int>(); list.ItemsAdded.Subscribe(e => count++); list.ItemsRemoved.Subscribe(e => count--); for (int i = 0; i < 100; i++) { list.Add(i); } for (int i = 0; i < 100; i+=2) { list.Remove(i); } Assert.That(count, Is.EqualTo(list.Count));
      
      





テストは成功したした。 コレクションを埋める原則を倉曎し、䞀床に倚くの芁玠を远加したす。

 list.AddRange(Enumerable.Range(0, 10)); list.RemoveAll(Enumerable.Range(0, 5).Select(i => i * 2));
      
      





成功したした。 キャッチはないようです。 ちょっず埅っお 

 list.AddRange(Enumerable.Range(0, 100)); list.RemoveAll(Enumerable.Range(0, 50).Select(i => i * 2));
      
      





ああ テストは倱敗し、count == 0になりたした。䜕かを考慮しおいないようです。 正しくしたしょう。



問題は、ReactiveList <T>は芋かけほどプリミティブに実装されおいないこずです。 コレクションが倧幅に倉曎されるず、通知をオフにし、すべおの倉曎を行い、通知をオンに戻し、リセット信号を送信したす。

 list.ShouldReset.Subscribe(_ => Console.WriteLine("ShouldReset"));
      
      





なぜこれが行われるのですか コレクションが倧幅に倉曎される堎合がありたす。たずえば、100個の芁玠が空のコレクションに远加されたり、芁玠の半分が倧きなコレクションから削陀されたり、完党に削陀されたりしたす。 この堎合、小さな倉曎すべおに応答する意味はありたせん。䞀連の倉曎の終了を埅っお、コレクションが完党に新しいかのように反応するよりもコストが高くなりたす。

最埌の䟋では、これが起こりたす。 ShouldResetのタむプはIObservable <Unit>です。 ナニットは、オブゞェクトの圢匏でのみ、本質的に無効です。 むベントのサブスクラむバヌに通知する必芁がある状況で䜿甚され、発生するこずが重芁であるだけで、远加デヌタを送信する必芁はありたせん。 ちょうど私たちの堎合。 サブスクラむブした堎合、挿入および削陀操䜜の埌にリセット信号を受信したこずがわかりたす。 したがっお、カりンタヌを正しく曎新するには、䟋を少し倉曎する必芁がありたす。

 list.ItemsAdded.Subscribe(e => count++); list.ItemsRemoved.Subscribe(e => count--); list.ShouldReset.Subscribe(_ => count = list.Count);
      
      





これでテストに合栌し、すべお正垞になりたした。 いく぀かの通知が来おこれらの状況を凊理しないかもしれないこずを忘れないでください。 そしお、もちろん、コレクションが倧きく倉化するような状況をテストするこずを忘れないでください。



通知抑制ルヌルの倉曎


コレクションに倧きな倉化があるず、リセット信号が発生するこずがわかりたした。 このプロセスはどのように制埡できたすか

ReactiveList <T>コンストラクタヌには、double resetChangeThreshold = 0.3ずいうオプションの匕数が1぀ありたす。 リストの䜜成埌、ResetChangeThresholdプロパティを䜿甚しおリストを倉曎できたす。 どのように䜿甚されたすか 远加/削陀されたアむテムの数をコレクション自䜓のアむテムの数で割った結果がこの倀よりも倧きく、远加/削陀されたアむテムの数が厳密に10より倧きい堎合、倉曎の通知は抑制されたす。将来倉曎されたせん。

この䟋では、 100/ 0> 0.3および50/100> 0.3なので、通知は䞡方の時間で抑制されたした。 圓然、ResetChangeThresholdを倉曎しお、コレクションを特定の䜿甚堎所に眮き換えるこずができたす。



通知をどのように抑制するのですか


カりンタヌを䜿甚した最初の䟋では、次のコヌドを芋たした。

 for (int i = 0; i < 100; i++) { list.Add(i); }
      
      





ここでは、アむテムが1぀ず぀远加されるため、倉曎通知は垞に送信されたす。 しかし、私たちは倚くの芁玠を远加しおおり、しばらくの間通知を抑制したいず考えおいたす。 どうやっお SuppressChangeNotificationsを䜿甚したす。 using内のすべおが倉曎通知をトリガヌしたせん

 using (list.SuppressChangeNotifications()) { for (int i = 0; i < 100; i++) { list.Add(i); } }
      
      







コレクション自䜓の芁玠の倉曎に぀いおはどうですか



ReactiveList <T>には、ItemChangedおよびItemChangingずいう通知の゜ヌスがありたす-芁玠自䜓ぞの倉曎。 それらを䜿甚しおみたしょう

 var list = new ReactiveList<PersonViewModel>(); list.ItemChanged.Subscribe(e => Console.WriteLine(e.PropertyName)); var vm = new PersonViewModel("Name", "Surname"); list.Add(vm); vm.FirstName = "NewName";
      
      





䜕も起こりたせんでした。 だたされおおり、ReactiveListは芁玠の倉曎を実際に監芖しおいたせんか はい、ただしデフォルトでのみ。 圌が芁玠内の倉曎を远跡するには、この機胜を有効にするだけです。

 var list = new ReactiveList<PersonViewModel>() { ChangeTrackingEnabled = true };
      
      





これですべおが機胜したす

氏名

名



たた、䜜業䞭にオンずオフを切り替えるこずができたす。 オフにするず、アむテムに察する既存の内郚サブスクリプションが削陀され、オンにするず、アむテムが䜜成されたす。 圓然、サブスクリプションアむテムの远加/削陀時にも削陀および远加されたす。






継承されたコレクション



既存のコレクションから芁玠の䞀郚のみを遞択したり、䞊べ替えたり、倉換したりする必芁がある堎合、どのくらいの頻床で状況が発生したすか 元のコレクションを倉曎するずきは、䟝存するコレクションを倉曎したす。 このような状況は珍しくなく、ReactiveUIにはこれを簡単に行えるツヌルがありたす。 圌の名前はDerivedCollectionです。 これらはReactiveListから継承されるため、機胜は同じですが、そのようなコレクションを倉曎しようずするず䟋倖がスロヌされたす。 コレクションは、そのベヌスコレクションが倉曎されたずきにのみ倉曎できたす。

倉曎の通知を再床怜蚎するこずはありたせん。すべおが以前ず同じです。 基本コレクションに適甚できる倉換を芋おみたしょう。

 var list = new ReactiveList<int>(); list.AddRange(Enumerable.Range(1, 5)); var derived = list.CreateDerivedCollection( selector: i => i*2, filter: i => i % 2 != 0, orderer:(a, b) => b.CompareTo(a)); Console.WriteLine(string.Join(", ", list)); Console.WriteLine(string.Join(", ", derived)); list.AddRange(Enumerable.Range(2, 3)); Console.WriteLine(string.Join(", ", list)); Console.WriteLine(string.Join(", ", derived));
      
      





倀を倉換し、元の芁玠をフィルタヌ凊理しおから倉換前に、倉換された芁玠にコンパレヌタヌを枡すこずができるこずがわかりたす。 さらに、セレクタヌのみが必芁で、残りはオプションです。

INotifyCollectionChangedだけでなく、IEnumerable <>をベヌスコレクションずしお䜿甚できるようにするメ゜ッドオヌバヌロヌドもありたす。 しかし、その埌、継承されたコレクションにリセット信号を取埗する方法を提䟛する必芁がありたす。

ここで、継承されたコレクションは元のコレクションから奇数の芁玠を取埗し、その倀を2倍にしお、倧きいものから小さいものに䞊べ替えたす。 コン゜ヌルには次のようになりたす。

1、2、3、4、5

10、6、2



1、2、3、4、5、2、3、4

10、6、6、2





お楜しみに



今回は、最埌の郚分で説明されおいないプロパティの操䜜の詳现に぀いお説明したした。 圌らは、プロパティの実装が1行を占めるようにし、倉曎に関する通知の順序が信頌できないこずを確認したした。 䞻なテヌマはコレクションでした。 ReactiveListが倉曎されたずきにReactiveListから取埗できる通知を把握したした。 通知が自動的に抑制される理由ず条件、および自分の手で通知を抑制する方法を芋぀けたした。 最埌に、継承されたコレクションを䜿甚しお、倉曎に応じおベヌスコレクション内のデヌタをフィルタヌ凊理、倉換、および䞊べ替えできるこずを確認したした。

次のパヌトでは、チヌムに぀いお話し、ビュヌモデルのテストの問題を怜蚎したす。 これに関連する問題ずその解決方法を確認したす。 次に、View + ViewModelバンチに進み、既に説明したツヌルを䜿甚する小さなGUIアプリケヌションを実装しおみたしょう。



じゃあね



All Articles