DotNetSlackers: ASP.NET News for lazy Developers

Saturday, January 8, 2011

Silver Draw - A Silverlight Based Collaboration White Board with Drawing and Chat


Introduction

Silverlight + WCF Polling Duplex services = Real Awesomeness. In this article, I'll show how to create a real-time white board that can sync information between various participants, using Silverlight + Polling Duplex. Silverlight + WCF Polling Duplex services enables you to write Silverlight apps that can share information almost real time between users, over HTTP.
Users can draw together in the white board, and may chat with each other. Here is a quick screenshot of the client running in two different browsers. And if you are more interested, have a look at this video of the app, in my blog.
The app has a login panel, a couple of drawing tools (pencil, pen, and a brush), a drawing canvas, a chat area, a color picker to choose color for line and fill, and a notification area to view activities from users in the session.

Internals made simple

Needless to say, the objective of the article is to explain how Polling Duplex can be implemented along with Silverlight and WCF. The application has two parts:
  1. Server - A WCF service with a polling duplex end point for clients to register, to publish information, and to receive notifications.
  2. Client - A Silverlight client that consumes the end point to register the user, to publish the user's drawing, and to receive and plot the drawing data from other users.
The server side web service interface (IDuplexDrawService) has these important methods:
  • Register - A client may call this to register itself to a conversation.
  • Draw - A client may call this to request the server to pump the drawing information to other clients in the conversation
  • NotifyClients - From the Draw method, we'll invoke NotifyClients to iterate the registered clients, to publish the data.
Also, as we are using Polling Duplex, we need a callback interface too - so that the server can 'call back' or notify the clients. (In Polling Duplex, you may need to note that what is happening is not actually a direct callback from server to client. The client should poll the server periodically over HTTP to fetch any possible information that the server is willing to pass to the client, to simulate a callback.) Anyway, our 'call back' service interface (IDuplexDrawCallback) has a Notify method, for the server to notify the rest of the clients when some client calls the Draw method.
So, in short - o party may publish some information using 'Draw', and others can subscribe to the published information. This is a simple implementation of the Publisher/Subscriber pattern.
When you load the client the first time, you'll be asked for a username to join the session. From there, the logic goes something like:
In client-side:
  • The client will try to connect to the service end point.
  • If success, the client will register itself with the client, by calling the Register method, and by passing the username.
  • The client will hook up a few event handlers in the proxy. Mainly, the NotifyCompleted event. NotifyCompleted will be fired each time you receive a callback from the server.
In server-side:
  • Inside the Register method, the server will grab the client's session ID and callback channel from the current operation context, and add it to a list.
  • Whenever a client submits information by calling 'Draw', the server will pump this information by iterating each registered client, and by calling their 'Notify' method.

Server implementation

Let us have a quick look at how the server side implementation is done. First of all, you need to add a reference toSystem.ServiceModel.PollingDuplexService.dll in your Silverlight SDK's Server folder. For me, this path is: C:\Program Files\Microsoft SDKs\Silverlight\v3.0\Libraries\Server\.

Service and callback interfaces

Now, let us create a simple interface for our service - IDuplexDrawService.
 Collapse
[ServiceContract (CallbackContract = typeof(IDuplexDrawCallback))]
public interface IDuplexDrawService
{
    [OperationContract(IsOneWay = true)]
    void Register(string name);
    [OperationContract(IsOneWay = true)]
    void Draw(string data);
}
You may note that we are specifying the CallbackContract attribute as the type of the callback interface. The callback interface looks like:
 Collapse
[ServiceContract]
public interface IDuplexDrawCallback
{
    [OperationContract(IsOneWay = true, 
          AsyncPattern = true, Action = DrawData.DrawAction)]
    IAsyncResult BeginNotify(Message message, 
           AsyncCallback callback, object state);
    void EndNotify(IAsyncResult result);
}
Nothing special there, other than that we are specifying our Notify method as two parts, to facilitate an asynchronous call. Later, we'll create a message from our custom object to pass it as the first parameter of BeginNotify. We should specify a message contract as well.
 Collapse
[MessageContract]
public class DrawData
{
    public const string DrawAction = 
               "http://amazedsaint.net/SilverPaint/draw";

    [MessageBodyMember]
    public string Content { get; set; }

    [MessageBodyMember]
    public string From { get; set; }
}
Alright, now we have the pieces ready, so let us create the concrete service. At this point, you might need to have a look at the DuplexDraw service source code for a side reference. Firstly, note that we are specifying a couple of attributes for the service.
 Collapse
[ServiceBehavior
        (ConcurrencyMode = ConcurrencyMode.Multiple, 
        InstanceContextMode = InstanceContextMode.Single)]
public class DuplexDrawService : IDuplexDrawService
{
   //Code here
}
InstanceContextMode is set to Single, which means only one instance context is used for handling all incoming client calls. Also,ConcurrencyMode is set to Multiple so that the service instance will be multi-threaded (we'll end up doing some explicit locking).
We may not go through all the methods in the service, but essentially the idea is as follows. As mentioned earlier, after creating a connection, the client should call the Register method. Inside the Register method in the server, we'll grab the callback channel and add it to a dictionary, with Session ID as the key.
 Collapse
string sessionId = OperationContext.Current.Channel.SessionId;
var callback = 
    OperationContext.Current.GetCallbackChannel   
                            <IDuplexDrawCallback>();

lock (syncRoot)
{
    clients[sessionId] = callback;
    userNames[sessionId] = name;
}

Draw and NotifyClients

The Draw and NotifyClients methods in the service are self explanatory. When some client calls the Draw method, we'll iterate the dictionary we have, to publish the data to all subscribed clients. Here is the essence of what is happening in the Draw and NotifyClients methods.
Inside the Draw method, we grab the incoming data to notify the connected clients.
 Collapse
/// A client will call this, to publish the drawing data
public void Draw(string data)
{
    lock (this.syncRoot)
    {
        string sessionId = 
          OperationContext.Current.Channel.SessionId;
        if (userNames.ContainsKey(sessionId))
            NotifyClients("@draw:" + data, userNames
           [sessionId],sessionId);
    }
}
Inside the NotifyClients method in the service, we actually pump the data to various clients. The NotifyClients method is as below. We create a message buffer from the DrawData.
 Collapse
//In the actual code, this is a global static variable
static TypedMessageConverter messageConverter = 
TypedMessageConverter.Create(
  typeof(DrawData),
  DrawData.DrawAction,
  "http://schemas.datacontract.org/2004/07/SilverPaintService");


/// Send the notification to all clients
public void NotifyClients(string data,string from,string sessionId)
{
    MessageBuffer notificationMessageBuffer = 
        messageConverter.ToMessage
        (new DrawData 
      { Content = data, From = from }).CreateBufferedCopy
        (65536);
    foreach (var client in clients.Values)
    {
        try
        {
           client.BeginNotify
               (notificationMessageBuffer.CreateMessage(), 
               onNotifyCompleted, client);
        }
        catch
        {}
    }
}
If you are wondering what is there in the data variable, it is a JSON serialized string that contains the drawing board information. We'll see more about that when we walk over the client side.

Endpoint configuration

And finally, a quick word on configuring the end points. Have a look at the system.ServiceModel section in the server web.config. Especially, have a look at the extensions section, where we are adding a new binding extension, named pollingDuplex. Then, we need to create a custom binding type (customBinding) and specify that as the binding of our endpoint.
 Collapse
<extensions>
    <bindingElementExtensions>
      <add name="pollingDuplex" 
        type="System.ServiceModel.Configuration.PollingDuplexElement, 
              System.ServiceModel.PollingDuplex"/>
    </bindingElementExtensions>
    <bindingExtensions>
      <add name="pollingDuplex" 
        type="System.ServiceModel.Configuration.PollingDuplexElement, 
              System.ServiceModel.PollingDuplex"/>
    </bindingExtensions>
  </extensions>
  <behaviors>
    <serviceBehaviors>
      <behavior
         name="SilverdrawServiceBehaviour">
        <serviceMetadata httpGetEnabled="true"/>
        <serviceDebug 
            includeExceptionDetailInFaults="true"/>
        <serviceThrottling maxConcurrentSessions
                 ="2147483647"/>
      </behavior>
    </serviceBehaviors>
  </behaviors>
  <bindings>
    <customBinding>
      <binding name="SilverdrawServiceBinding">
        <binaryMessageEncoding />
        <pollingDuplex 
          inactivityTimeout="02:00:00" 
          serverPollTimeout="00:05:00"
          maxPendingMessagesPerSession="2147483647" 
          maxPendingSessions="2147483647" />
        <httpTransport />
      </binding>
    </customBinding>
  </bindings>
  <services>
    <service behaviorConfiguration="SilverdrawServiceBehaviour" 
         name="Silverdraw.Server.DuplexDrawService">
      <endpoint address="" 
         binding="customBinding"
         bindingConfiguration="SilverdrawServiceBinding"
         contract="Silverdraw.Server.IDuplexDrawService"/>
      <endpoint address="mex" 
         binding="mexHttpBinding" 
         contract="IMetadataExchange"/>
    </service>
  </services>
Now, let us examine what is there in the client side.

Silverlight client

You need to add a reference to System.ServiceModel.PollingDuplexService.dll in your Silverlight SDK's Client folder. For me, this path is: C:\Program Files\Microsoft SDKs\Silverlight\v3.0\Libraries\Client\.

Connecting to the server

In the client side, most of the work is done inside the DuplexClientHelper class. First of all, we need to add a service reference, and create a proxy for the service we created.
DuplexClientHelper is a thin wrapper on top of the proxy generated out of the service.
When the Silverlight control is loaded, we'll show a quick login panel, and from the button click there, we'll invoke the Initialize method inDuplexClientHelper.cs, via the InitServiceConnection method in Page.xaml.cs.
There, we need to create a new client instance, and wire up the events to receive notifications.
 Collapse
public void Initialize(string endPointAddress)
{
    this.client = new Proxy.DuplexDrawServiceClient(
    new PollingDuplexHttpBinding(),
    new EndpointAddress(endPointAddress));

    this.client.NotifyReceived += new 
        EventHandler<proxy.notifyreceivedeventargs>
                (client_NotifyReceived);
}
As you might have guessed, NotifyReceived is invoked whenever the server pumps a message to the client. When we receive the notification, we may fetch the DrawingData from the request. Here is a stripped down version of the event handler:
 Collapse
/// Callback to get the notification
void client_NotifyReceived
   (object sender, Proxy.NotifyReceivedEventArgs e)
{
    var data = e.request.GetBody<drawdata>();
}

Drawing logic

Most of the drawing logic is in DrawingArea.cs and Page.xaml.cs in our Silverlight client. Most of the code is self explanatory - we use the traditional way of drawing - setting a boolean flag to true in mouse down, to draw the elements in mouse move.
Shapes are drawn to the canvas with respect to the selected tool (brush, pen etc.), by invoking a couple of methods in DrawingArea.cs, from the mouse down events in Page.xaml.cs.
Remember - when we add shapes to the canvas, we need to publish this information to other clients, right? For this, we have a temporary list of shapes in the DrawingArea class - where we keep the shapes added to the local canvas. When the number of shapes in our temporary list reaches a specific count, we serialize the shapes to a JSON string, and pass it to the server to publish - using the Draw method described earlier.
You may also note that we are converting the shapes to a serializable object model (see ScreenObject.cs) before doing the actual serialization. The methods for converting the shapes back and forth from the ScreenObject type resides in the CanvasHelper.cs file.
And how do we convert this to JSON? We have a couple of extension methods in the JsonSerializerHelper.cs file. Also, I have a blog post here on these extension methods and how JSON works.

No comments:

Post a Comment