Grouped UITableViews with MVVMCross

I recently had a requirement to implement a grouped iOS table view in a project that was using MVVMCross for its MVVM architecture. MVVMCross provides a very clever way of binding data (as a collection of ViewModels) to a table view source, but neither the official documentation nor Google searches gave any straightforward examples of implementing grouped sections within a UITableView, so here’s how I managed to achieve this:

Notes and assumptions

  • I’ve assumed if you’re reading this then you’re probably keen on using custom table cells, so the examples include the most basic of custom cells with a corresponding .xib and a ViewModel to bind to. You shouldn’t have any problems binding to properties in a basic cell type.

  • At the time of writing, the examples I’m using are buit for and using:

  • MVVMCross 5.6.3
  • Xamarin.iOS 11.6

  • The full example source code can be found here

The code

For this example I’ve added a UITableView to my main view controller .xib, and named it MyTableView:

Main view controller:

using System;
using Foundation;
using MvvmCross.Binding.BindingContext;
using MvvmCross.Binding.ExtensionMethods;
using MvvmCross.Binding.iOS.Views;
using MvvmCross.Core.ViewModels;
using MvvmCross.iOS.Views;
using MvvmCross.iOS.Views.Presenters.Attributes;
using MvvmCross.Platform.Core;
using MvvmCrossGroupedTableView.iOS.Views.Cells;
using MvvmCrossGroupedTableView.ViewModels;
using UIKit;

namespace MvvmCrossGroupedTableView.iOS.Views
{
    [MvxRootPresentation(WrapInNavigationController = true)]
    public partial class MainView : MvxViewController<MainViewModel>
    {
        public MainView() : base("MainView", null)
        {
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            Title = "Grouped Table View";

            // Apply a custom MvxTableViewSource to the table view
            var myTableViewSource = new CustomTableViewSource(MyTableView);
            MyTableView.Source = myTableViewSource;

            // Bind the ItemsSource property in the table source to the view model
            var set = this.CreateBindingSet<MainView, MainViewModel>();
            set.Bind(myTableViewSource).To(vm => vm.MyData);
            set.Apply();

            MyTableView.ReloadData();
        }
    }
}

View model for the main view:

using MvvmCross.Core.ViewModels;

namespace MvvmCrossGroupedTableView.ViewModels
{
    public class MainViewModel : MvxViewModel
    {

        public MainViewModel()
        {
        }

        // A simple collection to provide data for the table view, yours will be more dynamic obviously 🙂
        public MvxObservableCollection<MvxObservableCollection<CustomCellViewModel>> MyData
        {
            get 
            {
                var firstGroup = new MvxObservableCollection<CustomCellViewModel>();
                var secondGroup = new MvxObservableCollection<CustomCellViewModel>();

                firstGroup.Add(new CustomCellViewModel("Section: 0, Row: 0"));
                firstGroup.Add(new CustomCellViewModel("Section: 0, Row: 1"));
                firstGroup.Add(new CustomCellViewModel("Section: 0, Row: 2"));

                secondGroup.Add(new CustomCellViewModel("Section: 1, Row: 0"));
                secondGroup.Add(new CustomCellViewModel("Section: 1, Row: 1"));
                secondGroup.Add(new CustomCellViewModel("Section: 1, Row: 2"));
                secondGroup.Add(new CustomCellViewModel("Section: 1, Row: 3"));

                return new MvxObservableCollection<MvxObservableCollection<CustomCellViewModel>>()
                {
                    firstGroup, secondGroup
                };
            }
        }

    }
}

View model for my custom cell:

using MvvmCross.Core.ViewModels;
using MvvmCross.Platform.UI;

namespace MvvmCrossGroupedTableView.ViewModels
{
    public class CustomCellViewModel : MvxViewModel
    {
        string _text;

        public CustomCellViewModel(string text)
        {
            _text = text;
        }

        public string Text
        {
            get => _text;
            set => SetProperty(ref _text, value);
        }
    }
}

My custom table view cell:

using System;
using Foundation;
using MvvmCross.Binding.BindingContext;
using MvvmCross.Binding.iOS.Views;
using MvvmCrossGroupedTableView.ViewModels;
using MvvmCross.Plugins.Color;
using UIKit;

namespace MvvmCrossGroupedTableView.iOS.Views.Cells
{
    public partial class CustomCell : MvxTableViewCell
    {
        public static readonly NSString Key = new NSString("CustomCell");
        public static readonly UINib Nib = Nib = UINib.FromName("CustomCell", NSBundle.MainBundle);

        public static CustomCell Create()
        {
            return Nib.Instantiate(null, null)[0] as CustomCell;
        }

        protected CustomCell(IntPtr handle) : base(handle)
        {
            this.DelayBind(() => {
                var set = this.CreateBindingSet<CustomCell, CustomCellViewModel>();
                set.Bind(TextLabel).To(vm => vm.Text);
                set.Apply();
            });
        }
    }
}

Finally, we need to create our custom table view source, which is where the magic happens. You need to create a custom source which inherits from MvxTableViewSource as follows:

public class CustomTableViewSource : MvxTableViewSource
    {
        public CustomTableViewSource(UITableView tableView) : base(tableView)
        {
        }

        // # 1:
        public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
        {
            var group = ItemsSource.ElementAt(indexPath.Section) as MvxObservableCollection<CustomCellViewModel>;
            var item = group.ElementAt(indexPath.Row) as CustomCellViewModel;

            var cell = GetOrCreateCellFor(tableView, indexPath, item);

            return cell;
        }

        // # 2:
        protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
        {
            var cell = tableView.DequeueReusableCell(CustomCell.Key) as CustomCell;

            if (cell == null)
            {
                cell = CustomCell.Create();
            }

        // # 3:
            var bindable = cell as IMvxDataConsumer;
            if (bindable != null)
                bindable.DataContext = item;

            return cell;
        }

        // # 4:
        public override nint NumberOfSections(UITableView tableView)
        {
            return ItemsSource.Count();
        }

        // # 5:
        public override nint RowsInSection(UITableView tableview, nint section)
        {
            var group = ItemsSource.ElementAt((int)section) as MvxObservableCollection<CustomCellViewModel>;
            return group.Count();
        }

        public override string TitleForHeader(UITableView tableView, nint section)
        {
            return string.Format($"Header for section {section}");
        }
  1. Override the GetCell() method as you would normally do in a standard UITableView source. First of all, parse your ItemsSource (the default data source which your view model data is bound to, which is IEnumerable) to get the cell’s custom view model. Next, use the GetOrCreateCellFor() override to generate the cell to return.

  2. In a MvxTableViewSource you must override the GetOrCreateCellFor() method, as this is where your cell is set up and data bindings are connected. Because we’ve overridden GetCell(), we can pass the specific cell view model to the cell we’re creating.

  3. This chunk of code is vital to set up the bindings between the custom cell and the cell’s view model that you’ve passed in.

  4. and 5. As with a normal UITableViewSource, you’ll want to override these methods to explicitly control the number of sections and rows for each section.

I hope this guide provides enough information for you to implement your own grouped tables, if there’s anything that’s missing or if you have any questions, comment below or get in touch via Github.

By:

Posted in:


4 responses to “Grouped UITableViews with MVVMCross”

  1. I don’t understand how you get the selected cell on Selection.
    Always I’ve the collection of the group as selected object Item and not the selected Item from Cell.

    in your sample I return with following bind
    set.Bind(myTableViewSource).For(s => s.SelectionChangedCommand).To(vm => vm.ItemSelectedCommand);

    on selection Row 2 in Section 0

    3 Items
    firstGroup.Add(new CustomCellViewModel(“Section: 0, Row: 0”));
    firstGroup.Add(new CustomCellViewModel(“Section: 0, Row: 1”));
    firstGroup.Add(new CustomCellViewModel(“Section: 0, Row: 2”));

    Do you have any suggestions to get the selected cell?

    Like

    • You need to override RowSelected() in the UITableViewSource, this will give you your NSIndexPath which contains your section and row positions for the row that was selected. You can then query your collection using those. Something like:

      public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
      { …

      Like

Leave a comment