Peter McGrattan’s Weblog

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

Silverlight 2 WCF Polling Duplex Support – Part 4: Adding a WPF Client

Posted by petermcg on November 19, 2008

(Code download)

Introduction

This post attempts to highlight some of the intricacies involved in connecting a WPF Client to the existing Silverlight 2 Polling Duplex demo previously published on this blog.  To recap, the demo consists of a WCF service designed to push Stock data to previously connected Silverlight 2 clients over Http.  In a similar scenario to a chat application, the Silverlight 2 clients can also send ‘Note’ messages back to the server and have them propagated to other connected clients.

The aim of this post is to make the demo capable of also supporting connections from WPF clients.  Success is defined by enabling WPF clients to connect to the same WCF service, receive the same Stock updates at the same time as the Silverlight 2 clients and having Notes synchronized across all connected clients regardless of their underlying technology.  Below is a link to a screenshot of the final version of this demo running, shown is the Console Application hosting the WCF service with two Silverlight clients and one WPF client connected:

Server and All Clients Running

PollingDuplexHttpBinding and WPF clients

Up until this point the demo application has solely supported pushing data to Silverlight 2 clients, defining a PollingDuplexHttpBinding endpoint on the server side and creating a channel factory as part of the Silverlight 2 client as shown below:

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

However, attempting to use PollingDuplexHttpBinding for this purpose in a WPF application (as in the code above) currently results in a NotSupportedException exception being raised with a full explanation and even some architectural advice:

Polling Duplex NotSupportedException

PollingDuplexHttpBinding cannot currently be used in non-Silverlight clients without running into this exception.  In order to extend the demo to support pushing Stock data to a WPF client through Http the guidance from the above exception is to consider WSDualHttpBinding.  This binding predates Silverlight and is the closest alternative for achieving equivalent results to PollingDuplexHttpBinding in a non-Silverlight client application.  A look at the BindingElement objects each binding encapsulates reveals WSDualHttpBinding is actually a much more mature binding than PollingDuplexHttpBinding:

WSDualHttpBinding PollingDuplexHttpBinding
TransactionFlowBindingElement PollingDuplexBindingElement
ReliableSessionBindingElement TextMessageEncodingBindingElement
SymmetricSecurityBindingElement HttpTransportBindingElement
CompositeDuplexBindingElement  
OneWayBindingElement  
TextMessageEncodingBindingElement  
HttpTransportBindingElement  
The WSDualHttpBinding Endpoint

The outcome of acting on the aforementioned advice in the demo application is the addition of a new endpoint to the WCF service exposed from the StockServer Console Application.  This could easily be specified entirely in a configuration file (see this post for how to configure the Silverlight policy and duplex endpoints) but in the download is achieved in code as shown below:

using System;
using System.ServiceModel;
using System.ServiceModel.Description;

namespace StockServer
{
    public class StockServiceHost : ServiceHost
    {
        public StockServiceHost(object singletonInstance, params Uri[] baseAddresses)
            : base(singletonInstance, baseAddresses)
        {
        }

        public StockServiceHost(Type serviceType, params Uri[] baseAddresses)
            : base(serviceType, baseAddresses)
        {
        }

        protected override void InitializeRuntime()
        {
            this.AddServiceEndpoint(
                typeof(IPolicyProvider),
                new WebHttpBinding(),
                new Uri("http://localhost:10201/")).Behaviors.Add(new WebHttpBehavior());

            this.AddServiceEndpoint(
                typeof(IStockService),
                new PollingDuplexHttpBinding(),
                new Uri("http://localhost:10201/SilverlightStockService"));

            this.AddServiceEndpoint(
                typeof(IStockService),
                new WSDualHttpBinding(WSDualHttpSecurityMode.None),
                new Uri("http://localhost:10201/WpfStockService"));

            base.InitializeRuntime();
        }
    }
}

Although the WSDualHttpBinding endpoint is registered using a different URI, the endpoint is simply just another entry point into the same WCF service called by the existing Silverlight 2 clients using PollingDuplexHttpBinding.  Before you can push data from a WCF duplex service to a client, the client must initiate the session by calling the service.  The Register method serves this purpose in the demo application and the only change to the code in the WCF service itself (although there are no method signature changes) is in this method as shown below:

public void Register(Message message)
{
    client = new StockClient(OperationContext.Current.GetCallbackChannel<IStockClient>(),
                                  "StockClient/IStockService/Receive",
                                  message.Version);
    client.Faulted += OnClientFaulted;

    InitialReply();

    StockGenerator.Instance.DeltaReceived += OnDeltaReceived;
}

The change is minor but essential: extracting the MessageVersion from the initial message sent by the client and passing it to the constructor of the StockClient object along with a reference to the channel back to that client.  The variety of MessageVersion used by each of the bindings in the demo application is as follows:

Binding MessageVersion
WSDualHttpBinding
MessageVersion.Soap12WSAddressing10
PollingDuplexHttpBinding
MessageVersion.Soap11

Messages sent between WSDualHttpBinding endpoints use the SOAP 1.2 protocol; between PollingDuplexHttpBinding endpoints the SOAP 1.1 protocol is employed.  There is a clue in the name of MessageVersion. Soap12WSAddressing10 that WSDualHttpBinding also requires addressing headers to support reliable messaging sessions via ReliableSessionBindingElement.  In the case of these two bindings, attempting to use a mismatch of MessageVersion and binding, for example MessageVersion.Soap11 for messages sent to a WSDualHttpBinding endpoint, results in an InvalidOperationException as shown below:

MessageVersion InvalidOperationException

Storing the message version along with the channel reference in an instance of the StockClient class (see code download) ensures the reply messages are pushed back to the client in the format the relevant binding expects.  The rest of the WCF service remains the same: a new instance of StockService is created per session regardless of the endpoint used.  Each session registers it’s interest in the DeltaReceived event of the singleton StockGenerator class.  This event is raised approx every 250 milliseconds and each session’s handler (OnDeltaReceived) sends the same generated delta to its respective client via the stored channel reference using the appropriate message protocol.

The WPF Client Application

In a good advertisement for how nearly all of what you write in Silverlight 2 can be used without modification in WPF, the code in the WPF client application is extremely similar to that contained in the Silverlight 2 client project.  The markup in StockWindow.xaml and code in StockWindow.xaml.cs is virtually identical to Page.xaml and Page.xaml.cs in the Silverlight 2 project.

One difference between the WPF and Silverlight client code is the WPF DataGrid which is from the October 2008 Release of the WPF Toolkit available from codeplex.  Another difference already highlighted is that PollingDuplexHttpBinding cannot be used in a non-Silverlight client application.  In its place we use WSDualHttpBinding on the WPF client side also and due to this binding’s maturity the code in StockTicker.cs is less verbose as shown below:

using System;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Windows.Threading;
using Common;

namespace WpfStockClient
{
    public sealed class StockTicker : IStockClient
    {
        // Reference to layout instance that created the StockTicker
        private readonly Dispatcher owner = null;

        // Serializes instances of the Stock type before they are sent on the wire
        private readonly DataContractSerializer stockSerializer = new DataContractSerializer(typeof(Stock));

        // Proxy for communication to WCF service
        private IStockService proxy;

        // 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()
        {
            EndpointAddress endPoint = new EndpointAddress("http://localhost:10201/WpfStockService");

            proxy = new DuplexChannelFactory<IStockService>(this,
                                                            new WSDualHttpBinding(WSDualHttpSecurityMode.None),
                                                            endPoint).CreateChannel();

            proxy.Register(Message.CreateMessage(MessageVersion.Soap12WSAddressing10, "StockClient/IStockService/Register"));
        }

        public void Receive(Message message)
        {
            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);
        }

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

        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;
                }
            }
        }
    }
}

There are many similarities between the above code and the Silverlight 2 version; the changes are all centred on the body of the SubscribeDeltas method.  This is where the client instance of the WSDualHttpBinding class is instantiated and used to establish a composite duplex channel (two one-way channels) with the new endpoint at the address exposed in the WCF service.  The first parameter passed to the DuplexChannelFactory<IStockService>  constructor specifies ‘this’ as the instance of the IStockService interface to receive the pushed messages from the WCF service in the Receive method.  After the channel is created the initial message is sent using the correct MessageVersion as described earlier.

Linked Common Files

Those class files that are identical across both client projects are added to the Common Solution folder and are referenced using links from the respective projects where they are used:

Common Files Linked

The highlighted classes are written in one place and as such are assured of being the same structure throughout the solution (and where relevant on both sides of the wire as a consequence).  As the files are linked, the code they contain will be compiled into IL that targets the respective version of the CLR implied by the type of project the classes are linked from.  For the WPF project the linked classes will be compiled into an assembly that targets the desktop CLR, for the Silverlight project the linked classes will be compiled into an assembly that targets the more compact CoreCLR.  Linking common files in this manner is one solution to the problem normally solved by using a class library when a common CLR is targeted by all projects.

Animating the Stock Price changes

Illustrating changes to Stock price instances displayed in the DataGrid by creating the illusion that the DataGrid cell background is changing color takes less code in WPF than in Silverlight.  The code for this animation for both WPF and Silverlight 2 is contained in the StockHighlight class.  The entire contents of the StockHighlight.cs file is shown below, the code between the #if and #endif directives is compiled only if the SILVERLIGHT symbol is defined as it is in the Build tab of project properties on all default Silverlight 2 projects:

using System;
using System.Runtime.Serialization;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;

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

#if SILVERLIGHT
        private Storyboard askPosStory = new Storyboard();
        private Storyboard askNegStory = new Storyboard();
        private Storyboard bidPosStory = new Storyboard();
        private Storyboard bidNegStory = new Storyboard();
#endif

        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);

#if SILVERLIGHT
            InitializeAnimation(AskHighlight, askPosStory, askNegStory);
            InitializeAnimation(BidHighlight, bidPosStory, bidNegStory); 
#endif
        }

#if SILVERLIGHT
        private static void InitializeAnimation(SolidColorBrush 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(SolidColorBrush.ColorProperty));
            Storyboard.SetTargetProperty(negAnim, new PropertyPath(SolidColorBrush.ColorProperty));
        }

        private void BeginHighlight(Storyboard storyBoard)
        {
            storyBoard.Begin();
        }
#else
        private void BeginHighlight(SolidColorBrush brush, Color startColor)
        {
            ColorAnimationUsingKeyFrames anim = new ColorAnimationUsingKeyFrames();
            anim.KeyFrames.Add(new LinearColorKeyFrame() { KeyTime = TimeSpan.FromSeconds(0.0), Value = startColor });
            anim.KeyFrames.Add(new LinearColorKeyFrame() { KeyTime = DefaultDuration.TimeSpan, Value = DefaultColor });
            anim.Duration = DefaultDuration;

            brush.BeginAnimation(SolidColorBrush.ColorProperty, anim);
        }
#endif
        public override double Ask
        {
            get
            {
                return base.Ask;
            }
            set
            {
                if ((base.Ask.Equals(value) != true))
                {
#if SILVERLIGHT
                    BeginHighlight((value > this.Ask) ? askPosStory : askNegStory);
#else
                    BeginHighlight(AskHighlight, (value > this.Ask) ? Colors.Green : Colors.Red);
#endif

                    base.Ask = value;

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

        public override double Bid
        {
            get
            {
                return base.Bid;
            }
            set
            {
                if ((base.Bid.Equals(value) != true))
                {
#if SILVERLIGHT
                    BeginHighlight((value > this.Bid) ? bidPosStory : bidNegStory);
#else
                    BeginHighlight(BidHighlight, (value > this.Bid) ? Colors.Green : Colors.Red);
#endif

                    base.Bid = value;

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

The targets of the animation are the two public SolidColorBrush properties, AskHighlight and BidHighlight.  The Color dependency property of these brushes is animated using color key frame animation from green for a positive change or red for a negative change.  The animation for both WPF and Silverlight begins with a call to the BeginHighlight method when the corresponding Ask or Bid price property changes.  The SILVERLIGHT debug symbol makes it clear to see the extra Storyboard objects and extra initialization required in the InitializeAnimation method to achieve the desired effect in Silverlight 2.  You can see the references to these AskHighlight and BidHighlight properties in the WPF XAML below, also notice the xmlns:data for the DataGrid from the WPF Toolkit:

<Window x:Class="WpfStockClient.StockWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:data="http://schemas.microsoft.com/wpf/2008/toolkit"
    Title="Stock Window" Width="640" Height="520">

    <Grid x:Name="LayoutRoot" Background="White"  Margin="5,5,5,5">
        <Grid.RowDefinitions>
            <RowDefinition MaxHeight="30" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width=".80*" />
            <ColumnDefinition Width=".20*" />
        </Grid.ColumnDefinitions>

        <TextBox x:Name="Notes" Grid.Row="0" Grid.Column="0" Margin="0,0,5,0" />
        <Button x:Name="AddNotes" Click="AddNotes_Click" Content="Add Notes" Grid.Row="0" Grid.Column="1" Margin="5,0,0,0" />

        <data:DataGrid x:Name="StocksGrid"
                       AutoGenerateColumns="False"
                       Grid.Row="1"
                       Grid.ColumnSpan="2"
                       GridLinesVisibility="Horizontal"
                       HeadersVisibility="Column"
                       RowBackground="AliceBlue"
                       CanUserResizeColumns="False"
                       SelectionChanged="StocksGrid_SelectionChanged"
                       SelectionMode="Single"
                       Margin="0,5,0,0">
            <data:DataGrid.Columns>
                <data:DataGridTemplateColumn Header="Symbol">
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Symbol}" />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTemplateColumn Header="Bid">
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <StackPanel Background="{Binding BidHighlight}">
                                <TextBlock Text="{Binding Bid}" />
                            </StackPanel>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTemplateColumn Header="Ask">
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <StackPanel Background="{Binding AskHighlight}">
                                <TextBlock Text="{Binding Ask}" />
                            </StackPanel>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTemplateColumn MinWidth="150" Header="Notes">
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Notes}" />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                </data:DataGridTemplateColumn>
            </data:DataGrid.Columns>
        </data:DataGrid>
    </Grid>
</Window>

For more detail and to see the application running the entire solution can be downloaded using the link at the start of this post.

About these ads

9 Responses to “Silverlight 2 WCF Polling Duplex Support – Part 4: Adding a WPF Client”

  1. jdstuart said

    Again. Great post Peter! We launched a live Silverlight trading system using pollingduplex wcf service on 20 October. Your examples, the previous duplex service ones, helped me a lot to achieve this.

    Thanks a lot.
    JD

  2. jdstuart said

    Oh yea. What application are you using to copy and paste code from VS to your blog?

  3. Peter McGrattan said

    Hi Jdstuart,

    Thanks for the comments, I use Windows Live Writer and the Paste from Visual Studio plugin.

    Hope this helps,

    Peter

  4. tioneb said

    Hi,
    Thank a lot!

    But, is there any possibility to implement “PollingDuplexHttpBinding” on the client side of a WPF (or winform) application? (other than waitting for Framework 4.0…)

    In fact, “WSDualHttpBinding” don’t works on secure networks (firewalls, etc…) and Sylverlight in not always possible for all applications…

    Thanks!

  5. MarkoS said

    Great example. I’m a WCF beginner and this is really inspiring. I ran your example on local machine, and it worked immediately, but I can’t make WpfStockClient work over the internet(Silverlight client works). So, when I run StockServer on one machine, and WpfStockClient on another, different IP, I get Timeout exception on proxy.Register method. I changed only this part on wpf client side:


    public void SubscribeDeltas()
    {
    EndpointAddress endPoint = new EndpointAddress("http://localhost:10201/WpfStockService");
    //EndpointAddress endPoint = new EndpointAddress("http://212.200.221.33:10201/WpfStockService");

    proxy = new DuplexChannelFactory(this,
    new WSDualHttpBinding(WSDualHttpSecurityMode.None),
    endPoint).CreateChannel();

    proxy.Register(Message.CreateMessage(MessageVersion.Soap12WSAddressing10, "StockClient/IStockService/Register"));
    }

    Firewalls are disabled on both machines. Any suggestion?

    Thanks in advance.

  6. strother said

    Fantastic example. Being a novice, it took me a week to realise that the clients create an instance of the WCF service – I relised this when trying to implement a broadcast message. I’ve got it all together now. Of all the examples I looked at on the web this was the most suitable and gave me a HUGE jumstart. Perfect.

  7. Siama said

    Hi,
    I’m interested in Tioneb’s question:

    “Is there any possibility to implement “PollingDuplexHttpBinding” on the client side of a WPF (or winform) application?”

    Thanks!

  8. Quinton said

    Hi

    Is it possible to port the entire solution to Silverlight 3?

    I have tried my best, but get some strange errors that I cannot debug when connecting the SL3 client to the Server.

    Thanks
    Q

  9. Mazhar Karimi said

    Great Post,

    But how can we seperate the Duplex calls from UI Thread? Is there any possibility that client gets a deadlock while serving incoming/ outgoing duplex calls,

Sorry, the comment form is closed at this time.

 
Follow

Get every new post delivered to your Inbox.

%d bloggers like this: