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:
Родитель
29ba746743
Коммит
ab55162596
|
@ -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 -> '%(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)
|
||||
|
|
Загрузка…
Ссылка в новой задаче