Second Life of a Hungarian SharePoint Geek

February 25, 2010

Multivalue AutoComplete WinForms TextBox for tagging

Filed under: SharePoint — Tags: — Peter Holpar @ 00:00

In one of my recent work I created a WinForms application that interacts with WSS 3.0 through web services. On the server side I created some kind of tagging with built-in WSS fields, but I had to solve the tagging in the client application as well.

The TextBox control has the necessary properties to create a single value autocomplete control, see the AutoCompleteMode and related properties.

You can find a sample for their usage here:

There are several implementations for custom autocomplete textboxes on the web, but none of these I’ve tried met my plans. I was to create something similar to implementation as the Windows Live Writer handles tags for blog posts.

Finally I’ve decided to create my own control. To tell the truth, I think it took less time than I’ve spent for looking up an existing solution previously.

In this post I present the results and some lessons I’ve learnt on the job.

I’ve implemented my control derived from the TextBox control. It has a private ListBox control to handle suggestions.

  1. private void ShowListBox()
  2. {
  3.     if (!_isAdded)
  4.     {
  5.         Parent.Controls.Add(_listBox);
  6.         _listBox.Left = this.Left;
  7.         _listBox.Top = this.Top + this.Height;
  8.         _isAdded = true;
  9.     }
  10.     _listBox.Visible = true;
  11. }
  12.  
  13. private void ResetListBox()
  14. {
  15.     _listBox.Visible = false;
  16. }

I had to handle the KeyUp event to update the ListBox to show the matching items, and the KeyDown event to handle special keys. These special keys are Up and Down to navigate between suggestions and the Tab to accept suggestions.

  1. private void this_KeyUp(object sender, KeyEventArgs e)
  2. {
  3.     UpdateListBox();
  4. }
  5.  
  6. private void this_KeyDown(object sender, KeyEventArgs e)
  7. {
  8.     switch (e.KeyCode)
  9.     {
  10.         case Keys.Tab:
  11.             {
  12.                 if (_listBox.Visible)
  13.                 {
  14.                     InsertWord((String)_listBox.SelectedItem);
  15.                     ResetListBox();
  16.                     _formerValue = this.Text;
  17.                 }
  18.                 break;
  19.             }
  20.         case Keys.Down:
  21.             {
  22.                 if ((_listBox.Visible) && (_listBox.SelectedIndex < _listBox.Items.Count – 1))
  23.                 {
  24.                     _listBox.SelectedIndex++;
  25.                 }
  26.                 break;
  27.             }
  28.         case Keys.Up:
  29.             {
  30.                 if ((_listBox.Visible) && (_listBox.SelectedIndex > 0))
  31.                 {
  32.                     // it is a double – for the case the blog engine removes on of them
  33.                     _listBox.SelectedIndex–;
  34.                 }
  35.                 break;
  36.             }
  37.     }
  38. }

Since by default Tab is to change the focus and the control that accepts user input, we had to override the IsInputKey method.

  1. protected override bool IsInputKey(Keys keyData)
  2. {
  3.     switch (keyData)
  4.     {
  5.         case Keys.Tab:
  6.             return true;
  7.         default:
  8.             return base.IsInputKey(keyData);
  9.     }
  10. }

In this implementation I use the semicolon character (‘;’) as a value separator, but it is easy to extend the solution to a configurable separator.

The UpdateListBox method updates the suggestion based on matches for the word start. It only suggests values not already selected. The GetWord method gets the “word” (the text between separator character or begin  / end of the text) we are currently typing. InsertWord inserts the accepted suggestion into the place we are typing in.

  1. private void UpdateListBox()
  2. {
  3.     if (this.Text != _formerValue)
  4.     {
  5.         _formerValue = this.Text;
  6.         String word = GetWord();
  7.  
  8.         if (word.Length > 0)
  9.         {
  10.             String[] matches = Array.FindAll(_values,
  11.                 x => (x.StartsWith(word) && !SelectedValues.Contains(x)));
  12.             if (matches.Length > 0)
  13.             {
  14.                 ShowListBox();
  15.                 _listBox.Items.Clear();
  16.                 Array.ForEach(matches, x => _listBox.Items.Add(x));
  17.                 _listBox.SelectedIndex = 0;
  18.                 _listBox.Height = 0;
  19.                 _listBox.Width = 0;
  20.                 this.Focus();
  21.                 using (Graphics graphics = _listBox.CreateGraphics())
  22.                 {
  23.                     for (int i = 0; i < _listBox.Items.Count; i++)
  24.                     {
  25.                         _listBox.Height += _listBox.GetItemHeight(i);
  26.                         // it item width is larger than the current one
  27.                         // set it to the new max item width
  28.                         // GetItemRectangle does not work for me
  29.                         // we add a little extra space by using '_'
  30.                         int itemWidth = (int)graphics.MeasureString(((String)_listBox.Items[i]) + "_", _listBox.Font).Width;
  31.                         _listBox.Width = (_listBox.Width < itemWidth) ? itemWidth : _listBox.Width;
  32.                     }
  33.                 }
  34.             }
  35.             else
  36.             {
  37.                 ResetListBox();
  38.             }
  39.         }
  40.         else
  41.         {
  42.             ResetListBox();
  43.         }
  44.     }
  45. }
  46.  
  47. private String GetWord()
  48. {
  49.     String text = this.Text;
  50.     int pos = this.SelectionStart;
  51.  
  52.     int posStart = text.LastIndexOf(';', (pos < 1) ? 0 : pos – 1);
  53.     posStart = (posStart == -1) ? 0 : posStart + 1;
  54.     int posEnd = text.IndexOf(';', pos);
  55.     posEnd = (posEnd == -1) ? text.Length : posEnd;
  56.  
  57.     int length = ((posEnd – posStart) < 0) ? 0 : posEnd – posStart;
  58.  
  59.     return text.Substring(posStart, length);
  60. }
  61.  
  62. private void InsertWord(String newTag)
  63. {
  64.     String text = this.Text;
  65.     int pos = this.SelectionStart;
  66.     
  67.     int posStart = text.LastIndexOf(';', (pos < 1) ? 0 : pos – 1);
  68.     posStart = (posStart == -1) ? 0 : posStart + 1;
  69.     int posEnd = text.IndexOf(';', pos);
  70.  
  71.     String firstPart = text.Substring(0, posStart) + newTag;
  72.     String updatedText = firstPart + ((posEnd == -1) ? "" : text.Substring(posEnd, text.Length – posEnd));
  73.  
  74.  
  75.     this.Text = updatedText;
  76.     this.SelectionStart = firstPart.Length;
  77. }

The Values and SelectedValues properties are to interact with the host application. Values accepts the possible suggestions, SelectedValues returns the tags applied.

  1. public String[] Values
  2. {
  3.     get
  4.     {
  5.         return _values;
  6.     }
  7.     set
  8.     {
  9.         _values = value;
  10.     }
  11. }
  12.  
  13. public List<String> SelectedValues
  14. {
  15.     get
  16.     {
  17.         String[] result = Text.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
  18.         return new List<String>(result);
  19.     }            
  20. }

The following code shows a simple host application:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Windows.Forms;
  4.  
  5. namespace AutoComplete
  6. {
  7.     public partial class TestForm : Form
  8.     {
  9.         private String[] _values = { "one", "two", "three", "tree", "four" };
  10.  
  11.         public TestForm()
  12.         {
  13.             InitializeComponent();
  14.             // AutoComplete is our special textbox control on the form
  15.             AutoComplete.Values = _values;
  16.         }
  17.  
  18.         // ShowSelection is a button to populate
  19.         // the SelectedList listbox with selected values
  20.         private void ShowSelection_Click(object sender, EventArgs e)
  21.         {
  22.             SelectedList.Items.Clear();
  23.             List<String> selectedValues = AutoComplete.SelectedValues;
  24.             Array.ForEach(selectedValues.ToArray(), selectedValue => SelectedList.Items.Add(selectedValue.Trim()));
  25.         }
  26.     }
  27. }

The following figures show the application in action.

On the first one you can see that the list box is cropped by the application borders.

image

It is a known issue. I’ve experienced with the ToolStripDropDown as described in this thread:

Make user control display outside of form boundry

I’ve found, that in this case the control is really not cropped, but I had issues by handling key events, for example, when I typed further it has no effect to the content of the textbox. Since I felt it is more serious that the cropping effect, I decided to keep with the simpler original solution.

The next figure illustrate that the suggestion list contains only the items (tags) not already selected. In this case the suggestion three is missing from the list.

image

When pressing the >> button, the selected values from the textbox control are transferred to the ListBox of the host application.

image

You can find the full source of the control and the host application on CodePlex.

Advertisements

2 Comments »

  1. what if the list box is cropped (or overlap ?) by the controls below the textbox control

    Comment by cvacangaran — November 22, 2012 @ 07:36

  2. Hello Peter,

    This is really an excellent control .Thanks for the same.But there is one small glitch.The list displayed is overlapped by the corresponding control that is placed below the text box.

    Say for example in any mail client we have To,CC and BCC fields below each other.So while using this control the list is overlapped by control below .Could you please set BringToFront property of the listbox in the ShowListBox() method.Also could you please add the select functionality from the list using _listBox_MouseClick event and upload this changes to codeplex for any new users who can use this control readily.

    Comment by Sachin — May 7, 2013 @ 07:57


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: