📜 ⬆️ ⬇️

Introduction to ReactiveUI: Collections

Hi, Habr!

Part 1: Introduction to ReactiveUI: we pump properties in ViewModel

In the previous article we talked about properties in ViewModel, and what we can do with them using ReactiveUI. We managed to put in order the dependencies between the properties and collect in one place the model view the information about the dependencies between the properties in it.
This time we will talk a little more about properties, and then proceed to the collections. Let's try to understand what problems there are with ordinary collections, and why it was to create new ones, with notifications of changes. And, of course, try to use them.

A few words about the properties


Before turning to the main topic, I will say a few words about the properties. Last time we came up with the following syntax:
private string _firstName; public string FirstName { get { return _firstName; } set { this.RaiseAndSetIfChanged(ref _firstName, value); } } 

Spending 6 lines of code on each property is quite wasteful, especially if there are many such properties and the implementation is always the same. In the C # language, auto-properties were added to solve this problem in due time, and it is easier to live stato. What can we do in our case?
Fody was mentioned in the comments - a tool that can change the IL-code after assembling the project. For example, in the implementation of auto-property add a notice of change. And for ReactiveUI there is even a corresponding extension: ReactiveUI.Fody . Let's try to use it. By the way, for the classical implementation, there is also an extension: PropertyChanged , but it does not suit us, since we need to call RaiseAndSetIfChanged () .
')
Install from NuGet:> Install-Package ReactiveUI.Fody
FodyWeavers.xml appears in the project. Add the installed extension to it:
 <?xml version="1.0" encoding="utf-8" ?> <Weavers> <ReactiveUI /> </Weavers> 

And change our properties:
 [Reactive] public string FirstName { get; set; } 

With this tool you can also implement the FullName property based on ObservableAsPropertyHelper <>. The way is described in documentation on GitHub, here we will lower it. I think that two lines is quite an acceptable option, and I don’t really want to use a third-party method instead of ToProperty (), which allows ReactiveUI.Fody to implement this property correctly.

Check that nothing is broken. How? I test unit tests. ReactiveUI is friendly to them, no wonder he has a MVVM framework <...> to create elegant, testable User Interfaces .... To check the triggering of the event, it is not necessary to subscribe to it with your hands, save the data somewhere when triggered, and then process it.
Observable.FromEventPattern () will help us, which will turn event triggering into IObservable <> with all the necessary information. And to turn IObservable <> into a list of events and check it for correctness, it is convenient to use the extension method .CreateCollection () . It creates a collection in which, as long as the source does not call OnComplete () or we call Dispose (), the elements that came through IObservable <> will be added, in our case - information about the events that have been triggered.
Notice that the collection returns to us immediately, and the elements are added to it later, asynchronously. This behavior is different from, for example, .ToList (), which does not return control and, therefore, the collection itself to OnComplete (), which is fraught with perpetual waiting in the case of a regular subscription to the event.
 [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))); } 

The test script itself (setting a new property value) is executed inside using, and the checks after. This is necessary so that during the test, some event does not accidentally work and does not spoil our collection. Of course, it is often unnecessary to do this, but sometimes it can be important.

Now let's check that IObservable <> Changed will return the same.
 [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))); } 

And ... The test fell. But we just changed the source of information about changing properties! Let's try to understand why the test failed:
 vm.Changed.Subscribe(n => Console.WriteLine(n.PropertyName)); vm.FirstName = "OMG"; 

And we get:
FullName
Firstname

Like this. This does not seem to be a framework error, but rather an implementation detail. This is understandable: both properties have already changed and the order of notification is unimportant. On the other hand, it is inconsistent with the order of events and does not meet expectations, which may be fraught. Of course, building application logic based on the order of notifications is obviously a bad idea. But, for example, when reading the application log, we will see that the dependent property notified about the change BEFORE its dependency changes, which can be confusing. So be sure to remember this feature.
So, we made sure that ReactiveUI.Fody works properly and significantly reduces the amount of code. We will continue to use it.



And now for the collections.


The INotifyPropertyChanged interface, as we know, is used when changing view model properties, for example, to notify a visual element that something has changed and that the interface needs to be redrawn. But what to do when there is a collection of many elements in the view model (for example, a news feed), and we need to add fresh entries to those already shown? Notify that the property in which the collection is located has changed? It is possible, but it will lead to rebuilding of the entire list in the interface, and this can be a slow operation, especially if we are talking about mobile devices. No, that won't do. It is necessary that the collection itself reports that something has changed in it. Fortunately, there is a wonderful interface:
 public interface INotifyCollectionChanged { /// <summary>Occurs when the collection changes.</summary> event NotifyCollectionChangedEventHandler CollectionChanged; } 

If the collection implements it, then when adding / deleting / replacing, etc. events triggered by CollectionChanged. And now you do not need to rebuild the list of news again and generally look into the collection of records, it is enough just to supplement it with new elements that came through the event. In .NET there are collections that implement it, but we are talking about ReactiveUI. What is in it?
A whole set of interfaces: IReactiveList <T>, IReadOnlyReactiveList <T>, IReadOnlyReactiveCollection <T>, IReactiveCollection <T>, IReactiveNotifyCollectionChanged <T>, IReactiveNotifyCollectionItemChanged <T>. I will not give a description of each here, I think it should be clear by name what they are.
But let's look at the implementation in more detail. Meet ReactiveList <T>. He implements them all and much more. Since we are interested in tracking changes in the collection, we will look at the corresponding properties of this class.
Properties for tracking changes in IReactiveList & lt; T & gt;
Quite a bit of! The addition, removal, movement of elements, the number of elements, the collection emptiness and the need to reset are monitored. Consider all this in more detail. Of course, events from INotifyCollectionChanged, INotifyPropertyChanged and paired with * Changind are also implemented, but we will not talk about them, they work side-by-side with the “observable” properties shown in the picture and there is nothing unique there.
For a start, a simple example. Subscribe to some sources of notifications and work a little with the collection:
 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"); 


We get the result:
#Add 'first'
Count changing: 0
Before added: first
Count changed: 1
IsEmpty changed: False
Added: first

#Add 'second'
Count changing: 1
Before added: second
Count changed: 2
Added: second

#Remove 'first'
Count changing: 2
Before removed: first
Count changed: 1
Removed: first

We are notified of what has been added or removed, as well as a change in the number of elements and the sign of the emptiness of the collection.
Moreover:
- ItemsAdded / ItemsRemoved / BeforeItemsAdded / BeforeItemsRemoved return the added / deleted item itself
- CountChanging / CountChanged return the number of elements before and after the change.
- IsEmptyChanged returns the new value of the collection empty sign

There is one subtlety


So far, everything is predictable. Now imagine that we only want to count the number of records in the collection based on the addition and deletion notifications. What could be easier?
 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)); 

The test was successful. Let's change the principle of filling the collection, add many elements at once:
 list.AddRange(Enumerable.Range(0, 10)); list.RemoveAll(Enumerable.Range(0, 5).Select(i => i * 2)); 

Successfully. It seems there is no dirty trick. Although stand ...
 list.AddRange(Enumerable.Range(0, 100)); list.RemoveAll(Enumerable.Range(0, 50).Select(i => i * 2)); 

Oh! The test failed and count == 0. It seems that we did not take into account something. Let's figure it out.

The thing is that ReactiveList <T> is implemented not as primitive as it may seem. When the collection changes significantly, it disables notifications, makes all changes, turns notifications back on and sends a reset signal:
 list.ShouldReset.Subscribe(_ => Console.WriteLine("ShouldReset")); 

Why is this done? Sometimes the collection changes significantly: for example, 100 elements are added to an empty collection, half of the elements are removed from a large collection, or it is completely cleared. In this case, there is no sense in reacting to every small change - it will be more expensive than waiting for the end of a series of changes and reacting as if the collection is completely new.
In the last example, this is what happens. ShouldReset is of type IObservable <Unit>. Unit is essentially void, only in the form of an object. It is used in situations where you need to notify the subscriber of a certain event, and it is only important that it happened, you do not need to transmit any additional data. Just our case. If we subscribed to it, we would see that after the insert and delete operations we received a reset signal. Accordingly, to update the counter correctly, we need to slightly change our example:
 list.ItemsAdded.Subscribe(e => count++); list.ItemsRemoved.Subscribe(e => count--); list.ShouldReset.Subscribe(_ => count = list.Count); 

Now the test passes and everything is wonderful again. Just do not forget that some notifications may not come, and handle these situations. And, of course, do not forget to test such situations when collections change a lot.

Rules for Suppressing Change Notifications

We saw that with a strong change in the collection, a reset signal appears. How can you control this process?
There is one optional argument in the ReactiveList <T> constructor: double resetChangeThreshold = 0.3. And after creating the list, you can change it through the ResetChangeThreshold property. How is it used? Notifications of changes will be suppressed if the result of dividing the number of elements added / removed by the number of elements in the collection itself is greater than this value, and if the number of elements added / removed is strictly greater than 10. This is evident from the source code and no one guarantees that these rules will not change in the future.
In our example, 100/0> 0.3 and 50/100> 0.3 , so the notifications were suppressed both times. Naturally, you can vary ResetChangeThreshold and substitute the collection for a specific place of use.

How do we suppress notifications ourselves?

In the first example with a counter, we saw the following code:
 for (int i = 0; i < 100; i++) { list.Add(i); } 

Here, items are added one by one, so change notifications are always sent. But we add a lot of elements and want to suppress notifications for a while. How? Using SuppressChangeNotifications (). Anything inside using will not trigger change notifications:
 using (list.SuppressChangeNotifications()) { for (int i = 0; i < 100; i++) { list.Add(i); } } 


What about changes to the elements of the collection?


We saw that in ReactiveList <T> there are sources of notifications ItemChanged and ItemChanging - changes to the elements themselves. Let's try to use them:
 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"; 

Nothing has happened. We were deceived, and ReactiveList doesn’t really follow the element changes? Yes, but only by default. In order for it to track changes inside its elements, you just need to enable this feature:
 var list = new ReactiveList<PersonViewModel>() { ChangeTrackingEnabled = true }; 

Now everything works:
FullName
Firstname

In addition, it can be turned on and off as you work. When you turn off the existing internal subscriptions to the items will be deleted, when turned on - they will be created. Naturally, when adding / removing items, subscriptions are also deleted and added.



Inherited collections


How often do situations arise when you need to select only a part of the elements from an existing collection, or sort them, or convert? And when you change the original collection to change dependent. Such situations are not uncommon, and there is a tool in ReactiveUI that makes this easy to do. His name is DerivedCollection. They are inherited from ReactiveList and, therefore, the possibilities are the same, except that when trying to change such a collection an exception will be thrown. A collection can change only when its base collection changes.
We will not consider notifications of changes again, everything is as it was. Let's see what transformations can be applied to the base collection.
 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)); 

We see that it is possible to transform the value, filter the original elements (before the transformation!) And pass a comparator for the transformed elements. Moreover, only the selector is required, the rest is optional.
There are also method overloads that allow using not only INotifyCollectionChanged as a base collection, but even IEnumerable <>. But then you have to give the inherited collection a way to get a reset signal.
Here, the inherited collection takes odd elements from the source, doubles their value and sorts from larger to smaller. The console will be:
1, 2, 3, 4, 5
10, 6, 2

1, 2, 3, 4, 5, 2, 3, 4
10, 6, 6, 2


Stay tuned


This time we discussed some details of working with properties not described in the last part. We ensured that the implementation of the property took one line, and found out that the order of notifications about their changes cannot be believed. The main theme was the collection. We figured out what notifications can be received from ReactiveList when it changes. We found out why and under what conditions notifications are suppressed automatically, as well as how to suppress them with our own hands. Finally, we tried to use inherited collections and made sure that they know how to filter, transform and sort the data of the basic collection, responding to its changes.
In the next part we will talk about the teams and consider the question of testing the view model. We will find out what problems are connected with this and how they are solved. And then we move on to the View + ViewModel bundle and try to implement a small GUI application that uses the tools already described.

See you!

Source: https://habr.com/ru/post/303898/


All Articles