In my previous post, http://highvaluecode.com.au/what-can-you-do-if-observablecollection-is-limiting-you/, I’ve discussed the limitations of the ObservableCollection in the standard .NET platform and a possible solution for it. As mentioned in the article, there was a reason, why those limitations were hindering me, beside bad design practices. In this post, I’m going to walk you through the project that needed a better behaving INotifyCollectionChanged implementation.
In my project, I had to use a filtering solution but with the third-party library used by project there were a few limitations imposed. In my opinion, those were incorrect behaviors, so I’ve agreed with the vendor to disagree on the fact if it is a bug or a feature. I can just only assume, that they’ve run into the same problems with the ObservableCollection as I did – and as probably everyone else in the .NET community. The standard strategies around this, as mentioned in the previous post, were to ignore it…
This article is going to describe how a proper filtering on an INotifyCollectionChanged interface should work in my opinion correctly. While theoretically, the code implementation should work correctly for all INotifyCollectionChanged, the fact that the ObservableCollection is not behaving correctly forced me to expose in the library that I’m using ObservableList objects. This exposure prevents incorrectly supplying an ObservableCollection and getting into trouble when consuming the library. You can find the library’s source code on GitHub (https://github.com/atzimler/ObservableListFilters) and the NuGet package on NuGet.org (https://www.nuget.org/packages/ATZ.ObservableListFilters).
Short note: I’m going to use the list and collection words interchangeably in the remaining of this article while referring to the INotifyCollectionChanged implementation. It’s worth noting that the implementation in the .NET Framework also does this, because the ObservableCollection exposes list information too, like indexing. Just keep in mind, that you need the behavior where change is allowed when multiple change notification handler is attached to allow functioning of the solution – in case you wish to relax the ItemsSource property to be only of type of INotifyCollectionChanged.
With the introduction, publication locations and clarification out of the way, let’s see:
How the filtering code works?
The basic idea behind the library is that we have two observable list that are monitoring each other for change notifications. The reason for monitoring in both directions is that we want to hide any implementation details for both how the filtering works and the fact that there is a filter et all:
Monitoring changes in the ItemsSource collection allows us to hide the filter from the code producing the content.
Monitoring changes in the FilteredItems that are occurring independently from our actions allow us to hide the fact that there is a filter at all from the consuming, possibly UI code. This second step also allows the code to just manipulate any item in the collection without caring that the changes should be applied to the source being filtered not the filtered result.
Changes to consider on ItemsSource
For all the changes, the item’s location in the ItemsSource list and its possible location in the FilteredItems collection should be translated between each other correctly. For now, just keep this in mind, while considering the changes to watch for. More on the index translation later in the article.
Adding an item to the ItemsSource should result in the item to be added to the FilteredItems if the item is evaluated to pass through the filter.
Moving the item in the ItemsSource should result in moving the item between its translated indexes in the FilteredItems.
Removing the item from the ItemsSource should result in its removal from the FilteredItems too, in case it is present there.
Replacing the item in the ItemsSource should result in a replace operation if the new value is passing the filter function and in a removal from the FilteredItems otherwise.
Clearing the ItemsSource should result in clearing the FilteredItems too.
Changes to consider on the FilteredItems
Adding an item to the FilteredItems should be considered as the client code expressing the intent for the item to be added into the ItemsSource. After adding the item in the translated location into the ItemsSource, it should be considered as the item should be in the FilteredItems at the first place. If the item is not passing the filter function, then it should be removed from the FilteredItems. This will not result in a loss of reference, since we already added the item to the ItemsSource.
Moving the item in the FilteredItems should result in moving the item between its translated indexes in the ItemsSouce.
Remove the item from the FilteredItems should be considered as the client code expressing the intent for the item to be removed from the ItemsSouce. As a result, the item should be removed from the ItemsSource too.
Replacing the item in the FilteredItems should be considered as the client code expressing the intent for the item to be replaced in the ItemsSource. As a result, the item should be replaced in the ItemsSouce too, and after being replaced it should be removed from the FilteredItems if the replacement value does not pass the filter function.
Clearing the FilteredItems should be in theory disallowed, which we could implement by using a specialized collection as the FilteredItems. However, the corresponding enumeration value for this action is NotifyCollectionChangedAction.Reset, so to be consistent with the intention on the interface, the FilteredItems should be cleared and then all items to be re-evaluated in the ItemsSource and re-added if the pass the filter. This results in the FilteredItems to be rebuilt from scratch. After all, if the client code is considered with performance regarding this, it should not call the Clear method on the FilteredItems, in the first place.
Some thoughts on the index translation
There are possible multiple ways to translate the location of the indexes when there are items missing. As a standard user expectation, you would assume though, that if we would remove the items from the ItemsSource that are not passing through the filter, we would end up with the FilteredItems – the reason behind the whole filtering exercise.
For making it easier to express the index translation, I’ll use source index when referring to indexes in the ItemsSource collection and target index when referring to indexes in the FilteredItems collection. While this does not necessarily seem intuitive at the first glance, it makes sense, because we are transforming the ItemsSource to the FilteredItems and you can think of those two collections as the source and target of the transformation.
As a result, I think the most intuitive solution – as in term of where you would expect the item, not necessarily as in term of performance –, when translating from source index to target index, is that to stick the item right after the previous one that is passing the filter. If no such item exists, then the currently filtered item should be the first item in the FilteredItems.
There is a much more interesting problem though, when the items are moved in the FilteredItems list. While playing with various source codes, I found that my expectations would be the following in moving the items across items that are ignored:
When moving items toward the beginning of the list, it should jump before the ignored items and as a result, stick together with the item before it in both the source and the target collection. But this still leaves open what should happen in the case when the item becomes the first in the target. In my opinion, this means that the user expressed his or her intention designating the item to be the first one, and as a result it should be first in the source, as well. This will as a side effect prevent the need for reordering the items when an item before the moved item in the source collection, which did not pass the filter previously also passes the filter as a result of a change to it.
When moving items toward the end of the list, it should move right before the next one in the source collection that passes the filter. If no such item exists, it should be moved to the last place. This should prevent the same issues as with the moving to the first index.