Pushing Data to a Silverlight Client with a WCF Duplex Service – Part II

In Part 1 of this series on pushing data to a Silverlight client with a WCF polling duplex service I demonstrated how service contracts and operations can be defined on the server.  WCF has built-in support for duplex communication (two-way communication between a service and a client) but does require a reference to System.ServiceModel.PollingDuplex.dll to make it work with Silverlight.  This assembly is provided in the Silverlight SDK and is currently in “evaluation” mode (the Silverlight go-live license doesn’t apply to it).  With the polling duplex model the Silverlight client does poll the service to check if any messages are queued so it’s not as “pure” as the sockets option available in Silverlight when it comes to pushing data from a server to a client.  However, it offers much greater flexibility when compared to sockets since it isn’t limited to a specific port range and works over HTTP. 

Let’s take a look at how a Silverlight client can send and receive messages from a polling duplex WCF service and what types of messages are sent between the two.

Understanding Polling Duplex Messages

A polling duplex service communicates with a Silverlight client using WCF Message types.  This provides complete control over the data sent between the client and the service and allows communication between the two to be loosely coupled.  The downside of this is that messages must be manually serialized/deserialized by the client and service since the WSDL type information uses the xs:any element.  Here’s what the service’s WSDL types section looks like (notice the inclusion of the xs:any element) when a service uses the Message type as a parameter for an operation:

<xs:schema elementFormDefault="qualified" targetNamespace="http://schemas.microsoft.com/Message" xmlns:xs="http://www.w3.org/2001/XMLSchema" 
  xmlns:tns="http://schemas.microsoft.com/Message">
<xs:complexType name="MessageBody">
  <xs:sequence>
    <xs:any minOccurs="0" maxOccurs="unbounded" namespace="##any"/>
  </xs:sequence>
</xs:complexType>
</xs:schema>

An example of using the WCF Message type in a WCF service is shown next.  Details about this code were covered in Part I of this series.

using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Threading;

namespace WCFPushService
{
    public class GameStreamService : IGameStreamService
    {
        IGameStreamClient _Client;
        Game _Game = null;
        Timer _Timer = null;
        Random _Random = new Random();

        public GameStreamService()
        {
            _Game = new Game();
        }

        public void GetGameData(Message receivedMessage)
        {

            //Get client callback channel
            _Client = OperationContext.Current.GetCallbackChannel<IGameStreamClient>();

            SendData(_Game.GetTeamData());
            //Start timer which when fired sends updated score information to client
            _Timer = new Timer(new TimerCallback(_Timer_Elapsed), null, 5000, Timeout.Infinite);
        }

        private void _Timer_Elapsed(object data)
        {
            SendData(_Game.GetScoreData());
            int interval = _Random.Next(3000, 7000);
            _Timer.Change(interval, Timeout.Infinite);
        }

        private void SendData(object data)
        {
            Message gameDataMsg = Message.CreateMessage(
                MessageVersion.Soap11,
                "Silverlight/IGameStreamService/ReceiveGameData", data);

            //Send data to the client
            _Client.ReceiveGameData(gameDataMsg);
        }
    }
}


Creating a Silverlight Duplex Polling Receiver Class

Calling and receiving data in Silverlight requires a fair amount of code to be written.  Before showing the code to interact with a polling duplex service it’s important to understand the general steps involved.  Here’s what you need to do to send and receive data in a Silverlight client:

Reference Assemblies and Namespaces

  1. Reference System.ServiceModel.dll and System.ServiceModel.PollingDuplex.dll in your Silverlight project.  Additional details on where to find the System.ServiceModel.PollingDuplex.dll assembly used by Silverlight can be found here.
  2. Import the System.ServiceModel and System.ServiceModel.Channels namespaces.

Create a Factory Object

  1. Create a PollingDuplexHttpBinding object instance and set the PollTimeout and InactivityTimeout properties (both were discussed in Part 1).
  2. Use the PollingDuplexHttpBinding object to build a channel factory.
  3. Open the channel factory and define an asynchronous callback method that is called when the open completes.

Create a Channel Object

  1. Use the factory class to create a channel that points to the service’s HTTP endpoint.
  2. Open the channel and define an asynchronous callback method that is called when the open completes.
  3. Define a callback method that is called when the channel closes.

Send/Receive Messages

  1. Create a Message object and send it asynchronously to the service using the channel object.  Define an asynchronous callback method that is called when the send completes.
  2. Start a message receive loop to listen for messages “pushed” from the service and define a callback method that is called when a message is received.
  3. Process data pushed by the server and dispatch it to the Silverlight user interface for display.

Now that you’ve seen the fundamental steps, let’s take a look at the code that makes this process work.  The following code shows a class named PushDataReceiver that encapsulates the factory and channel classes and handles all of the asynchronous operations that occur.  The class allows an object of type IProcessor to be passed into it along with a service URL, service action and initial data to send to the service (if any).  The IProcessor object represents the actual Silverlight Page class used to update data on the user interface in this case.  As data is received the Page class’s ProcessData() method will be called.

using System;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Threading;
using System.IO;
using System.Xml.Serialization;

namespace SilverlightPushClient
{
    public interface IProcessor
    {
        void ProcessData(object receivedData);
    }

    public class PushDataReceiver
    {
        SynchronizationContext _UiThread = null;
        public IProcessor Client { get; set; }
        public string ServiceUrl { get; set; }
        public string Action { get; set; }
        public string ActionData { get; set; }

        public PushDataReceiver(IProcessor client, string url, string action, string actionData)
        {
            Client = client;
            ServiceUrl = url;
            Action = action;
            ActionData = actionData;
            _UiThread = SynchronizationContext.Current;
        }

        public void Start()
        {
            // Instantiate the binding and set the time-outs
            PollingDuplexHttpBinding binding = new PollingDuplexHttpBinding()
            {
                PollTimeout = TimeSpan.FromSeconds(10),
                InactivityTimeout = TimeSpan.FromMinutes(1)
            };

            // Instantiate and open channel factory from binding
            IChannelFactory<IDuplexSessionChannel> factory =
                binding.BuildChannelFactory<IDuplexSessionChannel>(new BindingParameterCollection());

            IAsyncResult factoryOpenResult =
                factory.BeginOpen(new AsyncCallback(OnOpenCompleteFactory), factory);
            if (factoryOpenResult.CompletedSynchronously)
            {
                CompleteOpenFactory(factoryOpenResult);
            }
        }

        void OnOpenCompleteFactory(IAsyncResult result)
        {
            if (result.CompletedSynchronously)
                return;
            else
                CompleteOpenFactory(result);
        }

        void CompleteOpenFactory(IAsyncResult result)
        {
            IChannelFactory<IDuplexSessionChannel> factory =
                (IChannelFactory<IDuplexSessionChannel>)result.AsyncState;

            factory.EndOpen(result);

            // The factory is now open. Create and open a channel from the channel factory.
            IDuplexSessionChannel channel =
                factory.CreateChannel(new EndpointAddress(ServiceUrl));

            IAsyncResult channelOpenResult =
                channel.BeginOpen(new AsyncCallback(OnOpenCompleteChannel), channel);
            if (channelOpenResult.CompletedSynchronously)
            {
                CompleteOpenChannel(channelOpenResult);
            }
        }

        void OnOpenCompleteChannel(IAsyncResult result)
        {
            if (result.CompletedSynchronously)
                return;
            else
                CompleteOpenChannel(result);
        }

        void CompleteOpenChannel(IAsyncResult result)
        {
            IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState;

            channel.EndOpen(result);

            // Channel is now open. Send message
            Message message =
                Message.CreateMessage(channel.GetProperty<MessageVersion>(),
                 Action , ActionData);
            IAsyncResult resultChannel =
                channel.BeginSend(message, new AsyncCallback(OnSend), channel);
            if (resultChannel.CompletedSynchronously)
            {
                CompleteOnSend(resultChannel);
            }

            //Start listening for callbacks from the service
            ReceiveLoop(channel);
        }

        void OnSend(IAsyncResult result)
        {
            if (result.CompletedSynchronously)
                return;
            else
                CompleteOnSend(result);
        }

        void CompleteOnSend(IAsyncResult result)
        {
            IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState;
            channel.EndSend(result);
        }

        void ReceiveLoop(IDuplexSessionChannel channel)
        {
            // Start listening for callbacks.
            IAsyncResult result = channel.BeginReceive(new AsyncCallback(OnReceiveComplete), channel);
            if (result.CompletedSynchronously) CompleteReceive(result);
        }

        void OnReceiveComplete(IAsyncResult result)
        {
            if (result.CompletedSynchronously)
                return;
            else
                CompleteReceive(result);
        }

        void CompleteReceive(IAsyncResult result)
        {
            //A callback was received so process data
            IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState;

            try
            {
                Message receivedMessage = channel.EndReceive(result);

                // Show the service response in the UI.
                if (receivedMessage != null)
                {
                    string text = receivedMessage.GetBody<string>();
                    _UiThread.Post(Client.ProcessData, text);
                }

                ReceiveLoop(channel);
            }
            catch (CommunicationObjectFaultedException exp)
            {
                _UiThread.Post(delegate(object msg) { System.Windows.Browser.HtmlPage.Window.Alert(msg.ToString()); }, exp.Message);
            }
        }

        void OnCloseChannel(IAsyncResult result)
        {
            if (result.CompletedSynchronously)
                return;
            else
                CompleteCloseChannel(result);
        }

        void CompleteCloseChannel(IAsyncResult result)
        {
            IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState;
            channel.EndClose(result);
        }
    }
}

When the PushDataReceiver class’s Start() method is called by Silverlight it creates a channel factory instance which is used to create a channel instance.  The CompleteOpenChannel() callback method shown previously then sends an initial message to the service endpoint and encapsulates the data to be sent in a WCF Message object.  The message data is then sent along with the proper service action to call on the server.  After the initial message is sent a receive loop is started (see the ReceiveLoop() method) which listens for any messages sent from the server to the client and processes them accordingly.  Once a message is received the CompleteReceive() method is called and the message data is routed back to the Silverlight Page class.

Processing Data Using the XmlSerializer Class

The PushDataReceiver class shown earlier dispatches data received from the server back to the Silverlight Page class for processing.  Data sent from the server is in XML format and multiple techniques can be used to process it in Silverlight ranging from the XmlReader class to LINQ to XML functionality to the XmlSerializer class.  I chose to use the XmlSerializer class to process the data since it provides a simple way to map XML data to CLR types with a minimal amount of code.  Although you can create the CLR classes that XML data maps to by hand, I chose to create an XSD schema and use .NET’s xsd.exe tool to generate code from the schema for me.  The xsd.exe tool provides a simple way to generate C# or VB.NET code and ensures that the XML data will be successfully mapped to the appropriate CLR type’s properties.  An example of using the tool is shown next:

xsd.exe /c /namespace:SomeNamespace Teams.xsd

The /c switch tells the tool to generate classes (as opposed to strongly-typed DataSets) while the /namespace switch allows you to control what namespace is added into the auto-generated code.  Other switches are available which you can read more about here.

One of the XSD schemas used to generate C# code with xsd.exe is shown next:

<?xml version="1.0" encoding="utf-16"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="Teams">
    <xs:complexType>
      <xs:sequence>
        <xs:element maxOccurs="unbounded" name="Team">
          <xs:complexType>
            <xs:sequence>
              <xs:element maxOccurs="unbounded" name="Player">
                <xs:complexType>
                  <xs:attribute name="ID" type="xs:string" use="required" />
                  <xs:attribute name="Name" type="xs:string" use="required" />
                </xs:complexType>
              </xs:element>
            </xs:sequence>
            <xs:attribute name="Name" type="xs:string" use="required" />
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

Note: If you use the xsd.exe tool to generate classes that will be used in a Silverlight client you’ll have to remove a few lines that don’t compile from the auto-generated code.  The xsd.exe tool generates code designed to run on the full version of the .NET framework but with a few minor modifications you can also use the code with Silverlight.  Simply remove the namespaces and attributes that the compiler says are invalid from the auto-generated code.

Once data is received by the Silverlight client from the WCF polling duplex service it’s processed by a method named ProcessData() (the method called by the PushDataReceiver class) in the sample application.  ProcessData() uses the XmlSerializer class to deserialize XML data into custom Teams and ScoreData objects (the Teams and ScoreData classes were generated from XSD schemas using the xsd.exe tool mentioned earlier).

public void ProcessData(object receivedData)
{
    StringReader sr = null;
    try
    {
        string data = (string)receivedData;
        sr = new StringReader(data);
        //Get initial team data
        if (_Teams == null && data.Contains("Teams"))
        {
            XmlSerializer xs = new XmlSerializer(typeof(Teams));
            _Teams = (Teams)xs.Deserialize(sr);
            UpdateBoard();
        }

        //Get updated score data
        if (data.Contains("ScoreData"))
        {
            XmlSerializer xs = new XmlSerializer(typeof(ScoreData));
            ScoreData scoreData = (ScoreData)xs.Deserialize(sr);
            //ScoreDataHandler handler = new ScoreDataHandler(UpdateScoreData);
            //this.Dispatcher.BeginInvoke(handler, new object[] { scoreData });
            UpdateScoreData(scoreData);
        }
    }
    catch { }
    finally
    {
        if (sr != null) sr.Close();
    }
}

As team and score data is pushed from the server to the client it’s updated on the Silverlight interface as shown next:

The complete code for the application including the WCF duplex polling service and the Silverlight client can be downloaded here.

 

Twitter
Already on Twitter and interested in getting live updates about blog posts and other information?  Subscribe to my Twitter feed at http://www.twitter.com/DanWahlin.

comments powered by Disqus

7 Comments

  • How about some details on how to host a WCF duplex service on IIS, which a silverlight client can consume.

  • Josh,

    All of this is running on IIS. Check out Part 1 of the article for additional details. HTTP is used behind the scenes for polling duplex services that are consumed by Silverlight.

  • Could you please provide some more info on what you mean when you say "All of this is running on IIS" ?

    For example I can see hardcoded portnumber when the client's ChannelFactory creates a channel (port 5661), this port is used for hosting the WCF service by the development server.
    I can also see development server port 7611 for hosting the html.
    (Is the ClientAccessPolicy.xml included to avoid cross-domain problems between the two development servers ?)

    Could you please give some more details regarding hosting by IIS (actaul changes and how to setup the web app in IIS, preferably by avoiding the cross-domain (html and service by the same domain) ?

  • RoXX,

    No problem. The sample code that's available to download uses the built-in VS 2008 Web server ("mini" IIS) but the sample could just as easily be hosted on the full-blown IIS. The main point (regardless of where it's hosted) is that HTTP is used behind the scenes. The only reason you see the port number is because I wanted to make the sample easy to run without requiring someone to have IIS installed on their box. If you have VS 2008 installed you should be able to run it.

  • RoXX,

    To add to my previous comment, you would create an IIS application, copy the WCF service into it and even the Silverlight host page if you wanted to have it all in the same app. There's no special setup in IIS required though. The ClientAccessPolicy.xml file you asked about is to avoid the cross-domain problems although I ended up adding the Silverlight host into the Website where the actual service runs.

  • Hi, i get error 'webdev.webserver.exe has encountered a problem ' after i stop running the application. even in iis i get 'system.timeout exception in aspnet_wp.exe'.
    Is it bcoz when server finish sending all data or client app. is closed, the client callback channel is not closed.

    Also if client has to make more than 1 calls to server, like stop, pause or restart pushing, multiple channels will be opened and only callback channel on for the last client-call will be pushing. How can other channels be closed.

    May be i have not understood or missed some part.

    can you explain how the client-callback-channel can be closed.

    Thanks
    Hemant

  • Raag,

    If you use sockets (see my other posts) you can synchronize the clients. I'm not aware of a way to do that with the duplex polling service due to how it works although I haven't looked into it at all. If I find out otherwise I'll post about it. As far as host name changes you mention, that really depends on how you have things setup on your system and if the policy file allows it. You should be able to use your computer name though since it's normally mapped to 127.0.0.7.

Comments have been disabled for this content.