Click here to Skip to main content
15,885,278 members
Articles / Programming Languages / C#
Article

A text-based Name List + Lookup Control

Rate me:
Please Sign up or sign in to vote.
3.64/5 (10 votes)
20 Jan 20044 min read 71.8K   706   29   8
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.

C#
//
// 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.

C#
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.

C#
// 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.

C#
//
// 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 :).

C#
// 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.

C#
//
// 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.

C#
// 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.

C#
// 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).

C#
// 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.

C#
// 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


Written By
Web Developer
Austria Austria
Born in Vienna, Austria, in 1976 I had strong interest in computers and started developing with friends around 1990 - back then in Turbo Basic. Since then, I've learned PHP, ASP, TSQL, ColdFusion, .Net, VB and have worked for various companies as network administrator, developer and CTO. Currently my focus is on developing business applications, mainly as intranets, but also windows apps, as needed.

Comments and Discussions

 
General.NET 2.0 Pin
Haggy29-Jun-06 23:34
Haggy29-Jun-06 23:34 
GeneralRe: .NET 2.0 Pin
uTILLIty30-Jun-06 7:06
uTILLIty30-Jun-06 7:06 
GeneralCould be useful... Pin
Colin Angus Mackay21-Jan-04 2:15
Colin Angus Mackay21-Jan-04 2:15 
GeneralRe: Could be useful... Pin
uTILLIty21-Jan-04 20:15
uTILLIty21-Jan-04 20:15 
GeneralRe: Could be useful... Pin
Colin Angus Mackay21-Jan-04 22:11
Colin Angus Mackay21-Jan-04 22:11 
GeneralRe: Could be useful... Pin
webbsk7-Apr-06 10:27
webbsk7-Apr-06 10:27 
GeneralRe: Could be useful... Pin
dcarl6617-Jun-06 10:59
dcarl6617-Jun-06 10:59 
TILLIty,

I have two questions regarding you're nice example.

1. how is the control declared and used in a small c# program.

2. how do I get past the following compile error:

Error 2 'uTILLIty.Windows.Forms.TextObjectList.Validate()': cannot override inherited member 'System.Windows.Forms.ContainerControl.Validate()' because it is not marked virtual, abstract, or override C:\dcper\c#\richtext1\richtext1\uTILLIty.Windows.Forms.TextObjectList.cs 235
GeneralRe: Could be useful... Pin
uTILLIty30-Jun-06 6:01
uTILLIty30-Jun-06 6:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.