Building an Editable ComboBox for WPF

Let's say we want to have in our WPF application an Editable ComboBox in which the user can select an item, but also type some free text. Unfortunately this is a little bit more work than setting the IsEditable property of a ComboBox to true. A standard ComboBox falls short if e.g. we want to make sure that -when binding is used- the source is updated at the expected moments, or when we want to apply a maximum length to the input. And while a ComboBox is supposed to be a combination of a TextBox and a DropDown (hence its name Combo), in WPF's standard implementation the TextBox is kept internal.

This little class shows you how you can control the binding updates and make inner TextBox properties accessible:

//-----------------------------------------------------------------------
// <copyright file="EditableComboBox.cs" company="DockOfTheBay">
//     http://www.dotbay.be
// </copyright>
// <summary>Defines the EditableComboBox class.</summary>
//-----------------------------------------------------------------------
namespace DockOfTheBay
{
    using System.ComponentModel;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Input;
 
    /// <summary>
    /// Editable combo box which updates its data source on the following events:
    /// - Selection changed (e.g. via the DropDown)
    /// - Lost focus (e.g. after Tab Key)
    /// - Enter or Return pressed.
    /// </summary>
    /// <remarks>
    /// To work properly, this EditableComboBox assumes/requires the following about its binding:
    /// - The data source should be bound to the Text property of the ComboBox (not SelectedValue)
    /// - The binding expression UpdateSourceTrigger property should be set to LostFocus
    /// </remarks>
    public class EditableComboBox : ComboBox
    {
        /// <summary>
        /// The Maximum Length of the TextBox's content.
        /// </summary>
        /// <remarks>
        /// It's implemented as a Dependency Property, so you can set it in XAML 
        /// </remarks>
        public static readonly DependencyProperty MaxLengthProperty =
            DependencyProperty.Register(
                "EditableTextBox",
                typeof(int),
                typeof(EditableComboBox),
                new UIPropertyMetadata(int.MaxValue));
 
        /// <summary>    
        /// Initializes a new instance of the <see cref="EditableComboBox"/> class.    
        /// </summary>    
        public EditableComboBox()
        {
            // Avoid non-intuitive behavior
            this.IsTextSearchEnabled = false;
            this.IsEditable = true;
        }
 
        /// <summary>
        /// Gets or sets the maximum length of the text in the EditableTextBox.
        /// </summary>
        /// <value>The maximum length of the text in the EditableTextBox.</value>
        [Description("Maximum length of the text in the EditableTextBox.")]
        [Category("Editable ComboBox")]
        [DefaultValue(int.MaxValue)]
        public int MaxLength
        {
            [System.Diagnostics.DebuggerStepThrough]
            get
            {
                return (int)this.GetValue(MaxLengthProperty);
            }
 
            [System.Diagnostics.DebuggerStepThrough]
            set
            {
                this.SetValue(MaxLengthProperty, value);
                this.ApplyMaxLength();
            }
        }
 
        /// <summary>
        /// Gets a reference to the internal editable textbox.
        /// </summary>
        /// <value>A reference to the internal editable textbox.</value>
        /// <remarks>
        /// We need this to get access to the real MaxLength.
        /// </remarks>
        protected TextBox EditableTextBox
        {
            get
            {
                return this.GetTemplateChild("PART_EditableTextBox") as TextBox;
            }
        }
 
        /// <summary>
        /// Makes sure that the design time settings are applied when the control 
        /// becomes operational. If the MaxLength setter is called too early, we miss
        /// the assignment.
        /// </summary>
        /// <remarks>
        /// This feels like a hack, but
        /// - OnInitialized is fired too early
        /// - OnRender in fired too often
        /// </remarks>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            this.ApplyMaxLength();
        }
 
        /// <summary>    
        /// Updates the Text property binding when the user presses the Enter key.  
        /// </summary>
        /// <remarks>
        /// KeyDown is not raised for Arrows, Tab and Enter keys.
        /// They are swallowed by the DropDown if it is open.
        /// So use the KeyUp instead.   
        /// </remarks>
        /// <param name="e">A Key EventArgs.</param>
        protected override void OnKeyUp(KeyEventArgs e)
        {
            base.OnKeyUp(e);
 
            if (e.Key == Key.Return || e.Key == Key.Enter)
            {
                this.UpdateDataSource();
            }
        }
 
        /// <summary>    
        /// Updates the Text property binding when the selection changes. 
        /// </summary>
        /// <param name="e">A SelectionChanged EventArgs.</param>
        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            base.OnSelectionChanged(e);
            this.UpdateDataSource();
        }
 
        /// <summary>
        /// Applies the MaxLength value to the EditableTextBox.
        /// </summary>
        private void ApplyMaxLength()
        {
            if (this.EditableTextBox != null)
            {
                this.EditableTextBox.MaxLength = this.MaxLength;
            }
        }
 
        /// <summary>    
        /// Updates the data source.
        /// </summary>    
        private void UpdateDataSource()
        {
            BindingExpression expression = GetBindingExpression(ComboBox.TextProperty);
            if (expression != null)
            {
                expression.UpdateSource();
            }
        }
    }
}


You have now access to the MaxLength property, even from XAML:

<local:EditableComboBox x:Name="EditableComboBox1" 
                        Text="{Binding Value, UpdateSourceTrigger=LostFocus}"  
                        MaxLength="15" />

13 comments:

  1. Hi, I have a question - can I use this (somehow) to have editable combobox in my properties window (in visual studio) of MyElement? You know - I have for example MyRadioButton and it has property OtherElements. This property should be editable combobox with names of all other elements on page.

    ReplyDelete
  2. Hello,
    What should I bind the Text property to?

    My first try:
    <local:EditableComboBox
    Text="{Binding ID, UpdateSourceTrigger=LostFocus"
    IsEditable="True" ItemsSource="{Binding FieldList}" DisplayMemberPath="Name" SelectedValuePath="ID" IsSynchronizedWithCurrentItem="True" />
    does not give any good results.

    ReplyDelete
  3. I can't get this to work either, the example usage is far to terse or either it just doesnt work, The example provided doesnt even include an itemssource binding. Please help and provide a full example on how to use this control from Xaml.

    ReplyDelete
  4. binding and editing is ok but It doesnt pop up or pop down combobox? It looks more like a textbox for me.

    ReplyDelete
  5. To use this to add to your ComboBox ItemsSource you must add a KeyUp handler so you can add the entered text into the ItemsSource.

    What this does accomplish is it works around the nagging "bug" of the WPF ComboBox which happens when you select an item but are bound to the Text property, the ComboBox will clear the selected text from the TextProperty.

    ReplyDelete
  6. Thanks for this, saved me many hours of scratching my head!

    ReplyDelete
  7. I was searching online for a way to set MaxLength on a ComboBox and found several posts that were not very helpful. Yours, however, is GREAT! Thanks for posting it!

    ReplyDelete
  8. This: return this.GetTemplateChild("PART_EditableTextBox") as TextBox;

    always returns NULL for me. Anyone know why?

    ReplyDelete
  9. This comment has been removed by the author.

    ReplyDelete
  10. This comment has been removed by the author.

    ReplyDelete
  11. Thank you! This worked great for me. Here's an example than complements the use of ItemsSource to show a list of users and allows to enter domain user accounts and adds them to the list:

    < helpers:EditableComboBox ItemsSource="{Binding CurrentUsers, NotifyOnSourceUpdated=True}" Text="{Binding AccountName, UpdateSourceTrigger=LostFocus}" SelectedIndex="{Binding UserSelectedIndex}" />

    Add the following properties to your ViewModel class. (Model is my model class and Users is a class that basically contains a List Names, an int Index and a string Selected to keep track of the users list and the currently selected user, you can simplify as needed):

    public List CurrentUsers
    {
    get { return Model.Users.Names; }
    }

    public string AccountName
    {
    get { return Model.AccountName; }
    set
    {
    Model.AccountName = value;
    Model.Users.Add(value);
    OnPropertyChanged(() => UserSelectedIndex);
    OnPropertyChanged(() => CurrentUsers);
    }
    }

    public int UserSelectedIndex
    {
    get { return Model.Users.Index; }
    set
    {
    Model.Users.Index = value;
    Model.AccountName = Model.Users.Selected;
    OnPropertyChanged(() => AccountName);
    }
    }

    ReplyDelete
  12. Are you looking to make money from your visitors by popup ads?
    In case you are, have you ever used EroAdvertising?

    ReplyDelete