-
Notifications
You must be signed in to change notification settings - Fork 1.9k
[Bug] [iOS] [Android] ListView bug for CachingStrategy="RecycleElementAndDataTemplate" #9998
Comments
Source code of typed ViewCells Type1Cell:
Type2Cell:
Type3Cell:
DataTemplateSelector:
|
What I also notice is that the description ("This item should have...") is the same each time? |
If you have a look on my demo project source code, you will see that there are 3 items added to the ListView with 3 different types.
And, according to the DataTemplateSelector code (see it above), 3 different data templates should be selected. |
I have seen this as well. Using a DataTemplateSelector that allocates static readonly DataTemplates created using the DataTemplate(Type t) constructor. With a ListView using CachingStrategy="RecycleElement" everything works as expected, using CachingStrategy="RecycleElementAndDataTemplate" the first DataTemplate used gets used everywhere irrespective of the type that should be allocated. In short CachingStrategy="RecycleElementAndDataTemplate" is completely broken when using DataTemplateSelectors. It probably still works if using a single DataRemplate. |
Ok I created an IssuePage in the control gallery and could verify the issue. I can clearly see that you use the same Model/ViewModel for every DataTemplate and you are not using polymorphism to have different relationships between DataTemplates and ViewModels/Models. The thing is that the Dictionary<Type, DataTemplate> _dataTemplates = new Dictionary<Type, DataTemplate>(); When the first item is added then the overriden DataTemplate dataTemplate = null;
if (recycle && _dataTemplates.TryGetValue(item.GetType(), out dataTemplate))
return dataTemplate; This is why you always get the first DataTemplate. And this happens for all platforms as this DataTemplateSelector is defined in the Xamarin.Froms assembly. However, there are 2 ways to solve this (as far as I know). 2.) Use Example: class ItemModel1 : ItemModel{}
class ItemModel2 : ItemModel{}
class ItemModel3 : ItemModel{}
var items = new ObservableCollection<ItemModel>()
{
new ItemModel1() { Type = ItemModel.ItemModelType.Type1, Action1Command = Action1GlobalCommand, Action2Command = Action2GlobalCommand, Action3Command = Action3GlobalCommand },
new ItemModel2() { Type = ItemModel.ItemModelType.Type2, Action1Command = Action1GlobalCommand, Action2Command = Action2GlobalCommand, Action3Command = Action3GlobalCommand },
new ItemModel3() { Type = ItemModel.ItemModelType.Type3, Action1Command = Action1GlobalCommand, Action2Command = Action2GlobalCommand, Action3Command = Action3GlobalCommand }
}; How should this be handled?I currently don't know what's the best solution for this issue. 1.) Update the docs and tell that the ViewModels/Models should be unique when using a 2.) Enable to functionality to use one vm foreach Waiting for feedback. |
Here is the page for the control gallery, just in case someone wants to try it. using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;
namespace Xamarin.Forms.Controls.Issues
{
[Preserve(AllMembers = true)]
[Issue(IssueTracker.Github, 9998, "[Bug] [iOS] [Android] ListView bug for CachingStrategy='RecycleElementAndDataTemplate'", PlatformAffected.Android & PlatformAffected.iOS)]
public class Issue9998 : ContentPage
{
public Issue9998()
{
Action1GlobalCommand = new Command<ItemModel>(async (model) => await Action1GlobalCommandHandlerAsync(model));
Action2GlobalCommand = new Command<ItemModel>(async (model) => await Action2GlobalCommandHandlerAsync(model));
Action3GlobalCommand = new Command<ItemModel>(async (model) => await Action3GlobalCommandHandlerAsync(model));
var items = new ImprovedObservableCollection<ItemModel>()
{
new ItemModel() { Type = ItemModel.ItemModelType.Type1, Action1Command = Action1GlobalCommand, Action2Command = Action2GlobalCommand, Action3Command = Action3GlobalCommand },
new ItemModel() { Type = ItemModel.ItemModelType.Type2, Action1Command = Action1GlobalCommand, Action2Command = Action2GlobalCommand, Action3Command = Action3GlobalCommand },
new ItemModel() { Type = ItemModel.ItemModelType.Type3, Action1Command = Action1GlobalCommand, Action2Command = Action2GlobalCommand, Action3Command = Action3GlobalCommand }
};
var listView = new ListView(ListViewCachingStrategy.RecycleElementAndDataTemplate)
{
HasUnevenRows = true,
};
listView.ItemsSource = items;
listView.ItemTemplate = new ItemModelTemplateSelector();
Content = new StackLayout
{
Children = {
new Label { Text = "Welcome to Xamarin.Forms!" },
listView
}
};
}
public ICommand Action1GlobalCommand { get; }
public ICommand Action2GlobalCommand { get; }
public ICommand Action3GlobalCommand { get; }
async Task Action1GlobalCommandHandlerAsync(ItemModel model) =>
await DisplayAlert("Information", $"Action 1 for {model.TypeName}", "OK");
async Task Action2GlobalCommandHandlerAsync(ItemModel model) =>
await DisplayAlert("Information", $"Action 2 for {model.TypeName}", "OK");
async Task Action3GlobalCommandHandlerAsync(ItemModel model) =>
await DisplayAlert("Information", $"Action 3 for {model.TypeName}", "OK");
class ItemModel
{
public enum ItemModelType
{
Type1,
Type2,
Type3
}
public string TypeName { get { return this.Type.ToString(); } }
public ItemModelType Type { get; set; }
public ICommand Action1Command { get; set; }
public ICommand Action2Command { get; set; }
public ICommand Action3Command { get; set; }
}
[Preserve(AllMembers = true)]
class ItemModelTemplateSelector : DataTemplateSelector
{
readonly DataTemplate Type1Template = new DataTemplate(typeof(Type1Cell));
readonly DataTemplate Type2Template = new DataTemplate(typeof(Type2Cell));
readonly DataTemplate Type3Template = new DataTemplate(typeof(Type3Cell));
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
if(!(item is ItemModel itemModel)) throw new ArgumentException();
var template = itemModel.Type switch
{
ItemModel.ItemModelType.Type1 => Type1Template,
ItemModel.ItemModelType.Type2 => Type2Template,
ItemModel.ItemModelType.Type3 => Type3Template,
_ => throw new ArgumentException()
};
return template;
}
}
[Preserve(AllMembers = true)]
class Type1Cell : ViewCell
{
public Type1Cell()
{
var menuItem1 = new MenuItem() { Text = "Action1" };
var menuItem2 = new MenuItem() { Text = "Action2" };
menuItem1.SetBinding(MenuItem.CommandProperty, $"{nameof(ItemModel.Action1Command)}");
menuItem1.SetBinding(MenuItem.CommandParameterProperty, $".");
menuItem2.SetBinding(MenuItem.CommandProperty, $"{nameof(ItemModel.Action2Command)}");
menuItem2.SetBinding(MenuItem.CommandParameterProperty, $".");
ContextActions.Add(menuItem1);
ContextActions.Add(menuItem2);
var l1 = new Label();
l1.SetBinding(Label.TextProperty, $"{nameof(ItemModel.TypeName)}");
View = new StackLayout()
{
Orientation = StackOrientation.Vertical,
MinimumHeightRequest = 60,
Padding = 20,
Children =
{
new StackLayout()
{
Orientation = StackOrientation.Horizontal,
Children =
{
new Label(){Text = "ItemModel Type = "},
l1
}
},
new Label(){Text="Template1"},
new Label(){Text="Should have ContextActions 1 and 2"},
}
};
}
}
[Preserve(AllMembers = true)]
class Type2Cell : ViewCell
{
public Type2Cell()
{
var l1 = new Label();
l1.SetBinding(Label.TextProperty, $"{nameof(ItemModel.TypeName)}");
View = new StackLayout()
{
Orientation = StackOrientation.Vertical,
MinimumHeightRequest = 60,
Padding = 20,
Children =
{
new StackLayout()
{
Orientation = StackOrientation.Horizontal,
Children =
{
new Label() { Text = "ItemModel Type = " },
l1
}
},
new Label() { Text = "Template2" },
new Label() { Text = "Should have NO ContextActions" },
}
};
}
}
[Preserve(AllMembers = true)]
class Type3Cell : ViewCell
{
public Type3Cell()
{
var menuItem3 = new MenuItem() { Text = "Action3" };
menuItem3.SetBinding(MenuItem.CommandProperty, $"{nameof(ItemModel.Action3Command)}");
menuItem3.SetBinding(MenuItem.CommandParameterProperty, $".");
ContextActions.Add(menuItem3);
var l1 = new Label();
l1.SetBinding(Label.TextProperty, $"{nameof(ItemModel.TypeName)}");
View = new StackLayout()
{
Orientation = StackOrientation.Vertical,
MinimumHeightRequest = 60,
Padding = 20,
Children =
{
new StackLayout()
{
Orientation = StackOrientation.Horizontal,
Children =
{
new Label() { Text = "ItemModel Type = " },
l1
}
},
new Label() { Text = "Template3" },
new Label() { Text = "Should have ContextAction 3" },
}
};
}
}
}
} |
Thank you, @BrayanKhosravian! Now it's clear to me.
I think that option 1 is better: |
Yes I also think that option 1 is better. I wonder how the xamarin.forms team sees this, as this change would have API impact. But I think that we could use the modified DataTemplateSelector for the iOS CollectionView. I spent a lot of time with this issue #10842. The iOS CollectionView PreRegisters all DataTemplate Types and does not do that during runtime. protected virtual void RegisterViewTypes()
{
CollectionView.RegisterClassForCell(typeof(HorizontalDefaultCell), HorizontalDefaultCell.ReuseId);
CollectionView.RegisterClassForCell(typeof(VerticalDefaultCell), VerticalDefaultCell.ReuseId);
CollectionView.RegisterClassForCell(typeof(HorizontalCell), HorizontalCell.ReuseId);
CollectionView.RegisterClassForCell(typeof(VerticalCell), VerticalCell.ReuseId);
} |
Description
One of the pages of our application contains a ListView control. And we try to use CachingStrategy="RecycleElementAndDataTemplate". Our item model has the Type property.
Also, we have a DataTemplateSelector and 3 typed ViewCells. The DataTemplateSelector selects one of data templates with typed ViewCell according to an item model type.
Each of ViewCells has a little bit different xaml layout and different ViewCell.ContextActions menu items.
And the bug is that the ListView control uses the first typed ViewCell for all items no matter what the type has this item and what the typed of ViewCell is selected by the DataTemplateSelector.
This bug appears only we set the CachingStrategy="RecycleElementAndDataTemplate" for the ListView control. If we set "RetainElement" or "RecycleElement", all works as expected.
This bug appears on both iOS and Android platforms. Moreover, on iOS platform the ViewCell.ContextActions menu items are swapped with each other.
Basic Information
Xamarin.Forms version 4.4.0.991757
When I debug the app from the Visual Studio, the following versions are used:
When I build the app on Azure DevOps on the Hosted MacOS build agent, the following versions are used:
Reproduction Link
I created a small application that demonstrates this bug. The source code of this project is provided.
ListViewDataTemplateIssue_demo_app_source_code.zip
Large screenshots
Android_screenshots.zip
iOS_screenshots.zip
The text was updated successfully, but these errors were encountered: