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!