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:
- Server - A WCF service with a polling duplex end point for clients to register, to publish information, and to receive notifications.
- 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 conversationNotifyClients
- From theDraw
method, we'll invokeNotifyClients
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