Replies: 8 comments 45 replies
-
How do the other data binding platforms work? I think that I usually have to marshal myself for say UWP? |
Beta Was this translation helpful? Give feedback.
-
I would ask what we believe the real replacement is to this, because I don't believe that anyone ever should write
In their VM. Forms code does not belong in your ViewModel. Are we suggesting everyone wraps that in an IMainThread interface in their project and injects it into their ViewModel? Are we suggesting we use SynchronizationContext and get the one from the UI thread and inject that? |
Beta Was this translation helpful? Give feedback.
-
I'm not convinced that forcing developers to marshall data updates to the UI thread in ViewModels will solve the underlying issue. The issue as described appears to be that the control is referencing out of date data when there's an update queued to the main thread but not yet processed, so to me it seems more logical that the fix should be applied to that mechanism once, in the control, rather than every developer having to create their own version of it for all time from here on out. |
Beta Was this translation helpful? Give feedback.
-
I agree with the idea that this behavior should not be implicit. I'm currently working on that scenario, and I have already been hit by a few bugs in X.F related to the assumption that there is only ever a single window. BTW, Device.BeginInvokeOnMainThread() should be avoided for that reason. |
Beta Was this translation helpful? Give feedback.
-
What is the correct way to marshal to the main thread? For example, lets say I press a button which downloads some data. This operation has a callback/event that fires on the download background thread. And, this VM that I am in does not reference the Xamarin.Forms (or any UI) framework. What is the recommendation? |
Beta Was this translation helpful? Give feedback.
-
@hartez I just saw the update in your initial post. I think that it is too much reading for everyone :) I would suggest having a short example right from the start to illustrate the problem that this proposal is trying to solve. Maybe, you can copy the |
Beta Was this translation helpful? Give feedback.
-
if the result of this rollback is more performance & stability, less bug, more maintanability, and align with UWP/WPF behaviours this is a no-brainer yes to me.... We should not even speaking of this, the PROS are simply too big :) Maybe we can maintain the auto-marshaling for collections as opt-in (or opt-out) and update the documentation with a clear statement that this behaviour will have performance and stability downsides (basically: use it at your own risk). With this we dont break any existing code and we can move one from those nasty bugs. |
Beta Was this translation helpful? Give feedback.
-
UPDATE
If you have not read the background information provided in this post AND the discussion of PR #5866, stop. Hit the 'Cancel' button below the reply you're typing. Read this post, read the discussion thread of PR #5866, and then come back.
END UPDATE
This proposal is a breaking change for some users.
In all cases where Forms automatically marshals handling of the INotifyCollectionChanged interface's CollectionChanged event to the UI thread, we instead should throw an exception if the event is raised on a non-UI thread.
Background
In all of the platforms supported by Xamarin.Forms, any changes to the UI have to be made on the main/UI thread. Trying to make a UI change from a non-UI thread (e.g., a thread from the thread pool) will result in some version of a cross-thread exception.
In .NET, we often bind controls with multiple items to collections which implement the INotifyCollectionChanged interface. The interface contains a CollectionChanged event which the control can listen for. When the event is raised, the control can update itself to reflect the change in the underlying data.
These two concepts collide when changes are made to one of these collections from a non-UI thread. When the data changes in a non-UI thread, the CollectionChanged event is raised and handled on that same non-UI thread; if those changes would result in a control change which modifies the UI (as most of them do), then those UI changes are happening off of the UI thread. A cross-thread exception ensues.
There are two options to avoid this problem:
Option 1 is the better option. Option 2 introduces some subtle problems, as we'll see later.
A long, long time ago (so far back that I can't link the commit because it's in the pre-open-source repo), a well-intentioned change was made to the ListProxy class (which handles a lot of bookkeeping for the ListView control). The most recent version of the change can be found here. This code is basically automating option 2. The handler for the CollectionChanged event is running the update action using the Dispatcher. The Dispatcher checks to see if it's on the UI thread; if so, it immediately runs the update action. If not, it queues up the action on the UI thread. In the latter case, the action won't actually be executed until the UI thread can get to it, which might be immediately or it might be at some point in the future.
The intent of this change was to make the ListView easier to use; if you doing something off the main thread (say, processing a bunch of data) and needed to update the collection, you could safely do so - the actual update to the UI would be marshaled to the UI thread by ListProxy, and you wouldn't get a cross-thread exception.
At the time, this was the only control in Forms which performed automatic marshaling when bound to an INotifyCollectionChanged source. Other controls notably did not do this. The Pins collection for Xamarin.Forms.Maps, for example, required you to handle the marshaling yourself: see the discussion in PR 5866.
After a whole lot of discussion, we decided to make the other INotifyCollectionChanged sources in Forms consistent with ListView. So we merged PR 5866 (for the Map Pins) and PR 8568 (for CollectionView). The intent was to make working with these collections/bindings off of the UI thread as simple and easy as possible. We thought we were improving the product.
We were wrong.
Why the current solution does not work
The problem occurs under the following conditions:
It's during part 3 that things go pear-shaped. The control has not yet been notified that the underlying data has been changed; it has expectations about the shape of the data (e.g., number of items, order of items) which are no longer valid. When it tries to query the underlying data, the results are not in the expected shape and the control has to throw a consistency exception.
An example of this is issue 10735. The Android RecyclerView is attempting to animate some data updates, but the underlying data has already been changed; it's querying for data based on an index which is no longer valid, and an IndexOutOfBoundsException is thrown. Issue 9753 is a similar case.
The problem is the gap between the update of the actual underlying data and the notification to the native control (the RecyclerView or ListView or UICollectionView) that the data has changed. Because the updates have to be queue on the UI thread, they are sometimes delayed, and during that delay any work the control does which queries the underlying data will get inconsistent results.
So simply pushing the handling of CollectionChanged onto the UI thread is a bust; it'll work a lot of the time, but when it fails it fails hard and in ways which are difficult to reproduce/diagnose.
In the end, this means that the only way to safely update the data in a bound observable collection (INotifyCollectionChanged) is to do so on the UI thread (i.e., option 1 above).
Thus, the proposal to remove all automatic marshaling for these collections.
What things look like if we don't accept this proposal
We currently have a workaround for this on Android in the form of PR 11235's MarshalingObservableCollection, which automatically wraps any INCC used as a CollectionView ItemsSource on that platform. (To be clear - this problem is not limited to Android; all the platforms have it. Android is just the most common problem area right now, and we may apply MOC to the other platforms in the future). MOC basically watches for off-UI-thread changes in the wrapped collection and presents a snapshot of the pre-modification data to the CollectionView until the notifications can be processed. In short, it lies.
While this works to fix the two reported issues, we don't know if it covers all possibilities. And it adds additional overhead (in both memory allocation and CPU cycles) for everyone using CollectionView, even if they aren't adding items off of the UI thread. And it adds additional maintenance overhead for Forms (and going forward, .NET MAUI).
So the trade-off is "more maintenance and less performance" for "some devs can add collection items from thread pool threads without having to call BeginInvokeOnMainThread". Keep in mind that we cannot even guarantee that the continued behavior won't run into further bugs.
What things look like if we accept this proposal
From a maintenance perspective for Forms and .NET MAUI, things get simpler. We can drop some classes and their tests, and we have an entire class of bugs we don't have to worry about anymore.
The trade off is that those devs who are relying on the current behavior (that they can add items to a collection off of the main thread without cross-thread exceptions) would have to change their code. The change would be from something like this:
to this:
In an attempt to surface this particular problem to the developer as soon as possible, at each location where we have been marshaling the event handler to the UI thread, we will instead throw an exception with very specific language regarding the change and how to address it.
Beta Was this translation helpful? Give feedback.
All reactions