Click here to Skip to main content
Click here to Skip to main content
Go to top

WPF AutoComplete Folder TextBox

, 3 Sep 2009
Rate this:
Please Sign up or sign in to vote.
This article demos how to create a textbox which can auto-suggest items at runtime based on input, in this case, disk drive folders.

Introduction

This article demos how to create a textbox which can auto-suggest items at runtime based on input, in this case, disk drive folders.

Background

There are a number of auto-complete textbox implementations around; however, some don't support data binding, and others don't support runtime items polling. After some Googling, I decided to write my own instead of continuing to look for one.

My design process

My first design is based on the ComboBox. I copy the default template and remove the drop down button and develop from that. It doesn't work because the combobox has its own autocomplete mechanism which will change the selection of the textbox when items are changed; it's not designed for items that change at real-time.

The second design is based on a TextBox. I create the following style:

<Style x:Key="autoCompleteTextBox" TargetType="{x:Type TextBox}">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type TextBoxBase}">
        <Grid x:Name="root">
          <ScrollViewer Margin="0" x:Name="PART_ContentHost"/>
          <Popup x:Name="PART_Popup" 
                      AllowsTransparency="true" 
                      Placement="Bottom" 
                      IsOpen="False"  
                      PopupAnimation="{DynamicResource 
                      {x:Static SystemParameters.ComboBoxPopupAnimationKey}}">
                <ListBox x:Name="PART_ItemList" 
                      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                      VerticalContentAlignment="Stretch" 
                      HorizontalContentAlignment="Stretch"
                      KeyboardNavigation.DirectionalNavigation="Contained" />
          </Popup>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
   </Setter>
</Style>

and then create a custom control and hook the style to it:

<TextBox x:Class="QuickZip.Controls.SelectFolderTextBox" 
  Style="{DynamicResource autoCompleteTextBox}" > 
  ... </TextBox>

PART_ContentHost is actually a TextBoxView; it is required for the TextBox template (with that name), or the control won't function. The other two parts (PART_Popup and PART_ItemList) are defined so I can use them in the custom control:

public partial class SelectFolderTextBox : TextBox
{
   Popup Popup { get { return this.Template.FindName("PART_Popup", this) as Popup; } }
   ListBox ItemList { get { return 
        this.Template.FindName("PART_ItemList", this) as ListBox; } }
   ScrollViewer Host { get { return 
        this.Template.FindName("PART_ContentHost", this) as ScrollViewer; } }
   UIElement TextBoxView { get { 
        foreach (object o in LogicalTreeHelper.GetChildren(Host)) 
             return o as UIElement; return null; } }
   ....
}

label.jpg

If text is changed, the suggestion item list is updated as well:

protected override void OnTextChanged(TextChangedEventArgs e)
{
  if (_loaded)
  {                                
    try
    {
      if (lastPath != Path.GetDirectoryName(this.Text))      
      {
        lastPath = Path.GetDirectoryName(this.Text);
        string[] paths = Lookup(this.Text); //Get subdirectory of current

        ItemList.Items.Clear();
        foreach (string path in paths)
          if (!(String.Equals(path, this.Text, 
            StringComparison.CurrentCultureIgnoreCase)))
              ItemList.Items.Add(path);
      }                        
                            
      Popup.IsOpen = true;
      
      //I added a Filter so Directory polling is only called once 
      //per directory, thus improve speed
      ItemList.Items.Filter = p =>
      {
        string path = p as string;
        return path.StartsWith(this.Text, StringComparison.CurrentCultureIgnoreCase)&&
         !(String.Equals(path, this.Text, StringComparison.CurrentCultureIgnoreCase));
      };
    }
    catch
    {
    }                
  }
}

A number of handlers is then defined:

public override void OnApplyTemplate()
{
  base.OnApplyTemplate();
  _loaded = true;
  this.KeyDown += new KeyEventHandler(AutoCompleteTextBox_KeyDown);
  this.PreviewKeyDown += new KeyEventHandler(AutoCompleteTextBox_PreviewKeyDown);
  ItemList.PreviewMouseDown += 
      new MouseButtonEventHandler(ItemList_PreviewMouseDown);
  ItemList.KeyDown += new KeyEventHandler(ItemList_KeyDown);
}

AutoCompleteTextBox_PreviewKeyDown

If the user pressed the down button, the textbox will move the focus to the listbox so the user can choose an item from it; this is placed in PreviewKeyDown instead of KeyDown because TextBox's mechanism will consume the event before it reaches KeyDown if the button is the down button.

void AutoCompleteTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
  if (e.Key == Key.Down && ItemList.Items.Count > 0
   && !(e.OriginalSource is ListBoxItem))
  {
    ItemList.Focus();
    ItemList.SelectedIndex = 0;
    ListBoxItem lbi = ItemList.
     ItemContainerGenerator.ContainerFromIndex(ItemList.SelectedIndex) as ListBoxItem;
    lbi.Focus();
    e.Handled = true;
  }
}

AutoCompleteTextBox_KeyDown

If the user presses the <Enter> button, the textbox will close the popup and update the binding.

void AutoCompleteTextBox_KeyDown(object sender, KeyEventArgs e)
{           
  if (e.Key == Key.Enter)
  {
    Popup.IsOpen = false;
    updateSource();
  }
}

ItemList_PreviewMouseDown and ItemList_PreviewMouseDown

If the user presses the <Enter> button (or select by mouse), the text textbox will be updated with ListBox.SelectedValue, and then we update the binding.

void ItemList_KeyDown(object sender, KeyEventArgs e)
{
  if (e.OriginalSource is ListBoxItem)
  {           
    ListBoxItem tb = e.OriginalSource as ListBoxItem;
    Text = (tb.Content as string);
    if (e.Key == Key.Enter)
    {                    
      Popup.IsOpen = false;
      updateSource();
    }
                
  }
}

void ItemList_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
  if (e.LeftButton == MouseButtonState.Pressed)
  {          {
    TextBlock tb = e.OriginalSource as TextBlock;
    if (tb != null)
    {
      Text = tb.Text;
      updateSource();
      Popup.IsOpen = false;
      e.Handled = true;
    }
  }
}

updateSource is required because I bound the text's UpdateSourceTrigger as Explicit; if updateSource is not called, it won't update the text:

void updateSource()
{
  if (this.GetBindingExpression(TextBox.TextProperty) != null)
    this.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}

The component is working now, but if you want to add validation as well, read below:

To support validation, a Validation Rule is written

If the path is not found or an exception is raised when looking up, it will return ValidationResult as false, and the error will be accessed by using the attached properties Validation.Errors and Validation.HasError.

public class DirectoryExistsRule : ValidationRule
{
  public static DirectoryExistsRule Instance = new DirectoryExistsRule();

  public override ValidationResult Validate(object value, 
         System.Globalization.CultureInfo cultureInfo)
  {
    try
    {
      if (!(value is string))
        return new ValidationResult(false, "Invalid Path");

      if (!Directory.Exists((string)value))
        return new ValidationResult(false, "Path Not Found");
    }
    catch (Exception ex)
    {
      return new ValidationResult(false, "Invalid Path");
    }
    return new ValidationResult(true, null);
  }
}

and we change the binding to use the created Validation Rule; note that UpdateSourceTrigger is Explicit.

<local:SelectFolderTextBox  x:Name="stb" 
           DockPanel.Dock="Bottom" Margin="4,0,0,0">
  <local:SelectFolderTextBox.Text>
    <Binding Path="Text" UpdateSourceTrigger="Explicit" >
      <Binding.ValidationRules>
        <t:DirectoryExistsRule />
      </Binding.ValidationRules>
    </Binding>
  </local:SelectFolderTextBox.Text>
</local:SelectFolderTextBox>

Now the textbox shows a red border if the directory does not exist. As a red border isn't clear enough, we can change the behavior to disable the default red border:

<Style x:Key="autoCompleteTextBox" TargetType="{x:Type TextBox}">
  <...>
  <Setter Property="Validation.ErrorTemplate">
    <Setter.Value>
      <ControlTemplate >
        <AdornedElementPlaceholder /> <!-- The TextBox Element -->
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

then change the control template, which will show the dockWarning when Validation.HasError:

<ControlTemplate TargetType="{x:Type TextBoxBase}">
  <Border Name="Border" CornerRadius="2"  
      Background="{StaticResource WindowBackgroundBrush}" 
      BorderBrush="{StaticResource SolidBorderBrush}" 
      BorderThickness="1" Padding="1" >
    <Grid x:Name="root">
      <...> 
      <DockPanel x:Name="dockWarning" 
              Visibility="Collapsed"  LastChildFill="False" >
      <Border DockPanel.Dock="Right"  BorderBrush="Red" Background="Red" 
          BorderThickness="2"  CornerRadius="2,2,0,0">
        <TextBlock x:Name="txtWarning" DockPanel.Dock="Right" 
                   Text="{TemplateBinding ToolTip}" VerticalAlignment="Top" 
                   Background="Red" Foreground="White"  FontSize="10" />
          <Border.RenderTransform>
            <TranslateTransform X="2" Y="{Binding ElementName=dockWarning, 
                              Path=ActualHeight,
                              Converter={x:Static t:InvertSignConverter.Instance}}"/>
            <!--TranslateTransform move the border to 
                   upper right corner, outside the TextBox -->
            <!--InvertSignConverter is a IValueConverter 
                   that change + to -, - to + -->
          </Border.RenderTransform>
        </Border>
      </DockPanel>  
    </Grid>
  </Border>
  <ControlTemplate.Triggers>
    <MultiTrigger>
      <MultiTrigger.Conditions>
        <Condition Property="Validation.HasError" Value="true" />
        <Condition SourceName="PART_Popup" Property="IsOpen" Value="False" />
      </MultiTrigger.Conditions>
      <Setter Property="ToolTip" Value="{Binding 
               RelativeSource={RelativeSource Self}, 
               Path=(Validation.Errors)[0].ErrorContent}"/>
      <Setter TargetName="dockWarning" Property="Visibility" Value="Visible" />
      <Setter TargetName="Border" Property="BorderThickness" Value="2" />
      <Setter TargetName="Border" Property="Padding" Value="0" />
      <Setter TargetName="Border" Property="BorderBrush" Value="Red" />
    </MultiTrigger>
  </ControlTemplate.Triggers>
</ControlTemplate>

History

  • 22-12-08: Initial version.
  • 25-12-08: Added ghost image when picking from ItemList.
  • 25-12-08: Handles PageUp/Down/Up buttons in the textbox.
  • 25-12-08: Handles Escape button in the ItemList.
  • 25-12-08: Disabled the caching system as it doesn't work well.
  • 25-12-08: Updated version 1.
  • 04-09-09: Disabled the TempVisual component.
  • 04-09-09: Popup repositions automatically when window is moved.
  • 04-09-09: Popup hides when deactivated (restores when activated).
  • 04-09-09: Updated version 2.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author


Comments and Discussions

 
GeneralMy vote of 4 Pinmemberanna.novikova2-May-12 0:09 
Don't think this control can be used out of box but it can be helpful during creating own auto complete text box.
GeneralMore Windows-Like behaviour PinmemberTEiseler24-Apr-11 9:16 
GeneralSolution: Repositioning of the Popup for the AutoCompleteBox PinmemberSilverLaw3-Sep-09 20:49 
GeneralRe: Solution: Repositioning of the Popup for the AutoCompleteBox [modified] PinmemberLeung Yat Chun3-Sep-09 22:30 
GeneralRe: Solution: Repositioning of the Popup for the AutoCompleteBox PinmemberLeung Yat Chun3-Sep-09 23:00 
GeneralRe: Solution: Repositioning of the Popup for the AutoCompleteBox PinmemberSilverLaw4-Sep-09 11:15 
GeneralPopup dont move with main window PinmemberGerhardKreuzer19-Jan-09 0:55 
AnswerRe: Popup dont move with main window [modified] PinmemberLeung Yat Chun19-Jan-09 1:39 
GeneralRe: Popup dont move with main window PinmemberGerhardKreuzer19-Jan-09 1:49 
GeneralRe: Popup dont move with main window PinmemberLeung Yat Chun20-Jan-09 7:03 
GeneralSource Code PinmemberGerhardKreuzer18-Jan-09 19:53 
GeneralRe: Source Code PinmemberLeung Yat Chun18-Jan-09 22:21 
GeneralRe: Source Code PinmemberKharcoff19-Nov-09 18:35 
GeneralMy vote of 1 PinmvpJohn Simmons / outlaw programmer21-Dec-08 23:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140922.1 | Last Updated 4 Sep 2009
Article Copyright 2008 by Leung Yat Chun
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid