Skip to content

Commit

Permalink
CollectionView - Recycle DataTemplates when using template selector (#…
Browse files Browse the repository at this point in the history
…12011)

* [Android] Use different view types per item template

Previously we were returning a single view type id for all 'items' in collectionview, which meant if you were using a template selector, and had a variety of returned different DataTemplates, each would share the same recycled item.  When a recycled item is reused for a different template, it causes the view for that template to be recreated to be correct for the template selected.

This change returns a unique item id per DataTemplate.Type, so that recycled items should always have the correct DataTemplate.Type and not need to be recreated with a different view hierarchy.

* [iOS] Distinct Cell reuse id's for different DataTemplates

Like on Android, we were using a single cell reuse identifier for iOS "items" regardless of if they had different DataTemplates.  If you were using a data template selector, and returned different data templates, whenever a recycled cell's data template didn't match the data template it needed to display for the new/recycled context, the whole view tree of that template would be recreated.

This change ensures we have a unique cell reuse id for every different DataTemplate.Type that might be returned by the template selector so that when a cell is recycled it should always be reused for the same Data Template it was created for, and so the view hierarchy won't be recreated.

This does require the DetermineCellReuseId to know the indexpath we're getting it for, so a new method was introduced, internal for NET7, protected for NET8+, and in NET8+ the old method is obsoleted.

* Switch up some xaml controls in the sample

* Expose Id internally to use for android view types

* Cache datatemplates for unique item view types

* Api changes (overrides)

* Improve sample

* Remove api txt change that shouldn't have been added

* Update src/Controls/src/Core/Handlers/Items/Android/Adapters/ItemsViewAdapter.cs

Co-authored-by: E.Z. Hart <hartez@users.noreply.github.com>

* Update src/Controls/src/Core/Handlers/Items/Android/Adapters/ItemsViewAdapter.cs

Co-authored-by: Rui Marinho <me@ruimarinho.net>

* Update src/Controls/src/Core/Handlers/Items/Android/Adapters/ItemsViewAdapter.cs

Co-authored-by: Rui Marinho <me@ruimarinho.net>

* Use DataTemplate Id for cellReuseId

* Calculate correct position for carousel adapter

The `CarouselViewAdapter` loops by basically returning a very large value for item count of its adapter/source which means the recyclerview is going to ask for view types with a position value that does not actually exist.

Previously this was ok because we returned only a single item view type ever, however now that we want to return a different type for template selector scenarios where there are potentially multiple template types, we need to override also the `GetItemViewType` and pass in the calculated position in the list properly (as we do in `OnBindViewHolder` here already) since the base `ItemsViewAdapter` class now calls `ItemsSource.GetItem(position)` in its `GetItemViewType()` call.

---------

Co-authored-by: Rui Marinho <me@ruimarinho.net>
Co-authored-by: E.Z. Hart <hartez@users.noreply.github.com>
Co-authored-by: Shane Neuville <shneuvil@microsoft.com>
  • Loading branch information
4 people committed May 30, 2023
1 parent eff3c8f commit 4dc138a
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,35 @@
<ContentPage.Resources>
<ResourceDictionary>
<DataTemplate x:Key="MilkTemplate">
<Frame BorderColor="Red" BackgroundColor="Wheat" HeightRequest="100">
<StackLayout HeightRequest="100">
<Border Stroke="Red" Background="Wheat" HeightRequest="100">
<Grid HeightRequest="100">
<Label Text="{Binding Name}" />
</StackLayout>
</Frame>
</Grid>
</Border>
</DataTemplate>

<DataTemplate x:Key="CoffeeTemplate">
<Frame BorderColor="Red" BackgroundColor="SaddleBrown" HeightRequest="50">
<StackLayout HeightRequest="50">
<Border Stroke="Red" Background="SaddleBrown" HeightRequest="50">
<Grid HeightRequest="50">
<Label Text="{Binding Name}" />
</StackLayout>
</Frame>
</Grid>
</Border>
</DataTemplate>

<DataTemplate x:Key="LatteTemplate" x:DataType="datatemplateselectorgalleries:Latte">
<Frame BorderColor="Red" BackgroundColor="BurlyWood" >
<StackLayout>
<Border Stroke="Red" Background="BurlyWood" >
<VerticalStackLayout>
<Label Text="{Binding Name, StringFormat='Drink name is: {0}'}"/>
<Label Text=" The ingredients are: " Margin="0,10,0,0"/>
<StackLayout BindableLayout.ItemsSource="{Binding Ingredients}" >
<VerticalStackLayout BindableLayout.ItemsSource="{Binding Ingredients}" >
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="datatemplateselectorgalleries:DrinkBase">
<Label Text="{Binding Name, StringFormat=' {0}'}" />
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</StackLayout>
</Frame>
</VerticalStackLayout>
</VerticalStackLayout>
</Border>
</DataTemplate>

<datatemplateselectorgalleries:DrinkTemplateSelector x:Key="VehicleTemplateSelector"
Expand All @@ -45,11 +45,7 @@
</ContentPage.Resources>

<ContentPage.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid RowDefinitions="*,Auto">

<CollectionView ItemsSource="{Binding Items}"
ItemTemplate="{StaticResource VehicleTemplateSelector}"
Expand All @@ -76,7 +72,6 @@

<Picker x:Name="TemplatePicker"
Title="Select a template"
TitleColor="Red"
SelectedItem="{Binding SelectedTemplate}">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,28 @@
<ContentPage.Resources>
<ResourceDictionary>
<DataTemplate x:Key="DefaultTemplate">
<Grid HeightRequest="50">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>

<Grid HeightRequest="50" RowDefinitions="Auto,Auto">

<Image Source="coffee.png" AutomationId="weekday"/>

<Label Grid.Row="1" Text="{Binding Date, StringFormat='{}{0:dddd}'}"></Label>
</Grid>
</DataTemplate>
<DataTemplate x:Key="WeekendTemplate">
<Grid HeightRequest="50">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>

<Grid HeightRequest="50" RowDefinitions="Auto,Auto">

<Image Source="oasis.jpg" AutomationId="weekend"/>

<Label Grid.Row="1" Text="It's the weekend! Woot!"></Label>
</Grid>
</DataTemplate>

<DataTemplate x:Key="EmptyTemplate">
<StackLayout>
<Label Text="{Binding ., StringFormat='({0}) does not match any day of the week.'}"></Label>
</StackLayout>
<Label Text="{Binding ., StringFormat='({0}) does not match any day of the week.'}"></Label>
</DataTemplate>

<DataTemplate x:Key="SymbolsTemplate">
<StackLayout BackgroundColor="Red">
<Label Text="{Binding ., StringFormat='({0}) _definitely_ does not match any day of the week.'}"></Label>
</StackLayout>
<Label Background="Red" Text="{Binding ., StringFormat='({0}) _definitely_ does not match any day of the week.'}"></Label>
</DataTemplate>

<local:WeekendSelector x:Key="WeekendSelector"
Expand All @@ -56,13 +44,13 @@

<ContentPage.Content>

<StackLayout>
<Grid RowDefinitions="Auto,*">

<SearchBar x:Name="SearchBar" Placeholder="Day of Week Filter" />
<SearchBar x:Name="SearchBar" Placeholder="Day of Week Filter" Grid.Row="0" />

<CollectionView x:Name="CollectionView" ItemTemplate="{StaticResource WeekendSelector}"
<CollectionView Grid.Row="1" x:Name="CollectionView" ItemTemplate="{StaticResource WeekendSelector}"
EmptyViewTemplate="{StaticResource SearchTermSelector}"/>

</StackLayout>
</Grid>
</ContentPage.Content>
</ContentPage>
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public DataTemplateSelectorGallery()
{
InitializeComponent();

_demoFilteredItemSource = new DemoFilteredItemSource(filter: ItemMatches);
_demoFilteredItemSource = new DemoFilteredItemSource(count: 200, filter: ItemMatches);

CollectionView.ItemsSource = _demoFilteredItemSource.Items;

Expand Down
2 changes: 2 additions & 0 deletions src/Controls/src/Core/DataTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public DataTemplate(Func<object> loadTemplate) : base(loadTemplate)

string IDataTemplateController.IdString => _idString;

internal int Id => _id;

int IDataTemplateController.Id => _id;

/// <include file="../../docs/Microsoft.Maui.Controls/DataTemplate.xml" path="//Member[@MemberName='SetBinding']/Docs/*" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,22 @@ internal CarouselViewAdapter(CarouselView itemsView, Func<View, Context, ItemCon
public override int ItemCount => CarouselView.Loop && !(ItemsSource is EmptySource)
&& ItemsSource.Count > 0 ? CarouselViewLoopManager.LoopScale : ItemsSource.Count;

public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
public override int GetItemViewType(int position)
{
if (CarouselView == null || ItemsSource == null)
return;
int positionInList = GetPositionInList(position);

bool hasItems = ItemsSource != null && ItemsSource.Count > 0;
if (positionInList < 0)
return ItemViewType.TextItem;

if (!hasItems)
return;
return base.GetItemViewType(positionInList);
}

public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
int positionInList = GetPositionInList(position);

int positionInList = (CarouselView.Loop && hasItems) ? position % ItemsSource.Count : position;
if (positionInList < 0)
return;

switch (holder)
{
Expand All @@ -40,5 +45,18 @@ public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int positi
break;
}
}

int GetPositionInList(int position)
{
if (CarouselView == null || ItemsSource == null)
return -1;

bool hasItems = ItemsSource != null && ItemsSource.Count > 0;

if (!hasItems)
return -1;

return (CarouselView.Loop && hasItems) ? position % ItemsSource.Count : position;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class ItemsViewAdapter<TItemsView, TItemsViewSource> : RecyclerView.Adapt

bool _disposed;
bool _usingItemTemplate = false;
DataTemplateSelector _itemTemplateSelector = null;

protected internal ItemsViewAdapter(TItemsView itemsView, Func<View, Context, ItemContentView> createItemContentView = null)
{
Expand Down Expand Up @@ -86,16 +87,36 @@ public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int

var itemContentView = _createItemContentView.Invoke(ItemsView, context);

// See if our cached templates have a match
if (_viewTypeDataTemplates.TryGetValue(viewType, out var dataTemplate))
{
return new TemplatedItemViewHolder(itemContentView, dataTemplate, IsSelectionEnabled(parent, viewType));
}

return new TemplatedItemViewHolder(itemContentView, ItemsView.ItemTemplate, IsSelectionEnabled(parent, viewType));
}

public override int ItemCount => ItemsSource.Count;

System.Collections.Generic.Dictionary<int, DataTemplate> _viewTypeDataTemplates = new();

public override int GetItemViewType(int position)
{
if (_usingItemTemplate)
{
return ItemViewType.TemplatedItem;
if (_itemTemplateSelector is null)
return ItemViewType.TemplatedItem;

var item = ItemsSource?.GetItem(position);

var template = _itemTemplateSelector?.SelectTemplate(item, ItemsView);
var id = template?.Id ?? ItemViewType.TemplatedItem;

// Cache the data template for future use
if (!_viewTypeDataTemplates.ContainsKey(id))
_viewTypeDataTemplates.Add(id, template);

return id;
}

// No template, just use the Text view
Expand All @@ -118,6 +139,11 @@ protected override void Dispose(bool disposing)
}
}

public override long GetItemId(int position)
{
return position;
}

public virtual int GetPositionForItem(object item)
{
return ItemsSource.GetPosition(item);
Expand All @@ -131,6 +157,7 @@ protected virtual void BindTemplatedItemViewHolder(TemplatedItemViewHolder templ
void UpdateUsingItemTemplate()
{
_usingItemTemplate = ItemsView.ItemTemplate != null;
_itemTemplateSelector = ItemsView.ItemTemplate as DataTemplateSelector;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public override UICollectionViewCell GetCell(UICollectionView collectionView, NS

if (Carousel?.Loop == true && _carouselViewLoopManager != null)
{
var cellAndCorrectedIndex = _carouselViewLoopManager.GetCellAndCorrectIndex(collectionView, indexPath, DetermineCellReuseId());
var cellAndCorrectedIndex = _carouselViewLoopManager.GetCellAndCorrectIndex(collectionView, indexPath, DetermineCellReuseId(indexPath));
cell = cellAndCorrectedIndex.cell;
var correctedIndexPath = NSIndexPath.FromRowSection(cellAndCorrectedIndex.correctedIndex, 0);

Expand Down Expand Up @@ -135,6 +135,9 @@ public override void UpdateItemsSource()

protected override UICollectionViewDelegateFlowLayout CreateDelegator() => new CarouselViewDelegator(ItemsViewLayout, this);

#if NET8_0_OR_GREATER
[Obsolete("Use DetermineCellReuseId(NSIndexPath indexPath) instead.")]
#endif
protected override string DetermineCellReuseId()
{
if (Carousel.ItemTemplate != null)
Expand Down
39 changes: 38 additions & 1 deletion src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.ComponentModel;
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;
using ObjCRuntime;
using UIKit;
Expand Down Expand Up @@ -34,6 +35,7 @@ public abstract class ItemsViewController<TItemsView> : UICollectionViewControll
UIView _emptyUIView;
VisualElement _emptyViewFormsElement;
Dictionary<object, TemplatedCell> _measurementCells = new Dictionary<object, TemplatedCell>();
List<string> _cellReuseIds = new List<string>();

protected UICollectionViewDelegateFlowLayout Delegator { get; set; }

Expand Down Expand Up @@ -89,7 +91,7 @@ protected override void Dispose(bool disposing)

public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
var cell = collectionView.DequeueReusableCell(DetermineCellReuseId(), indexPath) as UICollectionViewCell;
var cell = collectionView.DequeueReusableCell(DetermineCellReuseId(indexPath), indexPath) as UICollectionViewCell;

switch (cell)
{
Expand Down Expand Up @@ -339,6 +341,41 @@ protected virtual void CacheCellAttributes(NSIndexPath indexPath, CGSize size)
}
}

#if NET8_0_OR_GREATER
protected
#else
internal
#endif
virtual string DetermineCellReuseId(NSIndexPath indexPath)
{
if (ItemsView.ItemTemplate != null)
{
var item = ItemsSource[indexPath];

var dataTemplate = ItemsView.ItemTemplate.SelectDataTemplate(item, ItemsView);

var cellOrientation = ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Vertical ? "v" : "h";
var cellType = ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Vertical ? typeof(VerticalCell) : typeof(HorizontalCell);

var reuseId = $"_maui_{cellOrientation}_{dataTemplate.Id}";

if (!_cellReuseIds.Contains(reuseId))
{
CollectionView.RegisterClassForCell(cellType, new NSString(reuseId));
_cellReuseIds.Add(reuseId);
}

return reuseId;
}

return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
? HorizontalDefaultCell.ReuseId
: VerticalDefaultCell.ReuseId;
}

#if NET8_0_OR_GREATER
[Obsolete("Use DetermineCellReuseId(NSIndexPath indexPath) instead.")]
#endif
protected virtual string DetermineCellReuseId()
{
if (ItemsView.ItemTemplate != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Microsoft.Maui.Controls.Border.~Border() -> void
Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.FrameRenderer(Android.Content.Context! context, Microsoft.Maui.IPropertyMapper! mapper) -> void
Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.FrameRenderer(Android.Content.Context! context, Microsoft.Maui.IPropertyMapper! mapper, Microsoft.Maui.CommandMapper! commandMapper) -> void
override Microsoft.Maui.Controls.Handlers.Items.CarouselViewAdapter<TItemsView, TItemsViewSource>.GetItemViewType(int position) -> int
override Microsoft.Maui.Controls.Handlers.Items.ItemsViewAdapter<TItemsView, TItemsViewSource>.GetItemId(int position) -> long
Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView<TItemsView, TAdapter, TItemsViewSource>.~MauiRecyclerView() -> void
override Microsoft.Maui.Controls.Handlers.Items.ItemsViewHandler<TItemsView>.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size
override Microsoft.Maui.Controls.Handlers.Items.ItemsViewHandler<TItemsView>.PlatformArrange(Microsoft.Maui.Graphics.Rect frame) -> void
Expand Down

0 comments on commit 4dc138a

Please sign in to comment.