Mono

Using C# to Develop for iPhone, iPod Touch and iPad

Brian Long Consultancy & Training Services Ltd.
March 2011

Accompanying source files available through this download link

Page selection: Previous  Next

View Controllers

The simple application above has all the functionality in the App Delegate, which purists might suggest is best left to act as a delegate for the CocoaTouch Application object. Typical applications are more likely to use one or more view controllers (UIViewController or a descendant) as delegates for views on the various windows in the application. You get a view controller in the application if you start with the iPhone Navigation-based Project template or iPhone Utility Project template in MonoDevelop. Let’s make a new Navigation-based project.

The project we get from this template has a window and an App Delegate as before, but importantly also has a Navigation Controller, which works with a Navigation Bar. The idea of this is to support the common workflow in an application of going from one screen to another, and then maybe to another, etc., and being able to readily navigate back to any of those earlier screens. iPhones facilitate this using a Navigation Bar under the control of a Navigation Controller. The Navigation Bar reflects which screen you are on, where each of the navigable screens is actually a UIView descendant.

Note: when you double-click MainWindow.xib there are potentially two UI windows opened up by Interface Builder, as the .xib file defines both the main window, which is completely blank, and also the Navigation Controller, which has the Navigation bar etc. on. You can readily open up whichever one you choose using the Document Window.

The template sets us up a UITableView as a starting view with a corresponding UITableViewController, suitable for showing a very customizable list in a manner iPhone users will be very familiar with. As items are selected in the table (or list) the application has the option to navigate to other pages.

When you look at the two .xib files in Interface Builder you see the blue Navigation Bar at the top of the main window (you can give it some text by double-clicking it) as well as an indication that the rest of the window content comes from RootViewController.xib. This latter nib file file just contains a Table View, which is shown populated with sample data.

Navigation controller in Interface BuilderTable View in Interface Builder

We’ll see how this UITableView works by displaying some information from an SQLite database. The coding will take place in the source file that is associated with the Table View nib file: RootViewController.xib.cs (not to be confused with the code behind file, RootViewController.xib.designer.cs).

Using SQLite

Before worrying about the table, we’ll get some code in place to create a database, a table and some sample data when the main Table View is loaded. To keep things tidy we’ll also delete the database when it unloads, though clearly a real application may need to keep its database around between invocations. The contents of the database table will be read from the database and stored in a strongly typed list. Again, consideration should be given to memory requirements in a real application; in this sample there will only be a handful of records.

Since the list is to be strongly typed we’ll need a type to represent the data being read:

public class Customer
{
    public Customer ()
    {
    }
    
    public int    CustID    { get; set; }    
    public string FirstName { get; set; }    
    public string LastName  { get; set; }    
    public string Town      { get; set; }
}

The ViewDidLoad() and ViewDidUnload() overridden methods are already present in the template project so here’s the extra code that uses standard ADO.NET techniques with the Mono SQLite database types.

Note: This code requires you to edit the References used by the project (right-click on the References node in the project in the Solution Explorer and choose Edit References...) and then add in System.Data and Mono.Data.Sqlite to the list.

using System.Data;
using System.Collections.Generic;
using System.IO;
using Mono.Data.Sqlite;
...
SqliteConnection connection;
string dbPath;
List<Customer> customerList;
...
public override void ViewDidLoad()
{
    base.ViewDidLoad();
    
    //Create the DB and insert some rows
    var documents = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
    dbPath = Path.Combine(documents, "NavTestDB.db3");
    var dbExists = File.Exists(dbPath);
    if (!dbExists)
        SqliteConnection.CreateFile(dbPath);
    connection = new SqliteConnection("Data Source=" + dbPath);
    try
    {
        connection.Open();
        using (SqliteCommand cmd = connection.CreateCommand())
        {
            cmd.CommandType = CommandType.Text;
            if (!dbExists)
            {
                const string TblColDefs = 
                    " Customers (CustID INTEGER NOT NULL, FirstName ntext, LastName ntext, Town ntext)";
                const string TblCols = 
                    " Customers (CustID, FirstName, LastName, Town) ";
                
                string[] statements = {
                    "CREATE TABLE" + TblColDefs,
                    "INSERT INTO" + TblCols + "VALUES (1, 'John', 'Smith', 'Manchester')",
                    "INSERT INTO" + TblCols + "VALUES (2, 'John', 'Doe', 'Dorchester')",
                    "INSERT INTO" + TblCols + "VALUES (3, 'Fred', 'Bloggs', 'Winchester')",
                    "INSERT INTO" + TblCols + "VALUES (4, 'Walter P.', 'Jabsco', 'Ilchester')",
                    "INSERT INTO" + TblCols + "VALUES (5, 'Jane', 'Smith', 'Silchester')",
                    "INSERT INTO" + TblCols + "VALUES (6, 'Raymond', 'Luxury-Yacht', 'Colchester')" };
                foreach (string stmt in statements)
                {
                    cmd.CommandText = stmt;
                    cmd.ExecuteNonQuery();
                }
            }
            customerList = new List<Customer>();
            cmd.CommandText = "SELECT CustID, FirstName, LastName, Town FROM Customers ORDER BY LastName";
            using (SqliteDataReader reader = cmd.ExecuteReader())
            {
                while (reader.Read())
                {
                    var cust = new Customer 
                    { 
                        CustID = Convert.ToInt32(reader["CustID"]), 
                        FirstName = reader["FirstName"].ToString(),
                        LastName = reader["LastName"].ToString(),
                        Town = reader["Town"].ToString()
                    };
                    customerList.Add(cust);
                }
            }
        }
    } catch (Exception)
    {
        connection.Close();
    }
    
    this.TableView.Source = new DataSource(this);
}
 
public override void ViewDidUnload()
{
    // Release anything that can be recreated in viewDidLoad or on demand.
    // e.g. this.myOutlet = null;
    
    //Delete the sample DB. Pointlessly kill table in the DB first.
    using (SqliteCommand cmd = connection.CreateCommand())
    {
        cmd.CommandText = "DROP TABLE IF EXISTS Customers";
        cmd.CommandType = CommandType.Text;
        connection.Open();
        cmd.ExecuteNonQuery();
        connection.Close();
    }
    File.Delete(dbPath);
    
    base.ViewDidUnload();
}

Table View Data Source

After all that code that’s the Table View itself done. The remaining work is done in the Table View’s DataSource class, a descendant of UITableViewsource. You’ll notice a DataSource object being set up at the end of the template code in ViewDidLoad(), though in the sample project it has been renamed to CustomerDataSource. The data source class is set up in the template as a nested class defined within the Table View controller with a number of its virtual methods already overridden for you.

Tables can be split into multiple sections, each (optionally) with its own header. Our customer list will not need additional sections so NumberOfSections() should return 1. To tell the Table View how many rows should be displayed in this single section, RowsInSection() should return controller.customerList.Count (controller is set in the constructor, giving access to the view controller). To give the section a header you need to override the method TitleForHeader().

Overriding virtual methods is easy in MonoDevelop; type in override and start typing the method name and the Code Completion window will appear showing your options. Have it return the string Customers.

To populate the cells we use the GetCell() method, whose parameters are the Table View and the cell’s index path (the section number and row number within the section given by the Section and Row properties). The first thing to note about the code below is the innate support for virtual lists through reusable cells. If you wanted to display a very long list it may not be practical to create a UITableViewCell for every item due to the memory usage required. Instead you can take advantage of the Table View offering any cell that is scrolled off-screen as reusable. You can have various categories of reusable cells by simply using different cell identifiers.

public override UITableViewCell GetCell(UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
{
    string cellIdentifier = "Cell";
    var cell = tableView.DequeueReusableCell(cellIdentifier);
    if (cell == null)
    {
        cell = new UITableViewCell(UITableViewCellStyle.Subtitle, cellIdentifier);
        //Add in a detail disclosure icon to each cell
        cell.Accessory = UITableViewCellAccessory.DetailDisclosureButton;
    }
    
    // Configure the cell.
    var cust = controller.customerList[indexPath.Row];
    cell.TextLabel.Text = String.Format("{0} {1}", cust.FirstName, cust.LastName);
    cell.DetailTextLabel.Text = cust.Town;
 
    return cell;
}

This code creates cells that permit a text value and an additional smaller piece of text (a subtitle). These are accessed through the TextLabel and DetailTextLabel properties respectively.

During the cell setup a detail disclosure button is also added in. This adds in a little arrow in a circle on the right side of each cell. This then gives us two possible actions from the user: they can tap the row in general, which triggers RowSelected(), or tap the disclosure button, which triggers AccessoryButtonTapped(). Often, RowSelected() is used take you to another screen, so in this case we will leave RowSelected() doing nothing and just support the disclosure button, which issue’s an alert displaying some information about the selected customer. However, it is down to you to check Apple's Human Interface guidelines and decide whether ignoring RowSelected() is acceptable practice.

public override void AccessoryButtonTapped(UITableView tableView, NSIndexPath indexPath)
{
    var cust = controller.customerList[indexPath.Row];
    InfoAlert(string.Format("{0} {1} has ID {2}", cust.FirstName, cust.LastName, cust.CustID));
}

All of which gives us this application:

SQLite application running in the iPhone Simulator

Go back to the top of this page

Go back to start of this article

Previous page

Next page