Silverlight 2 Beta 2 - Doing Data Part V: Realtime StockQuotes / Scrolling Display

With Silverlight, most of the "data" you will use you'll get via some sort of networking call - whether it be sockets, HttpWebRequest/Response, WebClient, or a WCF / WebService proxy -- which uses WebRequest under the hood. Here we scrape realtime stock quotes and display them in a scrolling Silverlight "Marquee" display.

Silverlight makes all network - related calls using the Asynchronous method pattern. You cannot make a synchronous http request with Silverlight managed code. Recently there was a heated thread (the "bring back sync calls petition") on the Silverlight Forums with lots of people happy to chime in and sign up, but only a few really understanding why "it is what it is", and why it needs to be that way.

Silverlight wants -- and needs -- to be "very" cross-browser. In order to accomplish this, it must use the standardized NPAPI plugin architecture that most major browsers support. Unfortunately, this architecture enforces some limitations, the main one being that only asynchronous calls can be made. This is nothing new - but it was a real shocker to those who had gotten so used to previous beta Silverlight versions that didn't have the urgency to be true "browser plugins".

Now that's not all bad, because it becomes much more difficult to freeze up the host browser because a sync method call doesn't return in a timely manner. I know I got flamed for this on that forum, but I'll say it again: HTTP is NOT a reliable protocol -- HTTP calls can and do go into the Black Hole, and freeze up your browser. If it comes from Silverlight, and it's a sync call on the UI thread, then it's now Microsoft's fault. That's why the NPAPI is the way it is, so --no sync from Silverlight (or any other plug-in).

For this exercise, I was musing how interesting it is that virtually every major company that offers stock quotes now provides realtime quotes -- for free. I guess the competition for our eyeballs has heated up. Yahoo has a nice facility because all you need is a url with the stock symbol on the querystring. You don't need to be "logged in" (as far as I could see). And the format of the table <TD> element in the page that is produced, where your realtime quote may be found, seems to be consistent. That makes "screen - scraping" much easier.

As with any such exercise, the vendor may decide to change the HTML of the page and your wonderful screen-scrape job will be defunct. And, as a caution, I would recommend that you do not use this technique to redisplay stock quotes on a public website;  Yahoo is not likely to be very pleased about it. Consider it an "educational exercise" instead.

To do my "scraping", I use Simon Mourier's "HtmlAgilityPack".  What this class library does is essentially convert an HTML page into an SGML - compliant HtmlDocument instance on which one can reliably use familiar XPath query syntax. Obviously, this makes very sophisticated screen-scraping much easier.

I have two methods in my WebService - GetSingleQuote( string symbol)  and GetQuotes(string symbols). The second one accepts a space - delimited string of multiple stock symbols, splits them to an array, and uses the Threadpool to process them all in parallel. A strategically placed ManualResetEvent combined with a couple of simple counter variables allows me to return the complete set of quotes only when all have completed, as a List of type StockQuote. With default Service Reference settings, this ends up in your Silverlight app as an ObservableCollection. Because each quote request is truly executed in parallel, the order in which the quotes populate the returned List may change each time. This can be changed by ordering the List alphabetically by stock symbol, but I leave this not-quite-relevant issue as an exercise for the reader.

This List comes back to the Silverlight scrolling display band on the web page where I "unwind" it into a text display of the quotes that scrolls across the screen. I used a nice, simple technique from WynApse (Dave Campbell) to do the "marquee".

Following is the WebService codebehind, which is key to delivering our stock quote to the Silverlight (or any other) application:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Web.Services;
using HtmlAgilityPack;

namespace RealTimeQuotes
{
    /// <summary>
    /// Class to Retrieve scraped realtime quotes
    /// </summary>
    [WebService(Namespace = "http://myrealtimequotes.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ToolboxItem(false)]
    public class Service1 : WebService
    {
        private readonly ManualResetEvent mre = new ManualResetEvent(false);
        private readonly List<StockQuote> theQuotes = new List<StockQuote>();
        private int ctr;
        private int ctr2;

        [WebMethod]
        public StockQuote GetQuote(string symbol)
        {
            return GetSingleQuote(symbol);
        }

        /// <summary>
        /// Gets a single quote.
        /// </summary>
        /// <param name="symbol">The symbol.</param>
        /// <returns>Stockquote instance</returns>
        private StockQuote GetSingleQuote(string symbol)
        {
            var sq = new StockQuote();
            sq.Symbol = symbol;
            string result = "No Quote or wrong Symbol.";
            string url = "http://finance.yahoo.com/q/ecn?s=" + symbol;
            var hWeb = new HtmlWeb();
            hWeb.UseCookies = true;
            HtmlDocument doc = hWeb.Load(url);
            string xPathQuer = "//td[@class='yfnc_tabledata1 yfi_last_trade']";
            HtmlNode nod = doc.DocumentNode.SelectSingleNode(xPathQuer);
            if (nod != null)
            {
                sq.Quote = nod.InnerText;
                sq.LastTrade = DateTime.Now;
            }
            return sq;
        }

        /// <summary>
        /// Gets the quotes.
        /// </summary>
        /// <param name="symbols">The symbols(delimited with spaces)</param>
        /// <returns>List<StockQuote></returns>
        [WebMethod]
        public List<StockQuote> GetQuotes(string symbols)
        {
            // Get a string array of symbols
            string[] quotes = symbols.Split(' ');
            // set a counter
            ctr = quotes.Length;
            foreach (string q in quotes)
            {
                ThreadPool.QueueUserWorkItem(ProcessQuote, q);
            }
            // we don't want to go past this until we have all the quotes
            mre.WaitOne(5000);
            return theQuotes;
        }

        /// <summary>
        /// Callback to processes the quote.
        /// </summary>
        /// <param name="stateInfo">The state info.</param>
        private void ProcessQuote(object stateInfo)
        {
            // increment our second counter
            ctr2++;
            theQuotes.Add(GetSingleQuote((string) stateInfo));
            // did we get all our quotes yet?
            if (ctr2 > ctr) mre.Set();
        }
    }

    /// <summary>
    /// Quotation class to hold quote info
    /// </summary>
    [Serializable]
    public class StockQuote
    {
        public StockQuote()
        {
        }

        public StockQuote(string symbol, string quote, DateTime lastTrade)
        {
            Symbol = symbol;
            Quote = quote;
            LastTrade = lastTrade;
        }

        public string Symbol { get; set; }
        public string Quote { get; set; }
        public DateTime LastTrade { get; set; }
    }
}
And here is the Page.Xaml.cs codebehind that consumes it:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
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.Threading;
using SLQuotes.ServiceReference1;

namespace SLQuotes
{
    public partial class Page : UserControl
    {
        Storyboard _marqueeLoop = new Storyboard();
       
        public Page()
        {
            InitializeComponent();
            _marqueeLoop.Duration = TimeSpan.FromMilliseconds(10000); 
            _marqueeLoop.Completed += new EventHandler(MarqueeLoop); 
            _marqueeLoop.Begin();
        }

        void MarqueeLoop(object sender, EventArgs e)
        {
            ServiceReference1.Service1SoapClient c = new Service1SoapClient();
            c.GetQuotesCompleted += new EventHandler<GetQuotesCompletedEventArgs>(c_GetQuotesCompleted);
            c.GetQuotesAsync("CSCO MSFT YHOO T GOOG ADBE");
            _marqueeLoop.Begin();
        }

        void c_GetQuotesCompleted(object sender, GetQuotesCompletedEventArgs e)
        {
            this.ScrollingText.Text = "";
            this.ScrollingText2.Text = "";
            ObservableCollection<StockQuote> coll = e.Result;
            this.ScrollingText2.Text = "Last Trade: " + DateTime.Now.ToShortTimeString() + "... ";
            this.ScrollingText.Text = "Last Trade: " + DateTime.Now.ToShortTimeString() + "... ";
            foreach (StockQuote q in coll)
            {
                this.ScrollingText.Text += q.Symbol + ": " + q.Quote + "   ";
                this.ScrollingText2.Text += q.Symbol + ": " + q.Quote + "   ";
            }
        }
    }
}
You can see above that I am using the Storyboard object with it's Completed eventhandler to control the polling for stock prices. Here is what the scrolling display looks like in the browser:

 
That's really all there is to it, except for the unique storyboard animations on the two TextBlock controls that are used to display the scrolling text of the quotes. Those you can see in the XAML of the Page, which is included in the Visual Studio 2008 Solution that you can download here.
By Peter Bromberg   Popularity  (2630 Views)