.Net Timer or Cocoa NSTimer for MonoMac
Jan 10
.Net, C#, Cocoa, MonoMac 3 Comments
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.
RSS
Twitter
Mar 21, 2011 @ 00:12:38
The .NET timer callback runs in a different thread, while the NSTimer selector runs in the main thread. Hence the graphical glitches with the .NET timer. You can use InvokeOnMainThread.
Mar 23, 2011 @ 22:17:53
Hello Jamie
Thanks for the feedback.
I really need to update this post. Also wrapping in a NSAutoreleasePool should be done so as not to leak memory as follows:
using (NSAutoreleasePool pool = new NSAutoreleasePool ()) {
// update main thread
}
Kenneth
Jun 18, 2012 @ 11:22:45
You might want to spend a bit more time getting into what the “Main Thread” is in a cocoa application. In a WinForms application, every form on the screen has its own thread; and the form can only be modified within its thread. In a cocoa application, the “Main Thread” is very similar; except that the entire UI shares a single thread.
Manipulating a WinForms form from a System.Threading.Timer will always throw an exception; that’s why there’s the System.Windows.Forms Timer class and the Invoke method. Cocoa applications don’t throw an exception when their forms are manipulated from another thread; but you will essentially create race conditions when you do so. That’s why you need to use the NSTimer; much like how you will either use Invoke or System.Windows.Timer in Winforms.