One of the built-in classes that ships with ReactiveUI is an improved version
of .NET's ObservableCollection
(which is ironically, not an Observable).
ReactiveList
should be used in any place that you would normally use a List
or ObservableCollection, as it has additional useful Rx features.
Subscribing to Changes
ReactiveList
provides several useful Observables that can be subscribed to
in order to inform you about changes in the list, as well as providing you
with notifications that happen before a list is about to change:
- (Before)ItemsAdded - signals when items are added
- (Before)ItemsRemoved - signals when items are removed
- (Before)ItemsMoved - signals when items are moved
- CountChang(ing/ed) - signals when the number of items in the list changes for any reason
- Changed - passes along the
NotifyCollectionChangedEventArgs
from all changes (i.e. is an Observable version ofNotifyCollectionChanged
). - ShouldReset - signals that the observer should reread the entire collection, as it has changed significantly
Semantics of Reset
One thing that is particularly important to understand is the meaning of the ShouldReset Observable. The meaning of this event is, "This collection has changed drastically, you should reread the contents". Many people conflate Reset and Clear, thinking that this means the collection is now empty.
This is important, because if you only Subscribe to ItemsAdded
and
ItemsRemoved
, you will not be correctly tracking every item in the list.
ReactiveList will detect this scenario and attempt to warn you about it. For
example, here is an example of maintaining a "running count" of the number of
items in the list:
// Note that this code is hacky and for illustrative purposes, CountChanged
// would suffice for this
TweetList = new ReactiveList<Tweet>();
var count = 0;
var addedOrRemoved = Observable.Merge(
TweetList.ItemsAdded.Select(_ => 1),
TweetList.ItemsRemoved.Select(_ => -1));
addedOrRemoved.Subscribe(x => count += x);
TweetList.ShouldReset.Subscribe(_ => count = TweetList.Count);
Should you want to execute code on every object in a collection as they are
added or removed, the ActOnEveryObject
method documented in this guide will
handle many edge cases around this task automatically.
Using Change Tracking
Not only can ReactiveList watch changes to the list, it can optionally tell you about changes to any item in the list. One practical example of this use-case is, we'd like to be notified when any document in the document list becomes dirty. Here's one way to do that:
DocumentList = new ReactiveList<Document>() {
ChangeTrackingEnabled = true,
};
DocumentList.ItemChanged
.Where(x => x.PropertyName == "IsDirty" && x.Sender.IsDirty)
.Select(x => x.Sender)
.Subscribe(x => {
Console.WriteLine("Make sure to save {0}!", x.DocumentName);
});
Note that we had to set ChangeTrackingEnabled
here to true
, since change
tracking is disabled by default for performance reasons. Note that one thing
we didn't have to do in this code is, attempt to watch for changing elements
in the collection - ReactiveList does that all for us.
ReactiveList only tracks changes to its immediate objects, it won't track an
entire object hierarchy (i.e. listOfItems[0].Foo.Bar = true
won't trigger a
change notification, but listOfItems[0].Foo = Bar
will).
Suppressing Notifications
Since ReactiveLists are often bound to UI elements like ListBoxes, making many small changes to a list all at once can have a significant impact in performance - changes to the list result in UI elements being created or destroyed as well as relayout and rerendering, which are quite expensive operations.
Instead, when you want to make several changes to a list at the same time, use
SuppressChangeNotifications
- this will disable change notifications for the
duration of the operation, then send a Reset notification which will
trigger the UI to reload itself.
// Without this using statement, the ListBox associated with this List would
// refresh **four** separate times!
using (TweetsList.SuppressChangeNotifications()) {
TweetsList.Clear();
for(int page=0; page < 3; page++) {
var tweets = await GetTweets(page);
TweetsList.AddRange(tweets);
}
}
Range methods such as AddRange
, InsertRange
, etc automatically will
suppress change notifications if the percentage of the list being changed is
above a certain level (i.e. if you're changing 90% of the list, it makes sense
to signal a Reset, but if you're changing 5% of the list, it's better to
signal Adds/Deletes)
CreateCollection
One method that is often useful for testing, is to create a list whose contents automatically get populated by an Observable. While in this case, it's probably easier to test the object state directly, you can also check correctness via counting changes:
[Fact]
public void MakeSureTweetListGetsUpdatedOnRefresh()
{
var fixture = new TweetsListViewModel();
var output = fixture.TweetList.ItemsAdded.CreateCollection();
Assert.Equal(0, output.Count);
fixture.Refresh.Execute();
Assert.Equal(1, output.Count);
}