Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A text-based Name List + Lookup Control

0.00/5 (No votes)
20 Jan 2004 1  
A usable sample on how to build an outlook-style name-lookup control

Sample Image - TextObjectList.jpg

Introduction

Ever wanted to create an Outlook-style textbox, which parses the content one enters and validates it against some data source? Well, here is my implementation for such a control.

Step A - Internal Objects needed

I tried to keep the object-model as global as possible, so it suits most needs. For a base, I use the RichTextBox control, from which I inherit editing and formatted displaying of the text.

The features of the control: I allow for standard separation characters ';' and ',' - but as it's an array, you can add or change them as needed. Now, as I didn't want to write a text-editor, but focus on the lookup functionality, I created a class (RichTextList) which inherits from System.Windows.Forms.RichTextBox and wraps some logic for selecting and clicking into it. This way, I also have a native way to print the list with formatting (color, bold, underline etc). Of course, the RTF control is not really lightweight, so we might consider dropping it inline-rendering, but on the other hand, it's included in the .NET Framework, so it's not included binarily anyways.

  //

  // This class is only used internally -

  // out control will not expose any of it directly

  //

  class RichTextList : System.Windows.Forms.RichTextBox {
    ...
    public char[] SeparationChars = new char[] {';',','}; 
    // which chars are interpreted as object-separators

    ...

We override some inherited events so we can catch the user trying to click or select something. Wherever the user clicks, we try to select the item the user clicked into. We don't want the user to be able to change the writing of already validated items.

    protected override void OnSelectionChanged(EventArgs e) {
      if (this.NoSelChangeEvent) return;
      //at end of list

      if (base.SelectionStart == base.Text.Length) return;
      //selected whole list

      if (base.SelectionLength == base.Text.Length) return;
      if (base.SelectionLength == 0 //at sep-char

        && base.Text.IndexOfAny(this.SeparationChars,
        base.SelectionStart,1) > -1) return;
      this.MarkItem(base.SelectionStart); //within an item >> select it!

      base.OnSelectionChanged(e);
      if (base.SelectedText.Length > 0 && this.ItemSelected != null)
        this.ItemSelected(base.SelectedText);
    }

Here is our selection logic, which determines the beginning and end position of the currently clicked/selected item. Maybe, I'm old fashioned and should have used RegExp - not sure which way would have performed better.

    // this function actually marks the item in the textbox

    private void MarkItem(int Pos) {
      this.NoSelChangeEvent = true;
      /* Find first pos */
      if (Pos == base.Text.Length) Pos--;
      int x1 = base.Text.LastIndexOfAny(this.SeparationChars, Pos, Pos)+1;
      base.SelectionStart = x1;

      /* Find last pos */
      int x2 = base.Text.IndexOfAny(this.SeparationChars, Pos+1);
      base.SelectionLength = (x2<0?base.Text.Length:x2)-base.SelectionStart;
      this.NoSelChangeEvent = false;
    }

Now, we create a low-weight object which will represent an item in the list. This object will be used/instantiated for every parsed item we found that the user entered. Basically, it allows the developer to define the color it's displayed in, if it has been validated. Of course, to be globally usable, the control won't be able to validate any input by itself. It notifies the container through the ValidateItems event, which is only raised, if there are any unvalidated items in the list.

The point at which we validate is the OnValidate event which is raised automatically upon blur or when the parent/form requests validation.

  //

  // a low-weight Class to hold all parsed elements

  //

  public class ObjectItem {
    ...
    public System.Drawing.Color TextColor = System.Drawing.Color.Blue; 
    // the default color of a validated item

When the ValidateItems event is raised, the developer goes through the collection (see below) to determine the unvalidated items. To validate them, he simply processes the entered text and validates it against some back-end logic, which usually returns some object or a unique identifier for that object (such as a database ID, user object, DataRow, GUID or whatever). Whatever is returned, it can be hooked to the ObjectItem by assigning it to the ObjectRef. If ObjectRef is not null, the items go as being validated - very basic, very simple, very effective :).

    // wether this item has been validated or not

    public bool Validated {
      get { return (this.ObjectRef != null); }
    }

    // a reference to the validated source.

    // can be an ID value, an object-reference or any

    // other means of locating the resource. If this object is null,

    // then Validated returns false, else it returns true.

    public object ObjectRef;

But as we are likely to have more than one item in the list, we need a collection to hold all of them. We call this object ObjectItemCollection - as it's a collection of ObjectItems.

Let me focus you on these important implementations:

Whenever an item is removed from the list, we want to know about it! Usually, the developer wants to remove the item from some back-end resource (such as a database or business-object) as well, so an event is raised when this happens. Now, as all currently and previously explained objects don't comprise the primary control we are building, you'll see the relations of all these further below.

  //

  // The collection which holds all entered elements

  //

  public class ObjectItemCollection : CollectionBase {

    ...
    // we have the UI control raise an event, if an item 

    // has been removed from the collection

    protected override void OnRemoveComplete(int index, object value) {
      base.OnRemoveComplete (index, value);
      this.Textbox.OnRemoveItem((ObjectItem)value);
    }

Of course, the developer can add items at any time to our control - these are usually already validated, so he can provide the ObjectRef here as well. It wouldn't really make sense to programmatically add unvalidated items - in most cases anyways. Even if so, you just supply NULL for objRef.

    // implementing code can add items to the Text/Listbox using this

    // add method

    public ObjectItem Add(string itemName, object objRef) {
      ObjectItem it = new ObjectItem(itemName, objRef);
      it.isNew = true;
      List.Add(it);
      return it;
    }

Step B - Finally, the 'TextObjectList' UserControl itself

Next, we build the control itself, it will raise the events and manage the parsing and building of the list. This class I call TextObjectList and it will be the class of the Control (thus, it's public), so it must inherit from System.Windows.Forms.UserControl.

The events declared here are the ones you will be binding to. The sub-objects above will only report their doing to this control - it decides how to proceed and calls the shots.

    // our event delegates

    public delegate void ObjectItemRemovedEvent(TextObjectList list, 
                                                       ObjectItem item);
    public delegate void ObjectItemClickedEvent(TextObjectList list, 
                                    ObjectItem item, MouseEventArgs ev);
    public delegate void ValidateObjectItemsEvent(ObjectItemCollection col);

    public event ObjectItemClickedEvent ObjectItemClicked;
    public event ObjectItemRemovedEvent ObjectItemRemoved;
    public event ValidateObjectItemsEvent ValidateItems;

    // this collection holds all entered items - validated and not

    public ObjectItemCollection ObjectItems;

We override the Validate event so that we can act on user input (actually, act upon losing the focus or upon manual Validate request by the form).

    // we create our own validation code

    public override bool Validate() {
      base.Validate();
      bool AllValid = true;
      string txtEntered = this.NList.Text;
      string intSep = "";
      foreach (char sepChar in this.intSepChar) {
        intSep += sepChar.ToString();
      }

      /* Replace all allowed Sep-Chars with our internal one
       * so we can split the input */
      foreach (char sepChar in this.SeparationChars) {
        txtEntered = txtEntered.Replace(sepChar.ToString(), intSep);
      }

      /* Now split the input */
      string[] txtItems = txtEntered.Split(this.intSepChar);

      /* Then parse each item */
      ArrayList idxs = new ArrayList();
      foreach (string txtItem in txtItems) {
        if (txtItem.Trim() == string.Empty) continue;
        Debug.WriteLine(" .. parsing txtItem " + txtItem.Trim(), 
                                           "TextObjectList.Validate");
        if (this.ObjectItems.Contains(txtItem.Trim())) {
          idxs.Add( this.ObjectItems.IndexOf(
            this.ObjectItems.FindByName(txtItem.Trim())
            ));
          continue;
        }
        //not in collection yet, add it!

        ObjectItem it = new ObjectItem(txtItem.Trim());
        this.ObjectItems.Add(it);
        idxs.Add( this.ObjectItems.IndexOf(it) );
      }

      /* Now remove all items not in array */
      for (int i = this.ObjectItems.Count-1; i >= 0; i--) {
        if (idxs.Contains(i)) continue;
        if (this.ObjectItems.Item(i).isNew) continue;
        this.ObjectItems.RemoveAt(i);
      }

      /* Something to validate by host? */
      AllValid = true;
      foreach (ObjectItem it in this.ObjectItems) {
        if (!it.Validated) AllValid = false;
      }

      /* Now have the host validate all new items */
      if (!AllValid && this.ValidateItems != null)
        this.ValidateItems(this.ObjectItems);

      /* Finally visually display all items */
      AllValid = true;
      string newRtf = "";
      string colTbl = BuildColorTable();
      foreach (ObjectItem it in this.ObjectItems) {
        it.isNew = false;
        if (it.Validated) {
          newRtf += @"\cf" + this.colors[it.TextColor.ToArgb()] 
          + @"\ul\b " + it.ItemName + @"\b0\ulnone\cf0";
        } else {
          newRtf += @"\cf1 " + it.ItemName + @"\cf0";
          AllValid = false;
        }
        newRtf += " " + this.SeparationChars[0].ToString();
      }
      this.NList.Rtf = @"{\rtf1\ansi\ansicpg1252\deff0\deflang3079" +
        @"{\fonttbl{\f0\fswiss\fcharset0 Arial;}}" +
        @"{\colortbl ;\red255\green0\blue0;" + colTbl + "}" +
        @"{\*\generator TextObjectList.NET;}\viewkind4\uc1\pard\f0\fs20 " 
        + newRtf + @"\par}";
      return AllValid;
    }

Here are the events, being raised from objects below - we catch and handle them appropriately.

    // ah, an item in the textbox has been clicked,

     // we check which one it is in our 

    // collection and raise the appropriate event

    protected void NList_ItemClicked(string ItemName, MouseEventArgs e) {
      if (this.ObjectItemClicked == null) return;
      if (!this.ObjectItems.Contains(ItemName)) return;
      this.ObjectItemClicked(this, this.ObjectItems.FindByName(ItemName), e);
    }

    // our UI textbox wants to validate -

    // so we check all items and don't let the textbox

    // loose focus if an item in it could not be validated

    protected void NList_Validating(object sender, 
            System.ComponentModel.CancelEventArgs e) {
      e.Cancel = (!this.Validate());
    }


    // fire the event, if an item has been removed from the collection

    internal void OnRemoveItem(ObjectItem value) {
      if (this.ObjectItemRemoved != null)
        this.ObjectItemRemoved(this, value);
    }

Points of Interest

This sample actually gives you a quick intro on the following subjects and techniques: inheritance, delegates and events, control building, building collections. Since I use the ObjectItem object to work with objects, you can either just enhance it or inherit your own extended object from it to add even more features to it or form it to suit your needs.

I hope this code is useful. As I am only showing fragments here, please download the source-code using the above link. Feel free to contact me, if you have any questions.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here