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.
class RichTextList : System.Windows.Forms.RichTextBox {
...
public char[] SeparationChars = new char[] {';',','};
...
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;
if (base.SelectionStart == base.Text.Length) return;
if (base.SelectionLength == base.Text.Length) return;
if (base.SelectionLength == 0
&& base.Text.IndexOfAny(this.SeparationChars,
base.SelectionStart,1) > -1) return;
this.MarkItem(base.SelectionStart);
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.
private void MarkItem(int Pos) {
this.NoSelChangeEvent = true;
if (Pos == base.Text.Length) Pos--;
int x1 = base.Text.LastIndexOfAny(this.SeparationChars, Pos, Pos)+1;
base.SelectionStart = x1;
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.
public class ObjectItem {
...
public System.Drawing.Color TextColor = System.Drawing.Color.Blue;
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 :).
public bool Validated {
get { return (this.ObjectRef != null); }
}
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.
public class ObjectItemCollection : CollectionBase {
...
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
.
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.
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;
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).
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();
}
foreach (char sepChar in this.SeparationChars) {
txtEntered = txtEntered.Replace(sepChar.ToString(), intSep);
}
string[] txtItems = txtEntered.Split(this.intSepChar);
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;
}
ObjectItem it = new ObjectItem(txtItem.Trim());
this.ObjectItems.Add(it);
idxs.Add( this.ObjectItems.IndexOf(it) );
}
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);
}
AllValid = true;
foreach (ObjectItem it in this.ObjectItems) {
if (!it.Validated) AllValid = false;
}
if (!AllValid && this.ValidateItems != null)
this.ValidateItems(this.ObjectItems);
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.
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);
}
protected void NList_Validating(object sender,
System.ComponentModel.CancelEventArgs e) {
e.Cancel = (!this.Validate());
}
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.