Grouping with ListView and TableView and the TableViewItemsSourceBehavior

When I select a mountain area in my weather App I want to display a list of options. These options will include forecasts, weather observations at various weather stations, avalanche and other information. I’d like to group these and display this using the native grouping features for each platform.

Both Xamarin Forms ListView and TableView have the ability to group data. I will explore both these options and show how we can create one view model structure that will cater for both approaches.

ListView allows data to be grouped, and optionally includes jump lists for quick navigation. James Montemagno from Xamarin provides us with an excellent example on how to do this here.

I took my inspiration for his generic Grouping class to create my own view model called, unsurprisingly, GroupViewModel.

    public class GroupViewModel<TKey, TItem> : ObservableCollection<TItem>
    {
        public TKey Key { get; private set; }

        public GroupViewModel(TKey key, IEnumerable<TItem> items)
        {
            Key = key;
            if (items != null)
                foreach (var item in items)
                    Items.Add(item);
        }
    }

I want each item to have a Title and a Command that I can execute when selected, so I created the following IItemViewModel.

    public interface IItemViewModel
    {
        string Title { get; set; }

        ICommand Command { get; set; }
    }

Now I can create an ItemsGroupViewModel of IItemViewModel like this, which is keyed by a string.

    public class ItemGroupViewModel : GroupViewModel<string, IItemViewModel>
    {
        public ItemGroupViewModel(string key, IEnumerable<IItemViewModel> items = null)
            : base(key, items)
        {
        }
    }

I also want to associate an object with the item view model, which might differ for each item, so I created the following generic implementation of the above interface, which adds an Items property.

    public class ItemViewModel<T> : ViewModelBase, IItemViewModel
    {
        public ItemViewModel(string title, Action<T> action, T item)
        {
            Title = title;
            Item = item;
            Command = new Command<T>(action);
        }

        public ICommand Command { get; set; }

        public T Item { get; set; }
    }
 

This allows me to add any type of ItemViewModel to the group, or groups.

Next I need a View Model to represent a list of groups, which also needs to include a page title to display for the groups called ItemGroupsViewModel.

    public class ItemGroupsViewModel : ViewModelBase 
    {
        public ItemGroupsViewModel(string title)
        {
            Title = title;
        }

        public IEnumerable<ItemGroupViewModel> Groups { get; set; }
    }

Now in my MountainAreaViewModel all I need to do is to create a list of options, which I can display when a mountain area is selected.

public ItemsViewModel Options { get; set; }

private void Initialise()
{
    var groups = new List<ItemGroupViewModel>
    {
        new ItemGroupViewModel("Forecasts")
        {
            new ItemViewModel<ForecastReportViewModel>("Met Office 5 day forecast", ShowForecast, ForecastReport),
            new ItemViewModel<SummitForecastViewModel>("Met Office Summit Forecasts", ShowSummitForecast, SummitForecast),
            new ItemViewModel<MWISForecastViewModel>("MWIS Weather Forecast", ShowMWISForecast, MWISForecast)
        },
        new ItemGroupViewModel("Other")
        {
            new ItemViewModel<AvalancheReportViewModel>("Sais Avalanche Report", ShowAvalancheForecast, AvalancheReport),
            new ItemViewModel<AreaInfoViewModel>("Area Info", ShowAreaInfo, AreaInfo)
        }
    };

    var locations = _observationService.GetAreaObservationLocations(_location.Id);
    if (locations != null)
    {
        groups.Insert(1, new ItemGroupViewModel("Weather Stations", 
            locations.Select(location => new ItemViewModel<ObservationLocation>(location.Name,
            ShowObservations, location))));
    }

    Options = new ItemGroupsViewModel("Options") { Groups = groups };
}

Assume here that my observationService returns a list of weather observation locations, which I then use to create a grouped list of weather stations. Notice that each item calls one of the methods, ShowForecast, ShowObservations etc, which are passed as an action to the ItemViewModel.

And finally I create a view displaying a ListView with its ItemSource bound to the Options property and ground by Key.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:views="clr-namespace:Silkweb.Mobile.Core.Views;assembly=Silkweb.Mobile.Core"
             x:Class="Silkweb.Mobile.MountainWeather.Views.ItemsView"
             Title="{Binding Title}">
  <ListView ItemsSource="{Binding Groups}" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
            IsGroupingEnabled="True"
            GroupDisplayBinding="{Binding Key}">
    <ListView.ItemTemplate>
      <DataTemplate>
        <views:TextCellExtended Text="{Binding Title}"
                                ShowDisclosure="True" Command="{Binding Command}" CommandParameter="{Binding Item}" />
      </DataTemplate>
    </ListView.ItemTemplate>
  </ListView>
</ContentPage>

Notice here that I am also using the TextCellExtended, which allows me to display the disclosure chevron to indicate more items.

Running this in my app I can now see the following options when I select a weather area.

Screen Shot 2015-03-10 at 21.35.18

This is great, but it’s actually not quite what I wanted. I want the options to be displayed more like they are in the settings of the phone, which look more like this.

Screen Shot 2015-03-09 at 22.44.37

Notice here the groupings spaced out with a header. TableView allows us to create this kind of layout, defining TableSections for each group. Unfortunately TableView does not provide an ItemsSource property that allows it to bind to a list or items. You have to define each TableSection manually either in Xaml or in code. This got me thinking about creating a Behavior that would do this dynamically. What I need is a behavior that can bind to the above ItemGroupsViewModel and create the TableSections. This behavior also needs to be able to define an ItemTemplate property that defines the template for each item. I therefore created the following TableViewItemsSourceBehavior.

    public class TableViewItemsSourceBehavior : BindableBehavior<TableView>
    {
        public static readonly BindableProperty ItemsSourceProperty =
            BindableProperty.Create<TableViewItemsSourceBehavior, IEnumerable<ItemGroupViewModel>>(p => p.ItemGroupsSource, null, BindingMode.Default, null, ItemsSourceChanged);

        public IEnumerable<ItemGroupViewModel> ItemGroupsSource
        {
            get { return (IEnumerable<ItemGroupViewModel>)GetValue(ItemsSourceProperty); } 
            set { SetValue(ItemsSourceProperty, value); } 
        }

        public static readonly BindableProperty ItemTemplateProperty =
            BindableProperty.Create<TableViewItemsSourceBehavior, DataTemplate>(p => p.ItemTemplate, null);

        public DataTemplate ItemTemplate
        { 
            get { return (DataTemplate)GetValue(ItemTemplateProperty); } 
            set { SetValue(ItemTemplateProperty, value); } 
        }

        private static void ItemsSourceChanged(BindableObject bindable, IEnumerable<ItemGroupViewModel> oldValue, IEnumerable<ItemGroupViewModel> newValue)
        {
            var behavior = bindable as TableViewItemsSourceBehavior;
            if (behavior == null) return;
            behavior.SetItems();
        }

        private void SetItems()
        {
            if (ItemGroupsSource == null) return;

            AssociatedObject.Root.Clear();

            foreach (var group in ItemGroupsSource)
            {
                var tableSection = new TableSection { Title = group.Key };

                foreach (var itemViewModel in group)
                {
                    var content = ItemTemplate.CreateContent();
                    var cell = content as Cell;
                    if (cell == null) continue;
                    cell.BindingContext = itemViewModel;
                    tableSection.Add(cell);
                }

                AssociatedObject.Root.Add(tableSection);
            }
        }
    }

SetItems is called when the ItemsSource property is set. This simply iterates through the groups and creates a TableSection for each group, then iterates through each item and adds a Cell using the ItemTemplate.

You may have noticed that this behavior inherits from BindableBehavior. As you may recall I previously blogged about a bug with the Behavior class, but it turns out this is by design and is now briefly explained at the bottom of the behaviors guide here.

“Sharing Behaviors
Behaviors can be shared between multiple controls because they are easily added to a Style, which in turn can be applied to many controls either explicitly or implicitly.

This means that while you can add bindable properties to a behavior that are set or queried in XAML, if you do create behaviors that have state they should not be shared between controls in a Style in the ResourceDictionary.

This is also why the BindingContext is not set by Xamarin.Forms. User-created behaviors shouldn’t rely on the binding context for accessing data.”

Therefore any behaviors that define BindableProperties need to set the BindingContext on the behavior manually when the behavior is attached to the UI control. I also wanted to mimic the WPF Behavior class by providing an AssociatedObject property for the UI control the behavior belongs to. Here is the BindableBehavior class.

    public class BindableBehavior<T> : Behavior<T> where T : BindableObject
    {
        public T AssociatedObject { get; private set; }

        protected override void OnAttachedTo(T view)
        {
            base.OnAttachedTo(view);

            AssociatedObject = view;

            if (view.BindingContext != null)
                BindingContext = view.BindingContext;

            view.BindingContextChanged += OnTableViewOnBindingContextChanged;
        }

        protected override void OnDetachingFrom(T view)
        {
            view.BindingContextChanged -= OnTableViewOnBindingContextChanged;
        }

        private void OnTableViewOnBindingContextChanged(object sender, EventArgs e)
        {
            BindingContext = AssociatedObject.BindingContext;
        }
    }

This simply manages setting the BindingContext and the AssociatedObject used in the TableViewItemsSourceBehavior. Now lets use this behavior and replace the previous ItemsView with a TableView as follows.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:views="clr-namespace:Silkweb.Mobile.Core.Views;assembly=Silkweb.Mobile.Core"
             xmlns:behaviors="clr-namespace:Silkweb.Mobile.Core.Behaviors;assembly=Silkweb.Mobile.Core"
             x:Class="Silkweb.Mobile.MountainWeather.Views.ItemsView"
             Title="{Binding Title}">

  <TableView Intent="Menu">
      <TableView.Behaviors>
        <behaviors:TableViewItemsSourceBehavior ItemsSource="{Binding Groups}">
          <behaviors:TableViewItemsSourceBehavior.ItemTemplate>
            <DataTemplate>
              <views:TextCellExtended Text="{Binding Title}" ShowDisclosure="True" Command="{Binding Command}" CommandParameter="{Binding Item}" />
            </DataTemplate>
          </behaviors:TableViewItemsSourceBehavior.ItemTemplate>
        </behaviors:TableViewItemsSourceBehavior>
      </TableView.Behaviors>
    </TableView>

</ContentPage>

Here I have set the Intent of the TableView to Menu and added the Behavior which is bound to the Groups property of ItemGroupsViewModel. It also defines the DataTemplate for each item as a TextCellExtended control as before. Now lets run this and see how it looks.

Screen Shot 2015-03-10 at 21.33.30

Now that looks much better and is just what I wanted. I can now easily extend this list of options simply by adding new items to the ItemsGroupViewModel. It is also very easy to choose to use either a ListView or TableView with the help of the TableViewItemsSourceBehavior. This gives me great flexibility and reuse within my application for other grouped lists which I will need to add later.

7 thoughts on “Grouping with ListView and TableView and the TableViewItemsSourceBehavior

  1. Thanks so much for some fantastic articles on Xamarin Forms! I developed iOS and Android versions of an app using Xamarin a couple of years ago, then was pulled into desktop development for the last 18 months. I’m diving back into Xamarin again and have found your blog to be an excellent resource on the right way to architect apps using Xamarin’s latest and greatest offerings.

    I read through all of your posts yesterday and today. I attempted to hit your github account to grab the source before going through the posts a second time, but it looks like you deleted your github account. Is there any other source for your, uh, source? 🙂

    Like

    1. I’m really pleased you like my blog posts. It’s nice to know people find them useful.

      Yes, the github link does seem to be broken for public access. You can download the zip for the source code from the link below.

      http://1drv.ms/1ELEuQL

      Please note the weather images are not included in the source code for licensing reasons. Please see the readme in the image resource folders for more details.

      Like

  2. Awesome, thanks for providing a new link, and I understand regarding the weather images. After leaving my comment, I downloaded an earlier version of your code from your Part 8 source link, which still worked. I then made the Forms 1.3 updates and got an API key and successfully fetched data.

    I was planning to start applying the other changes you made in the more recent posts, but it is definitely better to have the latest source to review instead of possibly getting stuck on a missing detail.

    It takes a lot of time to create detailed posts like yours so again, its greatly appreciated.

    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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s