A text-based Name List + Lookup Control






3.64/5 (7 votes)
Jan 21, 2004
4 min read

72101

706
A usable sample on how to build an outlook-style name-lookup control
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 ObjectItem
s.
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.