AutoCompleteBox control / Worker Threads
I recently began a discussion of the Silverlight Toolkit and on the way towards explaining the AutoCompleteBox I became distracted by creating a list of words to use as our datasource.
I've actually reworked that example, slightly to build the list using a worker thread (to explore threading and to improve the UI) but I have broken through and actually managed to get to the point, which is adding an AutoCompleteBox to the page, and while I was at it, I included (per one of the examples provided) a slider to set the minimum number of characters you must put in before the box begins to show you matches
What we are seeing here, is the user is typing in letters and the autoCompleteBox is offering choices from our data source (the words retrieved from Swan's Way) that match what has been typed so far. The slider lets us set how many letters must be typed before choices are offered. Increasing the minimum prefix length cuts down on the clutter but offers less help (though it can vastly improve performance if the data is not local).
(Complete source code here)
Coding With the Auto Complete Box
Let's start with coding the auto-complete box and then circle back to the changes I made to gathering the data
The first step is to add the Toolkit library to your references
(image cropped)
With that we can add the namespace to the top of Page. xaml (the last name space in the UserControl)
<UserControl x:Class="AutoFill2.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="800" Height="601" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:controls="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls">
Finally, we'll add a grid within our outer grid to place our new controls. We want to add a prompt, an auto complete box, a second prompt for the prefix-length, the current value of the prefix length, two text boxes to indicate the range, and the slider itself,
Here's the Xaml,
<Grid x:Name="InnerGrid" Height="Auto" Margin="0,0,0,0"
VerticalAlignment="Stretch" Grid.Column="1" Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="0.385*"/>
<RowDefinition Height="41"/>
<RowDefinition Height="11"/>
<RowDefinition Height="15"/>
<RowDefinition Height="25"/>
<RowDefinition Height="59"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.442*"/>
<ColumnDefinition Width="0.558*"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="searchPrompt" Text="Search for: "
HorizontalAlignment="Right"
Margin="0,12,5,0" Grid.Row="1"
FontFamily="Verdana" FontSize="24" TextWrapping="Wrap" />
<controls:AutoCompleteBox x:Name="myAutoComplete"
Margin="5,0,0,0" Grid.Column="1" Grid.RowSpan="1" Grid.Row="1"
HorizontalAlignment="Left" Height="30" Width="195"
FontFamily="Verdana" FontSize="14" />
<TextBlock x:Name="minPrefix" Text="Minimum Prefix Length:"
Padding="5" FontFamily="Verdana"
Margin="0,0,0,0" Grid.Row="3"
HorizontalAlignment="Left" VerticalAlignment="Bottom"/>
<TextBlock x:Name="negOne"
HorizontalAlignment="Left" VerticalAlignment="Bottom"
Grid.Column="0" Grid.Row="4" FontFamily="Verdana" Text="-1"
Margin="5,0,0,0"/>
<TextBlock x:Name="eight"
Margin="0,0,5,0"
HorizontalAlignment="Right" VerticalAlignment="Bottom"
Grid.Column="0" Grid.Row="4"
FontFamily="Verdana" Text="8" />
<TextBlock x:Name="CurrentValue" Text="2"
HorizontalAlignment="Right" VerticalAlignment="Bottom"
Margin="5,0,0,0" Width="20"
Grid.Column="0" Grid.Row="3"
TextWrapping="Wrap" FontFamily="Verdana"
Foreground="#FFF6300B" FontSize="14"/>
<Slider x:Name="SetPrefixLength" Minimum="-1" Value="2" Maximum="8"
SmallChange="1" LargeChange="2" Width="160"
Grid.Row="4" Grid.Column="0" Margin="5,0,5,0" />
<Border Height="Auto" x:Name="Boundary"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Width="Auto" Margin="0,0,5,0"
Grid.Row="1" Grid.RowSpan="4" Grid.ColumnSpan="2"
Canvas.ZIndex="-1" Background="#FF73B8F2"/>
</Grid>
The Xaml declares an inner grid (making it easier to divide up the space for our search box). You can tell by the funky values that this grid was created in Blend,
A couple quick things to notice here… the trick to placing objects inside the inner grid is to make it the current container. You do that by double clicking on it. It will be surrounded by a yellow rectangle both in the Objects and Timeline window and in the art board. This gives the inner grid the same ability to draw rows and columns from the margins that you had with the outer grid.
Note also that we use a border control, this time not to draw a border around the other controls, but to provide a background color.
<Border Height="Auto" x:Name="Border"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Width="Auto" Margin="0,0,5,0"
Grid.Row="1" Grid.RowSpan="4" Grid.ColumnSpan="2"
Canvas.ZIndex="-1" Background="#FF73B8F2"/>
The zIndex="-1" ensures that the border will be behind all the other controls.
The AutoCompleteBox
Don't let the AutoCompleteBox get lost in all this discussion of set up (Remember the AutoCompleteBox? This is a posting about the AutoCompleteBox [ with apologies to Arlo Guthrie] )…
<controls:AutoCompleteBox x:Name="myAutoComplete"
Margin="5,0,0,0" Grid.Column="1" Grid.RowSpan="1" Grid.Row="1"
HorizontalAlignment="Left" Height="30" Width="195"
FontFamily="Verdana" FontSize="14" />
The Supporting Code for the AutoComplete box is in the Page.xaml.cs file. There are two steps here for supporting our control.
- After the WorkerThread runs to gather the data (covered below) we need to set that data as the ItemSource for the AutoCompleteBox
myAutoComplete.ItemsSource = SortedWords;
That is really the key and essence of setting up the AutoCompleteBox. However, we also want to remember to set up the event handler for the slider,
SetPrefixLength.ValueChanged +=
new RoutedPropertyChangedEventHandler<double>( SetPrefixLength_ValueChanged );
- When the user moves the slider, we will set the appropriate property on the AutoCompleteBox,
void SetPrefixLength_ValueChanged(
object sender, RoutedPropertyChangedEventArgs<double> e )
{
myAutoComplete.MinimumPrefixLength =
(int) Math.Floor(SetPrefixLength.Value);
CurrentValue.Text =
myAutoComplete.MinimumPrefixLength.ToString();
}
To set the value in the AutoCompleteBox (an integer) we must cast the double we retrieve from the the slider – making sure to truncate, not round. One way to do so is to use the Math.Floor function, which returns the highest integer value, as a double, that is less than the value of the argument. Eh? An example helps: if SetPrefixLength.value is equal to 7.3, 7.9 or 8.1 the value returned will be 7.0, 7.0 or 8.0 respectively.
We then cast that Floored double to an integer and assign it to the MinimumPrefixLength property of the AutoCompleteBox. The possible values in this example are –1 through 8. Note that a value of –1 turns off Autocompletion. Interestingly, the values of 0 and 1 have the same effect.
Getting The Data – In the Background
As promised, I reworked the code to obtain the data so that it is a bit more factored, and more important, the bulk of the work is done in a background tread making for a more rewarding UI. I won't review what I covered in the previous article, but I will show the changes.
The key is to initialize a private member variable of type BackgroundWorker, and to set the property WorkerReportsProgress in the constructor. You'll also need event handlers for:
- Thread starts (DoWork)
- Thread has progress to report (ProgressChanged)
- Thread completes (RunWorkerCompleted)
(NB: you can also choose to handle cancellation)
private BackgroundWorker worker = new BackgroundWorker();
//...
public Page()
{
InitializeComponent();
worker.WorkerReportsProgress = true;
worker.DoWork += new DoWorkEventHandler( worker_DoWork );
worker.ProgressChanged +=
new ProgressChangedEventHandler( worker_ProgressChanged );
worker.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler( worker_RunWorkerCompleted );
//...
}
We're going to kick off the thread when we determine that the user has chosen a file to open. We'll make sure the thread isn't already running and then call RunWorkerAsync (which will fire teh DoWork event) and we'll pass in the FileInfo object for the thread's edification.
void DataButton_Click( object sender, RoutedEventArgs e )
{
OpenFileDialog openFileDialog1 = new OpenFileDialog();
openFileDialog1.Filter = "Text Files (.txt)|*.txt|All Files (*.*)|*.*";
openFileDialog1.FilterIndex = 1;
openFileDialog1.Multiselect = false;
bool? userClickedOK = openFileDialog1.ShowDialog();
if ( userClickedOK == true )
{
// *** NEW ***
if ( worker.IsBusy != true )
worker.RunWorkerAsync(openFileDialog1.File);
}
}
This method is identical to the button click handler in the previous article, except that once the user identifies the file, we hand the fileInfo object to the worker thread and our job is done! We can now go eat lunch.
The Dowork method is called through the event delegate, passing in the sender (which you can safely cast to the BackgroundWorker) and a DoWorkEventArgs which contains, among other things, an argument property which in this case contains the FileInfo we passed in when we started the thread
1: void worker_DoWork( object sender, DoWorkEventArgs e )
2: {
3: const long MAXBYTES = 200000;
4: BackgroundWorker workerRef = sender as BackgroundWorker;
5:
6: if ( workerRef != null )
7: {
8: System.IO.FileInfo file = e.Argument as System.IO.FileInfo;
9:
10: if ( file != null )
11: {
12: System.IO.Stream fileStream = file.OpenRead();
13: using ( System.IO.StreamReader reader =
14: new System.IO.StreamReader( fileStream ) )
15: {
16: string temp = string.Empty;
17: try
18: {
19: do
20: {
21: temp = reader.ReadLine();
22: sb.Append( temp );
23: } while ( temp != null && sb.Length < MAXBYTES );
24: }
25: catch {}
26: } // end using
27: fileStream.Close();
28: string pattern = "\\b";
29: string[] allWords =
30: System.Text.RegularExpressions.Regex.Split(
31: sb.ToString(), pattern );
32:
33: long total = allWords.Length / 100;
34: long soFar = 0;
35: int newPctg = 0;
36: int pctg = 0;
37:
38: foreach ( string word in allWords )
39: {
40: newPctg = (int) ( (++soFar) / total );
41: if ( newPctg != pctg )
42: {
43: pctg = newPctg;
44: workerRef.ReportProgress( pctg );
45: }
46:
47: if ( words.Contains( word ) == false )
48: {
49: if ( word.Length > 0 && !IsJunk( word ) )
50: {
51: words.Add( word );
52: } // end if not junk
53: } // end if unique
54: } // end for each word in all words
55: } // end if file is not null
56: } // end if workerRef is not null
57: } // end method DoWork
Background Thread Processing
The method begins by casting the sender argument to the BackgroundWorker thread and making sure that the cast was successful (and not null). It then casts e.argument to be the FileInfo object (as described above) and again makes sure the cast is successful.
The next 20 lines are right out of the previous example, however starting on line 33 we begin to compute how far we've come in our work.
A true reading of our progress would take into account three stages:
- Reading the file
- Creating the collection of unique words
- Sorting the words
Since the first is very fast, and the third is instantaneous, and since this blog entry has gone on long enough, we'll constrain ourselves to reporting on progress on the second. We know how many words we have, and we know how many words we've processed as we iterate through the foreach loop so it is a simple matter to see when we've increased by a percentage. Each time we do, we call ReportProgress, passing in the new percentage figure.
foreach ( string word in allWords )
{
newPctg = (int) ( (++soFar) / total );
if ( newPctg != pctg )
{
pctg = newPctg;
workerRef.ReportProgress( pctg );
}
This fires an event that is caught in our UI thread,
void worker_ProgressChanged( object sender, ProgressChangedEventArgs e )
{
Message.Text = ( e.ProgressPercentage.ToString() + "%" );
}
The fact that this is caught in the main (UI) thread while the work is happening in the background worker thread, means that the UI is free to update,
When the thread completes it automagically calls the RunWorkerCompleted method, giving you a chance to clean up and to do any other work that can only be done once the thread finishes (in our case, setting the ItemSource for the AutoCompleteBox)
void worker_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e )
{
Message.Text = words.Count + " unique words added. ";
Display();
myAutoComplete.ItemsSource = SortedWords;
}
More on the AutoCompleteBox when I cover DataGrids and more on the Silverlight Toolkit very soon.
Thanks.
(If you like this article, you may wan to consider
subscribing. And it makes my boss happy).
=============
Special note: the animated gifs are an experiment. I think they convey a lot of information but they also make the page a lot bigger. If that is burdensome, let me know in the comments Thanks!
=============