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.
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:
<UserControl xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
x:Class="FM.Base.UI.Demos.Silverlight.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.Base.UI.Demos.Silverlight"
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 x:Name="start" Height="60" Width="100" Content="Start Streaming" Click="start_Click" Canvas.Left="0" Canvas.Top="350"></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).
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;
namespace FM.Base.UI.Demos.Silverlight
{
[ScriptableType]
public partial class MainPage : UserControl
{
/// <summary>
/// This property will be set from the client javascript once a client ID has been set
/// </summary>
public string ClientID { get; set; }
public MainPage()
{
InitializeComponent();
// make the page referenceable in js
HtmlPage.RegisterScriptableObject("MainPage", this);
}
/// <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=" + ClientID));
// don't let the user click twice
this.start.IsEnabled = false;
}
/// <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>
[ScriptableMember]
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></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;
}
}
}