locked
ComboBox TextSearch RRS feed

  • Question

  • Hello,

    I like the way the ComboBox's text search feature works but I need to modify the behavior a little bit. I would like for the search to kick in only after the user types the first 3 letters. Is this possible without creating an extended ComboBox?
    Friday, April 17, 2009 3:05 PM

Answers

  • DeviantSeev, yeah i believe that is a safe bet.  I have created a form of an AutoCompleteComboBox that does much the same thing, had to put a good bit of advanced logic in code behind.
    If this was helpful, please mark as answered
    Blog: AttachedWPF
    AttachedWPF
    • Marked as answer by DeviantSeev Tuesday, April 21, 2009 7:08 PM
    Monday, April 20, 2009 6:39 PM
  • Hello,

    I like the way the ComboBox's text search feature works but I need to modify the behavior a little bit. I would like for the search to kick in only after the user types the first 3 letters. Is this possible without creating an extended ComboBox?
    You have to forbidden the build in TextSearch function first, and then write your own TextSearch function.

    You can inherit an AutoFilteredComboBox from ComboBox and rewrite its OnTextChanged method. The text search function will only work when Text.Length >= 3.

    Below is a simple demo project that shows this approach.

    Hope it helps.

    XAML
    <Window x:Class="_temple.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:_temple"
        Title="Window1" Height="300" Width="300">
        <StackPanel>
            <local:AutoFilteredComboBox IsTextSearchEnabled="False"    TextSearch.Text=""        x:Name="MyComboBox" IsEditable="True">
                <local:AutoFilteredComboBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock>
                            <TextBlock.Text>
                            <Binding></Binding>
                                </TextBlock.Text>
                        </TextBlock>
                    </DataTemplate>
                </local:AutoFilteredComboBox.ItemTemplate>
            </local:AutoFilteredComboBox>
        </StackPanel>
    </Window>
    

    C#
    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.ComponentModel;
    using System.Globalization;
    using System.Collections.ObjectModel;
    namespace _temple
    {
        /// <summary>
        /// Interaction logic for Window1.xaml
        /// </summary>
        public partial class Window1 : Window
        {
            public ObservableCollection<string> data = new ObservableCollection<string>();
            public Window1()
            {
                InitializeComponent();
                data.Add("abc01dsadsa");
                data.Add("xgttzxxxx");
                data.Add("tbnxxxx");
                data.Add("4456ereere");
                data.Add("7uiikdssaaa");
                this.MyComboBox.ItemsSource = data;
            }
        }
        public class AutoFilteredComboBox : ComboBox
        {
            private int silenceEvents = 0;
            /// <summary>
            /// Creates a new instance of <see cref="AutoFilteredComboBox" />.
            /// </summary>
            public AutoFilteredComboBox()
            {
                DependencyPropertyDescriptor textProperty = DependencyPropertyDescriptor.FromProperty(
                    ComboBox.TextProperty, typeof(AutoFilteredComboBox));
                textProperty.AddValueChanged(this, this.OnTextChanged);
                this.RegisterIsCaseSensitiveChangeNotification();
            }
            #region IsCaseSensitive Dependency Property
            /// <summary>
            /// The <see cref="DependencyProperty"/> object of the <see cref="IsCaseSensitive" /> dependency property.
            /// </summary>
            public static readonly DependencyProperty IsCaseSensitiveProperty =
                DependencyProperty.Register("IsCaseSensitive", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(false));
            /// <summary>
            /// Gets or sets the way the combo box treats the case sensitivity of typed text.
            /// </summary>
            /// <value>The way the combo box treats the case sensitivity of typed text.</value>
            [System.ComponentModel.Description("The way the combo box treats the case sensitivity of typed text.")]
            [System.ComponentModel.Category("AutoFiltered ComboBox")]
            [System.ComponentModel.DefaultValue(true)]
            public bool IsCaseSensitive
            {
                [System.Diagnostics.DebuggerStepThrough]
                get
                {
                    return (bool)this.GetValue(IsCaseSensitiveProperty);
                }
                [System.Diagnostics.DebuggerStepThrough]
                set
                {
                    this.SetValue(IsCaseSensitiveProperty, value);
                }
            }
            protected virtual void OnIsCaseSensitiveChanged(object sender, EventArgs e)
            {
                if (this.IsCaseSensitive)
                    this.IsTextSearchEnabled = false;
                this.RefreshFilter();
            }
            private void RegisterIsCaseSensitiveChangeNotification()
            {
                System.ComponentModel.DependencyPropertyDescriptor.FromProperty(IsCaseSensitiveProperty, typeof(AutoFilteredComboBox)).AddValueChanged(
                    this, this.OnIsCaseSensitiveChanged);
            }
            #endregion
            #region DropDownOnFocus Dependency Property
            /// <summary>
            /// The <see cref="DependencyProperty"/> object of the <see cref="DropDownOnFocus" /> dependency property.
            /// </summary>
            public static readonly DependencyProperty DropDownOnFocusProperty =
                DependencyProperty.Register("DropDownOnFocus", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(true));
            /// <summary>
            /// Gets or sets the way the combo box behaves when it receives focus.
            /// </summary>
            /// <value>The way the combo box behaves when it receives focus.</value>
            [System.ComponentModel.Description("The way the combo box behaves when it receives focus.")]
            [System.ComponentModel.Category("AutoFiltered ComboBox")]
            [System.ComponentModel.DefaultValue(true)]
            public bool DropDownOnFocus
            {
                [System.Diagnostics.DebuggerStepThrough]
                get
                {
                    return (bool)this.GetValue(DropDownOnFocusProperty);
                }
                [System.Diagnostics.DebuggerStepThrough]
                set
                {
                    this.SetValue(DropDownOnFocusProperty, value);
                }
            }
            #endregion
            #region | Handle selection |
            /// <summary>
            /// Called when <see cref="ComboBox.ApplyTemplate()"/> is called.
            /// </summary>
            public override void OnApplyTemplate()
            {
                base.OnApplyTemplate();
                if (this.EditableTextBox != null)
                {
                    this.EditableTextBox.SelectionChanged += this.EditableTextBox_SelectionChanged;
                }
            }
            /// <summary>
            /// Gets the text box in charge of the editable portion of the combo box.
            /// </summary>
            protected TextBox EditableTextBox
            {
                get
                {
                    return ((TextBox)base.GetTemplateChild("PART_EditableTextBox"));
                }
            }
            private int start = 0, length = 0;
            private void EditableTextBox_SelectionChanged(object sender, RoutedEventArgs e)
            {
                if (this.silenceEvents == 0)
                {
                    this.start = ((TextBox)(e.OriginalSource)).SelectionStart;
                    this.length = ((TextBox)(e.OriginalSource)).SelectionLength;
                    this.RefreshFilter();
                }
            }
            #endregion
            #region | Handle focus |
            /// <summary>
            /// Invoked whenever an unhandled <see cref="UIElement.GotFocus" /> event
            /// reaches this element in its route.
            /// </summary>
            /// <param name="e">The <see cref="RoutedEventArgs" /> that contains the event data.</param>
            protected override void OnGotFocus(RoutedEventArgs e)
            {
                base.OnGotFocus(e);
                if (this.ItemsSource != null && this.DropDownOnFocus)
                {
                    this.IsDropDownOpen = true;
                }
            }
            #endregion
            #region | Handle filtering |
            private void RefreshFilter()
            {
                if (this.ItemsSource != null)
                {
                    ICollectionView view = CollectionViewSource.GetDefaultView(this.ItemsSource);
                    view.Refresh();
                    this.IsDropDownOpen = true;
                }
            }
            private bool FilterPredicate(object value)
            {
                // We don't like nulls.
                if (value == null)
                    return false;
                // If there is no text, there's no reason to filter.
                if (this.Text.Length <= 2)
                    return true;
                string prefix = this.Text;
                // If the end of the text is selected, do not mind it.
                if (this.length > 0 && this.start + this.length == this.Text.Length)
                {
                    prefix = prefix.Substring(0, this.start);
                }
                return value.ToString()
                    .StartsWith(prefix, !this.IsCaseSensitive, CultureInfo.CurrentCulture);
            }
            #endregion
            /// <summary>
            /// Called when the source of an item in a selector changes.
            /// </summary>
            /// <param name="oldValue">Old value of the source.</param>
            /// <param name="newValue">New value of the source.</param>
            protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
            {
                if (newValue != null)
                {
                    ICollectionView view = CollectionViewSource.GetDefaultView(newValue);
                    view.Filter += this.FilterPredicate;
                }
                if (oldValue != null)
                {
                    ICollectionView view = CollectionViewSource.GetDefaultView(oldValue);
                    view.Filter -= this.FilterPredicate;
                }
                base.OnItemsSourceChanged(oldValue, newValue);
            }
            private void OnTextChanged(object sender, EventArgs e)
            {
                if (this.Text.Length >= 3)
                {
                    if (!this.IsTextSearchEnabled && this.silenceEvents == 0)
                    {
                        this.RefreshFilter();
                        // Manually simulate the automatic selection that would have been
                        // available if the IsTextSearchEnabled dependency property was set.
                        foreach (object item in CollectionViewSource.GetDefaultView(this.ItemsSource))
                        {
                            int text = item.ToString().Length, prefix = this.Text.Length;
                            this.SelectedItem = item;
                            this.silenceEvents++;
                            this.EditableTextBox.Text = item.ToString();
                            this.EditableTextBox.Select(prefix, text - prefix);
                            this.silenceEvents--;
                            break;
                        }
                    }
                }
            }
        }
    }
    

    • Proposed as answer by Tao Liang Tuesday, April 21, 2009 3:39 AM
    • Marked as answer by DeviantSeev Tuesday, April 21, 2009 6:53 PM
    Tuesday, April 21, 2009 3:38 AM
  • Tao,

    Thank you, your approach will work but I was looking for a way to do it without Inherittance. I ended up extending it and actually implemented something that may be a little messy but much simpler than what you did. The only drawback or flaw is that it takes away the ability to use the IsTextSearchEnabled. We can live with that so it's alright. (In any case if it ever becomes a problem, I will put in logic to handle that.)

    Here is my final code:

            /// <summary>
            /// Registers the TextSearchCharacters Dependency Property
            /// </summary>
            public static readonly DependencyProperty TextSearchCharactersProperty =
                DependencyProperty.Register("TextSearchCharacters", typeof(int),
                        typeof(ExtendedComboBox), new FrameworkPropertyMetadata(3, null, null));
    
    
            /// <summary>
            /// Gets or sets the integer value which determines how many characters are typed by the user before
            /// the text search feature kicks in. Default is 3 characters.
            /// </summary>
            public int TextSearchCharacters
            {
                get
                {
                    return (int)GetValue(TextSearchCharactersProperty);
                }
                set
                {
                    SetValue(TextSearchCharactersProperty, value);
                }
            }
    
            /// <summary>
            /// Determines if the text filter should kick in or not on the Preview event
            /// </summary>
            /// <param name="e"></param>
            protected override void OnPreviewTextInput(System.Windows.Input.TextCompositionEventArgs e)
            {
                //Determine if the user has typed in enough characters to do the TextSearch
                if ((this.Text.Length + e.Text.Length) >= TextSearchCharacters)
                {
                    IsTextSearchEnabled = true;
                }
                else
                {
                    IsTextSearchEnabled = false;
                }
    
                base.OnPreviewTextInput(e);
            }
    
    
     
    • Marked as answer by DeviantSeev Tuesday, April 21, 2009 6:53 PM
    Tuesday, April 21, 2009 6:53 PM

All replies

  • Can I conclude that this type of behavior modification is impossible without extending the ComboBox control and overriding the methods?
    Monday, April 20, 2009 6:33 PM
  • DeviantSeev, yeah i believe that is a safe bet.  I have created a form of an AutoCompleteComboBox that does much the same thing, had to put a good bit of advanced logic in code behind.
    If this was helpful, please mark as answered
    Blog: AttachedWPF
    AttachedWPF
    • Marked as answer by DeviantSeev Tuesday, April 21, 2009 7:08 PM
    Monday, April 20, 2009 6:39 PM
  • Hello,

    I like the way the ComboBox's text search feature works but I need to modify the behavior a little bit. I would like for the search to kick in only after the user types the first 3 letters. Is this possible without creating an extended ComboBox?
    You have to forbidden the build in TextSearch function first, and then write your own TextSearch function.

    You can inherit an AutoFilteredComboBox from ComboBox and rewrite its OnTextChanged method. The text search function will only work when Text.Length >= 3.

    Below is a simple demo project that shows this approach.

    Hope it helps.

    XAML
    <Window x:Class="_temple.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:_temple"
        Title="Window1" Height="300" Width="300">
        <StackPanel>
            <local:AutoFilteredComboBox IsTextSearchEnabled="False"    TextSearch.Text=""        x:Name="MyComboBox" IsEditable="True">
                <local:AutoFilteredComboBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock>
                            <TextBlock.Text>
                            <Binding></Binding>
                                </TextBlock.Text>
                        </TextBlock>
                    </DataTemplate>
                </local:AutoFilteredComboBox.ItemTemplate>
            </local:AutoFilteredComboBox>
        </StackPanel>
    </Window>
    

    C#
    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.ComponentModel;
    using System.Globalization;
    using System.Collections.ObjectModel;
    namespace _temple
    {
        /// <summary>
        /// Interaction logic for Window1.xaml
        /// </summary>
        public partial class Window1 : Window
        {
            public ObservableCollection<string> data = new ObservableCollection<string>();
            public Window1()
            {
                InitializeComponent();
                data.Add("abc01dsadsa");
                data.Add("xgttzxxxx");
                data.Add("tbnxxxx");
                data.Add("4456ereere");
                data.Add("7uiikdssaaa");
                this.MyComboBox.ItemsSource = data;
            }
        }
        public class AutoFilteredComboBox : ComboBox
        {
            private int silenceEvents = 0;
            /// <summary>
            /// Creates a new instance of <see cref="AutoFilteredComboBox" />.
            /// </summary>
            public AutoFilteredComboBox()
            {
                DependencyPropertyDescriptor textProperty = DependencyPropertyDescriptor.FromProperty(
                    ComboBox.TextProperty, typeof(AutoFilteredComboBox));
                textProperty.AddValueChanged(this, this.OnTextChanged);
                this.RegisterIsCaseSensitiveChangeNotification();
            }
            #region IsCaseSensitive Dependency Property
            /// <summary>
            /// The <see cref="DependencyProperty"/> object of the <see cref="IsCaseSensitive" /> dependency property.
            /// </summary>
            public static readonly DependencyProperty IsCaseSensitiveProperty =
                DependencyProperty.Register("IsCaseSensitive", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(false));
            /// <summary>
            /// Gets or sets the way the combo box treats the case sensitivity of typed text.
            /// </summary>
            /// <value>The way the combo box treats the case sensitivity of typed text.</value>
            [System.ComponentModel.Description("The way the combo box treats the case sensitivity of typed text.")]
            [System.ComponentModel.Category("AutoFiltered ComboBox")]
            [System.ComponentModel.DefaultValue(true)]
            public bool IsCaseSensitive
            {
                [System.Diagnostics.DebuggerStepThrough]
                get
                {
                    return (bool)this.GetValue(IsCaseSensitiveProperty);
                }
                [System.Diagnostics.DebuggerStepThrough]
                set
                {
                    this.SetValue(IsCaseSensitiveProperty, value);
                }
            }
            protected virtual void OnIsCaseSensitiveChanged(object sender, EventArgs e)
            {
                if (this.IsCaseSensitive)
                    this.IsTextSearchEnabled = false;
                this.RefreshFilter();
            }
            private void RegisterIsCaseSensitiveChangeNotification()
            {
                System.ComponentModel.DependencyPropertyDescriptor.FromProperty(IsCaseSensitiveProperty, typeof(AutoFilteredComboBox)).AddValueChanged(
                    this, this.OnIsCaseSensitiveChanged);
            }
            #endregion
            #region DropDownOnFocus Dependency Property
            /// <summary>
            /// The <see cref="DependencyProperty"/> object of the <see cref="DropDownOnFocus" /> dependency property.
            /// </summary>
            public static readonly DependencyProperty DropDownOnFocusProperty =
                DependencyProperty.Register("DropDownOnFocus", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(true));
            /// <summary>
            /// Gets or sets the way the combo box behaves when it receives focus.
            /// </summary>
            /// <value>The way the combo box behaves when it receives focus.</value>
            [System.ComponentModel.Description("The way the combo box behaves when it receives focus.")]
            [System.ComponentModel.Category("AutoFiltered ComboBox")]
            [System.ComponentModel.DefaultValue(true)]
            public bool DropDownOnFocus
            {
                [System.Diagnostics.DebuggerStepThrough]
                get
                {
                    return (bool)this.GetValue(DropDownOnFocusProperty);
                }
                [System.Diagnostics.DebuggerStepThrough]
                set
                {
                    this.SetValue(DropDownOnFocusProperty, value);
                }
            }
            #endregion
            #region | Handle selection |
            /// <summary>
            /// Called when <see cref="ComboBox.ApplyTemplate()"/> is called.
            /// </summary>
            public override void OnApplyTemplate()
            {
                base.OnApplyTemplate();
                if (this.EditableTextBox != null)
                {
                    this.EditableTextBox.SelectionChanged += this.EditableTextBox_SelectionChanged;
                }
            }
            /// <summary>
            /// Gets the text box in charge of the editable portion of the combo box.
            /// </summary>
            protected TextBox EditableTextBox
            {
                get
                {
                    return ((TextBox)base.GetTemplateChild("PART_EditableTextBox"));
                }
            }
            private int start = 0, length = 0;
            private void EditableTextBox_SelectionChanged(object sender, RoutedEventArgs e)
            {
                if (this.silenceEvents == 0)
                {
                    this.start = ((TextBox)(e.OriginalSource)).SelectionStart;
                    this.length = ((TextBox)(e.OriginalSource)).SelectionLength;
                    this.RefreshFilter();
                }
            }
            #endregion
            #region | Handle focus |
            /// <summary>
            /// Invoked whenever an unhandled <see cref="UIElement.GotFocus" /> event
            /// reaches this element in its route.
            /// </summary>
            /// <param name="e">The <see cref="RoutedEventArgs" /> that contains the event data.</param>
            protected override void OnGotFocus(RoutedEventArgs e)
            {
                base.OnGotFocus(e);
                if (this.ItemsSource != null && this.DropDownOnFocus)
                {
                    this.IsDropDownOpen = true;
                }
            }
            #endregion
            #region | Handle filtering |
            private void RefreshFilter()
            {
                if (this.ItemsSource != null)
                {
                    ICollectionView view = CollectionViewSource.GetDefaultView(this.ItemsSource);
                    view.Refresh();
                    this.IsDropDownOpen = true;
                }
            }
            private bool FilterPredicate(object value)
            {
                // We don't like nulls.
                if (value == null)
                    return false;
                // If there is no text, there's no reason to filter.
                if (this.Text.Length <= 2)
                    return true;
                string prefix = this.Text;
                // If the end of the text is selected, do not mind it.
                if (this.length > 0 && this.start + this.length == this.Text.Length)
                {
                    prefix = prefix.Substring(0, this.start);
                }
                return value.ToString()
                    .StartsWith(prefix, !this.IsCaseSensitive, CultureInfo.CurrentCulture);
            }
            #endregion
            /// <summary>
            /// Called when the source of an item in a selector changes.
            /// </summary>
            /// <param name="oldValue">Old value of the source.</param>
            /// <param name="newValue">New value of the source.</param>
            protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
            {
                if (newValue != null)
                {
                    ICollectionView view = CollectionViewSource.GetDefaultView(newValue);
                    view.Filter += this.FilterPredicate;
                }
                if (oldValue != null)
                {
                    ICollectionView view = CollectionViewSource.GetDefaultView(oldValue);
                    view.Filter -= this.FilterPredicate;
                }
                base.OnItemsSourceChanged(oldValue, newValue);
            }
            private void OnTextChanged(object sender, EventArgs e)
            {
                if (this.Text.Length >= 3)
                {
                    if (!this.IsTextSearchEnabled && this.silenceEvents == 0)
                    {
                        this.RefreshFilter();
                        // Manually simulate the automatic selection that would have been
                        // available if the IsTextSearchEnabled dependency property was set.
                        foreach (object item in CollectionViewSource.GetDefaultView(this.ItemsSource))
                        {
                            int text = item.ToString().Length, prefix = this.Text.Length;
                            this.SelectedItem = item;
                            this.silenceEvents++;
                            this.EditableTextBox.Text = item.ToString();
                            this.EditableTextBox.Select(prefix, text - prefix);
                            this.silenceEvents--;
                            break;
                        }
                    }
                }
            }
        }
    }
    

    • Proposed as answer by Tao Liang Tuesday, April 21, 2009 3:39 AM
    • Marked as answer by DeviantSeev Tuesday, April 21, 2009 6:53 PM
    Tuesday, April 21, 2009 3:38 AM
  • Tao,

    Thank you, your approach will work but I was looking for a way to do it without Inherittance. I ended up extending it and actually implemented something that may be a little messy but much simpler than what you did. The only drawback or flaw is that it takes away the ability to use the IsTextSearchEnabled. We can live with that so it's alright. (In any case if it ever becomes a problem, I will put in logic to handle that.)

    Here is my final code:

            /// <summary>
            /// Registers the TextSearchCharacters Dependency Property
            /// </summary>
            public static readonly DependencyProperty TextSearchCharactersProperty =
                DependencyProperty.Register("TextSearchCharacters", typeof(int),
                        typeof(ExtendedComboBox), new FrameworkPropertyMetadata(3, null, null));
    
    
            /// <summary>
            /// Gets or sets the integer value which determines how many characters are typed by the user before
            /// the text search feature kicks in. Default is 3 characters.
            /// </summary>
            public int TextSearchCharacters
            {
                get
                {
                    return (int)GetValue(TextSearchCharactersProperty);
                }
                set
                {
                    SetValue(TextSearchCharactersProperty, value);
                }
            }
    
            /// <summary>
            /// Determines if the text filter should kick in or not on the Preview event
            /// </summary>
            /// <param name="e"></param>
            protected override void OnPreviewTextInput(System.Windows.Input.TextCompositionEventArgs e)
            {
                //Determine if the user has typed in enough characters to do the TextSearch
                if ((this.Text.Length + e.Text.Length) >= TextSearchCharacters)
                {
                    IsTextSearchEnabled = true;
                }
                else
                {
                    IsTextSearchEnabled = false;
                }
    
                base.OnPreviewTextInput(e);
            }
    
    
     
    • Marked as answer by DeviantSeev Tuesday, April 21, 2009 6:53 PM
    Tuesday, April 21, 2009 6:53 PM
  • Great work :)
    Wednesday, April 22, 2009 1:56 AM