Callback functionality in WCF allows the service to call back to the client and send information or notification back. In essence, reversing the client-service roles. It’s a rather simple model with great benefits, specially if you want to keep the client notified on a long running process and display said notifications on the UI. However, it’s not a trivial matter to deal with proper thread handling when using this functionality. Most WCF callback demos will simply popup message boxes to show you when a callback has taken place, but if you need to do something more substantial that is UI-related, there’s a few other things you need to take into consideration.
In the second installment of “This Week In Code”, I’ll give you a demonstration of a typical callback demo that you may run into often. Then I’ll change it to a more real-world scenario and introduce additional thread-handling details that you must undertake.
I’m not going to turn this posting into a WCF callback tutorial so I’m going to assume that you know how callbacks work and how to set them up. Here’s some client code that calls a service:
private void btnSimpleCall_Click(object sender, EventArgs e) { using (MyDuplexClient proxy = new MyDuplexClient(new InstanceContext(this))) { proxy.SimpleTest(); } }
As you can see, it’s called from a button’s Click event. This will become important later.
And here’s the service operation that calls back to the client:
public bool SimpleTest() { Console.WriteLine("Serviced called. Performing callback."); IMyCallback callback = OperationContext.Current.GetCallbackChannel<IMyCallback>(); if(callback != null) { callback.PerformCallback(); } Console.WriteLine("Callback performed."); return true; }
The client that made the service call is a Windows Form and as I stated, it was a button that fired off the event. The form itself is the implementor of the callback contract, which is why the InstanceContext instance wraps this. Of course, the form needs the callback contract’s implementation because that’s what gets called when the service executes the callback operation. Here’s that implementation now.
public void PerformCallback() { MessageBox.Show("Callback from service on the client"); }
This simple example will work just fine because the callback is simply throwing up a message box. This mean it’s not accessing the actual form’s UI so there will not be any threading issues. Unfortunately, most examples you see on WCF callbacks do only this and as soon as the developer mimics this pattern but performs some UI update to the actual form, they run into problems. Let me alter my code example and explain why.
Ok, so this next example is supposed to make a service call from the form’s button, then the service will perform a potentially long iteration and every so many items in the loop, it will call back to the client to report progress. The client simply wants to use the value in the callback to update a progress bar o the form:
private void button1_Click(object sender, EventArgs e) { int max = Convert.ToInt32(txtMax.Text); int step = Convert.ToInt16(txtStep.Text); progressBar1.Minimum = 0; progressBar1.Maximum = max; MyDuplexClient proxy = new MyDuplexClient(new InstanceContext(this)); int[] numbers = proxy.GetNumbers(max, step); foreach (int number in numbers) { lstNumbers.Items.Add(number.ToString()); } } public void ReportProgress(int progress) { progressBar1.Value = progress; }
So what do you think is going to happen here? Well we have a potential catch-22 with this scenario. By default the callback will me automatically be marshaled to the UI thread before it gets executed on the client. If that happens, accessing the progress bar directly from the callback operation is perfectly fine, except that the UI thread is currently in the middle of a service call, since it was initiated in the button click, so we have a deadlock scenario there.
One thing I can do is decorate the form (as the implementor of callback contract) with the CallbackBehavior attribute and tell it to not use the form’s Synchronization Context:
[CallbackBehavior(UseSynchronizationContext = false)] public partial class Form1 : Form, IMyCallback
This means that the callback will now be executed on a background thread and not the same thread as the one that made the service call initially (which is the default behavior). But now I’ve introduced another problem. The ReportProgress callback operation can no longer directly access the UI since it’s not on the UI thread, so I need to marshal up to the UI thread in order to access the progress bar.
The best way to do this is to use the form’s Synchronization Context; a feature introduced back in .NET 2.0 but still relatively unknown. Rather than use the old “invoke” technique, I’m going to grab the form’s synchronization context as early as possible and store it in a class-wide variable. It doesn’t get any earlier than the form’s constructor, so I’ll do it there:
public Form1() { InitializeComponent(); _SyncContext = SynchronizationContext.Current; } SynchronizationContext _SyncContext = null;
Now I have accessible to me at any time, a variable that represents the form’s thread execution context; and I can use this variable from any thread I want since it will always point back to the UI thread.
Next, I’ll modify the callback operation to use the _SyncContext object in order to fire code to the thread it represents, in this case the UI thread:
public void ReportProgress(int progress) { SendOrPostCallback setText = delegate { progressBar1.Value = progress; }; _SyncContext.Post(setText, null); }
Well, that takes care of my marshaling task, but I’ve just introduced the old problem all over again. Remember, the UI thread was in use by the button’s event which is not over yet. It seems I’m going round and round with nowhere to go. Well, I do have a way out of this. The solution to the problem lies in the way I fired off the service operation. If you recall, my goal was to fire off the operation, receive occasional callbacks, and then once the service call was complete, I want to display results to a list box. What I need to do is not lock up the UI while I make the service call so I’m going to fire off the service call on another thread; but not just the service call, the update to the list box as well. If I don’t do both, then the service call will fire on another thread and the list box will attempt to update right away. So the button’s Click event will make the service call on a separate thread, after which, on that same thread it will update the list box with the service operation results. But since that’s a UI operation, it needs to marshal that little bit up to the UI. But never fear, I have my old friend, _SynchronizationContext at my disposal.
Here’s the new button Click event:
private void button1_Click(object sender, EventArgs e) { int max = Convert.ToInt32(txtMax.Text); int step = Convert.ToInt16(txtStep.Text); progressBar1.Minimum = 0; progressBar1.Maximum = max; int[] numbers; MyDuplexClient proxy = new MyDuplexClient(new InstanceContext(this)); Thread thread = new Thread(delegate() { numbers = proxy.GetNumbers(max, step); SendOrPostCallback addListItem = delegate { foreach (int number in numbers) lstNumbers.Items.Add(number.ToString()); }; _SyncContext.Post(addListItem, null); }); thread.Start(); }
The only drawback with this technique is that as soon as the button is clicked, the user will have control of the UI returned. This may or may not be a good thing. As you can see, there’s a lot to be concerned about when you’re using WCF callback’s to update UI. The solution that immediately comes to mind to control the UI fully, is to use an asynchronous delegate instead of a Thread object. in fact, the Thread object is not a good idea anyway because it doesn’t use the thread pool and thread initiation carries a lot of weight. If I change the code to use an asynchronous delegate, I can declare the IAsyncResult object that the invoking the delegate will return, at a class level. Doing this, gives me access to the state of the asynchronous call anywhere on the form. Then I can check for the IsCompleted property before allowing other UI activity to occur.
Here’s the re-work using an asynchronous delegate instead. Notice that thanks to the Func delegate and its overloads, I didn’t need to create my own delegate for this. Also notice that true to the async delegate pattern, I use a callback method (not to be confused with the service callback) that will get fired after the task of the delegate is completed. And it is here where I’ll update my list box. The async delegate callback method gets fired on the same background thread that the delegate did its work, so I still need to marshal up to the UI thread in order to update it.
private void button2_Click(object sender, EventArgs e) { int max = Convert.ToInt32(txtMax.Text); int step = Convert.ToInt16(txtStep.Text); progressBar1.Minimum = 0; progressBar1.Maximum = max; MyDuplexClient proxy = new MyDuplexClient(new InstanceContext(this)); Func<int, int, int[]> proxyCall = new Func<int, int, int[]>(proxy.GetNumbers); asyncProxy = proxyCall.BeginInvoke(max, step, ServiceCallComplete, proxyCall); } void ServiceCallComplete(IAsyncResult ar) { Func<int, int, int[]> proxyCall = (Func<int, int, int[]>)ar.AsyncState; int[] numbers = proxyCall.EndInvoke(ar); SendOrPostCallback addListItem = delegate { foreach (int number in numbers) lstNumbers.Items.Add(number.ToString()); }; _SyncContext.Send(addListItem, null); }
Now from anywhere else in the form where a user action might take place, I can check the service call thread like this:
private void btnClear_Click(object sender, EventArgs e) { if (_AsyncProxy.IsCompleted) { lstNumbers.Items.Clear(); progressBar1.Value = 0; } }
As you can see, using WCF callbacks, while easy, is not trivial when the work you want to do back at the client is UI related. The first thing you should always ask yourself is, “do I really need to do this?”. There’s a lot of details to take care of and even then, you may not get the effect that you would like. In this last example, pressing the Clear button will simply do nothing. In reality, you may want to disable controls on the form while the service call is taking place. See, there’s quite a lot of worry about.
Disclaimer The opinions expressed herein are my own personal opinions and do not represent the view of any company or person discussed within the content in any way.