AsyncOperation Marshalling
I'm going to be open with you: this technique is at the core of the Event-Based Asynchronous Pattern, which is probably one of my favorite things to play with. I am biased in its favor, but I really feel like it has merits that the classic marshalling method does not. In the classic marshalling, you spend a lot of time writing methods that don't know if they are called from the right thread, so they have to determine what thread they are on and marshall to the right thread if needed. With this newer method, you can guarantee your methods are only called from the appropriate thread. It's about an equal amount of effort, but I feel like in the end you produce cleaner code using this method.
At the heart of this technique is the
System.ComponentModel.AsyncOperation class. You get an instance of this class by calling
System.ComponentModel.AsyncOperationManager.CreateOperation. This is a smart little method that determines if you're running in Windows Forms, ASP .NET, or some other application context and returns the correct
AsyncOperation for your application model.
AsyncOperation has two methods I'll discuss:
- Post invokes a delegate on the thread that created the AsyncOperation.
- PostOperationCompleted indicates the task is complete, and invokes a delegate on the thread that created the AsyncOperation.
The important thing to take away from this is that if you create an
AsyncOperation on the UI thread, you can always use
Post or
PostOperationCompleted to marshall a delegate onto the UI thread. This means that your methods don't have to worry about if they are on the right thread; if you call them via these two methods they are on the right thread.
Only one thing left before we can discuss implementation. The one inconvenience to
AsyncOperation is that
Post and
PostOperationCompleted don't take any delegate; they expact a
SendOrPostCallback:
Public Delegate Sub SendOrPostCallback(ByVal state As Object)
This means that if your control update method takes parameters, you'll end up having to write a wrapper method in order to call it. For example, consider
EnableButton from the last post. To use it with
Post, we need a wrapper like so:
Code:
Private Sub EnableButtonWrapper(ByVal state As Object)
EnableButton()
End Sub
This is aggravating in this particular case, but
AsyncOperation is primarily used in a different scenario where this is not a hassle so it's something we just have to live with.
Now, to implement this method, let's go over the steps. You can look at
AsyncOp.vb to see the implementation.
Step 1: Identify the actions you need to perform on controls.
This is the same as in the classic technique.
Step 2: Create a method for each control action.
This is the same as in the classic technique, but you will need to implement the methods this time. For example, here's the methods in
AsyncOp.vb:
Code:
Private Sub EnableButton()
_btn.Enabled = True
End Sub
Private Sub UpdateTextBox(ByVal text As String)
_txt.Text = text
End Sub
Step 3: Implement wrapper methods for the methods in step 2
You need a method that matches
SendOrPostCallback so the
AsyncOperation can marshall it. If your method takes one parameter, you can cheat if you want and make it an Object parameter. If your method takes more than one, you'll need a custom class to store the information. If your method takes 0 parameters, your wrapper can ignore its argument. For example, here's the wrapper method in
AsyncOp.vb:
Code:
Private Sub UpdateTextBoxWrapper(ByVal text As Object)
UpdateTextBox(text.ToString())
End Sub
Step 4: Use a variable to keep track of an AsyncOperation.
This is optional if you use some tricks that aren't relevant to this discussion. The simplest way to keep track of the operation is to store it in a class level variable. In
AsyncOp.vb, this variable is named
_operation.
Step 5: Create the operation when you start the thread.
You have to get an operation from somewhere, and the best time to do it is when you are starting the thread.
Make sure that you do this from the UI thread, not the worker thread. If you create the operation from the worker thread, it will post to the worker thread, which is not what you want. In
AsyncOp.vb, I create the operation in the
StartThread method immediately before starting the worker thread.
Step 6: When the thread needs to update a control, Post the proper method.
Now that you have your wrapper methods, use
Post to call them on the UI thread. There's a shorthand syntax for specifying delegates that you can abuse to make this less painful. For example, in
AsyncOp.vb, here's the long way I could have updated the textbox followed by the short way:
Code:
_operation.Post(New SendOrPostCallback(AddressOf UpdateTextBoxWrapper), i.ToString())
' -or-
_operation.Post(AddressOf UpdateTextBoxWrapper, i.ToString())
Do as you wish, but I prefer the short way.
Step 7: When the thread is finished, call PostOperationCompleted.
Here's the other aggravation when using this method for this simple case. I
think that calling
PostOperationCompleted is optional, but the documentation does not confirm this. If it is optional, you don't have to worry about it, but if it isn't, you'll have to create a "thread finished" callback whether you need it or not. In the case of
AsyncOp.vb, I happened to want to enable the button when the thread is finished, so I used it for that. If I were you, I'd call
PostOperationCompleted just to be safe.
Honestly this method is using
AsyncOperation a little bit outside of its intended design; it was intended for use with the Event-Based Asynchronous pattern and most of the "aggravations" I've mentioned here are things that would seem natural if you were raising an event. Still, if writing all of the "If InvokeRequired Then Else" loops and delegates is a hassle to you, this is a decent alternative.