Click here to Skip to main content
15,867,704 members
Articles / Programming Languages / C#
Article

Towards Cleaner Code II, a C# GUI Invoke/Async Helper

Rate me:
Please Sign up or sign in to vote.
4.82/5 (25 votes)
1 Oct 2008CPOL6 min read 74.2K   447   47   12
An async helper class in C# with GUI Invoke capability, that greatly reduces the code required to invoke and track tasks that affect forms and controls.

Introduction - Threading and User Interfaces

When doing GUI development on Windows you must very careful what thread you access a form or control from. Hopefully most Windows Forms developers know this by now, but in the early versions of the framework it could be a sneaky issue. As of .NET 2.0 they were kind enough to immediately cause an exception when a control was accessed from the wrong thread — forcing you to deal with it now instead of trying to figure out why you get exceptions like this 6 weeks after deploying your application:

c-error.png

I wont delve much in to why this is necessary, there are plenty of explanations on the subject — including many articles on CodeProject. The short story is that in .NET there is still a message loop that is running on the thread your form was run from, access from any other thread just isn’t thread-safe — you can’t use locking because it would block the user interface.

I'm a big fan of using threading in order to keep a User Interface responsive during long tasks and this issue comes up often. Let’s append the Async helper static class that we worked on in Part 1, giving it the ability to schedule tasks on the UI thread. This will go even further towards making an app cleaner and more maintainable.

Background

This is the second part in a series finding areas of necessarily messy or repeated code and find ways to make them more elegant and maintainable. We will discuss the old way, the new way with this helper class, and take a really quick look at the code behind it.

The Conventional way of Doing Things

Let's say we want to update a TextBox in our application. Following with tradition I'll show a few ways to accomplish this, and then how we can do better. Here is what we are trying to do, update the text of a TextBox:

C#
private void UpdateText(string myString){
    textBox1.Text = myString;
}

Simple as can be. The problem is when we want to be able to safely run it from anywhere (any thread) in our code. One way is to check if an invoke is required each time we run it. In this case I’m using anonymous methods to keep things as clean as possible.

C#
// ... //

// ask the control if we are on its UI thread
if(textBox1.InvokeRequired){
   //we are not, so an invoke is required.
   textBox1.Invoke(delegate{UpdateText("This is text");});
}else{
   //we are on the UI thread, we can directly modify the control
   UpdateText("This is text");
}

// ... //

A safer way is to build the invoke into the method itself, ensuring we don't mistakenly run it without checking our thread. Note that this time I am using BeginInvoke, the same concept as before, but it does not block — it runs the task asynchronously.

C#
private void SafeUpdateText(string myString){
	if(textBox1.InvokeRequired){
		//Invoke into this same method, but this time on the correct thread.
		textBox1.BeginInvoke(delegate{UpdateText(myString);});
		return; //important, so we don't fall through
	}
	//this only runs if the above check says an invoke is not required.
	textBox1.Text = myString;
}

Hopefully that isn’t too confusing. If our control's InvokeRequired is true we invoke our own method recursively, knowing that it will be on the right thread on the next time through. This isn’t too bad, but it can be a serious pain to make a method for every operation you need to do — once again its more things to remember and maintain, and more bloat for larger applications. In an application I recently refactored there were several dozen methods like this I was able to eliminate.

The Solution - Using the Code

So now let’s fix up our Async class to make this whole process easier. I decided to make a separate method for this, Async.Do had enough overloads as it was and I wanted it to be very clear at a glance whether the task was UI related. I’ll start with usage. Instead of all of the above, we can now do this:

C#
Async.UI(delegate { textBox1.Text = "This is way easier!"; }, textBox1, true);

Once again, something that might have needed a separate method with 5 to 10 lines of code, can know be distilled into a quick single method that takes care of the details for us. Here's the method prototype: public static AsyncRes UI(Dlg d, Control c, bool asynchronous); Let's check to see what those arguments are.

Our first argument is a delegate, same as we've done before — it can be an actual delegate instance, an anonymous method as above, or even a method without a parameter which .NET will silently wrap into a delegate.

The second parameter is the Control we'd like to invoke with. It can be the control you are accessing, our any of it's parent controls/forms. Of course in the method you pass, you can modify as many controls as you like, provided they are all from the same form / UI thread.

Third is a bool that if true, tells the method to use BeginInvoke to make the call non-blocking. Otherwise the caller will block while the UI thread finds time to run your task.

I, For One, Welcome our New UI Method Overloads

We have two overloads with more options, first, here are the prototypes:

C#
// Now with the ability to grab a return value for you
public static AsyncRes UI(DlgR d, bool getRetVal, Control c, bool async);

// And the last one lets you pass a state object that is returned on an EndInvoke,
// as well as control reentrance
public static AsyncRes UI(DlgR d, bool getRetVal, Control c, object state, bool async,
    ReenteranceMode rMode);

Got all that? It should all be pretty straight-forward. The ReentranceMode enum is not as relevant as before because with UI tasks we are typically invoking tasks on to the same UI thread, as opposed to their own thread — so with this class it is not normally possible for two tasks to run in parallel. I’ll demonstrate using the last overload with the option to obtain a return value.

C#
// ... //

AsyncRes result = Async.UI(
	//make sure the delegate/method returns a value:
	delegate { return textBox1.Text; },
	true, //yes, we want to get the return value
	myForm, //the control to invoke on
	null, //the state object, we don't need to track anything.
	true, //invoke asynchronously?
	ReenteranceMode.Allow); //don't worry about thread safety in this case.

// .... do other things ... //

// now make sure the task above has completed.. 
result.AsyncWaitHandle.WaitOne();

//and use the value
Console.WriteLine("The textbox says: " + result.ReturnValue);

// ... //

That’s about as complicated as it gets now; even the most advanced usage of the new class is simpler than invoking the most basic UI tasks without it. The call to WaitOne is only needed because we chose to invoke asynchronously — we need to make sure it has completed or our value might be null. If we had passed a false to the async parameter the call would have blocked and the value would immediately be guaranteed to be there.

The AsyncResult instance that is returned allows you to wait on the task to complete, get the return value of your method or delegate, get statistics like when the task started or ended, and also a reference to the Control you invoked.

Points of Interest

This functionality has been added to the same Async class we worked with, and it makes use of much of the same code: All of our UI methods simply call the Do method we worked with before, which has been expanded slightly. The only other change is adding all of the UI overloads, each one simply calls Do with various parameters.

The project that we are adding onto was designed for async tasks in a non-UI environment, usage is explained in Part 1, and the code is discussed on my blog.

We were able to add quite a bit of functionality to this handy class, with only a few lines of code!

Again, let me know if you have any problems, comments, suggestions, questions, compliments, or threats of bodily harm — I’ll take them in stride. Hopefully this will help keep your Windows Forms apps well refactored and easy to maintain.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect CodeToast.com
United States United States
Nicholas is the Chief Developer at SAM Systems, a software company that specializes in video surveillance, compression, and streaming. We have a full surveillance and compression platform for Windows, with SDK: www.samipvideo.com.

Nick is especially interested in high-performance, multi-threaded/parallel code, video processing, cross-platform development, and GUI design. He maintains a blog about software development and business at www.codetoast.com - check there for more detail on articles posted here, and other musings about the technology world.

I also play guitar, collect toasters and dead hard drives, and apparently like writing overly formal bio's in the third person.

Comments and Discussions

 
GeneralIt's nice when you've Button , but what about Pin
Marceli2823-Nov-08 1:49
Marceli2823-Nov-08 1:49 
GeneralRe: It's nice when you've Button , but what about Pin
Marceli2823-Nov-08 3:33
Marceli2823-Nov-08 3:33 
QuestionSafeUpdateText ot UpdateText? Pin
ozbear7-Oct-08 12:29
ozbear7-Oct-08 12:29 
AnswerRe: SafeUpdateText ot UpdateText? Pin
Nicholas Brookins7-Oct-08 12:38
Nicholas Brookins7-Oct-08 12:38 
GeneralCompiler error Pin
W. Kleinschmit4-Oct-08 3:40
W. Kleinschmit4-Oct-08 3:40 
GeneralRe: Compiler error Pin
Nicholas Brookins6-Oct-08 6:31
Nicholas Brookins6-Oct-08 6:31 
GeneralLink typo Pin
faulty4-Oct-08 0:23
faulty4-Oct-08 0:23 
The link to your "my blog" is pointing to "http://www.codeproject.com/KB/threads/www.codetoast.com/blog/archives/46", my guess it's just typo.

Anyway, good article


GeneralRe: Link typo Pin
Nicholas Brookins6-Oct-08 6:13
Nicholas Brookins6-Oct-08 6:13 
GeneralBeginInvoke is dangerous Pin
jpmik2-Oct-08 10:51
jpmik2-Oct-08 10:51 
GeneralRe: BeginInvoke is dangerous Pin
Nicholas Brookins3-Oct-08 2:47
Nicholas Brookins3-Oct-08 2:47 
QuestionIs there any way to find out if BeginInvoke will work (regardless of whether it's required)? Pin
supercat92-Oct-08 8:17
supercat92-Oct-08 8:17 
AnswerRe: Is there any way to find out if BeginInvoke will work (regardless of whether it's required)? Pin
Nicholas Brookins2-Oct-08 8:38
Nicholas Brookins2-Oct-08 8:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.