I ran into an issue this summer while helping some interns port Terrarium to use .Net 3.0 and since I haven't seen many posts about it I thought I'd write one in hopes of helping anyone else who runs into this. I'll also nudge Michael Marucheck to write a bit more when he has time since he knows a whole lot more about this than I do.

When using Duplex Channels it's often the case that you want to receive callbacks on a separate thread. For example, If you're waiting on some notification message from the server and would like to use a ManualResetEvent to block on receiving this message you'll find that in the default configuration your callback method never gets called. Over simplified versions of the 2 contracts in question are shown below:

[ServiceContract(CallbackContract = typeof(CallbackContract))]
public interface ServiceContract
{
    [OperationContract(IsOneWay=true)]
    void Register();
}
public interface CallbackContract
{
    [OperationContract(IsOneWay=true)]
    void Notification();
}

Assuming the following server implementation of the Register method

public void Register()
{
    OperationContext.Current.GetCallbackChannel<ICallbackContract>().Notify();
}

Given these 2 contracts one might expect to be able to write a button click event-handler in a WinForms client like this:

public partial class Client : Form, ICallbackContract
{
    static ManualResetEvent mre = new ManualResetEvent(false);

    public Client(string[] args)
    {
        InitializeComponent();
    } 

    private void button1_Click(object sender, EventArgs e)
    {
        IServiceContract service = DuplexChannelFactory<IServiceContract>.CreateChannel(this, "callback");

        service.Register();

        /*
         *
         * Do some other stuff here
         *
         */

        mre.WaitOne();

        /*
         *
         * Work that cannot be done until we receive a 
         * notification from the server.
         *
         */
    }

    void ICallbackContract.Notification()
    {
        Client.mre.Set();
    }
}

Unfortunately, the code in the Notification method above will never get called on the client side. This is because the default threading behavior of WCF causes callbacks to all run in the same SynchronizationContext. Since there is another thread in that synchronization context blocked waiting for notification to get called you effectively have deadlock.

Solution: Disable the use of synchronization contexts on your callback by applying a CallbackBehavior attribute to your callback implementation.

[CallbackBehavior(UseSynchronizationContext = false)]

Applying the attribute above to the class implementing your callback will allow you to receive callbacks while in a wait or while some other thread is executing in the WinForms SynchronizationContext. 

An alternative is to use the Background worker when making calls to the service. This will run outside the WinForms Synchronization context and allow incoming calls to run without blocking. Either way, you will need to be careful about calling Winforms methods from your callback methods since they won't run on the  UI thread. I like the first approach a little better especially if you set things up in the Form's Load method.

I don't know a whole lot about the SynchronizationContext class. I learned a bit from a post on Matt Dotson's blog and Stephen Toub talks about it a bit in a .Net Matters article. Hopefully someone on the WCF team will blog about this in a bit more detail.

 

Update: When I *nudged* Michael, he pointed out that I should just put the code I needed running after the postback all in the callback method. for a lot of cases this is indeed a better option. If you don't absolutely have to (and you rarely do) don't put a Wait in the UI thread. It's ugly and results i na bad user experience.

In our particular case we should have popped up a progress dialog with a spinning icon or other whiz-bang thing-ama-jig and used the background thread to wait. that way we could add a cancel dialog as well. However this was a port of exisitng code and refactoring the application wasn't an option.