Digging Deeper into Xamarin iOS Table Views with MVVM Light

This post continues on from the previous post Xamarin iOS Table Views and MVVM Light.

It is part of a series of posts investigating how to use the MVVM pattern on the Xamarin platforms using shared View Models/Models/Services.

Update 9th July 2016:- A new release of MVVM Light version 5.3 provides a ObservableTableViewSource class for Xamarin.iOS. This class allows the UITableViewController to be used correctly and removes for the workaround solutions described in this post. Please see the post Using the ObservableTableViewSource in MVVM Light V5.3 with Xamarin.iOS

Overview

In this post we delve a little deeper into using MVVM Light with Xamarin iOS TableViews, specifically how to customise the TableView to enable swipe to delete.

In attempting to enable this feature I ran into a shortcoming with the implementation of the MVVM Light ObservableTableViewController. This class derives from the UITableViewController, which provides as a convenience the methods CommitEditingStyle and CanEditRow amongst others. These methods are used to enable the swipe to delete but I found that attempting to derive from ObservableTableViewController and then overriding these methods didn’t work. I may have misunderstood something but ultimately ended up copying the MVVM Light classes and modifying them slightly to implement the swipe to delete feature.

Resources

Key Points

  • The MVVM Light ObservableTableViewController uses the class ObservableTableSource which derives from UITableViewSource. This base class contains the virtual methods CommitEditingStyle and CanEditRow which the UITableViewController also provides as a convenience.

  • To implement the swipe to delete was in fact relatively straight forward, I copied the ObservableTableViewController and modified the ObservableTableSource class to be public and made some minor modifications to the ObservableTableViewController so support this, mainly changing some private members to protected and protected methods to public. I could then derive from the ObservableTableSource and implement the overrides of the CommitEditingStyle and CanEditRow methods and derive from ObservableTableViewController and override the CreateSource method to create the derived ObservableTableSource.

The Example Step by Step

The example app extends the previous app with swipe to delete.

The code for this example is in GitHub.

The class names I have used are somewhat long but they reflect their intention and the source they derive from.

Taking the source code for the previous post as a starting point download the MVVM Light source code and copy the ObservableTableViewController class from the project Galasoft.MvvmLight.Platform (iOS) in the Helpers folder to the ObservableTables.iOS project. Extract the class ObservableTableSource and place in it’s own file.

The code listings that follow included the original commented out code so that the changes can be easily seen, they are in fact minimal.

The class diagram below reflects the new structure:-

Edit the ObservableTableViewController class with the following changes:-

Change the _tableSource to protected.

/*private*/ protected ObservableTableSource<T> _tableSource;

Change the BindCell method to public.

/*protected*/ public virtual void BindCell(UITableViewCell cell,
                                           object item,
                                           NSIndexPath indexPath)

Change the CreateCell method to public.

/*protected*/ public virtual UITableViewCell CreateCell(NSString reuseId)

Change the OnRowSelected method to public.

/*protected*/ public virtual void OnRowSelected(object item,
                                                NSIndexPath indexPath)

Edit the ObservableTableSource class with the following changes:-

Change the class to public.

/*protected*/ public class ObservableTableSource<T2> : UITableViewSource

Change the _controller to public.

/*private*/ protected readonly ObservableTableViewController<T2> _controller;

Change the GetCell method to use the DataSource property

public override UITableViewCell GetCell(UITableView tableView,  
                                        NSIndexPath indexPath)
{
  var cell = tableView.DequeueReusableCell(_reuseId) ??
  _controller.CreateCell(_reuseId);

  try
  {
    //var coll = _controller._dataSource;
    var coll = _controller.DataSource;

Change the RowSelected method to use the DataSource property.

public override void RowSelected(UITableView tableView,  
                                 NSIndexPath indexPath)
{
  //var item = _controller._dataSource != null ?
                 _controller._dataSource[indexPath.Row] : default(T2);
  var item = _controller.DataSource != null ?
               _controller.DataSource[indexPath.Row] : default(T2);

Change the RowsInSection method to use the DataSource property.

public override nint RowsInSection(UITableView tableView,  
                                   nint section)
{
  //var coll = _controller._dataSource;
  var coll = _controller.DataSource;

Now create a class TaskListObservableTableSource which derives from ObservableTableSource and adds overrides of the methods CanEditRow, CommitEditingStyle and EditingStyleForRow to implement the swipe to delete functionality.

using System;  
using UIKit;  
using Foundation;

namespace ObservableTables.iOS  
{
  public class TaskListObservableTableSource<T> :
                 ObservableTableSource<T>
  {
    public TaskListObservableTableSource(
             ObservableTableViewController<T> controller)
      : base(controller)
    {
    }

    public override bool CanEditRow(UITableView tableView,
                                    NSIndexPath indexPath)
    {
      return true;
    }

    public override void CommitEditingStyle(UITableView tableView,
                                            UITableViewCellEditingStyle editingStyle,
                                            NSIndexPath indexPath)
    {
      switch (editingStyle)
      {
        case UITableViewCellEditingStyle.Delete:
      // remove the item from the underlying data source
      _controller.DataSource.RemoveAt(indexPath.Row);
      // No need to delete the row from the table as the tableview
      // is bound to the data source
      break;
      }
  }

    public override UITableViewCellEditingStyle EditingStyleForRow(
                                                UITableView tableView,
                                                NSIndexPath indexPath)
    {
      return UITableViewCellEditingStyle.Delete;
    }
  }
}

Next create a class TaskListObservableTableViewController which derives from ObservableTableViewController. This class will override the CreateSource method to create a new instance of the TaskListObservableTableSource.

using UIKit;

namespace ObservableTables.iOS  
{
  public class TaskListObservableTableViewController<T> :
                 ObservableTableViewController<T>
  {
    public TaskListObservableTableViewController()
            : base()
    {
    }

    public TaskListObservableTableViewController(
             UITableViewStyle tableStyle)
            : base(tableStyle)
    {
    }

    protected override ObservableTableSource<T> CreateSource()
    {
      _tableSource = new TaskListObservableTableSource<T>(this);
      return _tableSource;
    }
  }
}

Create a static extensions class named ExtensionsMvvmLight and create an extension method GetTaskListController based on the GetController method in the ExtensionsApple class in the MVVM Light Galasoft.MvvmLight.Platform (iOS) project Helpers folder. This method is called in the TaskListViewController to create the TaskListObservableViewController.

using System;  
using System.Collections.Generic;  
using Foundation;  
using UIKit;

namespace ObservableTables.iOS  
{
  public static class ExtensionsMvvmLight
  {
    public static TaskListObservableTableViewController<T>
                    GetTaskListController<T>(
            this IList<T> list,
            Func<NSString, UITableViewCell> createCellDelegate,
            Action<UITableViewCell, T, NSIndexPath> bindCellDelegate)
    {
      return new TaskListObservableTableViewController<T>
      {
        DataSource = list,
        CreateCellDelegate = createCellDelegate,
        BindCellDelegate = bindCellDelegate
      };
    }
  }
}

Edit the TaskListViewController class such that the private member variable tableViewController is now of the type TaskListObservableTableViewContoller and call the GetTaskListController extension method in the ViewDidLoad method:-

using Foundation;  
using System;  
using UIKit;  
using GalaSoft.MvvmLight.Helpers;

using ObservableTables.ViewModel;

namespace ObservableTables.iOS  
{
  partial class TaskListViewController : UIViewController
  {
    private TaskListObservableTableViewController<TaskModel>
              tableViewController;

    private TaskListViewModel Vm => Application.Locator.TaskList;

    public TaskListViewController (IntPtr handle) : base (handle)
    {
    }

    public UIBarButtonItem AddTaskButton
    {
      get;
      private set;
    }

    public override void ViewDidLoad()
    {
      base.ViewDidLoad();

      AddTaskButton = new UIBarButtonItem(UIBarButtonSystemItem.Add);
      this.NavigationItem.SetLeftBarButtonItem (AddTaskButton, false);

      //note this was needed when deploying to a real iPhone but worked
      //on the simulator without it.
      //The Argument Exception Event not found clicked was thrown
      AddTaskButton.Clicked += (sender, e) => {};
      AddTaskButton.SetCommand("Clicked", Vm.AddTaskCommand);

      tableViewController =
        Vm.TodoTasks.GetTaskListController(CreateTaskCell,
                                           BindTaskCell);
      tableViewController.TableView = TasksTableView;
    }

    private void BindTaskCell(UITableViewCell cell,
                              TaskModel taskModel,
                              NSIndexPath path)
    {
      cell.TextLabel.Text = taskModel.Name;
      cell.DetailTextLabel.Text = taskModel.Notes;
    }

    private UITableViewCell CreateTaskCell(NSString cellIdentifier)
    {
      var cell = new UITableViewCell(UITableViewCellStyle.Subtitle,
                                     null);
      cell.TextLabel.TextColor = UIColor.FromRGB(55, 63, 255);
      cell.DetailTextLabel.LineBreakMode = 
                               UILineBreakMode.TailTruncation;

      return cell;
    }
  }
}

This should complete the example which now has the swipe to delete feature.

Richard Woollcott

Read more posts by this author.

Taunton, United Kingdom