Handle "groups but no actual cells" cases for reloading UICollectionView (#9911) Fixes #9580 Fixes #9686
* Handle "groups but no actual cells" case for reloading UICollectionView Fixes #9580 * Add check for reload when adding/removing groups Fixes #9686 * Prevent prevent reloads during batch updates Fixes #9686 * Removed obsolete TODO comment * Fix test (missing success condition) * Fix possible tap speed issue in UI test * Switch await loop to SemaphoreSlim * Fix semaphore deadlocks * Fix incorrect check for semaphore release * If this fixes it, I'm gonna be soooooo mad... * When in doubt, just use a button
This commit is contained in:
Родитель
c06932d499
Коммит
7cfe6155e7
|
@ -0,0 +1,88 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Xamarin.Forms.CustomAttributes;
|
||||||
|
|
||||||
|
#if UITEST
|
||||||
|
using Xamarin.UITest;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Xamarin.Forms.Core.UITests;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Xamarin.Forms.Controls.Issues
|
||||||
|
{
|
||||||
|
[Issue(IssueTracker.Github, 9580, "[Bug] CollectionView - iOS - Crash when adding first item to empty item group",
|
||||||
|
PlatformAffected.iOS)]
|
||||||
|
public class Issue9580 : TestContentPage
|
||||||
|
{
|
||||||
|
const string Success = "Success";
|
||||||
|
const string Test9580 = "9580";
|
||||||
|
|
||||||
|
protected override void Init()
|
||||||
|
{
|
||||||
|
var layout = new StackLayout();
|
||||||
|
|
||||||
|
var cv = new CollectionView
|
||||||
|
{
|
||||||
|
IsGrouped = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var groups = new ObservableCollection<_9580Group>()
|
||||||
|
{
|
||||||
|
new _9580Group() { Name = "One" }, new _9580Group(){ Name = "Two" }, new _9580Group(){ Name = "Three" },
|
||||||
|
new _9580Group() { Name = "Four" }, new _9580Group(){ Name = "Five" }, new _9580Group(){ Name = "Six" }
|
||||||
|
};
|
||||||
|
|
||||||
|
cv.ItemTemplate = new DataTemplate(() => {
|
||||||
|
var label = new Label() { Margin = new Thickness(5, 0, 0, 0) };
|
||||||
|
label.SetBinding(Label.TextProperty, new Binding("Text"));
|
||||||
|
return label;
|
||||||
|
});
|
||||||
|
|
||||||
|
cv.GroupHeaderTemplate = new DataTemplate(() => {
|
||||||
|
var label = new Label();
|
||||||
|
label.SetBinding(Label.TextProperty, new Binding("Name"));
|
||||||
|
return label;
|
||||||
|
});
|
||||||
|
|
||||||
|
cv.ItemsSource = groups;
|
||||||
|
|
||||||
|
var instructions = new Label { Text = $"Tap the '{Test9580}' button. The application doesn't crash, this test has passed." };
|
||||||
|
|
||||||
|
var result = new Label { };
|
||||||
|
|
||||||
|
var button = new Button { Text = Test9580 };
|
||||||
|
button.Clicked += (sender, args) =>
|
||||||
|
{
|
||||||
|
groups[0].Add(new _9580Item { Text = "An Item" });
|
||||||
|
result.Text = Success;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout.Children.Add(instructions);
|
||||||
|
layout.Children.Add(result);
|
||||||
|
layout.Children.Add(button);
|
||||||
|
layout.Children.Add(cv);
|
||||||
|
|
||||||
|
Content = layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _9580Item
|
||||||
|
{
|
||||||
|
public string Text { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class _9580Group : ObservableCollection<_9580Item>
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#if UITEST
|
||||||
|
[Category(UITestCategories.CollectionView)]
|
||||||
|
[Test]
|
||||||
|
public void AllEmptyGroupsShouldNotCrashOnItemInsert()
|
||||||
|
{
|
||||||
|
RunningApp.WaitForElement(Test9580);
|
||||||
|
RunningApp.Tap(Test9580);
|
||||||
|
RunningApp.WaitForElement(Success);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,191 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Xamarin.Forms.CustomAttributes;
|
||||||
|
|
||||||
|
#if UITEST
|
||||||
|
using Xamarin.UITest;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Xamarin.Forms.Core.UITests;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Xamarin.Forms.Controls.Issues
|
||||||
|
{
|
||||||
|
[Issue(IssueTracker.Github, 9686, "[Bug, CollectionView,iOS] Foundation.Monotouch Exception in Grouped CollectionView",
|
||||||
|
PlatformAffected.iOS)]
|
||||||
|
public class Issue9686 : TestContentPage
|
||||||
|
{
|
||||||
|
const string Success = "Success";
|
||||||
|
const string Run = "Run";
|
||||||
|
|
||||||
|
protected override void Init()
|
||||||
|
{
|
||||||
|
var layout = new StackLayout();
|
||||||
|
|
||||||
|
var cv = new CollectionView
|
||||||
|
{
|
||||||
|
IsGrouped = true
|
||||||
|
};
|
||||||
|
|
||||||
|
BindingContext = new _9686ViewModel();
|
||||||
|
|
||||||
|
cv.ItemTemplate = new DataTemplate(() => {
|
||||||
|
var label = new Label() { Margin = new Thickness(5, 0, 0, 0) };
|
||||||
|
label.SetBinding(Label.TextProperty, new Binding("Name"));
|
||||||
|
return label;
|
||||||
|
});
|
||||||
|
|
||||||
|
cv.GroupHeaderTemplate = new DataTemplate(() => {
|
||||||
|
var label = new Label();
|
||||||
|
label.SetBinding(Label.TextProperty, new Binding("GroupName"));
|
||||||
|
return label;
|
||||||
|
});
|
||||||
|
|
||||||
|
cv.SetBinding(ItemsView.ItemsSourceProperty, new Binding(nameof(_9686ViewModel.Groups)));
|
||||||
|
|
||||||
|
var instructions = new Label { Text = $"Tap the button once, then again. The application doesn't crash, this test has passed." };
|
||||||
|
|
||||||
|
var result = new Label { };
|
||||||
|
|
||||||
|
var button = new Button { Text = Run, AutomationId = Run };
|
||||||
|
button.Command = ((_9686ViewModel)BindingContext).ShowOrHideCommand;
|
||||||
|
button.CommandParameter = ((_9686ViewModel)BindingContext).Groups[0];
|
||||||
|
|
||||||
|
layout.Children.Add(instructions);
|
||||||
|
layout.Children.Add(button);
|
||||||
|
layout.Children.Add(result);
|
||||||
|
layout.Children.Add(cv);
|
||||||
|
|
||||||
|
Content = layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class _9686Item
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class _9686Group : List<_9686Item>
|
||||||
|
{
|
||||||
|
public string GroupName { get; set; }
|
||||||
|
|
||||||
|
public _9686Group(string groupName, ObservableCollection<_9686Item> items) : base(items)
|
||||||
|
{
|
||||||
|
GroupName = groupName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class _9686ViewModel
|
||||||
|
{
|
||||||
|
public ICommand ShowOrHideCommand { get; set; }
|
||||||
|
public ObservableCollection<_9686Group> Groups { get; set; }
|
||||||
|
|
||||||
|
public _9686Group PreviousGroup { get; set; }
|
||||||
|
|
||||||
|
public _9686ViewModel()
|
||||||
|
{
|
||||||
|
ShowOrHideCommand = new Command<_9686Group>((group) => ShowOrHideItems(group));
|
||||||
|
|
||||||
|
Groups = new ObservableCollection<_9686Group>
|
||||||
|
{
|
||||||
|
new _9686Group("Group 1", new ObservableCollection<_9686Item>()),
|
||||||
|
new _9686Group("Group 2", new ObservableCollection<_9686Item>()),
|
||||||
|
new _9686Group("Group 3", new ObservableCollection<_9686Item>()),
|
||||||
|
new _9686Group("Group 4", new ObservableCollection<_9686Item>()),
|
||||||
|
new _9686Group("Group 5", new ObservableCollection<_9686Item>()),
|
||||||
|
new _9686Group("Group 6", new ObservableCollection<_9686Item>())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShowOrHideItems(_9686Group group)
|
||||||
|
{
|
||||||
|
if (PreviousGroup == group)
|
||||||
|
{
|
||||||
|
if (PreviousGroup.Any())
|
||||||
|
{
|
||||||
|
PreviousGroup.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PreviousGroup.AddRange(new List<_9686Item>
|
||||||
|
{
|
||||||
|
new _9686Item
|
||||||
|
{
|
||||||
|
Name = "Item 1"
|
||||||
|
},
|
||||||
|
new _9686Item
|
||||||
|
{
|
||||||
|
Name = "Item 2"
|
||||||
|
},
|
||||||
|
new _9686Item
|
||||||
|
{
|
||||||
|
Name = "Item 3"
|
||||||
|
},
|
||||||
|
new _9686Item
|
||||||
|
{
|
||||||
|
Name = "Item 4"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCollection(PreviousGroup);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (PreviousGroup != null)
|
||||||
|
{
|
||||||
|
PreviousGroup.Clear();
|
||||||
|
UpdateCollection(PreviousGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
group.AddRange(new List<_9686Item>
|
||||||
|
{
|
||||||
|
new _9686Item
|
||||||
|
{
|
||||||
|
Name = "Item 1"
|
||||||
|
},
|
||||||
|
new _9686Item
|
||||||
|
{
|
||||||
|
Name = "Item 2"
|
||||||
|
},
|
||||||
|
new _9686Item
|
||||||
|
{
|
||||||
|
Name = "Item 3"
|
||||||
|
},
|
||||||
|
new _9686Item
|
||||||
|
{
|
||||||
|
Name = "Item 4"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
UpdateCollection(group);
|
||||||
|
PreviousGroup = group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateCollection(_9686Group group)
|
||||||
|
{
|
||||||
|
var index = Groups.IndexOf(group);
|
||||||
|
Groups.Remove(group);
|
||||||
|
if (group.Count == 0)
|
||||||
|
{
|
||||||
|
group.GroupName = Success;
|
||||||
|
}
|
||||||
|
Groups.Insert(index, group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if UITEST
|
||||||
|
[Category(UITestCategories.CollectionView)]
|
||||||
|
[Test]
|
||||||
|
public void AddRemoveEmptyGroupsShouldNotCrashOnInsert()
|
||||||
|
{
|
||||||
|
RunningApp.WaitForElement(Run);
|
||||||
|
RunningApp.Tap(Run);
|
||||||
|
RunningApp.WaitForElement("Item 1");
|
||||||
|
RunningApp.Tap(Run);
|
||||||
|
RunningApp.WaitForElement(Success);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,7 @@
|
||||||
<DependentUpon>Issue8902.xaml</DependentUpon>
|
<DependentUpon>Issue8902.xaml</DependentUpon>
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="$(MSBuildThisFileDirectory)Issue9580.cs" />
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Issue9682.xaml.cs">
|
<Compile Include="$(MSBuildThisFileDirectory)Issue9682.xaml.cs">
|
||||||
<DependentUpon>Issue9682.xaml</DependentUpon>
|
<DependentUpon>Issue9682.xaml</DependentUpon>
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
|
@ -216,6 +217,7 @@
|
||||||
<DependentUpon>Issue9783.xaml</DependentUpon>
|
<DependentUpon>Issue9783.xaml</DependentUpon>
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="$(MSBuildThisFileDirectory)Issue9686.cs" />
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Issue9694.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Issue9694.cs" />
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Issue9771.xaml.cs">
|
<Compile Include="$(MSBuildThisFileDirectory)Issue9771.xaml.cs">
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
|
|
||||||
|
@ -13,6 +15,7 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
readonly UICollectionViewController _collectionViewController;
|
readonly UICollectionViewController _collectionViewController;
|
||||||
readonly IList _groupSource;
|
readonly IList _groupSource;
|
||||||
bool _disposed;
|
bool _disposed;
|
||||||
|
SemaphoreSlim _batchUpdating = new SemaphoreSlim(1, 1);
|
||||||
List<ObservableItemsSource> _groups = new List<ObservableItemsSource>();
|
List<ObservableItemsSource> _groups = new List<ObservableItemsSource>();
|
||||||
|
|
||||||
public ObservableGroupedSource(IEnumerable groupSource, UICollectionViewController collectionViewController)
|
public ObservableGroupedSource(IEnumerable groupSource, UICollectionViewController collectionViewController)
|
||||||
|
@ -39,7 +42,7 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
|
|
||||||
public int GroupCount => _groupSource.Count;
|
public int GroupCount => _groupSource.Count;
|
||||||
|
|
||||||
int IItemsViewSource.ItemCount
|
public int ItemCount
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
@ -127,47 +130,53 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
|
async void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
if (Device.IsInvokeRequired)
|
if (Device.IsInvokeRequired)
|
||||||
{
|
{
|
||||||
Device.BeginInvokeOnMainThread(() => CollectionChanged(args));
|
await Device.InvokeOnMainThreadAsync(async () => await CollectionChanged(args));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
CollectionChanged(args);
|
await CollectionChanged(args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CollectionChanged(NotifyCollectionChangedEventArgs args)
|
async Task CollectionChanged(NotifyCollectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
switch (args.Action)
|
switch (args.Action)
|
||||||
|
|
||||||
{
|
{
|
||||||
case NotifyCollectionChangedAction.Add:
|
case NotifyCollectionChangedAction.Add:
|
||||||
Add(args);
|
await Add(args);
|
||||||
break;
|
break;
|
||||||
case NotifyCollectionChangedAction.Remove:
|
case NotifyCollectionChangedAction.Remove:
|
||||||
Remove(args);
|
await Remove(args);
|
||||||
break;
|
break;
|
||||||
case NotifyCollectionChangedAction.Replace:
|
case NotifyCollectionChangedAction.Replace:
|
||||||
Replace(args);
|
await Replace(args);
|
||||||
break;
|
break;
|
||||||
case NotifyCollectionChangedAction.Move:
|
case NotifyCollectionChangedAction.Move:
|
||||||
Move(args);
|
Move(args);
|
||||||
break;
|
break;
|
||||||
case NotifyCollectionChangedAction.Reset:
|
case NotifyCollectionChangedAction.Reset:
|
||||||
Reload();
|
await Reload();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException();
|
throw new ArgumentOutOfRangeException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Reload()
|
async Task Reload()
|
||||||
{
|
{
|
||||||
ResetGroupTracking();
|
ResetGroupTracking();
|
||||||
|
|
||||||
|
await _batchUpdating.WaitAsync();
|
||||||
|
|
||||||
_collectionView.ReloadData();
|
_collectionView.ReloadData();
|
||||||
_collectionView.CollectionViewLayout.InvalidateLayout();
|
_collectionView.CollectionViewLayout.InvalidateLayout();
|
||||||
|
|
||||||
|
_batchUpdating.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
NSIndexSet CreateIndexSetFrom(int startIndex, int count)
|
NSIndexSet CreateIndexSetFrom(int startIndex, int count)
|
||||||
|
@ -182,8 +191,14 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
return !_collectionViewController.IsViewLoaded || _collectionViewController.View.Window == null;
|
return !_collectionViewController.IsViewLoaded || _collectionViewController.View.Window == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Add(NotifyCollectionChangedEventArgs args)
|
async Task Add(NotifyCollectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
|
if (ReloadRequired())
|
||||||
|
{
|
||||||
|
await Reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _groupSource.IndexOf(args.NewItems[0]);
|
var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _groupSource.IndexOf(args.NewItems[0]);
|
||||||
var count = args.NewItems.Count;
|
var count = args.NewItems.Count;
|
||||||
|
|
||||||
|
@ -191,53 +206,40 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
// is to reset all the group tracking to get it up-to-date
|
// is to reset all the group tracking to get it up-to-date
|
||||||
ResetGroupTracking();
|
ResetGroupTracking();
|
||||||
|
|
||||||
if (NotLoadedYet())
|
// Queue up the updates to the UICollectionView
|
||||||
{
|
BatchUpdate(() => _collectionView.InsertSections(CreateIndexSetFrom(startIndex, count)));
|
||||||
_collectionView.ReloadData();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_collectionView.NumberOfSections() == 0)
|
|
||||||
{
|
|
||||||
// Okay, we're going from completely empty to more than 0 items; there's an iOS bug which apparently
|
|
||||||
// will just crash if we call InsertItems here, so we have to do ReloadData.
|
|
||||||
_collectionView.ReloadData();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_collectionView.InsertSections(CreateIndexSetFrom(startIndex, count));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Remove(NotifyCollectionChangedEventArgs args)
|
async Task Remove(NotifyCollectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
var startIndex = args.OldStartingIndex;
|
var startIndex = args.OldStartingIndex;
|
||||||
|
|
||||||
if (startIndex < 0)
|
if (startIndex < 0)
|
||||||
{
|
{
|
||||||
// INCC implementation isn't giving us enough information to know where the removed items were in the
|
// INCC implementation isn't giving us enough information to know where the removed items were in the
|
||||||
// collection. So the best we can do is a ReloadData()
|
// collection. So the best we can do is a complete reload
|
||||||
Reload();
|
await Reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a start index, we can be more clever about removing the item(s) (and get the nifty animations)
|
if (ReloadRequired())
|
||||||
var count = args.OldItems.Count;
|
{
|
||||||
|
await Reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Removing a group will change the section index for all subsequent groups, so the easiest thing to do
|
// Removing a group will change the section index for all subsequent groups, so the easiest thing to do
|
||||||
// is to reset all the group tracking to get it up-to-date
|
// is to reset all the group tracking to get it up-to-date
|
||||||
ResetGroupTracking();
|
ResetGroupTracking();
|
||||||
|
|
||||||
if (NotLoadedYet())
|
// Since we have a start index, we can be more clever about removing the item(s) (and get the nifty animations)
|
||||||
{
|
var count = args.OldItems.Count;
|
||||||
_collectionView.ReloadData();
|
|
||||||
}
|
// Queue up the updates to the UICollectionView
|
||||||
else
|
BatchUpdate(() => _collectionView.DeleteSections(CreateIndexSetFrom(startIndex, count)));
|
||||||
{
|
|
||||||
_collectionView.DeleteSections(CreateIndexSetFrom(startIndex, count));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Replace(NotifyCollectionChangedEventArgs args)
|
async Task Replace(NotifyCollectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
var newCount = args.NewItems.Count;
|
var newCount = args.NewItems.Count;
|
||||||
|
|
||||||
|
@ -254,7 +256,7 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
|
|
||||||
// The original and replacement sets are of unequal size; this means that everything currently in view will
|
// The original and replacement sets are of unequal size; this means that everything currently in view will
|
||||||
// have to be updated. So we just have to use ReloadData and let the UICollectionView update everything
|
// have to be updated. So we just have to use ReloadData and let the UICollectionView update everything
|
||||||
Reload();
|
await Reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Move(NotifyCollectionChangedEventArgs args)
|
void Move(NotifyCollectionChangedEventArgs args)
|
||||||
|
@ -287,7 +289,7 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
var enumerator = enumerable.GetEnumerator();
|
var enumerator = enumerable.GetEnumerator();
|
||||||
while (enumerator.MoveNext())
|
while (enumerator.MoveNext())
|
||||||
{
|
{
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
@ -339,6 +341,36 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
bool ReloadRequired()
|
||||||
|
{
|
||||||
|
// If the UICollectionView has never been loaded, or doesn't yet have any sections, or has no actual
|
||||||
|
// cells (just supplementary views like Header/Footer), any insert/delete operations are gonna crash
|
||||||
|
// hard. We'll need to reload the data instead.
|
||||||
|
|
||||||
|
return NotLoadedYet()
|
||||||
|
|| _collectionView.NumberOfSections() == 0
|
||||||
|
|| _collectionView.VisibleCells.Length == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BatchUpdate(Action update)
|
||||||
|
{
|
||||||
|
_collectionView.PerformBatchUpdates(() =>
|
||||||
|
{
|
||||||
|
if (_batchUpdating.CurrentCount > 0)
|
||||||
|
{
|
||||||
|
_batchUpdating.Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
},
|
||||||
|
(_) =>
|
||||||
|
{
|
||||||
|
if (_batchUpdating.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
_batchUpdating.Release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
|
|
||||||
|
@ -14,6 +16,7 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
readonly int _section;
|
readonly int _section;
|
||||||
readonly IEnumerable _itemsSource;
|
readonly IEnumerable _itemsSource;
|
||||||
bool _disposed;
|
bool _disposed;
|
||||||
|
SemaphoreSlim _batchUpdating = new SemaphoreSlim(1, 1);
|
||||||
|
|
||||||
public ObservableItemsSource(IEnumerable itemSource, UICollectionViewController collectionViewController, int group = -1)
|
public ObservableItemsSource(IEnumerable itemSource, UICollectionViewController collectionViewController, int group = -1)
|
||||||
{
|
{
|
||||||
|
@ -94,36 +97,36 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
|
async void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
if (Device.IsInvokeRequired)
|
if (Device.IsInvokeRequired)
|
||||||
{
|
{
|
||||||
Device.BeginInvokeOnMainThread(() => CollectionChanged(args));
|
await Device.InvokeOnMainThreadAsync(async () => await CollectionChanged(args));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
CollectionChanged(args);
|
await CollectionChanged(args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CollectionChanged(NotifyCollectionChangedEventArgs args)
|
async Task CollectionChanged(NotifyCollectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
switch (args.Action)
|
switch (args.Action)
|
||||||
{
|
{
|
||||||
case NotifyCollectionChangedAction.Add:
|
case NotifyCollectionChangedAction.Add:
|
||||||
Add(args);
|
await Add(args);
|
||||||
break;
|
break;
|
||||||
case NotifyCollectionChangedAction.Remove:
|
case NotifyCollectionChangedAction.Remove:
|
||||||
Remove(args);
|
await Remove(args);
|
||||||
break;
|
break;
|
||||||
case NotifyCollectionChangedAction.Replace:
|
case NotifyCollectionChangedAction.Replace:
|
||||||
Replace(args);
|
await Replace(args);
|
||||||
break;
|
break;
|
||||||
case NotifyCollectionChangedAction.Move:
|
case NotifyCollectionChangedAction.Move:
|
||||||
Move(args);
|
Move(args);
|
||||||
break;
|
break;
|
||||||
case NotifyCollectionChangedAction.Reset:
|
case NotifyCollectionChangedAction.Reset:
|
||||||
Reload();
|
await Reload();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException();
|
throw new ArgumentOutOfRangeException();
|
||||||
|
@ -132,11 +135,15 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
CollectionItemsSourceChanged?.Invoke(this, args);
|
CollectionItemsSourceChanged?.Invoke(this, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Reload()
|
async Task Reload()
|
||||||
{
|
{
|
||||||
|
await _batchUpdating.WaitAsync();
|
||||||
|
|
||||||
_collectionView.ReloadData();
|
_collectionView.ReloadData();
|
||||||
_collectionView.CollectionViewLayout.InvalidateLayout();
|
_collectionView.CollectionViewLayout.InvalidateLayout();
|
||||||
Count = ItemsCount();
|
Count = ItemsCount();
|
||||||
|
|
||||||
|
_batchUpdating.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
NSIndexPath[] CreateIndexesFrom(int startIndex, int count)
|
NSIndexPath[] CreateIndexesFrom(int startIndex, int count)
|
||||||
|
@ -151,70 +158,49 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NotLoadedYet()
|
async Task Add(NotifyCollectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
// If the UICollectionView hasn't actually been loaded, then calling InsertItems or DeleteItems is
|
if (ReloadRequired())
|
||||||
// going to crash or get in an unusable state; instead, ReloadData should be used
|
|
||||||
return !_collectionViewController.IsViewLoaded || _collectionViewController.View.Window == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Add(NotifyCollectionChangedEventArgs args)
|
|
||||||
{
|
|
||||||
if (NotLoadedYet())
|
|
||||||
{
|
{
|
||||||
_collectionView.ReloadData();
|
await Reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf(args.NewItems[0]);
|
|
||||||
var count = args.NewItems.Count;
|
var count = args.NewItems.Count;
|
||||||
|
Count += count;
|
||||||
|
var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf(args.NewItems[0]);
|
||||||
|
|
||||||
if (!_grouped && _collectionView.NumberOfItemsInSection(_section) == 0)
|
// Queue up the updates to the UICollectionView
|
||||||
{
|
BatchUpdate(() => _collectionView.InsertItems(CreateIndexesFrom(startIndex, count)));
|
||||||
// Okay, we're going from completely empty to more than 0 items; there's an iOS bug which apparently
|
|
||||||
// will just crash if we call InsertItems here, so we have to do ReloadData.
|
|
||||||
_collectionView.ReloadData();
|
|
||||||
Count += count;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_collectionView.PerformBatchUpdates(() =>
|
|
||||||
{
|
|
||||||
var indexes = CreateIndexesFrom(startIndex, count);
|
|
||||||
_collectionView.InsertItems(indexes);
|
|
||||||
Count += count;
|
|
||||||
}, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Remove(NotifyCollectionChangedEventArgs args)
|
async Task Remove(NotifyCollectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
if (NotLoadedYet())
|
|
||||||
{
|
|
||||||
_collectionView.ReloadData();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var startIndex = args.OldStartingIndex;
|
var startIndex = args.OldStartingIndex;
|
||||||
|
|
||||||
if (startIndex < 0)
|
if (startIndex < 0)
|
||||||
{
|
{
|
||||||
// INCC implementation isn't giving us enough information to know where the removed items were in the
|
// INCC implementation isn't giving us enough information to know where the removed items were in the
|
||||||
// collection. So the best we can do is a ReloadData()
|
// collection. So the best we can do is a ReloadData()
|
||||||
Reload();
|
await Reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReloadRequired())
|
||||||
|
{
|
||||||
|
await Reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a start index, we can be more clever about removing the item(s) (and get the nifty animations)
|
// If we have a start index, we can be more clever about removing the item(s) (and get the nifty animations)
|
||||||
var count = args.OldItems.Count;
|
var count = args.OldItems.Count;
|
||||||
|
Count -= count;
|
||||||
_collectionView.PerformBatchUpdates(() =>
|
|
||||||
{
|
// Queue up the updates to the UICollectionView
|
||||||
_collectionView.DeleteItems(CreateIndexesFrom(startIndex, count));
|
BatchUpdate(() => _collectionView.DeleteItems(CreateIndexesFrom(startIndex, count)));
|
||||||
Count -= count;
|
|
||||||
}, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Replace(NotifyCollectionChangedEventArgs args)
|
async Task Replace(NotifyCollectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
var newCount = args.NewItems.Count;
|
var newCount = args.NewItems.Count;
|
||||||
|
|
||||||
|
@ -229,7 +215,7 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
|
|
||||||
// The original and replacement sets are of unequal size; this means that everything currently in view will
|
// The original and replacement sets are of unequal size; this means that everything currently in view will
|
||||||
// have to be updated. So we just have to use ReloadData and let the UICollectionView update everything
|
// have to be updated. So we just have to use ReloadData and let the UICollectionView update everything
|
||||||
Reload();
|
await Reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Move(NotifyCollectionChangedEventArgs args)
|
void Move(NotifyCollectionChangedEventArgs args)
|
||||||
|
@ -293,5 +279,52 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NotLoadedYet()
|
||||||
|
{
|
||||||
|
// If the UICollectionView hasn't actually been loaded, then calling InsertItems or DeleteItems is
|
||||||
|
// going to crash or get in an unusable state; instead, ReloadData should be used
|
||||||
|
return !_collectionViewController.IsViewLoaded || _collectionViewController.View.Window == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReloadRequired()
|
||||||
|
{
|
||||||
|
if (NotLoadedYet())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UICollectionView doesn't like when we insert items into a completely empty un-grouped CV,
|
||||||
|
// and it doesn't like when we insert items into a grouped CV with no actual cells (just empty groups)
|
||||||
|
// In those circumstances, we just need to ask it to reload the data so it can get its internal
|
||||||
|
// accounting in order
|
||||||
|
|
||||||
|
if (!_grouped && _collectionView.NumberOfItemsInSection(_section) == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _collectionView.VisibleCells.Length == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BatchUpdate(Action update)
|
||||||
|
{
|
||||||
|
_collectionView.PerformBatchUpdates(() =>
|
||||||
|
{
|
||||||
|
if (_batchUpdating.CurrentCount > 0)
|
||||||
|
{
|
||||||
|
_batchUpdating.Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
},
|
||||||
|
(_) =>
|
||||||
|
{
|
||||||
|
if (_batchUpdating.CurrentCount > 0)
|
||||||
|
{
|
||||||
|
_batchUpdating.Release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче