Multi-input Autocomplete Textbox Print E-mail
  
Friday, 08 May 2009 16:42

I have an application where users wanted to "tag" business objects with either their own tag, or choose from set of previously used tags. Tags could include spaces or just about any other characters. If a "tag" was already typed in, then it would no longer be one of the auto-complete options. Auto-complete options would not include typed-in tags. Users should be able to switch to the next value using arrow key or tab/shift-tab. Capitalization should be corrected when the suggestion is accepted.

Later, users wanted to swtich to the next option by using the comma, so I added that as well. 

To accomplish this, I used a FlowLayoutPanel as the base control and added auto-suggest textboxes as needed to provide auto-suggest functionality.

It's not perfect yet, but it's a start. Here's the code:

 

// by Alex Franke
public class MultiItemTextBox : FlowLayoutPanel
{
    #region Fields
    private char _Delimiter = ',';
    private bool _AutoCompleteResetting = false;
    private List<string> _Options = new List<string>();
    #endregion

    #region Properties 
    public IEnumerable<string> Options
    {
        get { return _Options; }
        set
        {
            _Options.Clear(); 
            _Options.AddRange( value ); 
        }
    }
    public IEnumerable<string> Values
    {
        get 
        {
            List<string> retVal = new List<string>();
            foreach ( TextBox tb in this.Controls )
            {
                if ( !string.IsNullOrEmpty( tb.Text ) )
                    retVal.Add( tb.Text );
            }
            return retVal;
        }
        set
        {
            RebuildControls( value );
        }
    }

    #endregion 

    #region Initialization
    public MultiItemTextBox()
    {
        this.AutoScroll = true;
        this.Padding = new Padding( 1 );
        this.BackColor = Color.White;

        this.Click += new EventHandler( MultiItemTextBox_Click );
        this.Enter += new EventHandler( MultiItemTextBox_Enter );
        this.Leave += new EventHandler( MultiItemTextBox_Leave );
    }
    #endregion 

    #region Event Handlers
    #region "this" control 
    void MultiItemTextBox_Click( object sender, EventArgs e )
    {
        if ( !this.Focused )
        {
            if ( this.Controls.Count == 0 )
            {
                AddTextEntryControl( string.Empty );
                this.Controls[0].Focus(); 
            }
        }
    }
    void MultiItemTextBox_Enter( object sender, EventArgs e )
    {
        AddTextEntryControl( string.Empty );
    }
    void MultiItemTextBox_Leave( object sender, EventArgs e )
    {
        // Remove any blank textboxes. 
        List<TextBox> toRemove = new List<TextBox>();
        foreach ( TextBox tb in this.Controls )
        {
            if ( String.IsNullOrEmpty( tb.Text ) )
                toRemove.Add( tb );
        }
        foreach ( TextBox tb in toRemove )
            this.Controls.Remove( tb );
    }
    #endregion 
    #region Sub-controls 
    void txt_Enter( object sender, EventArgs e )
    {
        if ( _AutoCompleteResetting )
            return;

        TextBox txt = sender as TextBox;
        txt.BackColor = Color.Yellow;

        ResetAutoComplete( txt );

        // Find out the minimum width
        int width = 0;
        foreach ( string s in _Options )
            width = Math.Max( width, TextRenderer.MeasureText( s, this.Font ).Width );
        width = Math.Max( width, TextRenderer.MeasureText( txt.Text, this.Font ).Width );
        width = Math.Max( width, 50 );

        txt.Width = width;
    }

    void txt_Leave( object sender, EventArgs e )
    {
        if ( _AutoCompleteResetting )
            return;

        // Remove the textbox if it's empty
        TextBox txt = sender as TextBox;
        txt.BackColor = Color.White;
        if ( string.IsNullOrEmpty( txt.Text ) )
            this.Controls.Remove( txt );
        else
        {
            // adjust casing if it matches one of the options. Autocomplete doens't enforce case
            string val = txt.Text.Trim();
            string option = _Options.Find(
                delegate( string s ) { return s.Equals( val, StringComparison.CurrentCultureIgnoreCase ); } );

            txt.Text = String.IsNullOrEmpty( option ) ? val : option; 
            txt.Width = TextRenderer.MeasureText( txt.Text, txt.Font ).Width;
        }
    }
    void txt_PreviewKeyDown( object sender, PreviewKeyDownEventArgs e )
    {
        if ( e.KeyCode == Keys.Tab )
        {
            bool forward = !e.Shift;
            TextBox txt = sender as TextBox;

            // If it's the last control and it's empty, then release focus completely
            if ( String.IsNullOrEmpty( txt.Text ) )
            {
                if ( ( IsFirst( txt ) && !forward ) || ( IsLast( txt ) && forward ) )
                {
                    Control ctrl = this.GetNextControl( this, forward );
                    if ( ctrl != null )
                        ctrl.Focus();
                    return;
                }
            }
            MoveToNext( txt, forward, true );
            e.IsInputKey = true;
        }
    }
    void txt_KeyDown( object sender, KeyEventArgs e )
    {
        // allow arrow keys to switch between controls 
        TextBox txt = sender as TextBox;
        if ( ( e.KeyCode == Keys.Left ) && ( txt.SelectionStart <= 0 ) )
            MoveToNext( txt, false, false );
        if ( ( e.KeyCode == Keys.Right ) && ( txt.SelectionStart >= txt.Text.Length ) )
            MoveToNext( txt, true, false );
    }

    void txt_KeyPress( object sender, KeyPressEventArgs e )
    {
        // Add a new textbox if the delimiter key is pressed. 
        if ( e.KeyChar.Equals( _Delimiter ) )
        {
            ( (TextBox)sender ).Select( 0, 0 );
            e.Handled = true;
            AddTextEntryControl( string.Empty );
            MoveToNext( (TextBox)sender, true, false ); 
        }
    }

    #endregion 
    #endregion

    #region Methods
    #region Internal / Private 
    private void AddTextEntryControl( string value )
    {
        // Create the new textbox. 
        TextBox txt = new TextBox();
        txt.BorderStyle = BorderStyle.None;
        txt.Font = this.Font;
        txt.AutoCompleteMode = AutoCompleteMode.SuggestAppend;
        txt.AutoCompleteSource = AutoCompleteSource.CustomSource;
        txt.KeyPress += new KeyPressEventHandler( txt_KeyPress );
        txt.KeyDown += new KeyEventHandler( txt_KeyDown );
        txt.Enter += new EventHandler( txt_Enter );
        txt.Leave += new EventHandler( txt_Leave );
        txt.Margin = new Padding( 0 );
        txt.PreviewKeyDown += new PreviewKeyDownEventHandler( txt_PreviewKeyDown );
        txt.Text = value;

        txt.CreateControl();

        // Add the new textbox 
        this.Controls.Add( txt );
    }
    private void ResetAutoComplete( TextBox textbox )
    {
        _AutoCompleteResetting = true;

        // Set up the autocomplete strings. 
        AutoCompleteStringCollection strings = new AutoCompleteStringCollection();
        strings.AddRange( _Options.ToArray() );
        textbox.AutoCompleteCustomSource = strings;

        // remove the strings already being used. 
        foreach ( string s in this.Values )
            strings.Remove( s );

        _AutoCompleteResetting = false;
    }
    private int GetIndexOf( TextBox control )
    {
        return this.Controls.GetChildIndex( control, false );
    }
    private bool IsFirst( TextBox control )
    {
        return ( GetIndexOf( control ) == 0 );
    }
    private bool IsLast( TextBox control )
    {
        return ( GetIndexOf( control ) == ( this.Controls.Count - 1 ) );
    }
    private TextBox GetNext( TextBox control )
    {
        if ( IsLast( control ) )
            return null;
        else
            return this.Controls[GetIndexOf( control ) + 1] as TextBox;
    }
    private TextBox GetPrevious( TextBox control )
    {
        if ( IsFirst( control ) )
            return null;
        else
            return this.Controls[GetIndexOf( control ) - 1] as TextBox;
    }

    private void MoveToNext( TextBox current, bool forward, bool selectAll )
    {
        TextBox txt = null;
        int index = GetIndexOf( current );

        if ( forward )
        {
            txt = GetNext( current );
            if ( txt != null )
            {
                if ( selectAll )
                    txt.SelectAll();
                else
                    txt.SelectionStart = 0;
                txt.Focus();
            }
            else
                AddTextEntryControl( string.Empty );
        }
        else
        {
            txt = GetPrevious( current );
            if ( txt != null )
            {
                if ( selectAll )
                    txt.SelectAll();
                else
                    txt.SelectionStart = txt.Text.Length;
                txt.Focus();
            }
        }
    }

    private void RebuildControls( IEnumerable<string> strings )
    {
        this.Controls.Clear();

        if ( strings == null )
            strings = this.Values;

        if ( strings != null )
        {
            foreach ( string s in strings )
                AddTextEntryControl( s );
        }
    }
    #endregion 
    #endregion
  
}

Last Updated ( Tuesday, 14 July 2009 16:52 )
 

Google Tools

Gmail Docs Code Finance Maps Calendar