Peter McGrattan’s Weblog

Silverlight, WCF, ASP.NET, AJAX, Graphics, RIA

Silverlight 2 WCF Polling Duplex Support – Part 3: The Client

Posted by petermcg on September 3, 2008

Edited on 20/October/2008 : Code download updated to support Silverlight 2 RTW

(Code download)

Introduction

This is the third and final post in my current series on the support for duplex communication with a WCF service added to Silverlight 2 Beta 2.  I should point out that this technology in its current form has been deemed by Microsoft as not yet fit for production applications and is therefore currently for evaluation only.  In this post I introduce the client part of a sample application available for download (the download includes the server discussed in the previous post also) that uses the bi-directional communication discussed in part one.  The sample application as a whole represents a simple client and server application that illustrates polling duplex communication in both directions.  The client displays stock prices in a grid and animates updates received from the server.  The user of the client application can also enter notes against a stock and have those notes propagate to all other connected clients via the server using polling duplex communication in the other direction.  Here is a screenshot of the sample application running with the server and two connected clients, the same stock price updates are being displayed by both clients and the notes entered in Internet Explorer have been pushed to another instance of the client running in Firefox:

Sample Application Running

The Client

The layout of the Visual Studio 2008 solution is as follows:

PollingDuplexDemo Solution Explorer

There are three projects in the PollingDuplexDemo solution, the first is the Silverlight Application project StockClient that references the client-side version of System.ServiceModel.PollingDuplex.dll.  The .xap file produced by building this project is referenced by the Silverlight ASP.NET server control in the StockClientTestPage.aspx file as part of the hosting StockClientHost Web Application project.  The StockServer project is a Console Application project that self-hosts the polling duplex WCF service.  As you can see from the image there are multiple startup projects, the first to start is the StockServer project along with the StockClientHost project shortly after.

Page.xaml and Page.xaml.cs

The markup in Page.xaml (see code download) defines the StocksGrid Silverlight DataGrid along with the rest of the UI elements, the code in Page.xaml.cs adds some initialisation code to the default constructor and handlers for some UI events.  When the Silverlight Application loads and assigns an instance of the Page class defined across these files to the RootVisual property of the Application class, the constructor shown below is executed:

namespace StockClient
{
    public partial class Page : UserControl
    {
        private readonly StockTicker stockTicker = null;

        public Page()
        {
            InitializeComponent();

            stockTicker = new StockTicker(this.Dispatcher);

            // Bind DataGrid to ObservableCollection
            StocksGrid.ItemsSource = stockTicker.StockList;

            // Get deltas from stock delta service
            stockTicker.SubscribeDeltas();
        }

        private void AddNotes_Click(object sender, RoutedEventArgs e)
        {
            Stock selectedStock = StocksGrid.SelectedItem as Stock;

            if (selectedStock != null)
            {
                selectedStock.Notes = Notes.Text;

                stockTicker.Sync(selectedStock);
            }
        }

        private void StocksGrid_SelectionChanged(object sender, EventArgs e)
        {
            Stock selectedStock = StocksGrid.SelectedItem as Stock;

            if (selectedStock != null && !String.IsNullOrEmpty(selectedStock.Notes))
            {
                Notes.Text = selectedStock.Notes;
            }
        }
    }
}
The StockTicker Class

It’s apparent from the code above that the instance of the StockTicker class is central to everything happening in the client application and indeed it’s the code in this class that implements the client-side PollingDuplex support:

namespace StockClient
{
    public sealed class StockTicker
    {
        // Reference to layout instance that created the StockTicker
        private readonly Dispatcher owner = null;
        // Object used to let ThreadPool know to stop waiting and proceed
        private readonly AutoResetEvent waitObject = new AutoResetEvent(false);

        // Asynchronously begins an open operation on an ICommunicationObject with code to call EndOpen when it completes
        private static readonly Action<ICommunicationObject> Open = 
                                    ico => ico.BeginOpen(iar => ico.EndOpen(iar), ico);
        // Asynchronously begins a send operation on an IDuplexSessionChannel with code to call EndSend when it completes
        private static readonly Action<IDuplexSessionChannel, Message> Send = 
                                    (idc, msg) => idc.BeginSend(msg, iar => idc.EndSend(iar), idc);
        // Asynchronously begins a receive operation on an IDuplexSessionChannel with code to call an Action<Message> when it completes
        private static readonly Action<IDuplexSessionChannel, Action<Message>> Receive = 
                                    (idc, act) => idc.BeginReceive(iar => act(idc.EndReceive(iar)), idc);

        // Serializes instances of the Stock type before they are sent on the wire
        private readonly DataContractSerializer stockSerializer = new DataContractSerializer(typeof(Stock));
        // Channel for communication to WCF service
        private IDuplexSessionChannel channel;

        // List of stocks designed to be bound to a UI control
        private readonly StockList stockList = new StockList();

        public StockList StockList
        {
            get { return stockList; }
        }

        public StockTicker(Dispatcher owner)
        {
            if (owner == null)
            {
                throw new ArgumentNullException("owner");
            }

            this.owner = owner;
        }

        public void SubscribeDeltas()
        {
            // Create a channel factory capable of producing a channel of type IDuplexSessionChannel
            IChannelFactory<IDuplexSessionChannel> factory = 
                new PollingDuplexHttpBinding().BuildChannelFactory<IDuplexSessionChannel>();
            Open(factory);

            // Address of the polling duplex server and creation of the channel to that endpoint
            EndpointAddress endPoint = new EndpointAddress("http://localhost:10201/StockService");
            channel = factory.CreateChannel(endPoint);
            Open(channel);

            // Create a message with the appropriate SOAPAction and asynchronously send it on the channel
            Message message = 
                Message.CreateMessage(MessageVersion.Soap11, "Silverlight/IStockService/Register");
            Send(channel, message);

            // Use the thread pool to start only one asynchronous request to Receive messages from the server
            // Only start another asynchronous request when a signal is received that the first thread pool thread has received something
            ThreadPool.RegisterWaitForSingleObject(waitObject, 
                                                   delegate { Receive(channel, CompleteReceive); }, 
                                                   null, 
                                                   Timeout.Infinite, 
                                                   false);
            waitObject.Set();
        }

        public void Sync(Stock stock)
        {
            // Create a message with the appropriate SOAPAction and asynchronously send it on the channel with the serialized Stock as the body of the envelope
            Message message = 
                Message.CreateMessage(MessageVersion.Soap11, "Silverlight/IStockService/Sync", stock, stockSerializer);
            Send(channel, message);
        }

        private void CompleteReceive(Message message)
        {
            // Deserialize the body of the SOAP message into a Stock object
            Stock stock = message.GetBody<Stock>();

            // Queue a call to UpdateStockList on the Dispatcher of the thread that created this instance
            Action<Stock> action = UpdateStockList;
            owner.BeginInvoke(action, stock);

            // Signal the thread pool to start another single asynchronous request to Receive messages from the server
            waitObject.Set();
        }

        private void UpdateStockList(Stock delta)
        {
            // NOTE : CheckAccess is there but intellisense doesn't see it because of the [EditorBrowsable(EditorBrowsableState.Never)] attribute
            if (!owner.CheckAccess())
            {
                throw new InvalidOperationException("The calling thread cannot access this method because a different thread owns it.");
            }

            // Check if this Stock is already in the collection
            Stock existing = stockList.FirstOrDefault(s => s.Symbol == delta.Symbol);

            if (existing == default(Stock))
            {
                // This delta is a new Stock
                stockList.Add(new StockHighlight(delta));
            }
            else
            {
                // This delta is an existing Stock
                existing.Ask = delta.Ask;
                existing.Bid = delta.Bid;

                if (!String.IsNullOrEmpty(delta.Notes))
                {
                    existing.Notes = delta.Notes;
                }
            }
        }
    }
}

In order to create an instance of the StockTicker class a reference to an instance of the System.Threading.Dispatcher class is required.  In the initialisation of the StockTicker instance in the Page class constructor the Dispatcher property inherited all the way from System.Windows.DependencyObject is used.  This provides StockTicker with a queue to add tasks to for execution on the UI thread.  It is essential to update collections that may be bound to UI controls (such as the StockList property) from the UI thread to avoid an invalid cross-thread access (UnauthorizedAccessException) exception; this restriction also applies to the animation of individual stock prices using Storyboards managed by the StockHighlight class.  The stored Dispatcher reference is later used to queue calls to UpdateStockList to be executed on the UI thread from the CompleteReceive method and to check the correct thread has called UpdateStockList before updating the collection or the Stock properties that cause animation Storyboards to begin.

After the instance of StockTicker has been created in the constructor of the Page class the SubscribeDeltas method is called.  This method performs all of the client setup necessary to begin a polling duplex session with the WCF service.  The method body begins by creating an instance of the client-side version of PollingDuplexHttpBinding (see part one) and extracting an instance of a channel factory of type IDuplexSessionChannel from that binding.  The factory is then opened asynchronously and the channel is created, the client then initiates communication by asynchronously opening the channel to the WCF service specified in EndpointAddress.  The method progresses to create a SOAP 1.1 Message instance specifying the One Way Register method discussed in the previous post in the SOAPAction; the instance of this message is then sent asynchronously along the channel to the server.  Finally the method uses an AutoResetEvent to signal a ThreadPool thread to begin one asynchronous request to receive messages pushed from the server.  When a message is eventually received the CompleteReceive callback method is executed to queue a call back to the UI thread with the received Stock object before signaling the wait handle; this signal in turn prompts the thread pool to release another single asynchronous request to receive another message.

The Sync method creates a new SOAP 1.1 Message with a different SOAPAction and with the Stock parameter (containing the Notes entered by the user) as the body of the envelope serialised via the DataContractSerializer.  The method then uses the same channel opened in SubscribeDeltas to asynchronously send the message to the server for propagation to all connected clients.  The code for the WCF service methods being called here is discussed in part two, the make up of these asynchronous calls and their responses on the wire is discussed under the ‘Anatomy Of A Message Exchange’ heading In part one.

The UpdateStockList method checks the current thread as previously described and then searches the StockList collection for the current Stock object comparing Symbol properties.  A new instance of the StockHighlight class is created to add to StockList if the received stock does not exist in the collection.  As StockList is an ObservableCollection this insert will notify the DataGrid that it’s ItemsSource has changed and to update itself to honour the addition in the UI.  If the stock already exists then only the relevant price and notes properties on the existing StockHighlight instance are updated, triggering the relevant animations.  These updates in turn cause the PropertyChanged event from INotifyPropertyChanged implemented by the Stock class (see code download) to fire again causing the DataGrid to update the UI.

The StockHighlight Class

This class extends the basic Stock class overriding the Ask and Bid properties and exposing two public properties of type Brush:

namespace StockClient
{
    [DataContract(Name = "Stock", Namespace = "urn:petermcg.wordpress.com")]
    public class StockHighlight : Stock
    {
        public Brush AskHighlight { get; private set; }
        public Brush BidHighlight { get; private set; }

        private Storyboard askPosStory = new Storyboard();
        private Storyboard askNegStory = new Storyboard();
        private Storyboard bidPosStory = new Storyboard();
        private Storyboard bidNegStory = new Storyboard();

        private static readonly Duration DefaultDuration = new Duration(TimeSpan.FromSeconds(2.0));
        private static readonly Color DefaultColor = Colors.Transparent;

        public StockHighlight(Stock stock)
        {
            base.Ask = stock.Ask;
            base.Bid = stock.Bid;
            base.Notes = stock.Notes;
            base.Symbol = stock.Symbol;

            InitializeAnimation();
        }

        private void InitializeAnimation()
        {
            // Default highlight color to Transparent
            this.AskHighlight = new SolidColorBrush(DefaultColor);
            this.BidHighlight = new SolidColorBrush(DefaultColor);

            InitializeAnimation(AskHighlight, askPosStory, askNegStory);
            InitializeAnimation(BidHighlight, bidPosStory, bidNegStory);
        }

        private static void InitializeAnimation(Brush brush, Storyboard pos, Storyboard neg)
        {
            // Set up a unique Positive and Negative ColorAnimationUsingKeyFrames for  price
            ColorAnimationUsingKeyFrames posAnim = new ColorAnimationUsingKeyFrames();
            posAnim.KeyFrames.Add(new LinearColorKeyFrame() { KeyTime = TimeSpan.FromSeconds(0.0), Value = Colors.Green });
            posAnim.KeyFrames.Add(new LinearColorKeyFrame() { KeyTime = DefaultDuration.TimeSpan, Value = DefaultColor });
            posAnim.Duration = DefaultDuration;

            ColorAnimationUsingKeyFrames negAnim = new ColorAnimationUsingKeyFrames();
            negAnim.KeyFrames.Add(new LinearColorKeyFrame() { KeyTime = TimeSpan.FromSeconds(0.0), Value = Colors.Red });
            negAnim.KeyFrames.Add(new LinearColorKeyFrame() { KeyTime = DefaultDuration.TimeSpan, Value = DefaultColor });
            negAnim.Duration = DefaultDuration;

            // Add animations to storyboard
            pos.Children.Add(posAnim);
            neg.Children.Add(negAnim);

            // Target the  color animations to the  brush
            Storyboard.SetTarget(posAnim, brush);
            Storyboard.SetTarget(negAnim, brush);

            // Target the property of the  brush to animate
            Storyboard.SetTargetProperty(posAnim, new PropertyPath("(Brush.Color)"));
            Storyboard.SetTargetProperty(negAnim, new PropertyPath("(Brush.Color)"));
        }

        public override double Ask
        {
            get
            {
                return base.Ask;
            }
            set
            {
                if ((base.Ask.Equals(value) != true))
                {
                    Func<double, double, Storyboard> GetAskStory = (o, n) => (n > o) ? askPosStory : askNegStory;
                    GetAskStory(this.Ask, value).Begin();

                    base.Ask = value;
                }
            }
        }

        public override double Bid
        {
            get
            {
                return base.Bid;
            }
            set
            {
                if ((base.Bid.Equals(value) != true))
                {
                    Func<double, double, Storyboard> GetBidStory = (o, n) => (n > o) ? bidPosStory : bidNegStory;
                    GetBidStory(this.Bid, value).Begin();

                    base.Bid = value;

                    this.RaisePropertyChanged("Bid");
                }
            }
        }
    }
}

Each cell in the Bid and Ask columns of the DataGrid declared in Page.xaml contains a StackPanel whose Background property is bound to the respective Brush property.  This way each cell is able to play it’s own unique animation specific to the Stock displayed.  These Brush properties need to be part of each individual Stock object rather than global resources so the whole column does not change colour when one stock changes price.  The InitializeAnimation method associates animations of type ColorAnimationUsingKeyFrames with each of these Brushes for both positive and negative changes.  The appropriate Storyboard to play is chosen in the Bid and Ask property setters based on the new value.

The most salient parts of the client code are now covered, download the solution, press F5 to start the server and one client automatically, you can then browse to StockClientTestPage.aspx using the same address from multiple clients in separate browsers once the service is running.

In the future it looks likely that the client side Silverlight 2 Polling Duplex API will be simplified to enable a less verbose method of connecting to and receiving pushed messages from a WCF service.  This post talks more about the possibility of a DuplexReceiver<T> class for simple client scenarios in the future; it’s always prudent to know what such a class will be doing for you behind the scenes.

Further Reading

A few links on polling duplex support in Silverlight 2 Beta 2 you might find useful:

MSDN – Access a Duplex Service with the Channel Model

MSDN – How to: Build a Duplex Service

MSDN – Accessing Duplex Services

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

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

Detailed Overview of Silverlight 2 Beta 2 Web Service Features

"Pushing" Data to a Silverlight Application

About these ads

16 Responses to “Silverlight 2 WCF Polling Duplex Support – Part 3: The Client”

  1. Gopinath Varadharajan said

    Peter,

    You really rock! [7/10 !] wonderful demo with code..

    Thanks,

    Gopi

  2. john said

    Hi Peter,

    Great article! Its great to see how Duplex WCF contracts can be established programatically – allows for a great deal of flexibility. I was just wondering if you could post an example of client code for a WPF application connecting into this Stock server. I tried this when I had sl beta 2 installed (and .Net 3.5) and managed to get it working with no code change, however since moving to SL 2 RTW (and .NET 3.5SP1) pollingduplex is not supported in a WPF application. How would I get this working with something like WsDualHttpBinding in a WPF app?

    Thanks,

    John.

  3. Peter McGrattan said

    Hi John,

    You are correct: for a WPF client to connect to the same StockService you need to create a separate endpoint that uses WSDualHttpBinding, the client PollingDuplexHttpBinding is for Silverlight 2 use only.

    With the addition of the second WSDualHttpBinding endpoint it is possible to have both Silverlight 2 and WPF clients receive the same Stock updates and send each other notes. I actually have an example finished that does exactly this and will be posting on this subject in the near future. Stay tuned…

    Hope this helps,

    Peter

  4. Rob said

    Hi Peter,

    Is it safe to make sequential asynchronous calls like factory.BeginOpen, channel.BeginOpen, and channel.BeginSend as you do in your example? It seems that you are opening yourself up to the possibility that you try to Send on an unopened channel or try to create a channel with an unopened factory. Shouldn’t the call to channel.BeginSend occur inside the async callback from channel.BeginOpen?

    Rob

  5. Rob said

    BTW, I am finding your explanation extremely helpful. The MSDN documentation for this feature is not nearly as clear.

    Thanks
    Rob

  6. Peter McGrattan said

    Hi Rob,

    Thanks for your comments.

    You are absolutely correct, the possibility is a valid one. Although it works fine as is for the sample, in a production app the call to channel.BeginSend should occur inside the async callback from channel.BeginOpen. Open, Send and Receive should all be expanded to include exception handling also.

    A large part of my motivation for the client post and code was the verbosity of the MSDN example, I wanted to reduce the client example to quickly show in very minimal code what needs to be opened/sent etc. in what order. Because of this I’m able to show the full StockTicker class above but with the possibility you describe, thanks for pointing this out.

    Hope this helps,

    Peter

  7. Robert said

    Hi Peter,

    Thank you very much for posting this example. It runs as you described when I open a Firefox browser and IE browser, and register changes for the Notes, it changes on both browsers. I tried running a modified version of your example, using a WCF Service for the Polling “server” instead of a console app like you did. I basically send a message to the duplex, the duplex responds to the client, and I have a popup come up on the client, showing the response from the duplex. And for some reason, in my version with the WCF Service, the duplex only responds to the particular client that sent the message, not the other client(s). For example, if I send a message from Firefox, I get a popup response from the duplex service, but the popup does not show up in the IE browser. The reverse is also true. Is it possible that running this using a WCF Service instead of a console app could cause this discrepancy? I thought all clients connected to the service should receive any message that the duplex sends out (such as when the “Notes” get modified in your example)?

    Thanks,
    Robert

  8. Peter McGrattan said

    Hi Robert,

    Hopefully I can explain what’s happening here: Polling Duplex and indeed any other duplex support in WCF does not automatically push messages to all connected clients, this has to be done manually.

    With the WCF service in the polling duplex sample from this blog a new instance of StockService is created per session (i.e. for each Silverlight client that connects to the PollingDuplexHttpBinding WCF endpoint a new instance of StockService is instantiated). Each session registers it’s interest in the DeltaReceived event of the singleton StockGenerator class in the body of the Register method. The event is raised approx every 250 milliseconds and the session’s handler (OnDeltaReceived) sends the delta to its respective client only via the stored channel reference.

    With multiple clients there are multiple sessions and so multiple instances of StockService but only one instance of the singleton StockGenerator class where the stock deltas are generated.

    Hope this helps,

    Peter

  9. luoihocbk said

    Hi Peter,
    I have probleme quen deployed this website to IIS. Other computer not load data. When i run in local http://localhost/Stock/testpage.html, it’s OK. Perhaps other computer not connect to Port server http://localhost:xxxx/StockService open to send data.
    You can help me?
    Thanks!

  10. Al said

    Great example. However it seems to leak memory, I wan the sample code as is and my IE process memory jump immidiately and keep growing. It seems that the PollingDuplex library has a memory leak. -Thanks

  11. Quinton said

    Hi

    Excellent series of articles!!!!! Deserves a 10/10 and nothing less!!! I have followed all 4 and chop and changed a bit in a project that I am playing with.

    I have a problem though, I extended the Stock.cs by adding my own property and then added it as a datamember etc. with the highlights etc. When I update my value from the silverlight site (like u did with the add notes function),instead of updating my value, a whole new record gets displayed. I ran a debug on the StockServer and the code there functions correct as the list of stock does not increase, so it must be the client thats somehow thinking its a new record.

    What could I possibly be missing?

    Thanks
    Q

  12. Hi Quinton,

    Thanks for your comments, if you’ve made it through all four articles you deserve a medal!

    The logic that decides whether a new row gets added or an existing row’s cells get updated is in the UpdateStockList method of the client WpfStockClient.StockTicker class.

    If a new row is being added to the DataGrid then you are adding a new object to the StockList ObservableCollection instead of updating an existing one.

    In the latest code the check performed to determine whether a stock already exists in the collection is as follows:

    // Check if this Stock is already in the collection
    Stock existing = stockList.FirstOrDefault(s => s.Symbol == delta.Symbol);

    If a match on the symbol is positive then an update of the Ask, Bid and Notes (if the new Notes aren’t null) properties occurs, otherwise an insert occurs; check if this comparison is still valid for your changes.

    Best Regards,

    Peter

  13. Alexey said

    Is it possible to use PollingDuplex on simple TestPage.html without hosting on some website?
    I start “StockClient” in the demo and it crashes:

    An error occurred while trying to make a request to URI ‘http://localhost:10201/SilverlightStockService’. This could be due to attempting to access a service in a cross-domain way without a proper cross-domain policy in place, or a policy that is unsuitable for SOAP services. You may need to contact the owner of the service to publish a cross-domain policy file and to ensure it allows SOAP-related HTTP headers to be sent. Please see the inner exception for more details.

    If I start website “StockClientHost” it works fine.

    Thanks

  14. […] 2 WCF Polling Duplex Support” articles: Part 1: Architecture, Part 2: The Server and Part 3: The Client. For an interface that boils down to 3 methods: Subscribe, Publish and NotifyReceived; and […]

  15. […] Silverlight 2 WCF Polling Duplex Support – Part 3: The Client […]

  16. […] http://petermcg.wordpress.com/2008/09/03/silverlight-polling-duplex-part-3-the-client/ […]

Sorry, the comment form is closed at this time.

 
Follow

Get every new post delivered to your Inbox.

%d bloggers like this: