When I want to create a custom UITableViewCell in a MonoTouch app, I have been using the approach outlined in Simon Guindon's custom UITableViewCell tutorial that he wrote in 2009. I used that method of creating custom cells in my NDC 2010 open-source MonoTouch iPhone app. Craig Dunn's blog is a gold-mine for MonoTouch samples - and he too uses the same approach for creating custom cells.

In fact, almost every MonoTouch sample I have seen on the Internet uses the same approach. It seems to have become the "standard" or "accepted" way of doing custom cells in MonoTouch apps.

For some time now, I have had a feeling that this approach was not optimal and was somehow "wrong". It is essentially a UIViewController wrapped around a cell - the cell's Tag property being used to lookup the cell from a Dictionary. From the samples I have seen (and used myself) the Tag usually comes from Environment.TickCount.

Several things have always struck me as strange with this approach:

  • Sometimes during cell creation, the OS is so fast that the Environment.TickCount is the same for two cells created in quick succession - meaning we get a duplicate key exception for our Dictionary key!
  • I'm not sure we are supposed to be using the cell's Tag property for this purpose.
  • The GetCell method returns a UITableViewCell. Why am I dealing with UIViewController's wrapped around cells?
  • Why do I have to manage my own Dictionary to handle cell reuse - isn't the whole point of the tableView.DequeueReusableCell() method that it handles that for us?

With these questions in the back of my mind, I have always been uneasy about this approach, but never spent much time on improving it.

Miguel de Icaza discusses UITableViewCell's in a blog post, but he doesn't go into detail about how to design the cell in Interface Builder and instantiate it from a XIB file.

One of his comments on that blog post got me thinking, when he said: "I have no idea why anyone would create UIViewControllers for each cell. [...] It seems that those doing it are doing it because that is an easy way of instantiating the cell from a UI created in Interface Builder. They should have instead created a UITableViewCell template in IB, not a UIViewController that contained a cell."

I think this is what he was talking about...

A new approach

Add a new file to your solution for your custom table cell: "iPhone View". Give it the name "MyCustomCell".

NOTE: You are not adding "iPhone View with Controller" here. We aren't going to be seeing any form of UIViewController again :-)

Delete the UIView it gives you by default. Add a UITableViewCell.

With your UITableViewCell selected, go to the Inspector window in Interface Builder, and on the Attributes tab, give your cell an ID in the "Identifier" box. The ID can be anything, but it is important because we will use it again later so that cells can be reused during scrolling.

In the same window, but on the Identity tab, you will see a text box for "class". There, give it the name of your custom cell's class: "MyCustomCell". MonoTouch will generate a partial class with this name when we save and exit Interface Builder, and we will create the other half of that partial class later.

Use Interface Builder to design your cell: you probably want to add some UILabel's and other UI components.

To access those labels programmatically, create outlets for them and connect the outlets to the labels. In the library window, make sure that you are creating the outlets on the class that you chose earlier - and not on anything else (this often catches beginners out - creating the outlets on the wrong object).

You are done with Interface Builder now - so save and quit.

In the MyCustomCell.xib.designer.cs file, MonoTouch will have generated a partial class of type MyCustomCell. Don't touch that code - it will be auto-generated each time you make changes in Interface Builder.

You can and should change the MyCustomCell.xib.cs file, though. Create the other half of that partial class:

public partial class MyCustomCell : UITableViewCell
{
}

Notice that we inherit from UITableViewCell here - not UIViewController. You need to add that inheritance in yourself, because it won't be done automatically for you.

Add two constructors (you will need them both):

public MyCustomCell() : base()
{
}

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

I also like to put the logic for binding data to the cell in here rather than exposing each label and other UI components publicly via C# properties. I think this hides the nitty-gritty details from the outside world, and is "better practise" in terms of encapsulation. So I would also add a method like this:

public void BindDataToCell(Product product)
{
    productNameLabel.Text = product.Name;
}

In this example, however, I won't be sending in an object from my model, such as "Product" - I will just send in a string, but you get the idea.

Now jump over to your UITableViewSource class, where you should have a method called GetCell(). It is here where you will usually find the ugliness of Environment.TickCounts, UIViewControllers needlessly wrapped around cells, and Dictionaries hanging on to the cell UIViewControllers. But this time, we will not need any Dictionaries or UIViewControllers. Instead, we will use this code:

public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
    var cell = tableView.DequeueReusableCell("CellID") as MyCustomCell;
    
    if (cell == null)
    {
        cell = new MyCustomCell();
        var views = NSBundle.MainBundle.LoadNib("MyCustomCell", cell, null);
        cell = Runtime.GetNSObject( views.ValueAt(0) ) as MyCustomCell;
    }
    
    cell.BindDataToCell("You are on row " + indexPath.Row);
    
    return cell;
}

You will need this to make it compile:

using MonoTouch.ObjCRuntime;

A simple TableCellFactory

You wouldn't really want to copy and paste that code in every UITableViewSource you have throughout your app - because that would give us lots of code duplication. To avoid that, and add a little bit of elegance the code, I have taken the liberty to create a tiny, light-weight and generically-typed factory class which will take care of this cell-reuse and XIB-loading business for you:

public class TableCellFactory<T> where T : UITableViewCell
{
    private string cellId;
    private string nibName;
    
    public TableCellFactory(string cellId, string nibName)
    {
        this.cellId = cellId;
        this.nibName = nibName;
    }
    
    public T GetCell(UITableView tableView)
    {
        var cell = tableView.DequeueReusableCell(cellId) as T;
            
        if (cell == null)
        {
            cell = Activator.CreateInstance<T>();
            var views = NSBundle.MainBundle.LoadNib(nibName, cell, null);
            cell = Runtime.GetNSObject( views.ValueAt(0) ) as T;
        }
        
        return cell;
    }
}

If you put that somewhere in your iPhone app, you can reduce your GetCell() method to this:

public class MyTableSource : UITableViewSource
{
    private TableCellFactory<MyCustomCell> factory = new TableCellFactory<MyCustomCell>("CellID", "MyCustomCell");
    
    public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
    {
        var cell = factory.GetCell(tableView);
        cell.BindDataToCell("some data");
                
        return cell;
    }
}

I have a fully-working sample over on github, called MonoTouch.CustomTableCells.

These links also helped me out out:

I would appreciate feedback and discussion, and any improvements or tips, so please leave a comment if you have anything to add. Good luck!