Push Data From Server to Silverlight Clients With a WCF Duplex Service
Normally in a client-server architecture application, the client makes a request to the server, then the server responses to the client. The client side PULLs data from the server side. However, we sometimes want the server side to PUSH data to the client side whenever the data changed. With a WCF duplex service, we can push updated data to Silverlight clients. In the rest of this blog I will show you how to achieve this goal.
Many of the WCF services out there follow the simple request-response mechanism to exchange data which works well for many applications. However, in addition to standard HTTP bindings, WCF also supports several others including a polling duplex binding made specifically for Silverlight which allows a service to push data down to a client as the data changes. This type of binding isn't as "pure" as the push model available with sockets since the Silverlight client does poll the server to check for any queued messages, but it provides an efficient way to push data to a client without being restricted to a specific port range. Once a communication channel is opened messages can be sent in either direction. The Silverlight SDK states the following about how communication works between a Silverlight client and a duplex service:
"The Silverlight client periodically polls the service on the network layer, and checks for any new messages that the service wants to send on the callback channel. The service queues all messages sent on the client callback channel and delivers them to the client when the client polls the service."
I still use a sample application to demonstrate it.
1. Creating Base Contracts
When creating a WCF duplex service for Silverlight, the server creates a standard interface with operations. However, because the server must communicate with the client it also defines a client callback interface. The interfaces are defined as below.
IUniversalDuplexContract
[ServiceContract(Name="DuplexService", CallbackContract = typeof(IUniversalDuplexCallbackContract))]
public interface IUniversalDuplexContract
{
[OperationContract(IsOneWay = true)]
void SendToService(DuplexMessage msg);
}
This interface is a little different from the standard WCF interfaces you may have seen or created. First, it includes a CallbackContract property that points to the client interface. Second, the SendToService() operation is defined as a one way operation. Client calls are not immediately returned as a result of setting IsOneWay to true and are pushed to the client instead.
IUniversalDuplexCallbackContract
[ServiceContract]
public interface IUniversalDuplexCallbackContract
{
//[OperationContract(IsOneWay = true)]
//void SendToClient(DuplexMessage msg);
[OperationContract(IsOneWay = true, AsyncPattern = true)]
IAsyncResult BeginSendToClient(DuplexMessage msg, AsyncCallback acb, object state);
void EndSendToClient(IAsyncResult iar);
}
The IUniversalDuplexCallbackContract interface allows a message to be sent back to the client by calling the SendToClient() method.
2. Creating Base Duplex Service
Once the server and client contracts are defined a service class can be created that implements the IUniversalDuplexContract interface.DuplexService
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
public abstract class DuplexService : IUniversalDuplexContract
{
object syncRoot = new object();
Dictionary clients = new Dictionary();
///
/// This will be called when a new client is connected
///
/// Session ID of the newly-connected client
protected virtual void OnConnected(string sessionId) { }
///
/// This will be called when a client is disconnected
///
/// Session ID of the newly-disconnected client
protected virtual void OnDisconnected(string sessionId) { }
///
/// This will be called when a message is received from a client
///
/// Session ID of the client sending the message
/// The message that was received
protected virtual void OnMessage(string sessionId, DuplexMessage message) { }
///
/// Pushes a message to all connected clients
///
/// The message to push
protected void PushToAllClients(DuplexMessage message)
{
lock (syncRoot)
{
foreach (string session in clients.Keys)
{
PushMessageToClient(session, message);
}
}
}
///
/// Pushes a message to one specific client
///
/// Session ID of the client that should receive the message
/// The message to push
protected void PushMessageToClient(string clientSessionId, DuplexMessage message)
{
IUniversalDuplexCallbackContract ch = clients[clientSessionId];
IAsyncResult iar = ch.BeginSendToClient(message, new AsyncCallback(OnPushMessageComplete), new PushMessageState(ch, clientSessionId));
if (iar.CompletedSynchronously)
{
CompletePushMessage(iar);
}
}
void OnPushMessageComplete(IAsyncResult iar)
{
if (iar.CompletedSynchronously)
{
return;
}
else
{
CompletePushMessage(iar);
}
}
void CompletePushMessage(IAsyncResult iar)
{
IUniversalDuplexCallbackContract ch = ((PushMessageState)(iar.AsyncState)).ch;
try
{
ch.EndSendToClient(iar);
}
catch (Exception ex)
{
//Any error while pushing out a message to a client
//will be treated as if that client has disconnected
System.Diagnostics.Debug.WriteLine(ex);
ClientDisconnected(((PushMessageState)(iar.AsyncState)).sessionId);
}
}
void IUniversalDuplexContract.SendToService(DuplexMessage msg)
{
//We get here when we receive a message from a client
IUniversalDuplexCallbackContract ch = OperationContext.Current.GetCallbackChannel();
string session = OperationContext.Current.Channel.SessionId;
//Any message from a client we haven't seen before causes the new client to be added to our list
//(Basically, treated as a "Connect" message)
lock (syncRoot)
{
if (!clients.ContainsKey(session))
{
clients.Add(session, ch);
OperationContext.Current.Channel.Closing += new EventHandler(Channel_Closing);
OperationContext.Current.Channel.Faulted += new EventHandler(Channel_Faulted);
OnConnected(session);
}
}
//If it's a Disconnect message, treat as disconnection
if (msg is DisconnectMessage)
{
ClientDisconnected(session);
}
//Otherwise, if it's a payload-carrying message (and not just a simple "Connect"), process it
else if (!(msg is ConnectMessage))
{
OnMessage(session, msg);
}
}
void Channel_Closing(object sender, EventArgs e)
{
IContextChannel channel = (IContextChannel)sender;
ClientDisconnected(channel.SessionId);
}
void Channel_Faulted(object sender, EventArgs e)
{
IContextChannel channel = (IContextChannel)sender;
ClientDisconnected(channel.SessionId);
}
void ClientDisconnected(string sessionId)
{
lock (syncRoot)
{
if (clients.ContainsKey(sessionId))
clients.Remove(sessionId);
}
try
{
OnDisconnected(sessionId);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
}
//Helper class for tracking both a channel and its session ID together
class PushMessageState
{
internal IUniversalDuplexCallbackContract ch;
internal string sessionId;
internal PushMessageState(IUniversalDuplexCallbackContract channel, string session)
{
ch = channel;
sessionId = session;
}
}
}
The DuplexService can be used as base class of other business services.
3. Creating Base Duplex Service Factory
DuplexServiceFactory
///
/// Derive from this class to create a duplex Service Factory to use in an .svc file
///
/// The Duplex Service type (typically derived from DuplexService)
public abstract class DuplexServiceFactory : ServiceHostFactoryBase
where T : IUniversalDuplexContract, new()
{
T serviceInstance = new T();
///
/// This method is called by WCF when it needs to construct the service.
/// Typically this should not be overridden further.
///
public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses)
{
ServiceHost service = new ServiceHost(serviceInstance, baseAddresses);
CustomBinding binding = new CustomBinding(
new PollingDuplexBindingElement(),
new BinaryMessageEncodingBindingElement(),
new HttpTransportBindingElement());
service.Description.Behaviors.Add(new ServiceMetadataBehavior());
service.AddServiceEndpoint(typeof(IUniversalDuplexContract), binding, "");
service.AddServiceEndpoint(typeof(IMetadataExchange), MetadataExchangeBindings.CreateMexHttpBinding(), "mex");
return service;
}
}
The factory is responsible for creating the appropriate host while the host defines the service endpoint.
4. Create Base Duplex Message
///
/// Base message class. Please add [KnownType] attributes as necessary for every
/// derived message type.
///
[DataContract(Namespace = "http://samples.microsoft.com/silverlight2/duplex")]
[KnownType(typeof(ConnectMessage))]
[KnownType(typeof(DisconnectMessage))]
[KnownType(typeof(LiveDataMessage))]
public class DuplexMessage { }
Any business objects intend to be pushed from the duplex service to silverlight clients must derive from this DuplexMessage class, and with a [KnownType] attribute.
Now we have constructed the infrastructure of the duplex service. Next Let's create a concrete duplex business service to push business data to silverlight clients.
5. Create a Business Service
LiveDataMessage
[DataContract]
public class LiveDataMessage: DuplexMessage
{
[DataMember]
public int Value { get; set; }
[DataMember]
public string Description { get; set; }
}
public class LiveDataService :DuplexService
{
Timer liveDataTimer;
public LiveDataService()
{
//Set up a an update every 5 seconds
this.liveDataTimer = new Timer(new TimerCallback(LiveDataUpdate),null, 0, 5000);
}
void LiveDataUpdate(object o)
{
LiveDataMessage liveDataMessage = new LiveDataMessage()
{
Description = "Live Data at " + DateTime.Now.ToLongTimeString(),
Value = new Random().Next(0, 100)
};
PushToAllClients(liveDataMessage);
}
}
<%@ ServiceHost
Language="C#"
Debug="true"
Service="DuplexExample.Web.LiveDataService"
%>
6. Config the Duplex Service
The Web.config file looks like below.
receiveTimeout="00:04:00"
inactivityTimeout="00:03:00"
>
receiveTimeout="00:04:00"
inactivityTimeout="00:03:00"
>
behaviorConfiguration="DuplexService.OrderServiceBehavior">
binding="pollingDuplexHttpBinding"
behaviorConfiguration="devleapBehavior"
bindingConfiguration="duplexHttpBinding"
contract="Microsoft.Silverlight.Cdf.Samples.Duplex.IUniversalDuplexContract">
binding="pollingDuplexHttpBinding"
behaviorConfiguration="devleapBehavior"
bindingConfiguration="duplexHttpsBinding"
contract="Microsoft.Silverlight.Cdf.Samples.Duplex.IUniversalDuplexContract">
binding="mexHttpsBinding"
contract="IMetadataExchange"/>
7. Create a Silverlight Client Application
The core code to create DuplexServiceClient looks like below.
DuplexServiceClient receiver;
ObservableCollection liveDataMessages = new ObservableCollection();
string address = "http://localhost/DuplexService/LiveDataService.svc";
EndpointAddress endpoint = new EndpointAddress(address);
PollingDuplexHttpBinding binding = new PollingDuplexHttpBinding();
receiver = new DuplexServiceClient(binding,endpoint);
receiver.SendToClientReceived += (sender, e) =>
{
if (e.msg is LiveDataMessage)
{
LiveDataMessage msg = (LiveDataMessage)e.msg;
liveDataMessages.Add(string.Format("{0}. Value = {1}", msg.Description, msg.Value));
}
};
Of course it is required to add reference to the LiveDataService.svc we created earlier.
8. Build and Run the Application
Before the application can build and run, we must add reference to System.ServiceModel.PollingDuplex in both the web site hosting the duplex service and the silverlight project. There are 2 DLL file for service side and Silverlight side, they locate in:
%ProgramFiles%Microsoft SDKsSilverlightv3.0LibrariesServer (this is the one for service)
%ProgramFiles%Microsoft SDKsSilverlightv3.0LibrariesClient (this is the one for Silverlight)
Now build and run the application, and watch the result shown as below.
9. Step Forward
Now we have an executable sample application built on WCF duplex service and Silverlight3.
I did not apply security future on this application. You can refer to my other blogs to get knowledge about WCF service security.
Download Source Code
[@more@]