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:
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:
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:
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:
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.