Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

Force off-main-thread ItemsSource updates to update on main thread #11235

Merged
merged 11 commits into from
Jul 20, 2020

Conversation

hartez
Copy link
Contributor

@hartez hartez commented Jun 29, 2020

Description of Change

If a developer

  • uses an ObservableCollection (technically, anything which implements INotifyCollectionChange) as the ItemsSource for a CollectionView
  • makes changes to that ItemsSource off of the main thread (e.g., using a threadpool thread)
  • does all this on Android

for the most part, these changes will be automatically marshaled by ObservableItemsSource to the main thread, and all will be well.

However, if the changes are timed correctly (or occur quickly enough) and there is any scrolling involved (e.g., using KeepLastItemInView, or opening/closing the keyboard, or an ill-timed user scrolling action), it's possible that the RecyclerView will attempt to access the underlying collection in a way which will make the viewholder collection inconsistent. At this point, the user may see errors like:

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position

or

java.Lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter position

The issue is that the RecyclerView (via the adapter) is accessing the underlying source between the time the changes have been made (from a non-UI thread) and the time the adapter has been notified of the changes (which must happen on the UI thread).

To work around this problem, these changes provide a wrapper class which queues up the changes requested on the non-ui-thread and later processes them on the UI thread; in the intervening time, any requests for data are answered with a snapshot of the data before it was modified. The wrapper class is injected by the ItemsSourceFactory on Android.

Additionally, these changes address an issue where the ItemsViewRenderer was using the adapter's item count for scrolling rather than the item count in the layout manager. This would occasionally cause issues with the KeepLastItemInView option.

Issues Resolved

API Changes

Added to Core:

public class MarshalingObservableCollection

Platforms Affected

  • Android

Behavioral/Visual Changes

None

Before/After Screenshots

Not applicable

Testing Procedure

Issue 10735 manual test
UI tests
Automated platform tests

PR Checklist

  • Targets the correct branch
  • Tests are passing (or failures are unrelated)

@samhouts samhouts added Core p/Android a/collectionview Android10 i/high Completely doesn't work, crashes, or is unusably slow, has no obvious workaround; occurs less often t/bug 🐛 labels Jun 29, 2020
@samhouts samhouts added this to In Review in 4.7.0 Jun 29, 2020
@samhouts samhouts requested a review from rmarinho June 30, 2020 23:44
Copy link
Member

@rmarinho rmarinho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build failing ...

'MarshalingObservableCollectionTests.MarshalingTestPlatformServices' does not implement interface member 'IPlatformServices.GetHash(string)'

Copy link
Member

@StephaneDelcroix StephaneDelcroix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good

// collection which are made off of the main thread remain invisible to consumers on the main thread
// until they have been processed by the main thread.

public class MarshalingObservableCollection : List<object>, INotifyCollectionChanged
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used by Platform.Android, so it needs to be public.

case IList _ when itemsSource is INotifyCollectionChanged:
return new ObservableItemsSource(itemsSource as IList, notifier);
case IList list when itemsSource is INotifyCollectionChanged:
return new ObservableItemsSource(new MarshalingObservableCollection(list), notifier);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about iOS? Would it be necessary to use MarshalingObservableCollection in iOS too?

@samhouts samhouts added this to In Review in CollectionView Jul 13, 2020
@samhouts samhouts added the approved Has two approvals, no pending reviews, and no changes requested label Jul 20, 2020
@samhouts samhouts added the API-change Heads-up to reviewers that this PR may contain an API change label Jul 20, 2020
@samhouts samhouts changed the base branch from 4.7.0 to 4.8.0 July 20, 2020 20:32
@samhouts samhouts removed this from In Review in 4.7.0 Jul 20, 2020
@samhouts samhouts merged commit ab55162 into 4.8.0 Jul 20, 2020
CollectionView automation moved this from In Review to Done Jul 20, 2020
@samhouts samhouts deleted the facade-list branch July 20, 2020 22:47
@samhouts samhouts added this to Done in Sprint 173 Jul 21, 2020
@samhouts samhouts modified the milestone: 4.8.0 Jul 23, 2020
@samhouts samhouts added this to Done in vCurrent (4.8.0) Jul 30, 2020
@samhouts samhouts removed this from Done in CollectionView Aug 19, 2020
@bravequickcleverfibreyarn
Copy link

bravequickcleverfibreyarn commented Dec 9, 2020

@hartez, Still having this issue. CollectionView has ItemsUpdatingScrollMode="KeepItemsInView". Using the ObservableCollection for ItemsSource. Xamarin.Forms 4.8.0.1687. Some relaps? It seems issue goes on even when whole operation runs on UI thread but closely to each other (but not so close like just firing update from in-memory collection).

My case. ObservableCollection holds collections. These ones are replaced – one by one. If I used some technics like Thread.Sleep, some non-sense locking or just fire all changes manually instead of per collection replacement, issue is deferred but no solved.

Java.Lang.IndexOutOfBoundsException: 'Inconsistency detected. Invalid item position 17(offset:17).state:18 crc643f46942d9dd1fff9.CollectionViewRenderer{c1f8419 VFED..... ......ID 0,0-1080,1584 #1a}, adapter:crc643f46942d9dd1fff9.GroupableItemsViewAdapter_2@d9b48d5, layout:androidx.recyclerview.widget.LinearLayoutManager@d1bd9b6, context:android.view.ContextThemeWrapper@73ab760'

0x1 in System.Diagnostics.Debugger.Mono_UnhandledException at /Users/builder/jenkins/workspace/archive-mono/2020-02/android/release/mcs/class/corlib/System.Diagnostics/Debugger.cs:125,4	C#
0x3E in Android.Runtime.DynamicMethodNameCounter.113	C#
0x12 in System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw at /Users/builder/jenkins/workspace/archive-mono/2020-02/android/release/mcs/class/referencesource/mscorlib/system/runtime/exceptionservices/exceptionservicescommon.cs:157,13	C#
0x8E in Java.Interop.JniEnvironment.InstanceMethods.CallNonvirtualVoidMethod	C#
0x5F in Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod	C#
0x80 in AndroidX.RecyclerView.Widget.RecyclerView.OnLayout at D:\a\1\s\generated\androidx.recyclerview.recyclerview\obj\Release\monoandroid90\generated\src\AndroidX.RecyclerView.Widget.RecyclerView.cs:13220,5	C#
0x8 in Xamarin.Forms.Platform.Android.ItemsViewRenderer<Xamarin.Forms.GroupableItemsView,Xamarin.Forms.Platform.Android.GroupableItemsViewAdapter<Xamarin.Forms.GroupableItemsView,Xamarin.Forms.Platform.Android.IGroupableItemsViewSource>,Xamarin.Forms.Platform.Android.IGroupableItemsViewSource>.OnLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\CollectionView\ItemsViewRenderer.cs:68,4	C#
0x10 in AndroidX.RecyclerView.Widget.RecyclerView.n_OnLayout_ZIIII at D:\a\1\s\generated\androidx.recyclerview.recyclerview\obj\Release\monoandroid90\generated\src\AndroidX.RecyclerView.Widget.RecyclerView.cs:13204,4	C#
0x2F in Android.Runtime.DynamicMethodNameCounter.113	C#
0xFFFFFFFFFFFFFFFF in Java.Interop.NativeMethods.java_interop_jnienv_call_nonvirtual_void_method_a	C#
0x79 in Java.Interop.JniEnvironment.InstanceMethods.CallNonvirtualVoidMethod	C#
0x5F in Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod	C#
0x80 in AndroidX.SwipeRefreshLayout.Widget.SwipeRefreshLayout.OnLayout at D:\a\1\s\generated\androidx.swiperefreshlayout.swiperefreshlayout\obj\Release\monoandroid90\generated\src\AndroidX.SwipeRefreshLayout.Widget.SwipeRefreshLayout.cs:539,5	C#
0x8 in Xamarin.Forms.Platform.Android.RefreshViewRenderer.OnLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\Renderers\RefreshViewRenderer.cs:74,4	C#
0x10 in AndroidX.SwipeRefreshLayout.Widget.SwipeRefreshLayout.n_OnLayout_ZIIII at D:\a\1\s\generated\androidx.swiperefreshlayout.swiperefreshlayout\obj\Release\monoandroid90\generated\src\AndroidX.SwipeRefreshLayout.Widget.SwipeRefreshLayout.cs:523,4	C#
0x2F in Android.Runtime.DynamicMethodNameCounter.111	C#
0xFFFFFFFFFFFFFFFF in Java.Interop.NativeMethods.java_interop_jnienv_call_nonvirtual_void_method_a	C#
0x79 in Java.Interop.JniEnvironment.InstanceMethods.CallNonvirtualVoidMethod	C#
0x21 in Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeNonvirtualVoidMethod	C#
0x69 in Android.Views.ViewGroup.Layout	C#
0x118 in Xamarin.Forms.Platform.Android.VisualElementTracker.UpdateLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\VisualElementTracker.cs:105,5	C#
0xB in Xamarin.Forms.Platform.Android.RefreshViewRenderer.UpdateLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\Renderers\RefreshViewRenderer.cs:233,33	C#
0x3A in Xamarin.Forms.Platform.Android.VisualElementRenderer<Xamarin.Forms.View>.UpdateLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\VisualElementRenderer.cs:422,5	C#
0x1E in Xamarin.Forms.Platform.Android.VisualElementRenderer<Xamarin.Forms.View>.OnLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\VisualElementRenderer.cs:400,4	C#
0x8 in Xamarin.Forms.Platform.Android.Platform.DefaultRenderer.OnLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\Platform.cs:1313,5	C#
0x10 in Xamarin.Forms.Platform.Android.FormsViewGroup.n_OnLayout_ZIIII at D:\a\1\s\Xamarin.Forms.Platform.Android.FormsViewGroup\obj\Release\generated\src\Xamarin.Forms.Platform.Android.FormsViewGroup.cs:198,4	C#
0x2F in Android.Runtime.DynamicMethodNameCounter.33	C#
0xFFFFFFFFFFFFFFFF in Java.Interop.NativeMethods.java_interop_jnienv_call_nonvirtual_void_method_a	C#
0x79 in Java.Interop.JniEnvironment.InstanceMethods.CallNonvirtualVoidMethod	C#
0x5F in Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod	C#
0x97 in Xamarin.Forms.Platform.Android.FormsViewGroup.MeasureAndLayout at D:\a\1\s\Xamarin.Forms.Platform.Android.FormsViewGroup\obj\Release\generated\src\Xamarin.Forms.Platform.Android.FormsViewGroup.cs:181,5	C#
0x171 in Xamarin.Forms.Platform.Android.VisualElementTracker.UpdateLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\VisualElementTracker.cs:111,5	C#
0x1E in Xamarin.Forms.Platform.Android.VisualElementRenderer<Xamarin.Forms.View>.UpdateLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\VisualElementRenderer.cs:143,4	C#
0x3A in Xamarin.Forms.Platform.Android.VisualElementRenderer<Xamarin.Forms.Page>.UpdateLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\VisualElementRenderer.cs:422,5	C#
0x1E in Xamarin.Forms.Platform.Android.VisualElementRenderer<Xamarin.Forms.Page>.OnLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\VisualElementRenderer.cs:400,4	C#
0x8 in Xamarin.Forms.Platform.Android.PageRenderer.OnLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\Renderers\PageRenderer.cs:239,4	C#
0x10 in Xamarin.Forms.Platform.Android.FormsViewGroup.n_OnLayout_ZIIII at D:\a\1\s\Xamarin.Forms.Platform.Android.FormsViewGroup\obj\Release\generated\src\Xamarin.Forms.Platform.Android.FormsViewGroup.cs:198,4	C#
0x2F in Android.Runtime.DynamicMethodNameCounter.33	C#
0xFFFFFFFFFFFFFFFF in Java.Interop.NativeMethods.java_interop_jnienv_call_nonvirtual_void_method_a	C#
0x79 in Java.Interop.JniEnvironment.InstanceMethods.CallNonvirtualVoidMethod	C#
0x5F in Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod	C#
0x97 in Xamarin.Forms.Platform.Android.FormsViewGroup.MeasureAndLayout at D:\a\1\s\Xamarin.Forms.Platform.Android.FormsViewGroup\obj\Release\generated\src\Xamarin.Forms.Platform.Android.FormsViewGroup.cs:181,5	C#
0x171 in Xamarin.Forms.Platform.Android.VisualElementTracker.UpdateLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\VisualElementTracker.cs:111,5	C#
0x1E in Xamarin.Forms.Platform.Android.VisualElementRenderer<Xamarin.Forms.Page>.UpdateLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\VisualElementRenderer.cs:143,4	C#
0x6 in Xamarin.Forms.Platform.Android.PageContainer.OnLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\Renderers\PageContainer.cs:30,4	C#
0x53 in Xamarin.Forms.Platform.Android.ShellPageContainer.OnLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\Renderers\ShellPageContainer.cs:16,4	C#
0x10 in Android.Views.ViewGroup.n_OnLayout_ZIIII	C#
0x2F in Android.Runtime.DynamicMethodNameCounter.27	C#
0xFFFFFFFFFFFFFFFF in Java.Interop.NativeMethods.java_interop_jnienv_call_nonvirtual_void_method_a	C#
0x79 in Java.Interop.JniEnvironment.InstanceMethods.CallNonvirtualVoidMethod	C#
0x21 in Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeNonvirtualVoidMethod	C#
0x69 in Android.Views.ViewGroup.Layout	C#
0x3F in Xamarin.Forms.Platform.Android.ShellRenderer.UpdateLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\Renderers\ShellRenderer.cs:83,4	C#
0x2B in Xamarin.Forms.Platform.Android.AppCompat.Platform.Xamarin.Forms.Platform.Android.IPlatformLayout.OnLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\AppCompat\Platform.cs:235,4	C#
0x2D in Xamarin.Forms.Platform.Android.PlatformRenderer.OnLayout at D:\a\1\s\Xamarin.Forms.Platform.Android\PlatformRenderer.cs:75,4	C#
0x10 in Android.Views.ViewGroup.n_OnLayout_ZIIII	C#
0x2F in Android.Runtime.DynamicMethodNameCounter.27	C#```

@hartez
Copy link
Contributor Author

hartez commented Dec 9, 2020

My case. ObservableCollection holds collections. These ones are replaced – one by one.

Are you saying you have something like ObservableCollection<ObservableCollection<object>>?

@bravequickcleverfibreyarn
Copy link

bravequickcleverfibreyarn commented Dec 10, 2020

Are you saying you have something like ObservableCollection<ObservableCollection<object>>?

No, no. Let review the design. My collegue told me that the test regard this issue covers just the Add method. Maybe there is an another issue when replacing.

internal class CustomObservableCollection : ObservableCollection<CustomCollection>
{
    public void Replace(CustomCollection col, int index) => SetItem(index, col);
    
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) 
    { 
        BeginInvokeOnMainThread(() => base.OnCollectionChanged(e)); 
    } 
}

internal class CustomCollection: IReadOnlyCollection<CustomObject> { … }

Issue also happens when code is refactored to

…
 BeginInvokeOnMainThread(() => _customObservableCollection.Replace(col, index));
…

internal class CustomObservableCollection : ObservableCollection<CustomCollection>
{
    public void Replace(CustomCollection col, int index) => SetItem(index, col);    
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
a/collectionview Android10 API-change Heads-up to reviewers that this PR may contain an API change approved Has two approvals, no pending reviews, and no changes requested Core i/high Completely doesn't work, crashes, or is unusably slow, has no obvious workaround; occurs less often m/high impact ⬛ p/Android t/bug 🐛
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

None yet

6 participants