Click here to Skip to main content
15,867,832 members
Articles / Desktop Programming / WPF

WPF AutoComplete Folder TextBox

Rate me:
Please Sign up or sign in to vote.
3.14/5 (10 votes)
3 Sep 2009CPOL3 min read 122.7K   3.6K   34   14
This article demos how to create a textbox which can auto-suggest items at runtime based on input, in this case, disk drive folders.

Image 1

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:

XML
<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:

XML
<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:

C#
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:

C#
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:

C#
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.

C#
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.

C#
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.

C#
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:

C#
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.

C#
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.

XML
<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:

XML
<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:

XML
<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)


Written By
Founder
Hong Kong Hong Kong

Comments and Discussions

 
GeneralMy vote of 4 Pin
Anna Novikova2-May-12 0:09
professionalAnna Novikova2-May-12 0:09 
GeneralMore Windows-Like behaviour Pin
TEiseler24-Apr-11 9:16
TEiseler24-Apr-11 9:16 
GeneralSolution: Repositioning of the Popup for the AutoCompleteBox Pin
SilverLaw3-Sep-09 20:49
SilverLaw3-Sep-09 20:49 
GeneralRe: Solution: Repositioning of the Popup for the AutoCompleteBox [modified] Pin
Leung Yat Chun3-Sep-09 22:30
Leung Yat Chun3-Sep-09 22:30 
GeneralRe: Solution: Repositioning of the Popup for the AutoCompleteBox Pin
Leung Yat Chun3-Sep-09 23:00
Leung Yat Chun3-Sep-09 23:00 
GeneralRe: Solution: Repositioning of the Popup for the AutoCompleteBox Pin
SilverLaw4-Sep-09 11:15
SilverLaw4-Sep-09 11:15 
GeneralPopup dont move with main window Pin
GerhardKreuzer19-Jan-09 0:55
GerhardKreuzer19-Jan-09 0:55 
AnswerRe: Popup dont move with main window [modified] Pin
Leung Yat Chun19-Jan-09 1:39
Leung Yat Chun19-Jan-09 1:39 
GeneralRe: Popup dont move with main window Pin
GerhardKreuzer19-Jan-09 1:49
GerhardKreuzer19-Jan-09 1:49 
GeneralRe: Popup dont move with main window Pin
Leung Yat Chun20-Jan-09 7:03
Leung Yat Chun20-Jan-09 7:03 
GeneralSource Code Pin
GerhardKreuzer18-Jan-09 19:53
GerhardKreuzer18-Jan-09 19:53 
GeneralRe: Source Code Pin
Leung Yat Chun18-Jan-09 22:21
Leung Yat Chun18-Jan-09 22:21 
GeneralRe: Source Code Pin
Kharcoff19-Nov-09 18:35
Kharcoff19-Nov-09 18:35 
GeneralMy vote of 1 Pin
#realJSOP21-Dec-08 23:47
mve#realJSOP21-Dec-08 23:47 

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

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