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

* Fixed CollectionView issue adding data in different thread on Android

* Fixed build error

* Changes to fix the build

* Force off-main-thread ObservableCollection changes to marshal to main thread

* Update Xamarin.Forms.Controls.Issues.Shared.projitems

* Update interface

* Restore old method

Co-authored-by: Javier Suárez Ruiz <javiersuarezruiz@hotmail.com>
Co-authored-by: Samantha Houts <samhouts@users.noreply.github.com>
Co-authored-by: Rui Marinho <me@ruimarinho.net>

fixes #10735
fixes #9753
This commit is contained in:
E.Z. Hart 2020-07-20 16:47:40 -06:00 коммит произвёл GitHub
Родитель 29ba746743
Коммит ab55162596
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 830 добавлений и 5 удалений

Просмотреть файл

@ -2,6 +2,8 @@
"solution": {
"path": "Xamarin.Forms.sln",
"projects": [
"PagesGallery\\PagesGallery.Droid\\PagesGallery.Droid.csproj",
"PagesGallery\\PagesGallery\\PagesGallery.csproj",
"Stubs\\Xamarin.Forms.Platform.Android\\Xamarin.Forms.Platform.Android (Forwarders).csproj",
"XFCorePostProcessor.Tasks\\XFCorePostProcessor.Tasks.csproj",
"Xamarin.Flex\\Xamarin.Flex.shproj",

Просмотреть файл

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8" ?>
<controls:TestContentPage
xmlns:controls="clr-namespace:Xamarin.Forms.Controls"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="Xamarin.Forms.Controls.Issue10735"
Title="Issue 10735">
<StackLayout>
<Label
Padding="12"
BackgroundColor="Black"
TextColor="White"
Text="If this sample works without exceptions, the test has passed."/>
<CollectionView
x:Name="_collectionView"
ItemsSource="{Binding Items}"
VerticalOptions="Fill"
ItemSizingStrategy="MeasureAllItems"
ItemsUpdatingScrollMode="KeepLastItemInView">
<CollectionView.ItemTemplate>
<DataTemplate>
<Label
Text="{Binding}"
HorizontalOptions="Center"
VerticalOptions="Center"
FontSize="30" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<StackLayout
VerticalOptions="End"
Orientation="Horizontal">
<Editor
x:Name="_editor"
HorizontalOptions="CenterAndExpand"
AutoSize="TextChanges" />
<Button
x:Name="_button"
HorizontalOptions="CenterAndExpand"/>
</StackLayout>
</StackLayout>
</controls:TestContentPage>

Просмотреть файл

@ -0,0 +1,77 @@
using Xamarin.Forms.Internals;
using Xamarin.Forms.CustomAttributes;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
#if UITEST
using Xamarin.Forms.Core.UITests;
using Xamarin.UITest;
using NUnit.Framework;
#endif
namespace Xamarin.Forms.Controls
{
#if UITEST
[Category(UITestCategories.CollectionView)]
#endif
[Preserve(AllMembers = true)]
[Issue(IssueTracker.Github, 10735, "[Bug] [Fatal] [Android] CollectionView Causes Application Crash When Keyboard Opens", PlatformAffected.Android)]
public partial class Issue10735 : TestContentPage
{
readonly int _addItemDelay = 300;
int _item = 0;
#if APP
readonly int _changeFocusDelay = 1000;
View _lastFocus;
#endif
public Issue10735()
{
#if APP
InitializeComponent();
BindingContext = this;
StartAddingMessages();
#endif
}
public ObservableCollection<string> Items { get; } = new ObservableCollection<string>();
protected override void Init()
{
}
void StartAddingMessages()
{
Task.Run(async () =>
{
while (true)
{
await Task.Delay(_addItemDelay);
Items.Add(_item.ToString());
_item++;
}
});
#if APP
Task.Run(async () =>
{
while (true)
{
await Task.Delay(_changeFocusDelay);
Device.BeginInvokeOnMainThread(() =>
{
_lastFocus?.Unfocus();
if (_lastFocus == _editor)
_lastFocus = _button;
else
_lastFocus = _editor;
_lastFocus.Focus();
});
}
});
#endif
}
}
}

Просмотреть файл

@ -1405,10 +1405,14 @@
<Compile Include="$(MSBuildThisFileDirectory)Issue10530.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue7780.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue8958.xaml.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue10735.xaml.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue10497.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue10477.xaml.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue10875.xaml.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue10708.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue9711.xaml.cs">
<DependentUpon>Issue9711.xaml</DependentUpon>
<SubType>Code</SubType>
@ -1654,6 +1658,9 @@
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue8958.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue10735.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue10477.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>

Просмотреть файл

@ -0,0 +1,338 @@
using System;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using Xamarin.Forms.Internals;
namespace Xamarin.Forms.Core.UnitTests
{
[TestFixture]
public class MarshalingObservableCollectionTests
{
MarshalingTestPlatformServices _services;
[SetUp]
public void Setup()
{
_services = new MarshalingTestPlatformServices();
Device.PlatformServices = _services;
_services.Start();
}
[TearDown]
public void TearDown()
{
_services.Stop();
_services = null;
}
[Test]
[Description("Added items don't show up until they've been processed on the UI thread")]
public async Task AddOffUIThread()
{
int insertCount = 0;
var countFromThreadPool = -1;
var source = new ObservableCollection<int>();
var moc = new MarshalingObservableCollection(source);
moc.CollectionChanged += (sender, args) => {
if (args.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
insertCount += 1;
}
};
// Add an item from a threadpool thread
await Task.Run(() =>
{
var x = Thread.CurrentThread.ManagedThreadId;
source.Add(1);
countFromThreadPool = moc.Count;
});
// Check the result on the main thread
var onMainThreadCount = await Device.InvokeOnMainThreadAsync<int>(() =>
{
return moc.Count;
});
Assert.That(countFromThreadPool, Is.EqualTo(0), "Count should be zero because the update on the UI thread hasn't run yet");
Assert.That(onMainThreadCount, Is.EqualTo(1), "Count should be 1 because the UI thread has updated");
Assert.That(insertCount, Is.EqualTo(1), "The CollectionChanged event should have fired with an Add exactly 1 time");
}
[Test]
[Description("Intial item count should match wrapped collection.")]
public async Task InitialItemCountsMatch()
{
var source = new ObservableCollection<int> { 1, 2 };
var moc = new MarshalingObservableCollection(source);
Assert.That(source.Count, Is.EqualTo(moc.Count));
}
[Test]
[Description("Clears don't show up until they've been processed on the UI thread")]
public async Task ClearOnUIThread()
{
var countFromThreadPool = -1;
var source = new ObservableCollection<int>
{
1,
2
};
var moc = new MarshalingObservableCollection(source);
// Call Clear from a threadpool thread
await Task.Run(() =>
{
source.Clear();
countFromThreadPool = moc.Count;
});
// Check the result on the main thread
var onMainThreadCount = await Device.InvokeOnMainThreadAsync<int>(() => moc.Count);
Assert.That(countFromThreadPool, Is.EqualTo(2), "Count should be pre-clear");
Assert.That(onMainThreadCount, Is.EqualTo(0), "Count should be zero because the Clear has been processed");
}
[Test]
[Description("A Reset should reflect the state at the time of the Reset")]
public async Task ClearAndAddOffUIThread()
{
var countFromThreadPool = -1;
var source = new ObservableCollection<int>
{
1,
2
};
var moc = new MarshalingObservableCollection(source);
// Call Clear from a threadpool thread
await Task.Run(() =>
{
source.Clear();
source.Add(4);
countFromThreadPool = moc.Count;
});
// Check the result on the main thread
var onMainThreadCount = await Device.InvokeOnMainThreadAsync<int>(() => moc.Count);
Assert.That(countFromThreadPool, Is.EqualTo(2), "Count should be pre-clear");
Assert.That(onMainThreadCount, Is.EqualTo(1), "Should have processed a Clear and an Add");
}
[Test]
[Description("Removed items are still there until they're removed on the UI thread")]
public async Task RemoveOffUIThread()
{
var countFromThreadPool = -1;
var source = new ObservableCollection<int> { 1, 2 };
var moc = new MarshalingObservableCollection(source);
// Call Clear from a threadpool thread
await Task.Run(() =>
{
source.Remove(1);
countFromThreadPool = moc.Count;
});
// Check the result on the main thread
var onMainThreadCount = await Device.InvokeOnMainThreadAsync<int>(() => moc.Count);
Assert.That(countFromThreadPool, Is.EqualTo(2), "Count should be pre-remove");
Assert.That(onMainThreadCount, Is.EqualTo(1), "Remove has now processed");
}
[Test]
[Description("Until the UI thread processes a change, the indexer should remain consistent")]
public async Task IndexerConsistent()
{
int itemFromThreadPool = -1;
var source = new ObservableCollection<int> { 1, 2 };
var moc = new MarshalingObservableCollection(source);
// Call Remove from a threadpool thread
await Task.Run(() =>
{
source.Remove(1);
itemFromThreadPool = (int)moc[1];
});
Assert.That(itemFromThreadPool, Is.EqualTo(2), "Should have indexer value from before remove");
}
[Test]
[Description("Don't show replacements until the UI thread has processed them")]
public async Task ReplaceOffUIThread()
{
int itemFromThreadPool = -1;
var source = new ObservableCollection<int> { 1, 2 };
var moc = new MarshalingObservableCollection(source);
// Replace a value from a threadpool thread
await Task.Run(() =>
{
source[0] = 42;
itemFromThreadPool = (int)moc[0];
});
// Check the result on the main thread
var onMainThreadValue = await Device.InvokeOnMainThreadAsync(() => moc[0]);
Assert.That(itemFromThreadPool, Is.EqualTo(1), "Should have value from before replace");
Assert.That(onMainThreadValue, Is.EqualTo(42), "Should have value from after replace");
}
[Test]
[Description("Don't show moves until the UI thread has processed them")]
public async Task MoveOffUIThread()
{
int itemFromThreadPool = -1;
var source = new ObservableCollection<int> { 1, 2 };
var moc = new MarshalingObservableCollection(source);
// Replace a value from a threadpool thread
await Task.Run(() =>
{
source.Move(1, 0);
itemFromThreadPool = (int)moc[0];
});
// Check the result on the main thread
var onMainThreadValue = await Device.InvokeOnMainThreadAsync(() => moc[0]);
Assert.That(itemFromThreadPool, Is.EqualTo(1), "Should have value from before move");
Assert.That(onMainThreadValue, Is.EqualTo(2), "Should have value from after move");
}
// This class simulates running a single UI thread with a queue and non-UI threads;
// this allows us to test IsInvokeRequired/BeginInvoke without having to be on an actual device
class MarshalingTestPlatformServices : IPlatformServices
{
int _threadId;
bool _running;
BlockingCollection<Action> _todo = new BlockingCollection<Action>();
public void Stop()
{
_running = false;
_todo.CompleteAdding();
}
public void Start()
{
_running = true;
Task.Run(() => {
if (_threadId == 0)
{
_threadId = Thread.CurrentThread.ManagedThreadId;
}
while (_running)
{
try
{
_todo.Take()?.Invoke();
}
catch (Exception ex)
{
Stop();
}
}
});
}
public bool IsInvokeRequired => Thread.CurrentThread.ManagedThreadId != _threadId;
public void BeginInvokeOnMainThread(Action action)
{
_todo.Add(action);
}
public OSAppTheme RequestedTheme { get; }
public string RuntimePlatform { get; }
public Ticker CreateTicker()
{
throw new NotImplementedException();
}
public Assembly[] GetAssemblies()
{
throw new NotImplementedException();
}
public Color GetNamedColor(string name)
{
throw new NotImplementedException();
}
public double GetNamedSize(NamedSize size, Type targetElementType, bool useOldSizes)
{
throw new NotImplementedException();
}
public SizeRequest GetNativeSize(VisualElement view, double widthConstraint, double heightConstraint)
{
throw new NotImplementedException();
}
public Task<Stream> GetStreamAsync(Uri uri, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public IIsolatedStorageFile GetUserStoreForApplication()
{
throw new NotImplementedException();
}
public void OpenUriAction(Uri uri)
{
throw new NotImplementedException();
}
public void QuitApplication()
{
throw new NotImplementedException();
}
public void StartTimer(TimeSpan interval, Func<bool> callback)
{
throw new NotImplementedException();
}
public string GetHash(string input)
{
throw new NotImplementedException();
}
public string GetMD5Hash(string input)
{
throw new NotImplementedException();
}
}
}
}

Просмотреть файл

@ -96,6 +96,7 @@
<Compile Include="Markup\ViewInFlexLayoutExtensionsTests.cs" />
<Compile Include="Markup\ViewInGridExtensionsTests.cs" />
<Compile Include="Markup\VisualElementExtensionsTests.cs" />
<Compile Include="MarshalingObservableCollectionTests.cs" />
<Compile Include="NumericExtensionsTests.cs" />
<Compile Include="RefreshViewTests.cs" />
<Compile Include="MockDispatcherProvider.cs" />
@ -257,7 +258,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Xamarin.Forms.Core\Xamarin.Forms.Core.csproj">
<Project>{57B8B73D-C3B5-4C42-869E-7B2F17D354AC}</Project>
<Project>{57b8b73d-c3b5-4c42-869e-7b2f17d354ac}</Project>
<Name>Xamarin.Forms.Core</Name>
</ProjectReference>
<ProjectReference Include="..\Xamarin.Forms.Maps\Xamarin.Forms.Maps.csproj">
@ -290,7 +291,7 @@
<ItemGroup />
<Target Name="_CopyNUnitTestAdapterFiles" AfterTargets="Build">
<ItemGroup>
<_NUnitTestAdapterFiles Include="$(NuGetPackageRoot)NUnit3TestAdapter\%(Version)\build\net35\**" Condition="@(PackageReference -&gt; '%(Identity)') == 'NUnit3TestAdapter'" InProject="False" />
<_NUnitTestAdapterFiles Include="$(NuGetPackageRoot)NUnit3TestAdapter\%(Version)\build\net35\**" Condition="@(PackageReference -> '%(Identity)') == 'NUnit3TestAdapter'" InProject="False" />
</ItemGroup>
<Copy SourceFiles="@(_NUnitTestAdapterFiles)" DestinationFolder="$(SolutionDir)packages\NUnitTestAdapter.AnyVersion\tools\%(RecursiveDir)" ContinueOnError="true" Retries="0" />
<Copy SourceFiles="@(_NUnitTestAdapterFiles)" DestinationFolder="$(SolutionDir)packages\NUnitTestAdapter.AnyVersion\build\%(RecursiveDir)" ContinueOnError="true" Retries="0" />

Просмотреть файл

@ -0,0 +1,157 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
namespace Xamarin.Forms
{
// Wraps a List which implements INotifyCollectionChanged (usually an ObservableCollection)
// and marshals all of the list modifications to the main thread. Modifications to the underlying
// 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
{
readonly IList _internalCollection;
public MarshalingObservableCollection(IList list)
{
if (!(list is INotifyCollectionChanged incc))
{
throw new ArgumentException($"{nameof(list)} must implement {nameof(INotifyCollectionChanged)}");
}
_internalCollection = list;
incc.CollectionChanged += InternalCollectionChanged;
foreach (var item in _internalCollection)
{
Add(item);
}
}
class ResetNotifyCollectionChangedEventArgs : NotifyCollectionChangedEventArgs
{
public IList Items { get; }
public ResetNotifyCollectionChangedEventArgs(IList items)
: base(NotifyCollectionChangedAction.Reset) => Items = items;
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
CollectionChanged?.Invoke(this, args);
}
void InternalCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
if (args.Action == NotifyCollectionChangedAction.Reset)
{
var items = new List<object>();
for(int n = 0; n < _internalCollection.Count; n++)
{
items.Add(_internalCollection[n]);
}
args = new ResetNotifyCollectionChangedEventArgs(items);
}
if (Device.IsInvokeRequired)
{
Device.BeginInvokeOnMainThread(() => HandleCollectionChange(args));
}
else
{
HandleCollectionChange(args);
}
}
void HandleCollectionChange(NotifyCollectionChangedEventArgs args)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
Add(args);
break;
case NotifyCollectionChangedAction.Move:
Move(args);
break;
case NotifyCollectionChangedAction.Remove:
Remove(args);
break;
case NotifyCollectionChangedAction.Replace:
Replace(args);
break;
case NotifyCollectionChangedAction.Reset:
Reset(args);
break;
}
}
void Move(NotifyCollectionChangedEventArgs args)
{
var count = args.OldItems.Count;
for (int n = 0; n < count; n++)
{
var toMove = this[args.OldStartingIndex];
RemoveAt(args.OldStartingIndex);
Insert(args.NewStartingIndex, toMove);
}
OnCollectionChanged(args);
}
void Remove(NotifyCollectionChangedEventArgs args)
{
var startIndex = args.OldStartingIndex + args.OldItems.Count - 1;
for (int n = startIndex; n >= args.OldStartingIndex; n--)
{
RemoveAt(n);
}
OnCollectionChanged(args);
}
void Replace(NotifyCollectionChangedEventArgs args)
{
var startIndex = args.NewStartingIndex;
foreach (var item in args.NewItems)
{
this[startIndex] = item;
startIndex += 1;
}
OnCollectionChanged(args);
}
void Add(NotifyCollectionChangedEventArgs args)
{
var startIndex = args.NewStartingIndex;
foreach (var item in args.NewItems)
{
Insert(startIndex, item);
startIndex += 1;
}
OnCollectionChanged(args);
}
void Reset(NotifyCollectionChangedEventArgs args)
{
if (!(args is ResetNotifyCollectionChangedEventArgs resetArgs))
{
throw new InvalidOperationException($"Cannot guarantee collection accuracy for Resets which do not use {nameof(ResetNotifyCollectionChangedEventArgs)}");
}
Clear();
foreach (var item in resetArgs.Items)
{
Add(item);
}
OnCollectionChanged(args);
}
}
}

Просмотреть файл

@ -0,0 +1,194 @@
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Android.OS;
using Java.Lang;
using NUnit.Framework;
using NUnit.Framework.Internal;
namespace Xamarin.Forms.Platform.Android.UnitTests
{
[TestFixture]
public class ObservrableItemsSourceTests
{
Handler _handler = new Handler(Looper.MainLooper);
[Test, Category("CollectionView")]
[Description("Off-main-thread modifications to the source should be reflected in the count when the main thread has processed them.")]
public async Task ObservableSourceItemsCountConsistent()
{
var source = new ObservableCollection<int>();
source.Add(1);
source.Add(2);
var ois = ItemsSourceFactory.Create(source, new MockCollectionChangedNotifier());
Assert.That(ois.Count, Is.EqualTo(2));
source.Add(3);
var count = await Device.InvokeOnMainThreadAsync(() => ois.Count);
Assert.That(ois.Count, Is.EqualTo(3));
}
[Test, Category("CollectionView")]
[Description("Off-main-thread Adds should be reflected once the main thread has processed them.")]
public async Task AddItemCountConsistentOnUIThread()
{
var notifier = new MockCollectionChangedNotifier();
var source = new ObservableCollection<int>();
IItemsViewSource ois = ItemsSourceFactory.Create(source, notifier);
int countBeforeNotify = -1;
// Add an item from a threadpool thread
await Task.Run(() => {
source.Add(1);
// Post a check ahead of the queued update on the main thread
_handler.PostAtFrontOfQueue(() => {
countBeforeNotify = ois.Count;
});
});
// Check the result on the main thread
var onMainThreadCount = await Device.InvokeOnMainThreadAsync(() => ois.Count);
Assert.That(countBeforeNotify, Is.EqualTo(0), "Count should still be reporting no items before the notify resolves");
Assert.That(onMainThreadCount, Is.EqualTo(1));
Assert.That(notifier.InsertCount, Is.EqualTo(1), "Should have recorded exactly one Add");
}
[Test, Category("CollectionView")]
[Description("????")]
public async Task RemoveitemCountConsistentOnUIThread()
{
var notifier = new MockCollectionChangedNotifier();
var source = new ObservableCollection<int> { 1 };
IItemsViewSource ois = ItemsSourceFactory.Create(source, notifier);
int countBeforeNotify = -1;
// Remove an item from a threadpool thread
await Task.Run(() =>
{
source.Remove(1);
// Post a check ahead of the queued update on the main thread
_handler.PostAtFrontOfQueue(() => countBeforeNotify = ois.Count);
});
// Check the result on the main thread
var onMainThreadCount = await Device.InvokeOnMainThreadAsync(() => ois.Count);
Assert.That(countBeforeNotify, Is.EqualTo(1));
Assert.That(onMainThreadCount, Is.EqualTo(0));
Assert.That(notifier.RemoveCount, Is.EqualTo(1));
}
[Test, Category("CollectionView")]
[Description("????")]
public async Task GetItemConsistentOnUIThread()
{
var notifier = new MockCollectionChangedNotifier();
var source = new ObservableCollection<string>
{
"zero",
"one",
"two"
};
IItemsViewSource ois = ItemsSourceFactory.Create(source, notifier);
string itemAtPosition2BeforeNotify = string.Empty;
// Add an item from a threadpool thread
await Task.Run(() => {
source.Insert(0, "foo");
// Post a check ahead of the queued update on the main thread
_handler.PostAtFrontOfQueue(() => itemAtPosition2BeforeNotify = (string)ois.GetItem(2));
});
// Check the result on the main thread
var onMainThreadGetItem = await Device.InvokeOnMainThreadAsync(() => (string)ois.GetItem(2));
Assert.That(itemAtPosition2BeforeNotify, Is.EqualTo("two"));
Assert.That(onMainThreadGetItem, Is.EqualTo("one"));
Assert.That(notifier.InsertCount, Is.EqualTo(1));
}
[Test, Category("CollectionView")]
[Description("????")]
public async Task GetPositionConsistentOnUIThread()
{
var notifier = new MockCollectionChangedNotifier();
var source = new ObservableCollection<string>
{
"zero",
"one",
"two"
};
IItemsViewSource ois = ItemsSourceFactory.Create(source, notifier);
int positionBeforeNotify = -1;
// Add an item from a threadpool thread
await Task.Run(() => {
source.Insert(0, "foo");
// Post a check ahead of the queued update on the main thread
_handler.PostAtFrontOfQueue(() => positionBeforeNotify = ois.GetPosition("zero"));
});
// Check the result on the main thread
var onMainThreadGetItem = await Device.InvokeOnMainThreadAsync(() => ois.GetPosition("zero"));
Assert.That(positionBeforeNotify, Is.EqualTo(0));
Assert.That(onMainThreadGetItem, Is.EqualTo(1));
Assert.That(notifier.InsertCount, Is.EqualTo(1));
}
class MockCollectionChangedNotifier : ICollectionChangedNotifier
{
public int InsertCount;
public int RemoveCount;
public void NotifyDataSetChanged()
{
}
public void NotifyItemChanged(IItemsViewSource source, int startIndex)
{
}
public void NotifyItemInserted(IItemsViewSource source, int startIndex)
{
InsertCount += 1;
}
public void NotifyItemMoved(IItemsViewSource source, int fromPosition, int toPosition)
{
}
public void NotifyItemRangeChanged(IItemsViewSource source, int start, int end)
{
}
public void NotifyItemRangeInserted(IItemsViewSource source, int startIndex, int count)
{
}
public void NotifyItemRangeRemoved(IItemsViewSource source, int startIndex, int count)
{
}
public void NotifyItemRemoved(IItemsViewSource source, int startIndex)
{
RemoveCount += 1;
}
}
}
}

Просмотреть файл

@ -54,6 +54,7 @@
<Compile Include="IsEnabledTests.cs" />
<Compile Include="Issues.cs" />
<Compile Include="IsVisibleTests.cs" />
<Compile Include="ObservrableItemsSourceTests.cs" />
<Compile Include="OpacityTests.cs" />
<Compile Include="RendererTests.cs" />
<Compile Include="PlatformTestFixture.cs" />

Просмотреть файл

@ -1,6 +1,8 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.IO;
#if __ANDROID_29__
using AndroidX.AppCompat.Widget;
using AndroidX.RecyclerView.Widget;
@ -21,8 +23,8 @@ namespace Xamarin.Forms.Platform.Android
switch (itemsSource)
{
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);
case IEnumerable _ when itemsSource is INotifyCollectionChanged:
return new ObservableItemsSource(itemsSource as IEnumerable, notifier);
case IEnumerable<object> generic:

Просмотреть файл

@ -667,7 +667,7 @@ namespace Xamarin.Forms.Platform.Android
{
if (ItemsView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView)
{
ScrollTo(new ScrollToRequestEventArgs(ItemsViewAdapter.ItemCount, 0,
ScrollTo(new ScrollToRequestEventArgs(GetLayoutManager().ItemCount, 0,
Xamarin.Forms.ScrollToPosition.MakeVisible, true));
}
else if (ItemsView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepScrollOffset)