The Problem or Puzzle
A few weeks ago, as mentioned in this post http://highvaluecode.com.au/long-time-no-see/, I had to implement a filtering solution for an ObservableCollection. I will post about the solution itself, once I’ve published the code, but one part led me to an interesting problem.
The solution for the filtering was basically two observable collections listening to each other. The reason for bidirectional listening is that the control manipulating the filtered result can add items into the collection without knowing about the filter, and if the item should not be shown in the filtered result it can be silently dropped from the filter result collection. This will result in a solution, where the control does not need to know about the filter itself.
The solution was working correctly in the unit test environment, but when tried to integrate it into the hosting code base, it was suddenly throwing exceptions. Took me a while to track it down while. If you want to figure it out by yourself as a puzzle, pause after the code block with an empty collection changed event handler, and come back when you decided to continue. I will give you another warning before the last code block for the puzzle.
For demonstration purposes, let’s consider that we have a list with the requirement of adding any objects to it. For some reason, we will base the solution on ObservableCollection, and we will issue a Clear operation inside of the CollectionChanged event handler if the notification is about that object has been added to the collection. If you really have this requirement, consider using different classes for implementing this, because there are easier solutions. However, we are doing this for demonstrating a problem later, so please ignore the fact that you are not supposed to implement this requirement as we are doing it now. This is obviously a demonstration code.
So far, so good. Now let’s suppose that someone else is listening to the CollectionChanged events.
This is the second warning for the situation if you want to do the puzzle. Read the next code block and stop reading after it if you want to solve the puzzle by yourself.
In theory, nothing really changed, but suddenly the code is crashing. If you want to run the above examples, the Should() function comes from the FluentAssertions NuGet package https://www.nuget.org/packages/FluentAssertions/.
The Reason for the Crash
It turned out that the ObservableCollection internally decided in the change notification handler that if only one listener is bound to it, you are allowed to change the collection, otherwise not. See the code lines below from https://referencesource.microsoft.com/#System/compmod/system/collections/objectmodel/observablecollection.cs,7412b30dfa1dc739, location 321-322 in the .NET Reference Source Code.
I was trying to understand what the rationale behind the decision is. The closest explanation that I was able to get from my internet searches was that if we allow multiple change handlers to change the collection, the situation becomes quickly very complicated.
I was also surprised, because I don’t remember when exactly the ObservableCollection was introduced into the .NET Framework, but it was there from really early stages. I must emphasize at this point that this limitation does not only apply two handlers changing the collection. As you can see from the above example or puzzle, it also applies to the situation, where one handler changing the code, while another totally empty observer is attached.
Solution proposed on different forums was, don’t attach two handlers. It was also surprising to see that no-one contested it, because for example to hide a filter’s inner working from the UI you will need to attach two handlers, without each other knowing about the other one, and technically that is the promise of the INotifyCollectionChanged interface. To be fair, you are not limited to use ObservableCollection when you need the INotifyCollectionChanged interface, just it is the most readily available implementation.
While I agree that if we don’t pay attention, things can get complicated quickly, but basing a control design on this only in my opinion would analogous to saying let’s not allow parallel computing because it is complicated. It also goes against good design practices because as a third object I need to consider if someone is listening or not to an object outside of my control.
So, what would be a better solution? I think we need to let the user of the API – the software developer building on the class – to make sure that changes are applied correctly to the list. From here I will use list and collection interchangeably because technically the ObservableCollection from the problem is a list as you can index into it. We can help the user by ignoring obviously bad requests, like at object already changed when requesting a move operation of it. If the client code is written correctly, this situation should never occur. We could also throw an exception in these cases, but that would most likely lead to a codebase where unexpected crashes are happening based on certain conditions between distant parts of the system, since these event handlers can be called recursively or just one or the other depending on the actual scenario. Those would be hard to track down scenarios, but feel free to adjust the code if that is your requirement, I’ve distributed the code under MIT License – see below.
So, here are the rules I’ve come up with in implementing the operations that I have used from the ObservableCollection, since I wanted a drop-in replacement of that. Any comparison is done via the defined equal operation for the objects in the list, so if you override the operations, it is reflected correctly. The changes in the information is meant between the time requesting the operation and the time when the list tries to process the operation.
|Move||Current position and requested position for the move operation are the same – request is not even placed into the queue.|
|Insert||Ignored when target position invalidated|
|Add||Always processed, and always inserted at the of the list at the time of processing the request.|
While Add can be considered as a shorthand for Insert in the case of a normal list, as you can see it from the above it has two different intentions and thus two different considerations in this case regarding what should happen if the size of the list changes. If the list grows, both are going to be processed, but the item will be inserted into different positions depending on the call.
You can find the source code under MIT License on my GitHub page at https://github.com/atzimler/ObservableLists, and the NuGet package in the NuGet gallery at https://www.nuget.org/packages/ATZ.ObservableLists/.
Let me know if I missed an operation that you would like to see in the code.