Mono

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

Brian Long Consultancy & Training Services Ltd.
February 2012

Accompanying source files available through this download link

Page selection: Previous  Next

Navigation-Based Applications

The Single View Application we've created has all the functionality in a single view. You can launch additional views using buttons or other UI mechanisms, but then you need 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. iOS apps 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. Another one of the MonoTouch application templates allows us to work with this, so create a new solution choosing a Master-Detail Application from the C#, MonoTouch, iPhone section of MonoDevelop's new solution dialog.

The project we get from this template has a Main.cs and AppDelegate.cs as before, but now has two sets of UI-related files.

The RootViewController.* files represent the main screen, which is set up to use a Table View (UITableView ), controlled by a table view controller (UITableViewController). This is 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.

The table can be populated as required but by default has just one value in it, the string Detail. When this table cell is touched it launches the detail view represented by the DetailViewController.* files. This detail view is a placeholder for some subsidiary screen, and we can later add additional screens that can be launched from the root view controller, or from elsewhere.

The project's app delegate sets up a navigation controller (UINavigationController) and ensures we get a Navigation Bar to go navigate through the application's screens.

Loading RootViewController.xib into Xcode's Interface Builder reveals just the Table View, which is shown populated with sample data.

Table View in Interface Builder

You can load DetailViewController.xib into Xcode's Interface Builder to design this secondary screen.

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.cs (not to be confused with the code behind file, RootViewController.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 window 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();
    }
    
    TableView.Source = new DataSource(this);
}
 
public override void ViewDidUnload ()
{
    //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 ();
    
    // Clear any references to subviews of the main view in order to
    // allow the Garbage Collector to collect them sooner.
    //
    // e.g. myOutlet.Dispose (); myOutlet = null;
    
    ReleaseDesignerOutlets ();
}

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 (clear out the code currently in the method) 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