Updating resources on the main UI thread: OSX & C#

By reeset / On / In Uncategorized

One of the things that has been a learning experience for me has been understanding how OSX handles threading, understanding what objects are thread-safe, and how to capture user feedback from within an thread running outside of the main UI thread.

Conceptually, Windows, OSX, Linux — UIs tend to be run through a single, primary thread.  This is why long-running processes can lock a window from updating.  In Windows, this can be overcome by creating background threads.  While these threads should not interact with the main UI objects — you do have the ability to spawn a new UI thread that can be used to capture interaction from users.  You just need to be careful to ensure that objects stay compartmentalized since these objects are not explicitly thread-safe.

Within the OSX environment, the UI components cannot be run from outside of the main thread.  This means that if you have a background threading process, you cannot interact with the user because all UI objects are marked as not safe for threading.  Not very practical since there are times when user interaction is definitely necessary.  For example, within MarcEdit’s Mac port — I need to be able to tell users when the program has updated — and I’d like this process to occur outside the primary UI thread so not to slow down application startup or user interactions.  To do this, you need to find a way to thread switch — so that you can encapsulate the code that will interact with the UI around the necessary code to switch execution back to the main UI thread.  In C# and on Windows, you can take non-thread-safe components and using a delegate and Invoke, thread switch within your application.  OSX has something similar, but after hours of looking, I wanted to make sure I document how it works here, so I won’t have to look this up again.  The two functions are: InvokeOnMainThread and BeginInvokeOnMainThread.  These two functions have some limitations — they can only be run within the context of a UI thread (so a window controller because you need access to the NSWindow handle) — but from within this block, you have the ability to generate an NSAlert class (though tricky) as well as generate new NSWindow objects to capture user input.

Here’s an example of how this would work below.  This is the code used for MarcEdit’s Mac notification service.  The program starts a thread when the application loads the main window for the first time — that thread initiates a System.Net.HttpWebRequest object to retrieve information about the current MarcEdit Mac Build.  The program then parses the version information, compares it to the local version and build number — and if the remote build number is newer — generates a UI element that tells the user what has changed, and provides buttons to download the new application.  This code lives in a couple of different classes, but these are the relevant parts.

//MainWindowController

public override void AwakeFromNib ()
{
	this.Window.BackgroundColor = NSColor.White;
	base.Window.Center();
	base.AwakeFromNib ();
				
	System.Threading.Thread objT = new System.Threading.Thread((System.Threading.ThreadStart)delegate { CheckUpdate();});
	objT.Start ();

}

void CheckUpdate ()
{
	helpers objH = new helpers ();
	string data = objH.GetWebContent(clsGlobal.MacNotificationURI);
	string sversion = data.Split (System.Environment.NewLine.ToCharArray ()) [0].Trim();

	if (!string.IsNullOrEmpty (data)) {
		NSObject ver = NSBundle.MainBundle.InfoDictionary ["CFBundleVersion"];
		NSObject ver2 = NSBundle.MainBundle.InfoDictionary ["CFBundleShortVersionString"];

		Version vweb = new Version (sversion);
		Version vlocal = new Version (ver2.ToString() + "." + ver.ToString ());

		if (vweb > vlocal) {
			//To generate a UI element from outside the main UI thread, 
			//use InvokeOnMainThread and create a delegate function block
			InvokeOnMainThread (() => {
				frmUpdateNotificationController objU = new frmUpdateNotificationController();
				objU.Window.Center();
				objU.ShowWindow(this);
				objU.LoadMessage("Build #" + sversion + System.Environment.NewLine + System.Environment.NewLine + 
					data);
			});	
                 } 
	}
}

//helpers class

public string GetWebContent(string uri) {

	System.Net.HttpWebRequest myReq =
				(System.Net.HttpWebRequest)System.Net.WebRequest.Create (new Uri (uri));
	System.Net.HttpWebResponse response = (System.Net.HttpWebResponse)myReq.GetResponse ();
	System.IO.Stream responseStream = response.GetResponseStream ();
	System.IO.StreamReader reader = new System.IO.StreamReader (responseStream, System.Text.Encoding.UTF8);
	string val = reader.ReadToEnd ();
	reader.Close ();
	response.Close ();
	return val;
}