Silverlight Toolkit: Autocomplete TextBox Stock Symbols and Chart

In a previous article, I presented an example of creating stock charts using the Silverlight Toolkit Charting control, getting the price history data "on demand" from Yahoo Finance. At that time I mentioned that one of the nice features for enhancement would be to provide an autocomplete search box to provide the correct symbol for the stock you want to chart. In this article, I show two different ways this can be done. I also provide a CSV format text file of over 20,000 current stock symbols

The Silverlight Toolkit is a work in progress, with controls listed in three different categories of "feature completion". The Autocomplete TextBox is listed in the "Preview" category, and my understanding is that in the December release, which is not yet available at this writing, there will be more fixes and features, plus support for Observable Collections. Currently the Autocomplete isn't smart enough for that; it's really expecting something like an array of strings. However, with a custom ItemFilter, it is possible to feed it an ObservableCollection of say a "StockSymbol" class containing symbol and description, and still accomplish the objective.

There is some excellent documentation in the standalone CHM help file for the Toolkit that details all the methods, properties and events of the Autocomplete control. However, Jeff Wilcox, who is one of the folks who wrote the code, has provided what I consider an excellent guide that he calls "The missing guide". If you intend to learn to use this control, I highly recommend downloading this page and printing it out for reference. Jeff did a great job of detailing the "ins and outs".

As mentioned, I have two versions of the application. The first does filtering "on the server" - in other words, we send the search string (once it is at least 3 characters) via a WCF service call, and the service returns us the matching ObservableCollection of StockSymbol objects to populate the control with. In the other version, I actually get the entire list of StockSymbols in the Page constructor, and we do all the filtering at the client.  I've provided a "loose" Page.xaml.cs.alternate file in the root of the downloadable solution so you can easily rename files and switch out to either version. So let's take a look at some sample code with comments:

As in the original article, we must first have a service to provide our data. Here is the new code in the added Global.asax.cs class  that handles the symbols:

public static List<stocks.StockSymbol> Symbols;

protected
void Application_Start(object sender, EventArgs e)
        {
            Symbols = new List<stocks.StockSymbol>();
             string s = File.ReadAllText(Server.MapPath("Symbols.txt"));
            s = s.Replace("\r", "");
            // split to array on end of line
            string[] rows = s.Split('\n');
            // split to the 2 needed columns in each row
            try
            {
                 foreach (string s2 in rows)
                 {
                     string[] cols = s2.Split(',');
                    var sym = new stocks.StockSymbol(cols[0], cols[1]);
                      Symbols.Add(sym);
                 }
                 // It's now in Global.Symbols so we don't need to load it again until the app recycles
            }
            catch (Exception ex)
             {
                 Debug.WriteLine(ex.ToString());
             }
         }

When the application starts (on the first request) the above code loads the textfile and splits it into string arrays on the linefeed character, then again on the comma delimiters, in order to populate our List of type StockSymbol, which looks like so:

        /// <summary>
        /// Class to hold a stock symbol and description
        /// </summary>
        public class StockSymbol
        {
             /// <summary>
            /// Initializes a new instance of the <see cref="StockSymbol"/> class.
            /// </summary>
            /// <param name="symbol">The symbol.</param>
            /// <param name="description">The description.</param>
            public StockSymbol(string symbol, string description)
            {
                Symbol = symbol;
                Description = description;
             }

             public StockSymbol()
             {
             }

             public string Symbol { get; set; }
             public string Description { get; set; }
        }

This list is a public static field in Global, so getting at it is as simple as "Global.Symbols". The method that returns the matching List follows:

        /// <summary>
        /// Gets the stock symbols.
        /// </summary>
        /// <param name="searchTerm">The search term.</param>
        /// <returns></returns>
        [OperationContract]
        public List<StockSymbol> GetStockSymbols(string searchTerm)
        {
            List<StockSymbol> symbols = Global.Symbols;
             if (String.IsNullOrEmpty(searchTerm))
                 return symbols;
             else
            {
                List<StockSymbol> retList = Contains(symbols, searchTerm);
                 return retList;
             }
        }

        /// <summary>
        /// Determines whether List<StockSymbol> contains the specified target search term.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="srch">The Search string.</param>
        /// <returns></returns>
        private List<StockSymbol> Contains(List<StockSymbol> target, string srch)
        {
             return target.FindAll(delegate(StockSymbol sym) { return sym.Description.Contains(srch); });
         }

The "Contains" method above is our predicate for the filtering process.  Now let's switch over to the Silverlight client and see what happens "under the hood" when you start typing in the Autocomplete Textbox:

using System;
using System.Collections.ObjectModel;
using System.Windows.Controls;
using Microsoft.Windows.Controls;
using StockCharts.StockService;

namespace StockCharts
{
    public partial class Page : UserControl
    {
         public Page()
        {
             InitializeComponent();
            // In this version we filter at the server, so Mode=None.
            autoComplete1.SearchMode = AutoCompleteSearchMode.None;
           
        }

        private void c_GetStockSymbolsCompleted(object sender, GetStockSymbolsCompletedEventArgs e)
        {
            autoComplete1.ItemsSource = e.Result;
            autoComplete1.MinimumPrefixLength = 3;
            autoComplete1.IsTextCompletionEnabled = false;
            // No itemfilter here since we are filtering at the server.
            autoComplete1.PopulateComplete();
        }

         private void c_GetStockDataCompleted(object sender, GetStockDataCompletedEventArgs e)
        {
            ObservableCollection<stocksStockData> stockDatas = e.Result;
            Chart1.LegendTitle = null;
            Chart1.Title = ((stocksStockSymbol) autoComplete1.SelectedItem).Description;
            Dispatcher.BeginInvoke(() => Chart1.DataContext = stockDatas);
             // CLEAR OUT so user can search again
            Dispatcher.BeginInvoke(() => autoComplete1.Text = "");
        }

         private void autoComplete1_SelectedItemChanged(object sender, SelectionChangedEventArgs e)
         {
             if (autoComplete1.SelectedItem == null) return;
            string symbol = ((stocksStockSymbol) autoComplete1.SelectedItem).Symbol;
            var c = new stocksClient();
            c.GetStockDataCompleted += c_GetStockDataCompleted;
            c.GetStockDataAsync(symbol, DateTime.Now.AddYears(-2), DateTime.Now);
         }

        private void autoComplete1_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
         {
             if(autoComplete1.Text.Length <3) return;
            autoComplete1.Populating += (s, args) =>
                                            {
                                                args.Cancel = true;
                                                var c = new stocksClient();
                                                c.GetStockSymbolsCompleted += c_GetStockSymbolsCompleted;
                                                    c.GetStockSymbolsAsync(autoComplete1.Text);
                                                };

        }
    }
}

The KeyUp handler is where I do the logic here. You can also use "TextChanged", but I found KeyUp to have more usable behavior in this approach. If the user hasn't typed at least 3 characters, we bail. The Populating event is provided with a delegate that goes out to the service, returns the appropriate list of symbols, and the c_GetStockSymbolsCompleted callback handler hands off the data to the control and calls its PopulateComplete method, which basically says "I'm ready to provide these selections to the user to choose from".

And here is a screenshot of everything "in action":



This is a really useful control, and it's going to get better very shortly. In fact, they are going to add it to WPF next. Yep, Silverlight first, WPF later. Go Figure!

You can download the full Visual Studio 2008 solution here, including the list of 20,000 plus symbols, descriptions and exchanges. The file has all NYSE, AMEX, NASDAQ, OTCBB ("Pink Sheet") stocks as well as a number of indexes. Not all these symbols will work with Yahoo finance, so be prepared to handle the occasional boo-boo!

By Peter Bromberg   Popularity  (8823 Views)