Page view counter

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

autocomplete3

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

AddingRefToAutoFill 
(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,

SearchBoxAnnotated

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,

BlendInnerGridForAutoComplete2

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:

  1. Reading the file
  2. Creating the collection of unique words
  3. 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,

 

autocompleteThread

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 RSS 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!

=============

Published Monday, November 03, 2008 4:14 PM by jesseliberty
Filed under: ,

Comments

# re: AutoCompleteBox control / Worker Threads

Very good article. I think the animation adds life to the article.

Thanks,

Rachida

Monday, November 03, 2008 4:34 PM by rachidadukes@live.com

# AutoCompleteBox - Custom Types &#038; Worker Threads

Pingback from  AutoCompleteBox - Custom Types &#038; Worker Threads

Tuesday, November 04, 2008 5:02 AM by AutoCompleteBox - Custom Types & Worker Threads

# re: AutoCompleteBox control / Worker Threads

Great post, Jesse. I want to bring up the suggestions list even if the AutoCompleteBox is empty. Any suggestions on how to implement this?

Thanks,

Syed Mehroz Alam

Tuesday, November 04, 2008 5:58 AM by mehroz

# Silverlight News for November 04, 2008

Pingback from  Silverlight News for November 04, 2008

Tuesday, November 04, 2008 7:08 AM by Silverlight News for November 04, 2008

# re: AutoCompleteBox control / Worker Threads

Jesse, you asked if this new format of screen shots (with animation) is helpful. Many people think a form with moving parts (animation) is just to jazz things up, but don't realize the value it can bring if used at the right time. For example in this case where you show how the Auto complete is working or how the counter works and so on, is much more intuitive than if it was "Static" image. I know it's much harder on you, but cases like this, can make a big difference.

I was looking at some Yoga information and I see One image before she started the move and one image when the move was completed. And then she was trying to "Describe" what's happens between. Well, can you guess how many different [wrong ways] you can do between and not even knowing it. So, animation can deliver very essential data that changes based on other factors like time or speed or heat and etc.

Great article Jesse!!!

Tuesday, November 04, 2008 9:53 AM by BenHayat

# re: AutoCompleteBox control / Worker Threads

I'll have to thank Tim who suggested the idea of using our screen capture software to make these gifs; I agree with you that used judiciously they can be more than just bling.

Thanks.

Tuesday, November 04, 2008 11:17 AM by jesseliberty

# Silverlight Cream for November 04, 2008 -- #419

In this issue: Rich Griffin, Martin Mihaylov, Tim Heuer, Jafar Husain, Jeff Prosise, Mike Snow, Jeff

Tuesday, November 04, 2008 6:28 PM by Community Blogs

# re: AutoCompleteBox control / Worker Threads

Jesse:  Do you know of any tutorials demonstrating using the autocompletebox calling a web service?

Friday, November 07, 2008 4:44 PM by camoore

# A Ridiculous List of Silverlight Toolkit Resources

The Silverlight Toolkit is off to a great start and lots of people have been spending time writing content

Friday, November 07, 2008 11:36 PM by Shawn Burke's Blog

# #.think.in infoDose #6 (3rd Nov - 8th Nov)

#.think.in infoDose #6 (3rd Nov - 8th Nov)

Sunday, November 09, 2008 5:33 PM by #.think.in

# re: AutoCompleteBox control / Worker Threads

I have problem setting the Background property of the AutoCompleteBox, anyone else with the same problem or a solution?

Regards,

Håkan

Monday, November 10, 2008 9:18 AM by hehrsson

# Silverlight News for November 11, 2008

Pingback from  Silverlight News for November 11, 2008

Tuesday, November 11, 2008 2:53 AM by Silverlight News for November 11, 2008

# re: AutoCompleteBox control / Worker Threads

I want to make similar background loading of selected files in OpenFileDialog.

But it not works, I got security error by file.OpenRead();

'((System.IO.FileStream)(fileStream)).Name' threw an exception of type 'System.Security.SecurityException'

Looks like BackgroundWorker have no access to selected files.

I'm missing something?

Sunday, November 16, 2008 11:36 PM by hk-pavel

# re: AutoCompleteBox control / Worker Threads

i'm sorry, my mistake. I used BitmapImage in worker, this caused SecurityException.

Monday, November 17, 2008 12:13 AM by hk-pavel

# re: AutoCompleteBox control / Worker Threads

Jesse,

What is the safest method for implementing threads in Silverlight...I tried using BackGround worker, which updates/refreshes my datagrid at regular invervals from the database.The problem I faced was the number of threads increases continously and the performance of the application is slower..Any other approach i can use?

Thanks,

Mohammad Sadiq

Thursday, November 20, 2008 10:00 AM by sadiq123