WebSync Stock Ticker with Silverlight

This example is built on the same idea as the ExtJS Demo, just replacing the ExtJS grid with a Silverlight version.

Click the "Start" button below to publish stock values to this page for 20 seconds. Note that for demonstration purposes, each page receives it's own set of changes to enable individual users to start and stop the demo.

This demo simply demonstrates the capabilities of WebSync. We don't recommend using these numbers to build a portfolio.

Get Microsoft Silverlight

How it works

The client

This example is slightly more complex than the ExtJS grid, because we're communicating across the javascript/Silverlight boundaries. That said, it's still not very complex. We have, as with all Silverlight projects, a Xaml file and a code behind. The Xaml is pretty straightfoward:

User Control Xaml

<UserControl xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"  
    x:Class="FM.Website.SilverlightDemo.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:local="clr-namespace:FM.Website.SilverlightDemo"
    mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
    <Canvas>
        <Canvas.Resources>
            <local:CurrencyConverter x:Key="currencyConverter"></local:CurrencyConverter>
        </Canvas.Resources>
        <data:DataGrid x:Name="grid" AutoGenerateColumns="False" 
                       IsReadOnly="True" Height="340" 
                       Canvas.Left="0" Canvas.Top="0" LoadingRow="grid_LoadingRow">
            
            <data:DataGrid.Columns>
                <data:DataGridTextColumn Binding="{Binding Company}" Header="Company"></data:DataGridTextColumn>
                <data:DataGridTextColumn Binding="{Binding Symbol}" Header="Symbol"></data:DataGridTextColumn>
                <data:DataGridTextColumn x:Name="Price" Binding="{Binding Price, Converter={StaticResource currencyConverter}}" Header="Price"></data:DataGridTextColumn>
                <data:DataGridTextColumn x:Name="Change" Binding="{Binding Change, Converter={StaticResource currencyConverter}}" Header="Change"></data:DataGridTextColumn>
                <data:DataGridTextColumn Binding="{Binding Updated}" Header="Last Updated"></data:DataGridTextColumn>
            </data:DataGrid.Columns>
        </data:DataGrid>
        <Button IsEnabled="False" x:Name="start" Height="60" Width="100" Content="Start Streaming" Canvas.Left="0" Canvas.Top="350" Click="start_Click" ></Button>
    </Canvas>
</UserControl>

The only unusual item in this is the "currencyConverter". This is to format the change and price nicely (why there's no "Format" option for a column in Silverlight I'll never know).

User Control Code Behind

OK, the code behind is a bit move involved, not because of anything to do with WebSync, but because a lot of functionality that should be relatively simple (column display settings, cell formatting, finding columns by name) has to be done manually. You'll see what I mean.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Browser;
using System.Json;
using System.Collections.ObjectModel;
using System.Windows.Data;
using System.Globalization;
using FM.WebSync.Silverlight.Core;

namespace FM.Website.SilverlightDemo
{
    public partial class MainPage : UserControl
    {
        private Client Client;

        public MainPage()
        {
            InitializeComponent();
            
            try
            {
                Preload();

                // here's the silverlight client...incredibly simple, it simply connects,
                // subscribes, and starts waiting
                Client = new Client(((App)App.Current).HandlerUrl);

                // nesting the requests like this is only necessary because
                // we're using the client Id; otherwise, you could simply
                // run each request right after the other
                Client.Connect(new ConnectArgs()
                {
                    OnSuccess = (connectSuccess) =>
                    {
                        Client.Subscribe(new SubscribeArgs()
                        {
                            Channel = "/fm/rates/" + Client.ClientId,
                            OnSuccess = (subscribeSuccess) =>
                            {
                                Dispatcher.BeginInvoke(()=>
                                {
                                    this.start.IsEnabled = true;
                                });
                            },
                            OnReceive = (receiveArgs) =>
                            {
                                Dispatcher.BeginInvoke(() =>
                                {
                                    UpdateGrid(receiveArgs.DataJson);
                                });
                            }
                        });
                    },
                    OnFailure = (connectFailure) =>
                    {
                        Dispatcher.BeginInvoke(() =>
                        {
                            MessageBox.Show(connectFailure.Exception.Message);
                        });
                        
                    }
                });
            }
            catch (Exception ex)
            {
                Dispatcher.BeginInvoke(() =>
                {
                    MessageBox.Show(ex.Message);
                });
            }
        }

        /// <summary>
        /// Our event for the start button, indicates we should get some rates published
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void start_Click(object sender, RoutedEventArgs e)
        {
            // seconds = how long the publisher should run
            int seconds = 20;
            // the publisher sleeps for 500ms between each publish, so this is an approximation of how many publishes will actually go out
            int publishes = (seconds * 2);

            // start the server publishing
            WebClient rateGetter = new WebClient();
            rateGetter.DownloadStringCompleted += new DownloadStringCompletedEventHandler(rateGetter_DownloadStringCompleted);            
            rateGetter.DownloadStringAsync(new Uri(Application.Current.Host.Source, "../RatePublisher.aspx?publishes=" + publishes + "&clientId=" + Client.ClientId));

            // don't let the user click twice
            this.start.IsEnabled = false;
        }

        void Preload()
        {
            WebClient initialRateGetter = new WebClient();
            initialRateGetter.DownloadStringCompleted += new DownloadStringCompletedEventHandler(initialRateGetter_DownloadStringCompleted);
            initialRateGetter.DownloadStringAsync(new Uri(Application.Current.Host.Source, "../GetRates.aspx"));
        }

        void initialRateGetter_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            UpdateGrid(e.Result);
        }

        /// <summary>
        /// Fires when the download is complete, and we can re-enabled the button.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void rateGetter_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            this.start.IsEnabled = true;
        }

        /// <summary>
        /// This method updates the grid with the incoming JSON data
        /// </summary>
        /// <param name="data">The JSON string in array/object format, eg [{"symbol": "123", "price": "72"}]</param>
        public void UpdateGrid(string data)
        {
            // convert the json into something usable
            JsonArray result = (JsonArray)JsonArray.Parse(data);

            if (grid.ItemsSource == null)
            {
                // if the grid has never had data in it, create the initial list
                // the publishes only contain the diff, so we have to keep the main reference available

                List<Ticker> tickers = new List<Ticker>();
                foreach (JsonObject jo in result)
                {
                    Ticker ticker = new Ticker();
                    ticker.Symbol = jo["symbol"];
                    ticker.Price = (double)jo["price"];
                    ticker.Company = jo["company"];
                    ticker.Updated = DateTime.Now.ToString();
                    ticker.Change = 0;
                    tickers.Add(ticker);
                }
                grid.ItemsSource = tickers;
            }
            else
            {
                // find and update the appropriate tickers

                List<Ticker> tickers = grid.ItemsSource as List<Ticker>;
                foreach (JsonObject jo in result)
                {
                    Ticker ticker = tickers.Single(t => t.Symbol == jo["symbol"]);
                    double price = ((double)jo["price"]);
                    ticker.Change = (price - ticker.Price);
                    ticker.Price = price;
                    ticker.Updated = DateTime.Now.ToString();
                }

                // hack to make the grid refresh...stupid stupid stupid...
                grid.ItemsSource = null;
                grid.ItemsSource = tickers;
            }
        }

        /// <summary>
        /// There's no nice way to color specific cells. I would love some sort of
        /// fancy attributes or something, but we're unfortunately left to either
        /// this method or using custom bindings.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void grid_LoadingRow(object sender, DataGridRowEventArgs e)
        {
            // get the ticker for this row
            Ticker ticker = e.Row.DataContext as Ticker;
            FrameworkElement el;

            // get the change cell
            el = this.grid.Columns.GetByName("change").GetCellContent(e.Row);
            DataGridCell changeCell = GetParent(el, typeof(DataGridCell)) as DataGridCell;

            // get the price cell
            el = this.grid.Columns.GetByName("price").GetCellContent(e.Row);
            DataGridCell priceCell = GetParent(el, typeof(DataGridCell)) as DataGridCell;

            // figure out what color we should use
            SolidColorBrush brush = new SolidColorBrush(Colors.Black);
            if (changeCell != null)
            {
                if (ticker.Change > 0)
                {
                    brush = new SolidColorBrush(Colors.Green);
                }
                else if (ticker.Change < 0)
                {
                    brush = new SolidColorBrush(Colors.Red);
                }
            }

            // and set the cell colors
            priceCell.Foreground = brush;
            changeCell.Foreground = brush;
        }

        /// <summary>
        /// Recursively checks an element's parent chain until the specified type is found.
        /// </summary>
        /// <param name="child"></param>
        /// <param name="targetType"></param>
        /// <returns>The first element of type targetType in the child's parent chain</returns>
        private FrameworkElement GetParent(FrameworkElement child, Type targetType)
        {
            object parent = child.Parent;
            if (parent != null)
            {
                if (parent.GetType() == targetType)
                {
                    return (FrameworkElement)parent;
                }
                else
                {
                    return GetParent((FrameworkElement)parent, targetType);
                }
            }
            return null;
        }
    }

    /// <summary>
    /// Our data object structure.
    /// </summary>
    public class Ticker
    {
        public double Price { get; set; }

        public string Symbol { get; set; }

        public string Company { get; set; }

        public double Change { get; set; }

        public string Updated { get; set; }
    }

    /// <summary>
    /// A nice extension method to find a column by name instead of index. Another item that should be built in to the framework.
    /// </summary>
    public static class MyExtensions
    {
        public static DataGridColumn GetByName(this ObservableCollection<DataGridColumn> col, string name)
        {
            return col.SingleOrDefault(p =>
                (string)p.GetValue(FrameworkElement.NameProperty).ToString().ToLower() == name.ToLower()
            );
        }
    }

    /// <summary>
    /// Ridiculous amount of code necessary to put a freaking $ and some decimal places into a datagrid.
    /// You'll notice the reference to this class as the local:CurrencyConverter in the Xaml.
    /// </summary>
    public class CurrencyConverter : IValueConverter
    {
        /// <summary>
        /// Converts a double to a string formatted with 2 decimals. If this were a real stock ticker, we'd 
        /// want to use a specific culture info (changes of 2USD is not the same as 2CAD or 2 pounds, etc)
        /// </summary>
        /// <param name="value"></param>
        /// <param name="targetType"></param>
        /// <param name="parameter"></param>
        /// <param name="culture"></param>
        /// <returns></returns>
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            double? inputDouble = value as double?;
            if (inputDouble.HasValue)
            {
                return inputDouble.Value.ToString("c2", culture);
            }
            else
            {
                decimal? inputDecimal = value as decimal?;
                if (inputDecimal.HasValue)
                {
                    return inputDecimal.Value.ToString("c2", culture);
                }
            }
            return String.Empty;
        }

        /// <summary>
        /// Converts a string in currency format to a double. We don't actually use this, since the grid is read-only.
        /// </summary>
        /// <param name="value"></param>
        /// <param name="targetType"></param>
        /// <param name="parameter"></param>
        /// <param name="culture"></param>
        /// <returns></returns>
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            string input = value as string;
            if (input != null)
            {
                if (targetType == typeof(double))
                {
                    return double.Parse(input, NumberStyles.Currency, culture);
                }
                else if (targetType == typeof(decimal))
                {
                    return decimal.Parse(input, NumberStyles.Currency, culture);
                }
            }
            return value;
        }
    }
}
There's a lot of "extra" code in this example simply because setting up a Silverlight application is rather verbose. Don't let that scare you away though; the actual WebSync code is quite simple!