diff --git a/Xamarin.Forms.Core.UnitTests/BindableLayoutTests.cs b/Xamarin.Forms.Core.UnitTests/BindableLayoutTests.cs index 562de506345..aeeee3e2d19 100644 --- a/Xamarin.Forms.Core.UnitTests/BindableLayoutTests.cs +++ b/Xamarin.Forms.Core.UnitTests/BindableLayoutTests.cs @@ -5,6 +5,7 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; namespace Xamarin.Forms.Core.UnitTests @@ -139,6 +140,144 @@ public void TracksClear() Assert.IsTrue(IsLayoutWithItemsSource(itemsSource, layout)); } + [Test] + public async Task TracksAddOnBackgroundThread() + { + var invokeOnMainThreadWasCalled = false; + Device.PlatformServices = new MockPlatformServices(x => invokeOnMainThreadWasCalled = true, isInvokeRequired: true); + var layout = new StackLayout + { + IsPlatformEnabled = true, + }; + + var itemsSource = new ObservableCollection(); + BindableLayout.SetItemsSource(layout, itemsSource); + + invokeOnMainThreadWasCalled = false; + await Task.Run(() => itemsSource.Add(1)); + Assert.IsTrue(invokeOnMainThreadWasCalled); + } + + [Test] + public async Task TracksInsertOnBackgroundThread() + { + var invokeOnMainThreadWasCalled = false; + Device.PlatformServices = new MockPlatformServices(x => invokeOnMainThreadWasCalled = true, isInvokeRequired: true); + var layout = new StackLayout + { + IsPlatformEnabled = true, + }; + + var itemsSource = new ObservableCollection() { 0, 1, 2, 3, 4 }; + BindableLayout.SetItemsSource(layout, itemsSource); + + invokeOnMainThreadWasCalled = false; + await Task.Run(() => itemsSource.Insert(2, 5)); + Assert.IsTrue(invokeOnMainThreadWasCalled); + } + + [Test] + public async Task TracksRemoveOnBackgroundThread() + { + var invokeOnMainThreadWasCalled = false; + Device.PlatformServices = new MockPlatformServices(x => invokeOnMainThreadWasCalled = true, isInvokeRequired: true); + var layout = new StackLayout + { + IsPlatformEnabled = true, + }; + + var itemsSource = new ObservableCollection() { 0, 1 }; + BindableLayout.SetItemsSource(layout, itemsSource); + + invokeOnMainThreadWasCalled = false; + await Task.Run(() => itemsSource.RemoveAt(0)); + Assert.IsTrue(invokeOnMainThreadWasCalled); + invokeOnMainThreadWasCalled = false; + await Task.Run(() => itemsSource.Remove(1)); + Assert.IsTrue(invokeOnMainThreadWasCalled); + } + + [Test] + public async Task TracksRemoveAllOnBackgroundThread() + { + var invokeOnMainThreadWasCalled = false; + Device.PlatformServices = new MockPlatformServices(x => invokeOnMainThreadWasCalled = true, isInvokeRequired: true); + var layout = new StackLayout + { + IsPlatformEnabled = true, + }; + + var itemsSource = new ObservableRangeCollection(Enumerable.Range(0, 10)); + BindableLayout.SetItemsSource(layout, itemsSource); + + invokeOnMainThreadWasCalled = false; + await Task.Run(() => itemsSource.RemoveAll()); + Assert.IsTrue(invokeOnMainThreadWasCalled); + } + + [Test] + public async Task TracksReplaceOnBackgroundThread() + { + var invokeOnMainThreadWasCalled = false; + Device.PlatformServices = new MockPlatformServices(x => invokeOnMainThreadWasCalled = true, isInvokeRequired: true); + var layout = new StackLayout + { + IsPlatformEnabled = true, + }; + + var itemsSource = new ObservableCollection() { 0, 1, 2 }; + BindableLayout.SetItemsSource(layout, itemsSource); + + invokeOnMainThreadWasCalled = false; + await Task.Run(() => itemsSource[0] = 3); + Assert.IsTrue(invokeOnMainThreadWasCalled); + invokeOnMainThreadWasCalled = false; + await Task.Run(() => itemsSource[1] = 4); + Assert.IsTrue(invokeOnMainThreadWasCalled); + invokeOnMainThreadWasCalled = false; + await Task.Run(() => itemsSource[2] = 5); + Assert.IsTrue(invokeOnMainThreadWasCalled); + } + + [Test] + public async Task TracksMoveOnBackgroundThread() + { + var invokeOnMainThreadWasCalled = false; + Device.PlatformServices = new MockPlatformServices(x => invokeOnMainThreadWasCalled = true, isInvokeRequired: true); + var layout = new StackLayout + { + IsPlatformEnabled = true, + }; + + var itemsSource = new ObservableCollection() { 0, 1 }; + BindableLayout.SetItemsSource(layout, itemsSource); + + invokeOnMainThreadWasCalled = false; + await Task.Run(() => itemsSource.Move(0, 1)); + Assert.IsTrue(invokeOnMainThreadWasCalled); + invokeOnMainThreadWasCalled = false; + await Task.Run(() => itemsSource.Move(1, 0)); + Assert.IsTrue(invokeOnMainThreadWasCalled); + } + + [Test] + public async Task TracksClearOnBackgroundThread() + { + var invokeOnMainThreadWasCalled = false; + Device.PlatformServices = new MockPlatformServices(x => invokeOnMainThreadWasCalled = true, isInvokeRequired: true); + var layout = new StackLayout + { + IsPlatformEnabled = true, + }; + + var itemsSource = new ObservableCollection() { 0, 1 }; + BindableLayout.SetItemsSource(layout, itemsSource); + + invokeOnMainThreadWasCalled = false; + await Task.Run(() => itemsSource.Clear()); + Assert.IsTrue(invokeOnMainThreadWasCalled); + } + [Test] public void TracksNull() { diff --git a/Xamarin.Forms.Core/BindableLayout.cs b/Xamarin.Forms.Core/BindableLayout.cs index 13a1093d849..fb60ab25eb9 100644 --- a/Xamarin.Forms.Core/BindableLayout.cs +++ b/Xamarin.Forms.Core/BindableLayout.cs @@ -297,14 +297,17 @@ void ItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArg return; } - e.Apply( - insert: (item, index, _) => layout.Children.Insert(index, CreateItemView(item, layout)), - removeAt: (item, index) => layout.Children.RemoveAt(index), - reset: CreateChildren); - - // UpdateEmptyView is called from within CreateChildren, therefor skip it for Reset - if (e.Action != NotifyCollectionChangedAction.Reset) - UpdateEmptyView(layout); + Device.BeginInvokeOnMainThread(() => + { + e.Apply( + insert: (item, index, _) => layout.Children.Insert(index, CreateItemView(item, layout)), + removeAt: (item, index) => layout.Children.RemoveAt(index), + reset: CreateChildren); + + // UpdateEmptyView is called from within CreateChildren, therefor skip it for Reset + if (e.Action != NotifyCollectionChangedAction.Reset) + UpdateEmptyView(layout); + }); } } } \ No newline at end of file