Notify Client Applications Using WCF Callbacks

This article shows how to use WCF Callbacks in a client-server scenario where the server notifies connected clients of any event, like changes in database records or message from a client that is broadcasted to other clients.

Introduction

I have worked on a WPF client application that displays a collection of orders from a customer in a data grid. The orders are stored in a server, which provides the necessary WCF services to the client to get those orders. A client can update the status of an order, from New to Open, Invoiced to Approved and so on. Updating the UI so that a change to an order is displayed is easy when there is only one client that can update an order. The problem arises when there are multiple clients that can update an order.

When one client updates an order, the other clients should update the order displayed in their data grid. One solution is polling the server. However, what we want is let the server notify the clients of these updates to avoid unnecessary polling.

Creating the Server Application

To demonstrate, let’s create a simple client-server application wherein a client may send string messages to the server and broadcasted to all other connected clients. First, let’s create the server application. Create a WCF service application like the one shown below.



This already generates the following files: IService1.cs, Service1.svc, Service1.svc.cs and Web.config. Let’s first rename Service1 to ChatService. Make sure the references in all files are replaced. In the IChatService.cs file, it already contains sample code and some notes to get you started. Replace this with the following code listing.

/// <summary>

/// Service contract containing chat-related operations.

/// </summary>

[ServiceContract(CallbackContract = typeof(IChatServiceCallback))]

public interface IChatService

{

    /// <summary>

    /// Subcribes a client for any message broadcast.

    /// </summary>

    /// <returns>An id that will identify a client.</returns>

    [OperationContract]

    Guid Subscribe();

 

    /// <summary>

    /// Unsubscribes a client from any message broadcast.

    /// </summary>

    /// <param name="clientId">The client id.</param>

    [OperationContract(IsOneWay = true)]

    void Unsubscribe(Guid clientId);

 

    /// <summary>

    /// Keeps the connection between the client and server.

    /// Connection between a client and server has a time-out,

    /// so the client needs to call this before that happens

    /// to remain connected to the server.

    /// </summary>

    [OperationContract(IsOneWay = true)]

    void KeepConnection(); 

 

    /// <summary>

    /// Broadcasts a message to other connected clients.

    /// </summary>

    /// <param name="clientId">The client id.</param>

    /// <param name="message">The message to be broadcasted.</param>

    [OperationContract]

    void SendMessage(Guid clientId, string message);

}

 

The interface is adorned with the ServiceContract attribute. This tells WCF to expose the specified interface to client applications as a WCF service. We also set the CallbackContract type. This is the IChatServiceCallback, which is shown the following listing.

 

/// <summary>

/// The callback contract to be implemented by the client

/// application.

/// </summary>

public interface IChatServiceCallback

{

    /// <summary>

    /// Implemented by the client so that the server may call

    /// this when it receives a message to be broadcasted.

    /// </summary>

    /// <param name="message">

    /// The message to broadcast.

    /// </param>

    [OperationContract(IsOneWay = true)]

    void HandleMessage(string message);

}

 

The callback method is implemented by the client so that it may receive the messages broadcasted by the server. We’ll look into the implementation later.

 

The Subscribe() method subscribes the client for notification. It returns a Guid which will identify the client. You could return any other object that will uniquely identify a client. The Unsubscribe() method does only the opposite.  

 

The KeepConnection() should be called by the client every now and then (before the specified time-out) so that the connection with the server is not lost due to inactivity.

 

The SendMessage() is the operation used for broadcasting messages. You could separate the operations into separate interfaces if you want to, depending on their functionalities.

 

Notice that some operation contracts have their IsOneWay property set to true. This indicates that the operation does not return a reply message. The caller of the operation will not wait for the operation to finish, thus it won’t be able to detect failures or exceptions. Since some of our operations don’t return a value and are simple enough, setting IsOneWay to true seems logical.

 

The listing below shows the implementation of the interface described above.

 

/// <summary>

/// Implements the chat service interface.

/// </summary>

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,

    ConcurrencyMode = ConcurrencyMode.Multiple)]

public class ChatService : IChatService

{

    private readonly Dictionary<Guid, IChatServiceCallback> clients =

        new Dictionary<Guid, IChatServiceCallback>();

 

    #region IChatService

 

    Guid IChatService.Subscribe()

    {

        IChatServiceCallback callback =

            OperationContext.Current.GetCallbackChannel<IChatServiceCallback>();

 

        Guid clientId = Guid.NewGuid();

 

        if (callback != null)

        {

            lock (clients)

            {

                clients.Add(clientId, callback);

            }

        }

 

        return clientId;

    }

 

    void IChatService.Unsubscribe(Guid clientId)

    {

        lock (clients)

        {

            if (clients.ContainsKey(clientId))

            {

                clients.Remove(clientId);

            }

        }

    }

 

    void IChatService.SendMessage(Guid clientId, string message)

    {

        BroadcastMessage(clientId, message);

    }

 

    #endregion

 

    /// <summary>

    /// Notifies the clients of messages.

    /// </summary>

    /// <param name="clientId">Identifies the client that sent the message.</param>

    /// <param name="message">The message to be sent to all connected clients.</param>

    private void BroadcastMessage(Guid clientId, string message)

    {

        // Call each client's callback method

        ThreadPool.QueueUserWorkItem

        (

            delegate

            {

                lock (clients)

                {

                    List<Guid> disconnectedClientGuids = new List<Guid>();

 

                    foreach (KeyValuePair<Guid, IChatServiceCallback> client in clients)

                    {

                        try

                        {

                            client.Value.HandleMessage(message);

                        }

                        catch (Exception)

                        {

                            // TODO: Better to catch specific exception types.                    

 

                            // If a timeout exception occurred, it means that the server

                            // can't connect to the client. It might be because of a network

                            // error, or the client was closed  prematurely due to an exception or

                            // and was unable to unregister from the server. In any case, we

                            // must remove the client from the list of clients.

 

                            // Another type of exception that might occur is that the communication

                            // object is aborted, or is closed.

 

                            // Mark the key for deletion. We will delete the client after the

                            // for-loop because using foreach construct makes the clients collection

                            // non-modifiable while in the loop.

                            disconnectedClientGuids.Add(client.Key);

                        }

                    }

 

                    foreach (Guid clientGuid in disconnectedClientGuids)

                    {

                        clients.Remove(clientGuid);

                    }

                }

            }

        );

    }

}

 

Notice that the class is adorned with the following attribute.

 

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,

    ConcurrencyMode = ConcurrencyMode.Multiple)]

 

There are 3 possible values that may be assigned to the InstanceContextMode behavior: PerSession, PerCall and Single. This specifies how many instances of the service should be created to handle client calls. In our example, we would use Single because there should only be one instance of the clients dictionary since we are adding or removing to it when the Subscribe() or Unsubscribe() method is called. Otherwise, we’ll be adding or removing to a new instance of the dictionary always when the said methods are called.

 

Another service behavior is the ConcurrencyMode. This controls how many threads can run within the service instance. It can also be set to 3 possible values: Single, Reentrant and Multiple. Specifying Single and Reentrant only allows a single thread running within a service operation. You can choose Multiple to allow multiple threads but make sure you handle the synchronization.

 

The clients dictionary is a list of key-value pairs of Guid and IChatServiceCallback. When a client sends a message, the server iterates through the dictionary and calls each callback method and supplying the message as the parameter.

 

The interface implementation seems straightforward, except how to get the callback method. You’ll notice that we get the current OperationContext in the Subscribe() method. The OperationContext provides the execution context of the service method. You can access other useful properties here as well like the SessionId, Channel and ServiceHost. In our case, we only need to get the callback channel using the GetCallbackChannel() method.

 

The BroadcastMessage() method may also need a bit of explanation. We added exception handling in the part where the server removes a client from the dictionary when it cannot call the client’s callback method. In this case, the client could have been disconnected, maybe due to a network problem so let’s just remove it so that the server won’t try to call the method again every time a client sends a message.

 

Another thing is that using WCF callbacks requires us to use a WCF duplex channel. As it name suggests, it is used in two-way communication. By default, our service uses a binding called WSHttpBinding, which does not support duplex channels. For now, let’s replace the WSHttpBinding with WSDualHttpBinding. The following code listing shows the updated binding in the Web.config file.

 

<service behaviorConfiguration="Services.ChatServiceBehavior" name="Services.ChatService">

  <endpoint address="" binding="wsDualHttpBinding" contract="Services.IChatService">

    <identity>

      <dns value="localhost" />

    </identity>

  </endpoint>

  <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />

</service>

 

Creating the Client Application

 

I’ll be creating a simple client application, which uses WPF, that has two text boxes (one for displaying messages and one for sending a message) and a send button, which is shown in the following figure.


 

 

One thing that is still missing is that the client doesn’t know of any service operation. We must add a service reference to our project. First, build and run the Service project we created earlier. I’ll be using the ASP .NET Development Server instead of IIS in this example. This will open the default browser. Click the link to the ChatService.svc file and this will show something similar to the figure below.


 


You could use svcutil.exe in generating the service proxy, but i prefer doing it in Visual Studio. To do this, right-click on the project and click “Add Service Reference...”. It will open a new dialog. Copy the link from the browser and paste it on dialog’s Address text box and click Go. The ChatService will appear in the list of Services.

 

 

Specify the namespace and then click OK. The service reference will be added to the project. Note that you can create another project for your service references to keep your solution organized.

 

This will also add configuration elements to the app.config file. The following code listing in app.config is not found in our default Web.config file.

 

<bindings>

    <wsDualHttpBinding>

        <binding name="WSDualHttpBinding_IChatService" closeTimeout="00:01:00"

            openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"

            bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"

            maxBufferPoolSize="524288" maxReceivedMessageSize="65536"

            messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true">

            <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"

                maxBytesPerRead="4096" maxNameTableCharCount="16384" />

            <reliableSession ordered="true" inactivityTimeout="00:10:00" />

            <security mode="Message">

                <message clientCredentialType="Windows" negotiateServiceCredential="true"

                    algorithmSuite="Default" />

            </security>

        </binding>

    </wsDualHttpBinding>

</bindings>

 

Copy that to the Web.config file under the system.serviceModel element. Note that the binding configuration should be the same in both the app.config and Web.config. Also add the following line of code to the endpoint element.

 

bindingConfiguration="WSDualHttpBinding_IChatService"

 

Now we can call the WCF services in our client. Let’s first define the IChatServiceCallback implementation. The following code listing shows the implementation.

 

/// <summary>

/// Implementation of the callback contract.

/// </summary>

public class ChatServiceCallback : IChatServiceCallback

{

    public event ClientNotifiedEventHandler ClientNotified;

 

    /// <summary>

    /// Notifies the client of the message by raising an event.

    /// </summary>

    /// <param name="message">Message from the server.</param>

    void IChatServiceCallback.HandleMessage(string message)

    {

        if (ClientNotified != null)

        {

            ClientNotified(this, new ClientNotifiedEventArgs(message));

        }

    }

}

 

public delegate void ClientNotifiedEventHandler(object sender, ClientNotifiedEventArgs e);

 

/// <summary>

/// Custom event arguments.

/// </summary>

public class ClientNotifiedEventArgs : EventArgs

{

    private readonly string message;

 

    /// <summary>

    /// Constructor.

    /// </summary>

    /// <param name="message">Message from server.</param>

    public ClientNotifiedEventArgs(string message)

    {

        this.message = message;

    }

 

    /// <summary>

    /// Gets the message.

    /// </summary>

    public string Message { get { return message; } }

}

 

The HandleMessage() method of the ChatServiceCallback is executed by the server. This in turn notifies the event handlers of the message. In our implementation, the MainWindow attaches an event handler to the ClientNotified event. The following shows the MainWindow code.

 

/// <summary>

/// Interaction logic for MainWindow.xaml

/// </summary>

public partial class MainWindow : Window

{

    private Guid clientId;

 

    private ChatServiceClient chatServiceClient;

 

    private InstanceContext instanceContext;

 

    /// <summary>

    /// Constructor.

    /// </summary>

    public MainWindow()

    {

        InitializeComponent();

    }

 

    /// <summary>

    /// Initializes the service client and subscribes to the server.

    /// </summary>

    /// <param name="sender">Main window.</param>

    /// <param name="e">Ignored.</param>

    private void Wnd_Loaded(object sender, RoutedEventArgs e)

    {

        ChatServiceCallback chatServiceCallback = new ChatServiceCallback();

        chatServiceCallback.ClientNotified += ChatServiceCallback_ClientNotified;

 

        instanceContext = new InstanceContext(chatServiceCallback);

        chatServiceClient = new ChatServiceClient(instanceContext);

 

        try

        {

            clientId = chatServiceClient.Subscribe();

        }

        catch

        {

            // TODO: Handle exception.

        }

 

        // Set up the timer

        Timer timer = new Timer(300000);

        timer.Elapsed +=

        (

            (object o, ElapsedEventArgs args) =>

            {

                try

                {

                    if (chatServiceClient.State == CommunicationState.Faulted)

                    {

                        chatServiceClient.Abort();

                        chatServiceClient = new ChatServiceClient(instanceContext);

                    }

 

                    chatServiceClient.KeepConnection();

                }

                catch

                {

                    // TODO: Handle exception.

                }

            }

        );

    }

 

    /// <summary>

    /// Release resources used by the service client and unsubscribes

    /// from the server.

    /// </summary>

    /// <param name="sender">Main window.</param>

    /// <param name="e">Ignored.</param>

    private void Wnd_Closed(object sender, EventArgs e)

    {

        if (chatServiceClient != null)

        {

            try

            {

                if (chatServiceClient.State != CommunicationState.Faulted)

                {

                    chatServiceClient.Unsubscribe(clientId);

                    chatServiceClient.Close();

                }

            }

            catch

            {

                chatServiceClient.Abort();

            }

        }

    }

 

    /// <summary>

    /// Sends the message to the server.

    /// </summary>

    /// <param name="sender">Send button.</param>

    /// <param name="e">Ignored.</param>

    private void Btn_Click(object sender, RoutedEventArgs e)

    {

        string message = messageTbx.Text;

 

        if (!string.IsNullOrEmpty(message))

        {

            try

            {

                if (chatServiceClient.State == CommunicationState.Faulted)

                {

                    chatServiceClient.Abort();

                    chatServiceClient = new ChatServiceClient(instanceContext);

                }

 

                chatServiceClient.SendMessage(clientId, message);

 

                messageTbx.Text = string.Empty;

            }

            catch

            {

                // TODO: Handle exception

            }

        }

    }

 

    /// <summary>

    /// Receives a message from the server. Adds the message to the

    /// text box.

    /// </summary>

    /// <param name="sender">ChatServiceCallback object.</param>

    /// <param name="e">Contains the message from the server.</param>

    private void ChatServiceCallback_ClientNotified(object sender, ClientNotifiedEventArgs e)

    {

        if (!string.IsNullOrEmpty(conferenceTbx.Text))

        {

            conferenceTbx.Text += "\n";

        }

 

        conferenceTbx.Text += e.Message;

    }

}

 

When the window is loaded, the ChatServiceClient member is initialized. The client calls the Subscribe() method and assigns the Guid to the clientId member variable. The clientId will be used in calling the SendMessage() service operation.

 

A timer is created so that when the specified time has elapsed, the client will call the KeepConnection() method to keep the connection between the client and server active. Otherwise, it won’t be able to receive messages after the inactivity time-out.

 

In the ChatServiceCallback_ClientNotified event handler, we just append the received message in the text box. Now, build the program and run multiple instances of the application.

 

The following screenshot shows 2 client applications. I wanted to show the ID of each client which sent the message but I didn’t want to show the Guid. Anyway, you get the idea.

 

Here is the source code of the example used in the article: NotifyClientAppsUsingWCFCallbacks.zip.

By Michael Detras   Popularity  (34247 Views)
Biography - Michael Detras
.NET developer. Interested in WPF, Silverlight, and XNA.
My blog