Sometimes, although suited to a very wide range of UI requirements, a ListView introduces more complexity than required, or simply doesn’t suit the purpose you’re trying to achieve. For example, I recently worked on a project that required a very specific looking UI similar to a “card view” on the main landing screen that a ListView just simply wasn’t playing nicely with and would have required more work than necessary to achieve something fairly simple, with no need for the performance gains of recycled views etc.
In this case, and something I’ve encountered several times, a Repeater control is far simpler on iOS to get working quickly if you’ve been given a design brief that requires any form of “accordion” or “expand/collapse” functionality (I’ll reserve my personal UI thoughts here but we all need to work with designers and don’t always have a choice).
This is where a repeating control comes in to play. A control that simply adds views one after another to the screen. This can then be wrapped in a ScrollView to get a list like effect if you need it or whatever you’re trying to achieve.
While the same result can be achieved with a ListView, often this is just faster to get a screen out because there’s no mucking around with custom row height handling or adding and removing of items in a list and worrying about platform specific things like animations etc.
I’ve seen dozens of different implementations of Repeaters for Xamarin.Forms people have shared online, however one thing that I’m very used to with ListView’s is using Data Template Selectors, to get a different view for the different view model types to easily build out views of similar but not identical data.
The part I like about this approach, is it’s very simple to break out different views to match their data type/view model and reuse things between different screens. I won’t go into the specifics of data template selectors in this article, but essentially the idea is to have a class that just says this view model should get this view.
So you can add your view models of different types like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
var mockItems = new List { new ItemA { Id = Guid.NewGuid().ToString(), Text = "First item", Description="This is an item A.", }, new ItemB { Id = Guid.NewGuid().ToString(), Text = "Second item", Description="This is an item B." }, new ItemC { Id = Guid.NewGuid().ToString(), Text = "Third item", Description="This is an item C." }, new ItemB { Id = Guid.NewGuid().ToString(), Text = "Fourth item", Description="This is also an item B." }, new ItemC { Id = Guid.NewGuid().ToString(), Text = "Fifth item", Description="This is also an item C." }, new ItemA { Id = Guid.NewGuid().ToString(), Text = "Sixth item", Description="This is also an item A." }, new ItemA { Id = Guid.NewGuid().ToString(), Text = "Seventh item", Description="This too, is also an item A." }, new ItemB { Id = Guid.NewGuid().ToString(), Text = "Eighth item", Description="This too, is also an item B." }, new ItemC { Id = Guid.NewGuid().ToString(), Text = "Ninth item", Description="This too, is also an item C." }, }; |
And then using a simple template selector, determine which item should have which view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class SimpleListTemplateSelector : DataTemplateSelector { private DataTemplate _simpleCellTemplate; private DataTemplate _switchCellTemplate; private DataTemplate _groupHeaderCellTemplate; protected override DataTemplate OnSelectTemplate(object item, BindableObject container) { switch (item) { case SwitchCellModel cellModel: return _switchCellTemplate ?? (_switchCellTemplate = new DataTemplate(typeof(Cells.SwitchCell))); case SimpleCellModel cellModel: return _simpleCellTemplate ?? (_simpleCellTemplate = new DataTemplate(typeof(StandardImageCell))); case GroupHeaderCellModel cellModel: return _groupHeaderCellTemplate ?? (_groupHeaderCellTemplate = new DataTemplate(typeof(GroupHeaderCell))); default: return null; } } } |
Over the years I’ve refined a repeating control from something that was quite crude (and still really is) to something that can be interchanged easily with a ListView in XAML when developing UI’s. I’ve used this to get something done faster because of less working around a ListView’s functionality because UI has been designed in a way that the ListView wasn’t intended for, as well as to performance test things to see if it really is actually worth the effort of the ListView versus the simplicity of a repeater control.
This means with a few simple code changes, I can swap out the controls and see what suits the requirement better quickly. E.g.
ListView
1 2 3 4 5 6 7 8 |
<ListView x:Name="ListViewItems" ItemsSource="{Binding Items}" VerticalOptions="FillAndExpand" HasUnevenRows="true"> <ListView.ItemTemplate> <templateselectors:SimpleTemplateSelector /> </ListView.ItemTemplate> </ListView> |
Repeater
1 2 3 4 5 6 7 8 9 10 11 |
<ScrollView> <StackLayout Orientation="Vertical"> <controls:Repeater x:Name="RepeaterItems" ItemsSource="{Binding Items}" VerticalOptions="FillAndExpand"> <controls:Repeater.ItemTemplate> <templateselectors:SimpleTemplateSelector /> </controls:Repeater.ItemTemplate> </controls:Repeater> </StackLayout> </ScrollView> |
And then from there you have all your controls as stock standard XAML (or C#) controls that you can work on as individual views:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<?xml version="1.0" encoding="UTF-8"?> <ViewCell xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="DataTemplateRepeaterControl.Views.Cells.ItemACell"> <ViewCell.View> <Grid Padding="8"> <Frame HorizontalOptions="FillAndExpand" HasShadow="true"> <Grid HorizontalOptions="FillAndExpand"> <Grid.ColumnDefinitions> <ColumnDefinition Width="100" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Image Grid.Column="0" Source="{Binding Image}" WidthRequest="80" HeightRequest="80" Aspect="AspectFit" /> <StackLayout Grid.Column="1" Orientation="Vertical"> <Label Text="{Binding Text}" /> <Label Text="{Binding Description}" /> </StackLayout> <Image Grid.Column="2" HeightRequest="24" Margin="0,15" VerticalOptions="Start" HorizontalOptions="End" Source="arrow.png" Aspect="AspectFit" /> </Grid> </Frame> </Grid> </ViewCell.View> </ViewCell> |
Here’s the complete code for the Repeater control (taken from a NuGet package I’ve previously made available, DarkIce.Toolkit.Core *, which you’re more than welcome to just add to your project and be done with it, but it’s always useful knowing what’s under the hood as well):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
using System; using System.Collections; using Xamarin.Forms; namespace DarkIce.Toolkit.Core.Controls { public class Repeater : StackLayout { public Repeater() { Orientation = StackOrientation.Vertical; Padding = 0; Spacing = 0; } public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(Repeater), default(DataTemplate), BindingMode.OneWay); public DataTemplate ItemTemplate { get { return (DataTemplate)GetValue(ItemTemplateProperty); } set { SetValue(ItemTemplateProperty, value); OnPropertyChanged(nameof(ItemTemplate)); } } public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(ICollection), typeof(Repeater), null, BindingMode.OneWay, propertyChanged: ItemsSourcePropertyChanged); public ICollection ItemsSource { get { return (ICollection)GetValue(ItemsSourceProperty); } set { SetValue(ItemsSourceProperty, value); OnPropertyChanged(nameof(ItemsSource)); } } private static void ItemsSourcePropertyChanged(object bindable, object oldValue, object newValue) { if (bindable is Repeater repeater && newValue is ICollection itemsSource) { repeater.ItemsLoaded = false; repeater.Children.Clear(); // Somehow under strange conditions, items changed during enumeration which caused an InvalidOperationException to be thrown. // Whilst that's extremely odd and shouldn't happen, it's just safer to loop through the old fashioned way where it // doesn't matter if something changes and won't crash the app. var itemsArray = new object[itemsSource.Count]; itemsSource.CopyTo(itemsArray, 0); for (var i = 0; i < itemsSource.Count; i++) { var item = itemsArray[i]; repeater.Children.Add(repeater.GetRowView(item)); } // Let any listeners know loading has finished so any UI updates like spinners etc can be changed repeater.ItemsLoaded = true; repeater.ItemsFinishedLoading?.Invoke(repeater, new EventArgs()); } } public static readonly BindableProperty ItemsLoadedProperty = BindableProperty.Create(nameof(ItemsLoaded), typeof(bool), typeof(Repeater), default(bool), BindingMode.OneWayToSource); public bool ItemsLoaded { get { return (bool)GetValue(ItemsLoadedProperty); } protected set { SetValue(ItemsLoadedProperty, value); OnPropertyChanged(nameof(ItemsLoaded)); } } public event EventHandler ItemsFinishedLoading; protected virtual View GetRowView(object item) { // If we haven't got a template we're not getting a view so just bail out now if (ItemTemplate == null) { return null; } object viewContent; // Determine if this is a straight up template or using a DataTemplateSelector if (ItemTemplate is DataTemplateSelector dts) { var template = dts.SelectTemplate(item, this); viewContent = template.CreateContent(); } else { viewContent = ItemTemplate.CreateContent(); } // Get view and bind the data var rowView = viewContent is View ? viewContent as View : ((ViewCell)viewContent).View; rowView.BindingContext = item; return rowView; } } } |
If you want the entire working example, please have a look here on GitHub.
* One day (hopefully soon) I’ll flesh out this NuGet library a bit more and add some documentation and make it available on GitHub. For now it’s just something I’ve been using to get a bunch of common functionality I’ve written for various apps and often use quickly when I get into a new project.
This was very nice. The only problem was that my collection changed dynamically. I essentially did:
if (newValue is INotifyCollectionChanged isource)
{
// if the source changes, we want to know it
isource.CollectionChanged += repeater.OnItemCollectionChanged;
}
and then on collection changed did a recalc of the control.
Thanks for the feedback 🙂