This article will cover how to build a Mac application with Mono.Mac, making use of XCode interface builder and NSComboBoxDataSource. TeaTimer will let us chose a tea variety and it will the time how long it will take to make it.
Open Xamarin studio and start a new project. Select a Mono.Mac application.
Open the MainWindow.xib, which will be our main application window. This will start up XCode interface builder. From the Object library, drag and drop onto the existing window:
- a combo box, which will display our tea menu,
- a button, to start the timer,
- two labels: one for displaying the current time and one for additional information
After this is done, we are ready to code.
In the AwakeFromNib method, which gets called after the view and containing subviews are initialized, let’s define our main objects, populate them with data and attach behaviour to them. First, we will need to define what kind of tea our application can make. Let’s hold all these varieties in a menu list.
teaOptions = new TeaList(); teaOptions.DefineTeaVarieties ();
Once we made our choice, we need to set up a time, which will begin when we press the “Start” button.
StartButton.Activated += (object sender, EventArgs e) => { StartCountDown (); };
We will have two kinds of tea: green, which is ready in 10 minutes, and black, which is ready in 5 minutes. We can see each variety is described by a name and a time, so that can become our view model.
public class Tea { public string Name; public TimeSpan Duration; }
We can now use them to build our menu.
public TeaList () { teaOptions = new List (); } void DefineTeaVarieties () { teaOptions = new Dictionary<string, TimeSpan>(); teaOptions.Add ("Green Tea",new TimeSpan (0, 10, 0)); teaOptions.Add ("Black Tea",new TimeSpan (0, 5, 0)); }
Although now we build this menu in memory, the data might as well come from a database in the future. Extracting the essential methods from the tea list class into an interface makes sure that in the future the app won’t break if we want to change the data source.
public interface ITeaList { void Add(Tea tea); List GetTeaNamesList(); TimeSpan GetDurationForTea (string teaName); void DefineTeaVarieties (); }
After completing the TeaList class, we need to link it to the combo box, which will allow us select a tea name. So we will use the GetTeaNamesList method to populate the data source of the combo.
The TeaVarietiesDataSource extends the NSComboBoxDataSource class, which declares the methods that an NSComboBox object uses to access the contents of its data source object. We will need to override at least two method, one that gets the item count, and one that return an object at a certain index.
class TeaVarietiesDataSource : NSComboBoxDataSource { List varieties; public override int ItemCount (NSComboBox comboBox) {} public override NSObject ObjectValueForItem (NSComboBox comboBox, int index) {} }
The tea selection is now in place, so we can start build the timer. We create a Countdown class and pass it the time amount corresponding to the selected tea.
string varietyName = TeaChoicesCombo.DataSource.ObjectValueForItem (TeaChoicesCombo, TeaChoicesCombo.SelectedIndex).ToString(); //"Green tea" TimeSpan time = teaOptions.GetDurationForTea(varietyName); countdown = new Countdown (time, CountdownLabel, InfoLabel);
In the Coundown class, we have a reference to a a label that displays the current time and another label, which will display a message when the tea is ready. Each second, we need to subtract a second from the current time and update the time label, until the time is up.
private void StartCounting() { while (time.Seconds > 0) { UpdateCountdownLabel (); UpdateCounter (); } ShowDoneMessage (); }
This logic will be run on a separate thread, so the UI can still be responsive, in case we want to stop the current timer or start another one before the current’s one time is up. The timer thread will be sleeping for one second after each subtract operation.
private void UpdateCounter () { Thread.Sleep (timeUnit); time = time.Subtract (timeUnit); }
Because we are running on a separate thread, we must be mindful that UI objects can be only modified from the main thread.
private void UpdateCountdownLabel () { countdownLabel.InvokeOnMainThread (() => { countdownLabel.StringValue = time.ToString (); }); } void ShowDoneMessage () { infoLabel.InvokeOnMainThread (() => { infoLabel.StringValue = "Tea is ready!"; countdownLabel.StringValue = ""; }); }
To actually start the timer, we will need to create a new thread, which will call the StartCounting method when it is activated.
public Thread CountdownThread = null; CountdownThread = new Thread( new ThreadStart (this.StartCounting) ); CountdownThread.Start ();
When the StartCounting method returns, this thread is closed. If we want to close it before the method return, we need to pass in a flag that will be permanently checked. As long as it’s true, continue to update the timer. If it changes to false, politely ask the thread to kill itself.
private volatile bool pleaseStop; private void StartCounting() { while (time.Seconds > 0) { if (pleaseStop) return; //... } }
From the main thread, we will check if the timer thread is alive, and if it is, we will call the Stop function of the timer, which will change the flag value to false.
//In TeaTimer.cs private void StopPreviousCounter () { if(countdown.GetCountdownThread().IsAlive) { countdown.Stop (); } } //In Countdown.cs public void Stop() { pleaseStop = true; }
Tea is ready! We now have a functional application, which times how quick our tea will be ready and displays a message afterwards.
You can get the full source code here.