Brian Long Consultancy & Training Services
Ltd.
March 2011
Accompanying source files available through this
download link
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.
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).
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();
}
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:
Go back to the top of this page
Go back to start of this article