4
votes

I have a WPF Listbox control and I would like to allow the user to change the selected item by using type-ahead. The behavior I'm looking for is exactly like windows explorer. As you continue to type the text of a folder name, the list will keep selecting the more correct item.

For example assume this folder structure:

OtherFolderName
MyFirstFolder
MyFirstFileFolder
MyFirstList

If you select OtherFolderName with the mouse, then start typing MyFirstF the item MyFirstFolder will be selected, but if you continue typing MyFirstFi the item MyFirstFileFolder will be selected.

My WPF Listbox does not exhibit this behavor, I am hoping I can easily enable it, as the old WinForms listbox did just this.

2

2 Answers

8
votes

Take a look at the TextSearch class, specifically the TextSearch.TextPath attached property:

<ListBox TextSearch.TextPath="FolderName" ... />

The TextSearch.TextPath property enables text searching and specifies how to extract the search text from each item. In this case I assumed each of your Folder objects has a property named "FolderName".

If this doesn't do everything you're looking for, you'll probably have to implement your own search, since the TextSearch feature isn't particularly tweakable. To do this:

  1. Handle the TextInput event
  2. Compare the time of the current TextInput with the prior TextInput. If close enough together, append to prefix string otherwise set it to the single character typed.
  3. Search all Items for the given prefix & if found set SelectedItem.

I would build this as a separate class using an attached property, similar to the built-in TextSearch class.

0
votes

I use a hidden TextBox that appears briefly while the person is typing, and resets after a couple of seconds and clears, so that it does not try matching on its contents after the timer expires. The person would type in the ListBox, and its KeyUp event will fill in the TextBox because of the binding on SearchText. When SearchText is filled, it triggers MyFilteredItems() to perform a match between that text and the ListBox. Then, if the person presses Enter, the selection would go into another TextBox (not listed in the XAML, but is provided as commented out in the code) and be cleared out of lstPickList. The TextBox is then cleared and timer resets.

XAML:

<TextBox Name="txtPicker" IsReadOnly="True" Foreground="LightGreen" FontFamily="Consolas" Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"></TextBox>

<ListBox Name="lstPickList" Grid.Row="1" ItemsSource="{Binding MyFilteredItems}" KeyUp="lstPickList_KeyUp"></ListBox>

And then this is the relevant code-behind:

public partial class MainWindow : Window, INotifyPropertyChanged
{
    private Timer t = new Timer();    
    public System.Windows.Threading.DispatcherTimer tCleanup =
         new System.Windows.Threading.DispatcherTimer();

    private string _searchText; 
    public string SearchText
    {
        get { return _searchText; }
        set
        {
            _searchText = value;

            OnPropertyChanged("SearchText");
            OnPropertyChanged("MyFilteredItems");
        }
    }

    public List<string> MyItems { get; set; }        

    public IEnumerable<string> MyFilteredItems
    {
        get
        {
            if (SearchText == null) return MyItems;

            return MyItems.Where(x => x.ToUpper().StartsWith(SearchText.ToUpper()));
        }            
    }


    public MainWindow()
    {
        InitializeComponent();

        MyItems = new List<string>() { "ABC", "DEF", "GHI" };                      
        this.DataContext = this;

        t.Interval = 1000;
        t.Elapsed += new ElapsedEventHandler(timerCounter);
        tCleanup.Interval = new TimeSpan(0,0,1);
        tCleanup.Tick += new EventHandler(cleanupCounter_Tick);        
        txtPicker.Visibility = Visibility.Collapsed;
        tCleanup.Start();
    }
    private static int counter = 0;
    protected void timerCounter(object sender, ElaspedEventArgs e)
    {
        counter++;   
    }

   protected void cleanupCounter_Tick(object sender, EventArgs e)
   {
        if (counter > 2 && txtPicker.Visibility == Visibility.Visible)
            txtPicker.Visibility = Visibility.Collapsed;   
   }

   private void lstPickList_KeyUp(object sender, KeyEventArgs e)
   {
       ListBox lst = (ListBox)sender;
       string strg = Convert.ToString(e.Key.ToString().Replace("D",""));
       if (counter < 2)
       {
           txtPicker.Visibility = Visibility.Visible;
           t.Start();
           if (strg == "Return")
           {
                txtPicker.Text += "{Enter}";
                SearchText += "{Enter}";
           }
           else
           {
               txtPicker.Text += strg;
               SearchText += strg;
           }
      }
      else
      {
          SearchText = strg;
          txtPicker.Text = strg;
          t.Stop();
          counter = 0;
          t.Start();
       }

       if (strg == "Return")
       {
           // This next line would be if you had a "selected items" ListBox to store the item
           // lstSelectedList.Items.Add(lstPickList.SelectedItem);
           lstPickList.Items.Remove(lstPickList.SelectedItem);
           t.Stop();
           txtPicker.Visibility = Visibility.Collapsed;
           counter = 0;
           txtPicker.Text = String.Empty;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
}