I was going to go over this in the articles on KVC, KVO and Cocoa Bindings but since it was touched on in the mono-osx mailing list decided to write about my experience with using these two. Hopefully my experiences with them will help you decide which one to use and when.

MonoMac has been very enjoyable to work with over the last few months but some things just catch you off guard and cause some lost of time. One of those has been the use of the .Net Timer which has lead to lost time debugging when dealing with Cocoa on the Mac.

The following discussion uses the TestTimer code from here TestTimer solution file.

Code snippet:

		NSTimer myNSTimer;
		Timer myTimer;
 
                // lines truncated here
 
		public override void AwakeFromNib ()
		{
//			myNSTimer = NSTimer.CreateRepeatingScheduledTimer(1,delegate { 
//				timerLabel.StringValue = DateTime.Now.ToLongTimeString();
//			});
 
			myTimer = new Timer(1000);
			myTimer.Elapsed += delegate {
 
				timerLabel.StringValue = DateTime.Now.ToLongTimeString();
			};
			myTimer.Start();
 
			// Don't forget to stop timer
			Window.WillClose += delegate {
				//myNSTimer.Invalidate();
				myTimer.Stop();
			};
		}

NSAutoreleasePool

The first noticeable difference arises when using the .Net Timer is associated with messages about NSAutoreleasePool

2011-01-10 11:12:33.972 TimerTest[17251:7e03] *** __NSAutoreleaseNoPool(): Object 0x551ce0 of class NSCFString autoreleased with no pool in place - just leaking

When a MonoMac application, and MonoTouch for that matter, are executed the NSAutoreleasePool is handled within the main thread. If you start another thread, like with a .Net Timer, that uses Cocoa objects then you will need to manage this pool yourself.

It is no big deal to get rid of these messages by wrapping the code as follows:

using (NSAutoreleasePool pool = new NSAutoreleasePool ()) {
   // your code here
}

So the snippet above for the .Net Timer becomes the following:

			mytimer = new Timer(1000);
			myTimer.Elapsed += delegate {
			t.Elapsed += delegate {
				using (NSAutoreleasePool pool = new NSAutoreleasePool()) {
        				timerLabel.StringValue = DateTime.Now.ToLongTimeString();
                                }
			};
			myTimer.Start();

The code for NSTimer stays the same because when a NSTimer starts up it is automatically attached to the NSApplication loop. As per Apple’s docs – “For applications built using the Application Kit, the NSApplication object runs the main thread’s run loop for you.”

Six one half a dozen another. Here you can say it is a matter of preference as they work the same. Not sure about the Performance or Memory cost of creating the NSAutoreleasePool if the timer is set with a smaller interval. Probably there is no difference.

Intermittent graphical glitches

There were a couple of times when I tried using a .Net Timer where the interface was not being updated correctly or would just stop updating. It could be because the program was using Layer Views and truthfully am not sure. Never did pin it down nor able to recreate it reliably so did not report it as a bug.

Switched to a NSTimer and never looked back. So if you seem to be having problems with your user interface and it is being updated via a .Net Timer try using a NSTimer to see if it fixes the problem.

Of course these issues could be fixed now as this was a month or two back. Have not gone back to try.

Cocoa Bindings

Using manual Bindings, not through IB, where you are handling the property change events within your own custom class just plain does not work with a .Net Timer.

For an example of this take a look at the sample Glassy Clock.

Replace the code in ClockTimer.cs with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using System;
using System.Timers;
 
using MonoMac.Foundation;
using MonoMac.AppKit;
using MonoMac.CoreAnimation;
 
namespace GlossyClock
{
	public class ClockTimer : NSObject
	{
		//NSTimer myTTimer;
		Timer myTTimer;
		string property;
 
		public ClockTimer () : base()
		{
			outputString = DateTime.Now.ToString("hh:mm:ss");
			myTTimer = new Timer(1000);
			myTTimer.Elapsed += delegate { 
				using (NSAutoreleasePool pool = new NSAutoreleasePool()) {
					outputString = DateTime.Now.ToString("hh:mm:ss");
				}
 
				//outputString = DateTime.Now.ToString("hh:mm:ss");
			};
//			myTTimer = NSTimer.CreateRepeatingScheduledTimer(1,delegate { 
//				outputString = DateTime.Now.ToString("hh:mm:ss");
//			});
			myTTimer.Start();
		}
 
		[Export("outputString")]
		public string outputString
		{
			get 
			{ 
				return property; 
			}
			set 
			{ 
				WillChangeValue("outputString");
				property = value;
				DidChangeValue("outputString");
			}
 
		}
 
	}
}

The above code replaces the NSTimer with a .Net Timer for comparison.

The outputString property is bound to the clockFaceLayer in setupClockFaceLayer method as follows:

		private CALayer setupClockFaceLayer()
		{
			////// Code Truncated //////
 
			clockFaceLayer.Bind("string",clockTimer,"outputString", null);
 
			////// Code Truncated //////
		}

If you run the program with the modified ClockTimer code the string does not get updated. Now compare with the NSTimer and it should work.

Oh and before I forget the GlossyClock sample will only work with monomac git master code or the new MonoMac add-in that should be out today.

So here there is only one way for it to work and that is to use a NSTimer.

Is this a bug in MonoMac? If it is then please feel free to fill out a bug report for them. As for me the NSTimer is a perfectly good alternative.

Summary

If your timer code is not dealing with Cocoa gui controls then .Net Timer will be a good way to keep your program fully .Net.

If not then keep the following in mind.

  • NSAutoreleasePool – Matter of taste.
  • Intermittent graphical glitches – It depends and milage may vary. Go with the one that is sure to work or try the .Net Timer and if you run into problems then switch.
  • Cocoa Bindings – There is only one way right now and that is by using NSTimer.

I hope this has been helpful.