| Multi-input Autocomplete Textbox |
|
|
| 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 ) |

