Repeater or Bindable StackLayout

Intro

When designing a View (Page) we need to take into consideration that there might be a lot of content to show. Typically we should use a ListView, which by default is scrollable. However, what if you have to show more than one ListView on a single page? Nesting ScrollViews is a very bad practice that should be avoided unless natively supported. In this case it will most probably make sense to put all the content within a single ScrollView. But how? Here is where the Repeater or BindableStackLayout comes into play.

Custom Control

In order to solve the problem described in the Intro, we will have to extend a StackLayout and expose the next bindable properties:

  • ItemsSource – Gets/sets the source of items to template and display.
  • ItemDataTemplate – Gets/sets the item template.
  • Title – Gets/sets the header/title of the control.

The implementation is very simple and does not require any custom renderers since we are simply going to reuse a layout that arranges child views vertically or horizontally. We are going to use BindableProperties in order to be able to react to value changes in a real time.

Here is a very simple implementation in ±50 lines of code:

public class BindableStackLayout : StackLayout
{
    readonly Label header;

    public BindableStackLayout()
    {
        header = new Label();
        Children.Add(header);
    }

    public IEnumerable ItemsSource
    {
        get { return (IEnumerable)GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }
    public static readonly BindableProperty ItemsSourceProperty =
        BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(BindableStackLayout),
                                propertyChanged: (bindable, oldValue, newValue) => ((BindableStackLayout)bindable).PopulateItems());

    public DataTemplate ItemDataTemplate
    {
        get { return (DataTemplate)GetValue(ItemDataTemplateProperty); }
        set { SetValue(ItemDataTemplateProperty, value); }
    }
    public static readonly BindableProperty ItemDataTemplateProperty =
        BindableProperty.Create(nameof(ItemDataTemplate), typeof(DataTemplate), typeof(BindableStackLayout));

    public string Title
    {
        get { return (string)GetValue(TitleProperty); }
        set { SetValue(TitleProperty, value); }
    }
    public static readonly BindableProperty TitleProperty =
        BindableProperty.Create(nameof(Title), typeof(string), typeof(BindableStackLayout),
                                propertyChanged: (bindable, oldValue, newValue) => ((BindableStackLayout)bindable).PopulateHeader());

    void PopulateItems()
    {
        if (ItemsSource == null) return;
        foreach (var item in ItemsSource)
        {
            var itemTemplate = ItemDataTemplate.CreateContent() as View;
            itemTemplate.BindingContext = item;
            Children.Add(itemTemplate);
        }
    }

    void PopulateHeader() => header.Text = Title;
}

All the “stuff” is happening within the PopulateItems method which is called when a value of ItemSource property is changing. We simply iterate through the collection of items and add them as children to the root view. Please note that each child is represented by ItemDataTemplate, so all we have to do, is to invoke CreateContent method that will be generate the view for us.

Usage example:

public sealed class MyColor
{
    public string Name { get; }
    public string HexCode { get; }

    public MyColor(string name, string hexCode)
    {
        Name = name;
        HexCode = hexCode;
    }
} 

public class MainViewModel
{
    public IReadOnlyCollection<MyColor> MyColors { get; } = new List<MyColor>
    {
        new MyColor(“Black”, “#000000”),
        new MyColor(“Hot Pink”, “#ff69b4”),
        new MyColor(“Red”, “#ff0000”),
        new MyColor(“Sort of Green”, “#7cff00”)
    };
}

<?xml version=1.0 encoding=utf-8?>
<ContentPage
    xmlns=http://xamarin.com/schemas/2014/forms
    xmlns:x=http://schemas.microsoft.com/winfx/2009/xaml
    xmlns:local=clr-namespace:XFBindableStackLayout
    x:Class=XFBindableStackLayout.MainPage>
    <ContentPage.BindingContext>
        <local:MainViewModel />
    </ContentPage.BindingContext>
    <local:BindableStackLayout
        Title=Colors:
        ItemsSource={Binding MyColors}
        VerticalOptions=Center
        HorizontalOptions=Center>
        <local:BindableStackLayout.ItemDataTemplate>
            <DataTemplate>
                <StackLayout
                    Orientation=Horizontal>
                    <BoxView
                        WidthRequest=50
                        HeightRequest=50
                        Color={Binding HexCode} />
                    <Label
                        Text={Binding Name}
                        VerticalTextAlignment=Center />
                </StackLayout>
            </DataTemplate>
        </local:BindableStackLayout.ItemDataTemplate>
    </local:BindableStackLayout>
</ContentPage>

The result:

Conclusion

The implementation above is very simple, however some corner cases are not handled on purpose. This can be a nice homework for you to discover and handle those. The code above is fully available on github.

Good luck!

Advertisements

3 thoughts on “Repeater or Bindable StackLayout

  1. I have added SelectedItemChanged event to the original code. This event can be used to get the selected item.

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Input;
    using Xamarin.Forms;

    namespace applause
    {
    public class BindableStackLayout : StackLayout
    {
    readonly Label header;
    public BindableStackLayout()
    {
    header = new Label();
    Children.Add(header);

    ItemSelectedCommand = new Command(item =>
    {
    SelectedItem = item;
    });
    }

    public event EventHandler SelectedItemChanged;

    public IEnumerable ItemsSource
    {
    get { return (IEnumerable)GetValue(ItemsSourceProperty); }
    set { SetValue(ItemsSourceProperty, value); }
    }
    public static readonly BindableProperty ItemsSourceProperty =
    BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(BindableStackLayout),
    propertyChanged: (bindable, oldValue, newValue) => ((BindableStackLayout)bindable).PopulateItems());

    public DataTemplate ItemDataTemplate
    {
    get { return (DataTemplate)GetValue(ItemDataTemplateProperty); }
    set { SetValue(ItemDataTemplateProperty, value); }
    }
    public static readonly BindableProperty ItemDataTemplateProperty =
    BindableProperty.Create(nameof(ItemDataTemplate), typeof(DataTemplate), typeof(BindableStackLayout));

    public string Title
    {
    get { return (string)GetValue(TitleProperty); }
    set { SetValue(TitleProperty, value); }
    }
    public static readonly BindableProperty TitleProperty =
    BindableProperty.Create(nameof(Title), typeof(string), typeof(BindableStackLayout),
    propertyChanged: (bindable, oldValue, newValue) => ((BindableStackLayout)bindable).PopulateHeader());

    public object SelectedItem
    {
    get { return GetValue(SelectedItemProperty); }
    set { SetValue(SelectedItemProperty, value); }
    }
    public static readonly BindableProperty SelectedItemProperty = BindableProperty.Create(p => p.SelectedItem, default(object), BindingMode.TwoWay, null, OnSelectedItemChanged);

    private static void OnSelectedItemChanged(BindableObject bindable, object oldValue, object newValue)
    {
    var itemsView = (BindableStackLayout)bindable;
    if (newValue == oldValue)
    return;

    itemsView.SetSelectedItem(newValue);
    }

    protected virtual void SetSelectedItem(object selectedItem)
    {
    var handler = SelectedItemChanged;
    if (handler != null)
    handler(this, new SelectedItemChangedEventArgs(selectedItem));
    }

    void PopulateItems()
    {
    if (ItemsSource == null) return;
    foreach (var item in ItemsSource)
    {
    Children.Add(GetItemView(item));
    }
    }

    void PopulateHeader() => header.Text = Title;

    protected virtual View GetItemView(object item)
    {
    var content = ItemDataTemplate.CreateContent();

    var view = content as View;
    if (view == null)
    return null;

    view.BindingContext = item;

    var gesture = new TapGestureRecognizer
    {
    Command = ItemSelectedCommand,
    CommandParameter = item
    };

    AddGesture(view, gesture);

    return view;
    }

    protected readonly ICommand ItemSelectedCommand;

    protected void AddGesture(View view, TapGestureRecognizer gesture)
    {
    view.GestureRecognizers.Add(gesture);

    var layout = view as Layout;

    if (layout == null)
    return;

    foreach (var child in layout.Children)
    AddGesture(child, gesture);
    }
    }
    }

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s