summaryrefslogtreecommitdiff
path: root/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared
diff options
context:
space:
mode:
authorkingces95 <kingces95@users.noreply.github.com>2017-09-29 12:54:05 -0400
committerKangho Hur <kangho.hur@samsung.com>2017-10-23 13:33:49 +0900
commit3ce94fcf6515f68d29786bef9aad4e0e729d088f (patch)
treebf4be41dd2598ad717efde60c08fd8a84557ed18 /Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared
parente6902471745cd4d8d847354ea01e7d2030c2ef8e (diff)
downloadxamarin-forms-3ce94fcf6515f68d29786bef9aad4e0e729d088f.tar.gz
xamarin-forms-3ce94fcf6515f68d29786bef9aad4e0e729d088f.tar.bz2
xamarin-forms-3ce94fcf6515f68d29786bef9aad4e0e729d088f.zip
Prototypical Cell Cache for IsEnabled testing; UITest (#1153)
Diffstat (limited to 'Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared')
-rw-r--r--Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla52487.cs1036
-rw-r--r--Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems1
2 files changed, 1037 insertions, 0 deletions
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla52487.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla52487.cs
new file mode 100644
index 00000000..4f1d0971
--- /dev/null
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla52487.cs
@@ -0,0 +1,1036 @@
+using Xamarin.Forms.CustomAttributes;
+using Xamarin.Forms.Internals;
+using System.Linq;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Threading;
+
+#if UITEST
+using Xamarin.UITest;
+using NUnit.Framework;
+#endif
+
+namespace Xamarin.Forms.Controls.Issues
+{
+ [Preserve(AllMembers = true)]
+ [Issue(
+ IssueTracker.Bugzilla,
+ 502487,
+ "ListView with Recycle + HasUnevenRows generates lots (and lots!) of content view",
+ // https://bugzilla.xamarin.com/show_bug.cgi?id=52487
+ PlatformAffected.iOS
+ )]
+ public class Bugzilla52487 : TestContentPage
+ {
+#if __IOS__
+ const int MaxAskDelta = 0;
+ const int MaxViewDelta = 5;
+ const int MaxAttachDelta = 5;
+#elif __ANDROID__
+ const int MaxAskDelta = 0;
+ const int MaxViewDelta = 1;
+ const int MaxAttachDelta = 1;
+#else
+ const int MaxAskDelta = 0;
+ const int MaxViewDelta = int.MaxValue;
+ const int MaxAttachDelta = int.MaxValue;
+#endif
+
+ const int CountFontSize = 12;
+ const int CellFontSize = 12;
+ const int MinScrollDelta = 2;
+ const int ItemsCount = 1000;
+ const int GroupCount = 100;
+ const int DefaultItemHeight = 300 / 4;
+ const int MinimumItemHeight = 40;
+
+ // dis-enable item type when % item id is zero
+ const int DisableModulous = 11;
+
+ // generate alternate item type when % item id is zero
+ const int ItemTypeModulous = 7;
+
+ // render half height when % item id is zero (RecycleElement or RecycleElementAndDataTemplate)
+ const int HalfHeightModulous = 5;
+
+ // select alternate cell type when % item id is zero (RecycleElement)
+ const int DataTemplateModulous = 3;
+
+ static Tuple<int, int, int> Mix = new Tuple<int, int, int>(255, 255, 100);
+ static Tuple<int, int, int> AltMix = new Tuple<int, int, int>(100, 100, 255);
+ static IEnumerable<Color> ColorGenerator(Tuple<int, int, int> mix)
+ {
+ double colorDelta = 0;
+ while (true)
+ {
+ colorDelta += 2 * Math.PI / 100;
+ var r = (Math.Sin(colorDelta) + 1) / 2 * 255;
+ var g = (Math.Sin(colorDelta * 2) + 1) / 2 * 255;
+ var b = (Math.Sin(colorDelta * 3) + 1) / 2 * 255;
+
+ if (mix != null)
+ {
+ r = (r + mix.Item1) / 2;
+ g = (g + mix.Item2) / 2;
+ b = (b + mix.Item3) / 2;
+ }
+
+ yield return Color.FromRgb((int)r, (int)g, (int)b);
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ class LazyReadOnlyList<V> : IReadOnlyList<V>
+ where V : class
+ {
+ int _count;
+ object _context;
+ List<WeakReference<V>> _items;
+ Action<int> _onAsk;
+ Func<LazyReadOnlyList<V>, int, object, V> _activate;
+
+ internal LazyReadOnlyList(
+ int count,
+ object context,
+ Action<int> onAsk,
+ Func<LazyReadOnlyList<V>, int, object, V> activate)
+ {
+ _count = count;
+ _context = context;
+ _onAsk = onAsk;
+ _activate = activate;
+ _items = new List<WeakReference<V>>(
+ Enumerable.Range(0, count)
+ .Select(o => new WeakReference<V>(null))
+ );
+ }
+
+ protected object Context
+ {
+ get { return _context; }
+ set { _context = value; }
+ }
+ protected IEnumerable<WeakReference<V>> WeakItems =>
+ _items;
+
+ public V this[int index]
+ {
+ get
+ {
+ _onAsk(index);
+
+ var weakItem = _items[index];
+
+ V item;
+ if (!weakItem.TryGetTarget(out item))
+ {
+ _items[index] =
+ new WeakReference<V>(
+ item = _activate(this, index, _context));
+ }
+
+ return item;
+ }
+ }
+
+ public int Count
+ => _count;
+
+ public IEnumerator<V> GetEnumerator()
+ {
+ for (var i = 0; i < Count; i++)
+ yield return this[i];
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => GetEnumerator();
+ }
+
+ [Preserve(AllMembers = true)]
+ public abstract partial class ListViewSpy<T> : ListViewSpy
+ {
+ [Preserve(AllMembers = true)]
+ abstract class Selector : DataTemplateSelector
+ {
+ [Preserve(AllMembers = true)]
+ internal class SelectByData : Selector
+ {
+ public SelectByData() : base(
+ typeof(ItemViewCell.Selected.ByDataNormal),
+ typeof(ItemViewCell.Selected.ByDataAlternate)
+ )
+ { }
+
+ protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
+ {
+ // RecycleElement previously placed no restraint on the type of view
+ // the resulting DataTemplate could return. So the resulting DataTemplate
+ // could randomly pick a view type to render items even between appearances
+ // on screen.
+
+ // After this fix, the DataTempate will be required to return the same
+ // type of view although the type need not be a function of only the item type.
+ // So the type of view a DataTemplates chooses to return can be a function of
+ // the item _data_ and not only an items type.
+
+ __counter.OnSelectTemplate++;
+
+ // item could be either Item.Full type or Item.Half type...
+ if (!(item is Item.Normal) && !(item is Item.Alternate))
+ throw new ArgumentException();
+
+ // ... but selector chooses DataTemplate strictly via item _data_.
+ return ((Item)item).Id % DataTemplateModulous == 0 ?
+ _dataTemplateAlt : _dataTemplate;
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ internal class SelectByType : Selector
+ {
+ public SelectByType() : base(
+ typeof(ItemViewCell.Selected.ByTypeNormal),
+ typeof(ItemViewCell.Selected.ByTypeAlternate)
+ )
+ { }
+
+ protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
+ {
+ // RecycleElementAndDataTemplate requires that
+ // DataTempalte be a function of the item _type_
+
+ __counter.OnSelectTemplate++;
+
+ if (item is Item.Normal)
+ return _dataTemplate;
+
+ if (item is Item.Alternate)
+ return _dataTemplateAlt;
+
+ throw new ArgumentException();
+ }
+ }
+
+ DataTemplate _dataTemplate;
+ DataTemplate _dataTemplateAlt;
+
+ public Selector(Type nomral, Type alternate)
+ {
+ // RecycleElementAndDataTemplate requires
+ // that the DataTemplate use the .ctor that takes a type
+ _dataTemplate = new DataTemplate(nomral);
+ _dataTemplateAlt = new DataTemplate(alternate);
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ abstract class ItemViewCell : ViewCell
+ {
+ [Preserve(AllMembers = true)]
+ internal abstract class Selected : ItemViewCell
+ {
+ [Preserve(AllMembers = true)]
+ internal class ByTypeNormal : ByType
+ {
+ static Color NextColor() { Colors.MoveNext(); return Colors.Current; }
+ static readonly IEnumerator<Color> Colors = ColorGenerator(Mix).GetEnumerator();
+
+ public ByTypeNormal() : base(NextColor()) { }
+ }
+ [Preserve(AllMembers = true)]
+ internal class ByTypeAlternate : ByType
+ {
+ static Color NextColor() { Colors.MoveNext(); return Colors.Current; }
+ static readonly IEnumerator<Color> Colors = ColorGenerator(AltMix).GetEnumerator();
+
+ public ByTypeAlternate() : base(NextColor()) { }
+
+ protected override bool IsAlternate => true;
+ }
+ [Preserve(AllMembers = true)]
+ internal abstract class ByType : Selected
+ {
+ internal ByType(Color color)
+ : base(color) { }
+
+ protected override void OnBindingContextChanged()
+ {
+ base.OnBindingContextChanged();
+
+ if (BindingContext == null || !(BindingContext is ItemViewCell))
+ return;
+
+ // check that template is a function of the item type
+ var itemType = BindingContext.GetType();
+ var itemIsNormalType = itemType == typeof(Item.Normal);
+ var expectedTemplateType = itemIsNormalType ? typeof(ByTypeNormal) : typeof(ByTypeAlternate);
+
+ var templateType = GetType();
+ if (templateType != expectedTemplateType)
+ throw new ArgumentException(
+ $"BindingContext.GetType() = {itemType.Name}, " +
+ $"TemplateType {templateType.Name}!={expectedTemplateType.Name}");
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ internal class ByDataNormal : ByData
+ {
+ static Color NextColor() { Colors.MoveNext(); return Colors.Current; }
+ static readonly IEnumerator<Color> Colors = ColorGenerator(Mix).GetEnumerator();
+
+ public ByDataNormal() : base(NextColor()) { }
+ }
+ [Preserve(AllMembers = true)]
+ internal class ByDataAlternate : ByData
+ {
+ static Color NextColor() { Colors.MoveNext(); return Colors.Current; }
+ static readonly IEnumerator<Color> Colors = ColorGenerator(AltMix).GetEnumerator();
+
+ public ByDataAlternate() : base(NextColor()) { }
+
+ protected override bool IsAlternate => true;
+ }
+ [Preserve(AllMembers = true)]
+ internal abstract class ByData : Selected
+ {
+ internal ByData(Color color)
+ : base(color) { }
+
+ protected override void OnBindingContextChanged()
+ {
+ base.OnBindingContextChanged();
+
+ if (BindingContext == null)
+ return;
+
+ // check that template is a function of the item data
+ var isRemainderZero = BindingContext.Id % DataTemplateModulous == 0;
+ var expectedItemType = isRemainderZero ? typeof(Item.Alternate) : typeof(Item.Normal);
+ var expectedTemplateType = isRemainderZero ? typeof(ByDataAlternate) : typeof(ByDataNormal);
+
+ var templateType = GetType();
+ if (templateType != expectedTemplateType)
+ throw new ArgumentException(
+ $"Item.Id = {BindingContext?.Id}, " +
+ $"TemplateType {templateType.Name}!={expectedTemplateType.Name}");
+ }
+ }
+
+ internal Selected(Color color)
+ : base(color) { }
+ }
+
+ [Preserve(AllMembers = true)]
+ internal class Constant : ItemViewCell
+ {
+ static Color NextColor() { Colors.MoveNext(); return Colors.Current; }
+ static readonly IEnumerator<Color> Colors = ColorGenerator(Mix).GetEnumerator();
+
+ public Constant() : base(NextColor()) { }
+ }
+
+ readonly int _id;
+ readonly Label _label;
+
+ ItemViewCell(Color color)
+ {
+ _id = __counter.CellAlloc++;
+
+ View = _label = new Label
+ {
+ BackgroundColor = color,
+ VerticalTextAlignment = TextAlignment.Center,
+ HorizontalTextAlignment = TextAlignment.Center,
+ FontSize = CellFontSize
+ };
+
+ _label.SetBinding(HeightRequestProperty, nameof(Item.Value));
+ }
+
+ Item BindingContext
+ => (Item)base.BindingContext;
+ int ItemId
+ => BindingContext.Id;
+ int? ItemGroupId
+ => BindingContext.GroupId;
+ bool IsAlternateItem
+ => BindingContext is Item.Alternate;
+
+ protected virtual bool IsAlternate => false;
+
+ protected override void OnBindingContextChanged()
+ {
+ base.OnBindingContextChanged();
+
+ __counter.CellBind++;
+
+ if (BindingContext == null)
+ return;
+
+ // double check that item generator returned correct type of item
+ var isRemainderZero = BindingContext.Id % ItemTypeModulous == 0;
+ var expectedItemType = isRemainderZero ? typeof(Item.Alternate) : typeof(Item.Normal);
+ var itemType = BindingContext.GetType();
+ if (itemType != expectedItemType)
+ throw new ArgumentException(
+ $"Item.Id = {BindingContext?.Id}, ItemType {GetType().Name}!={expectedItemType.Name}");
+
+ }
+
+ protected override void OnAppearing()
+ {
+ IsEnabled = ItemId % DisableModulous == 0;
+
+ _label.Text = ToString();
+ _label.FontAttributes = IsEnabled ? FontAttributes.Italic : FontAttributes.None;
+
+ __counter.AttachCell(_id);
+ }
+
+ public new int Id
+ => _id;
+
+ // cell type is (1) constant or a function of the the Item (2) data or (3) type
+ public override string ToString()
+ => $"{ItemId}" +
+ (ItemGroupId == null ? "" : "/" + ItemGroupId) +
+ $"{(IsAlternateItem ? "*" : "")} ->" +
+ $" {_id}{(IsAlternate ? "*" : "")}";
+
+ ~ItemViewCell()
+ {
+ int id;
+ __counter.CellFree++;
+ __counter.DetachCell(_id);
+ // update would be off UI thread
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ abstract class Item : INotifyPropertyChanged
+ {
+ [Preserve(AllMembers = true)]
+ internal class Normal : Item
+ {
+ internal Normal(int id, int? groupId, int height)
+ : base(id, groupId, height) { }
+ };
+
+ [Preserve(AllMembers = true)]
+ internal class Alternate : Item
+ {
+ internal Alternate(int id, int? groupId, int height)
+ : base(id, groupId, height) { }
+ };
+
+ internal static Item Create(LazyItemList list, int index, int height)
+ {
+
+ var id = list.ItemIdOffset + index;
+
+ if (id % ItemTypeModulous == 0)
+ return new Alternate(id, list.Id, height);
+
+ return new Normal(id, list.Id, height);
+ }
+
+ int _allocId;
+
+ int _id;
+ int _index;
+ int? _groupId;
+ int _height;
+
+ private Item(int id, int? groupId, int height)
+ {
+ _allocId = Interlocked.Increment(ref __counter.ItemAlloc);
+
+ if (id % HalfHeightModulous == 0)
+ height = height / 2;
+
+ _groupId = groupId;
+ _height = height;
+ _id = id;
+ }
+
+ public int Id
+ => _id;
+ public int? GroupId
+ => _groupId;
+ public int Value
+ {
+ get { return _height; }
+ set
+ {
+ _height = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+
+ public override string ToString()
+ => _groupId == null ?
+ $"{Id}, value={Value}, _alloc={_allocId}" :
+ $"{Id}, group={GroupId}, value={Value}, _alloc={_allocId}";
+
+ ~Item()
+ {
+ Interlocked.Increment(ref __counter.ItemFree);
+ }
+ }
+
+ interface IItemList : IEnumerable, IDisposable
+ {
+ void UpdateHeights(double multipule);
+ int Count { get; }
+ void Dispose();
+ Item this[int index] { get; }
+ }
+
+ [Preserve(AllMembers = true)]
+ class LazyItemList : LazyReadOnlyList<Item>, IItemList
+ {
+ int _itemIdOffset;
+ int? _id;
+ int _count;
+
+ internal LazyItemList(int count)
+ : this(null /* grouping disabled */, 0, count) { }
+ internal LazyItemList(int? id, int itemIdOffset, int count)
+ : base(count, DefaultItemHeight,
+ onAsk: o => __counter.ViewModelAsk.Add(o),
+ activate: (self, subIndex, height) =>
+ Item.Create(
+ list: (LazyItemList)self,
+ index: subIndex,
+ height: (int)height
+ )
+ )
+ {
+ _id = id;
+ _itemIdOffset = itemIdOffset;
+ _count = count;
+ }
+
+ protected int Context
+ {
+ get { return (int)base.Context; }
+ set { base.Context = value; }
+ }
+
+ public void UpdateHeights(double multipule)
+ {
+ if (multipule < 1 && Context < MinimumItemHeight)
+ return;
+
+ Context = (int)(Context * multipule);
+
+ foreach (var weakItem in WeakItems)
+ {
+ Item item;
+ if (!(weakItem.TryGetTarget(out item)))
+ continue;
+
+ item.Value = Context;
+ }
+ }
+ public int ItemIdOffset
+ => _itemIdOffset;
+ public int? Id
+ => _id;
+
+ public void Dispose()
+ {
+ foreach (var weakItem in WeakItems)
+ weakItem.SetTarget(null);
+ }
+
+ public override string ToString()
+ => $"{_id}";
+ }
+
+ [Preserve(AllMembers = true)]
+ class LazyGroupedItemList : LazyReadOnlyList<LazyItemList>, IItemList
+ {
+ int _itemsPerGroup;
+
+ internal LazyGroupedItemList(int numberOfGroups, int count)
+ : base(numberOfGroups,
+ context: null,
+ onAsk: o => __counter.ViewModelAsk.Add(o),
+ activate: (self, groupId, context) =>
+ new LazyItemList(
+ id: groupId,
+ itemIdOffset: groupId * (count / numberOfGroups),
+ count: count / numberOfGroups
+ )
+ )
+ { _itemsPerGroup = count / numberOfGroups; }
+
+ Item IItemList.this[int index]
+ {
+ get
+ {
+ var group = index / _itemsPerGroup;
+ index = index % _itemsPerGroup;
+ return this[group][index];
+ }
+ }
+
+ public void UpdateHeights(double multipule)
+ {
+ foreach (var weakItem in WeakItems)
+ {
+ LazyItemList group;
+ if (!(weakItem.TryGetTarget(out group)))
+ continue;
+
+ group.UpdateHeights(multipule);
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (var weakItem in WeakItems)
+ {
+ LazyItemList group;
+ if (!(weakItem.TryGetTarget(out group)))
+ continue;
+
+ group.Dispose();
+ }
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ class SnapShot
+ {
+ Counter _counter;
+
+ internal int Views;
+ internal int Attached;
+ internal int Binds;
+ internal int Items;
+ internal int Asks;
+
+ public SnapShot(Counter counter)
+ {
+ _counter = counter;
+
+ Views = _counter.Views;
+ Attached = _counter.AttachedCells;
+ Binds = _counter.CellBind;
+ Items = _counter.Items;
+ Asks = _counter.OnSelectTemplate;
+ }
+
+ void Subtract()
+ {
+ Views = _counter.Views - Views;
+ Attached = _counter.AttachedCells - Attached;
+ Binds = _counter.CellBind - Binds;
+ Items = _counter.Items - Items;
+ Asks = _counter.OnSelectTemplate - Asks;
+ }
+
+ public void Update()
+ => Subtract();
+ }
+
+ [Preserve(AllMembers = true)]
+ class Counter
+ {
+ internal int CellAlloc;
+ internal int CellFree;
+ internal int CellBind;
+
+ internal int ItemAlloc;
+ internal int ItemFree;
+ internal int OnSelectTemplate;
+ internal HashSet<int> ViewModelAsk
+ = new HashSet<int>();
+
+ HashSet<int> CellAttached
+ = new HashSet<int>();
+
+ internal int AttachedCells
+ {
+ get
+ {
+ lock (this)
+ return CellAttached.Count;
+ }
+ }
+ internal void AttachCell(int id)
+ {
+ lock (this)
+ CellAttached.Add(id);
+ }
+ internal void DetachCell(int id)
+ {
+ lock (this)
+ CellAttached.Remove(id);
+ }
+
+ internal int Views => CellAlloc - CellFree;
+ internal int Items => ItemAlloc - ItemFree;
+ }
+
+ [Preserve(AllMembers = true)]
+ class CounterView : StackLayout
+ {
+ static Label CreateLabel()
+ => new Label() { FontSize = CountFontSize };
+
+ internal void Update()
+ {
+ ViewCountLabel.Text
+ = $"View={__counter.Views}";
+ AttachedCountLabel.Text
+ = $"Atch={__counter.AttachedCells}";
+ BindCountLabel.Text
+ = $"Bind={__counter.CellBind}";
+ ItemCountLabel.Text
+ = $"Item={__counter.Items}";
+ AskLabel.Text
+ = $"Ask={__counter.OnSelectTemplate}";
+ }
+
+ Label AttachedCountLabel = CreateLabel();
+ Label ViewCountLabel = CreateLabel();
+ Label BindCountLabel = CreateLabel();
+ Label ItemCountLabel = CreateLabel();
+ Label AskLabel = CreateLabel();
+
+ internal CounterView()
+ {
+ Children.Add(ViewCountLabel);
+ Children.Add(AttachedCountLabel);
+ Children.Add(BindCountLabel);
+ Children.Add(ItemCountLabel);
+ Children.Add(AskLabel);
+ Update();
+ }
+ }
+
+ // Cell is activated via DataTemplate using default ctor which
+ // makes it difficult to pass the counter to the cell. So we make
+ // it static to give cell access and create a different generic
+ // instantiation for each type of ListView to get different counters
+ static Counter __counter = new Counter();
+
+ ListView _listView;
+ int _appeared;
+ int _disappeared;
+ IItemList _itemsList;
+
+ public ListViewSpy()
+ {
+ __listViewSpyAlloc++;
+
+ var name = GetType().Name;
+
+ var hasUnevenRows = name.Contains("UnevenRows");
+
+ var isGrouped = name.Contains("Grouped");
+
+ _itemsList = isGrouped ? (IItemList)
+ new LazyGroupedItemList(GroupCount, ItemsCount) :
+ new LazyItemList(ItemsCount);
+
+ var strategy =
+ name.Contains("RecycleElementAndDataTemplate") ? ListViewCachingStrategy.RecycleElementAndDataTemplate :
+ name.Contains("RecycleElement") ? ListViewCachingStrategy.RecycleElement :
+ ListViewCachingStrategy.RetainElement;
+
+ var dataTemplate =
+ strategy == ListViewCachingStrategy.RecycleElement ? new Selector.SelectByData() :
+ strategy == ListViewCachingStrategy.RecycleElementAndDataTemplate ? new Selector.SelectByType() :
+ new DataTemplate(typeof(ItemViewCell.Constant));
+
+ _listView = new ListView(strategy)
+ {
+ HasUnevenRows = hasUnevenRows,
+ // see https://github.com/xamarin/Xamarin.Forms/pull/994/files
+ //RowHeight = 50,
+ ItemsSource = _itemsList,
+ ItemTemplate = dataTemplate,
+
+ IsGroupingEnabled = isGrouped,
+ GroupDisplayBinding = null,
+ GroupShortNameBinding = null,
+ GroupHeaderTemplate = null
+ };
+ Children.Add(_listView);
+
+ _listView.AutomationId = $"__ListView";
+
+ var counter = new CounterView();
+
+ _listView.ItemAppearing += (o, e) =>
+ {
+ _appeared = (e.Item as Item)?.Id ?? -1;
+ counter.Update();
+ };
+
+ _listView.ItemDisappearing += (o, e) =>
+ {
+ _disappeared = (e.Item as Item)?.Id ?? -1;
+ counter.Update();
+ };
+
+ Children.Add(counter);
+ }
+
+ void Scroll(int target)
+ {
+ var snapShot = new SnapShot(__counter);
+ _listView.ScrollTo(_itemsList[target], ScrollToPosition.MakeVisible, animated: true);
+ snapShot.Update();
+
+ // TEST
+ if (!_listView.IsGroupingEnabled &&
+ _listView.CachingStrategy == ListViewCachingStrategy.RecycleElementAndDataTemplate)
+ {
+ if (snapShot.Attached > MaxAttachDelta)
+ throw new Exception($"Attached Delta: {snapShot.Attached}");
+ if (snapShot.Views > MaxViewDelta)
+ throw new Exception($"Views Delta: {snapShot.Views}");
+ if (snapShot.Asks > MaxAskDelta)
+ throw new Exception($"Asks Delta: {snapShot.Asks}");
+ }
+ }
+
+ internal override void Down()
+ {
+ var target = Math.Max(_appeared, _disappeared);
+ target += Math.Abs(_appeared - _disappeared) + MinScrollDelta;
+ if (target >= _itemsList.Count)
+ target = _itemsList.Count - 1;
+
+ Scroll(target);
+ }
+ internal override void Up()
+ {
+ var target = Math.Min(_appeared, _disappeared);
+ target -= Math.Abs(_appeared - _disappeared) + MinScrollDelta;
+ if (target < 0)
+ target = 0;
+
+ Scroll(target);
+ }
+ internal override void UpdateHeights(double multipule)
+ => _itemsList.UpdateHeights(multipule);
+
+ internal override void Dispose()
+ => _itemsList.Dispose();
+
+ ~ListViewSpy()
+ {
+ __listViewSpyFree++;
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ public abstract partial class ListViewSpy : StackLayout
+ {
+ [Preserve(AllMembers = true)]
+ internal sealed class Retain :
+ ListViewSpy<Retain>
+ { }
+
+ [Preserve(AllMembers = true)]
+ internal sealed class UnevenRowsRecycleElement :
+ ListViewSpy<UnevenRowsRecycleElement>
+ { }
+
+ [Preserve(AllMembers = true)]
+ internal sealed class UnevenRowsRecycleElementAndDataTemplate :
+ ListViewSpy<UnevenRowsRecycleElementAndDataTemplate>
+ { }
+
+ [Preserve(AllMembers = true)]
+ internal sealed class EvenRowsRecycleElement :
+ ListViewSpy<EvenRowsRecycleElement>
+ { }
+
+ [Preserve(AllMembers = true)]
+ internal sealed class EvenRowsRecycleElementAndDataTemplate :
+ ListViewSpy<EvenRowsRecycleElementAndDataTemplate>
+ { }
+
+ [Preserve(AllMembers = true)]
+ internal sealed class GroupedRetain :
+ ListViewSpy<GroupedRetain>
+ { }
+
+ [Preserve(AllMembers = true)]
+ internal sealed class GroupedUnevenRowsRecycleElement :
+ ListViewSpy<GroupedUnevenRowsRecycleElement>
+ { }
+
+ [Preserve(AllMembers = true)]
+ internal sealed class GroupedUnevenRowsRecycleElementAndDataTemplate :
+ ListViewSpy<GroupedUnevenRowsRecycleElementAndDataTemplate>
+ { }
+
+ [Preserve(AllMembers = true)]
+ internal sealed class GroupedEvenRowsRecycleElement :
+ ListViewSpy<GroupedEvenRowsRecycleElement>
+ { }
+
+ [Preserve(AllMembers = true)]
+ internal sealed class GroupedEvenRowsRecycleElementAndDataTemplate :
+ ListViewSpy<GroupedEvenRowsRecycleElementAndDataTemplate>
+ { }
+
+ internal abstract void Down();
+ internal abstract void Up();
+ internal abstract void UpdateHeights(double difference);
+ internal abstract void Dispose();
+ }
+
+ static int __listViewSpyAlloc;
+ static int __listViewSpyFree;
+ ListViewSpy[] __listViews;
+
+ IEnumerable<ListViewSpy> ListViews()
+ => __listViews ?? Enumerable.Empty<ListViewSpy>();
+
+ void Update()
+ => Title = $"ListViews={__listViewSpyAlloc - __listViewSpyFree}";
+
+ Grid RecycleListViews(bool group = false)
+ {
+ // reclaim
+ foreach (var o in ListViews())
+ o.Dispose();
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+
+ __listViews = group ?
+ new ListViewSpy[]
+ {
+ new ListViewSpy.GroupedRetain(),
+ new ListViewSpy.GroupedEvenRowsRecycleElement(),
+ new ListViewSpy.GroupedEvenRowsRecycleElementAndDataTemplate(),
+ new ListViewSpy.GroupedUnevenRowsRecycleElement(),
+ new ListViewSpy.GroupedUnevenRowsRecycleElementAndDataTemplate(),
+ } :
+ new ListViewSpy[]
+ {
+ new ListViewSpy.Retain(),
+ new ListViewSpy.EvenRowsRecycleElement(),
+ new ListViewSpy.EvenRowsRecycleElementAndDataTemplate(),
+ new ListViewSpy.UnevenRowsRecycleElement(),
+ new ListViewSpy.UnevenRowsRecycleElementAndDataTemplate(),
+ };
+
+ var grid = new Grid();
+ foreach (var o in __listViews)
+ grid.Children.AddHorizontal(o);
+
+ Update();
+
+ return grid;
+ }
+
+ private class ButtonGird : Grid
+ {
+ Bugzilla52487 _test;
+
+ internal ButtonGird(Bugzilla52487 test)
+ {
+ _test = test;
+ }
+
+ private void ForEachListView(Action<ListViewSpy> onClick)
+ => _test.ListViews().ForEach(o => onClick(o));
+
+ public void Add(View view)
+ => Children.AddHorizontal(view);
+
+ public Switch AddSwitch(Action<bool> onToggle)
+ {
+ var toggle = new Switch();
+ toggle.Toggled += (o, s) => onToggle(s.Value);
+ Add(toggle);
+ return toggle;
+ }
+
+ public Button AddButton(string text, Action onClick)
+ {
+ var button = new Button() { Text = text };
+ button.Clicked += (o, s) => onClick();
+ Add(button);
+ return button;
+ }
+
+ public Button AddButton(string text, Action<ListViewSpy> onClick)
+ => AddButton(text, () => ForEachListView(onClick));
+
+ public Entry AddEntry()
+ {
+ var entry = new Entry();
+ Add(entry);
+ return entry;
+ }
+ }
+
+ protected override void Init()
+ {
+ var listViewGrid = new ContentView();
+ listViewGrid.Content = RecycleListViews(false);
+
+ var buttonsGrid = new ButtonGird(this);
+ var more = buttonsGrid.AddButton("More", x => x.UpdateHeights(2));
+ var less = buttonsGrid.AddButton($"Less", x => x.UpdateHeights(.5));
+ var up = buttonsGrid.AddButton("Up", x => x.Up());
+ var down = buttonsGrid.AddButton("Down", x => x.Down());
+ var group = buttonsGrid.AddSwitch(
+ isGrouped => listViewGrid.Content = RecycleListViews(isGrouped));
+
+ Content = new StackLayout
+ {
+ Children = {
+ listViewGrid,
+ buttonsGrid,
+ }
+ };
+
+ Update();
+ }
+ public static class Id
+ {
+ public static string Down = nameof(Down);
+ }
+#if UITEST
+ [Test]
+ public void Bugzilla52487Test()
+ {
+ try
+ {
+ RunningApp.WaitForElement(Id.Down);
+ RunningApp.Screenshot("Down");
+
+ RunningApp.Tap(Id.Down);
+ }
+
+ finally
+ {
+ RunningApp.Screenshot("Finally");
+ }
+ }
+#endif
+ }
+} \ No newline at end of file
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
index 30aa67b4..a692c0d6 100644
--- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
@@ -212,6 +212,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Bugzilla54649.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Bugzilla56609.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Bugzilla55912.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Bugzilla52487.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Bugzilla57317.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Bugzilla57114.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Bugzilla57515.cs" />