
Introduction
Controls like the MultiColumnCombo
, the TreeCombo
, the better DateTimePicker
or similar can be found in all sorts of implementations on almost every programming site. All of these implementations have their pros and cons, so it can be very hard to choose the best for your purpose and it gets even harder as soon as you have to mix them in your particular application. Since they tend to be designed in a slightly different way, it could well happen that you end up with an application where each of those specialized ComboBox
es has its own custom behavior. This can make your application quite irrational for your users to work with.
That's why in my daily work the requirement arose to build something like a library or template as basis for custom combos. There might be bugs in it, at least in the early versions, there might be better solutions around for some particular combo behavior, but all combos created from that template will at least have consistent base behavior. New features as well as bug fixes will have to be implemented only once.
Background
Writing a decent ComboBox
clone is not all that sophisticated, but can become complex enough once you start to take the details into account. Displaying a modal form instead of the drop-down area is fairly easy, but having the hosting form appear focused requires some extra care. Fortunately, someone a lot cleverer than me, Steve McMahon, the guy who runs vbaccelerator.com, already published a decent solution on popup windows in .NET, so that all I needed to do was to take his excellent work and just add a little bit of my own code. Because I needed to make Steve's code fit into a larger application infrastructure, I had to refactor the original class names, but apart from that, Steve's code was left next to unchanged.
Using the Code
DropDownPanel
is a simple UserControl
hosting a ComboBox
and exposing the interface IDropDownAware
and a single property DropDownControl
, that again is of type IDropDownAware
. To enable any control to show up in the drop-down area of the DropDownPanel
, all you have to do is implement two interfaces (one for the control itself and one for the value it exposes) and set the DropDownPanel
's DropDownControl
to an instance of it.
The DropDownPanel
then takes care to host your control in its internal DropDownForm
, that pops up instead of the drop-down area of its ComboBox
whenever the combo's DropDown
event is triggered. Firing the FinishEditing
event from your control or a mouse click outside the DropDownForm
will cause the DropDownForm
to close and subsequently the DropDownPanel
to fire its own FinishEditing
event to let your application decide the next steps.
IDropDownAware
This interface enables the DropDownPanel
to expose its value and provides 2 events, one that indicates that the user is done with the dropdown
control and one that indicates changes while editing is in process. Possibly the custom EventArgs
will be extended in future versions as the necessity arises.
public interface IDropDownAware
{
event DropDownValueChangedEventHandler FinishEditing;
event DropDownValueChangedEventHandler ValueChanged;
object Value { get; set; }
}
So let's start creating a very basic implementation of a DropDownTree
as can be seen in this article's screenshot. We derive a class from TreeView
and implement IDropDownAware
. Of course, some events have to be handled to inform the DropDownPanel
when we think the user has chosen a TreeNode
or canceled editing.
internal class DropDownTree : TreeView, IDropDownAware
{
public DropDownTree()
{
}
protected override void OnAfterSelect(
TreeViewEventArgs e)
{
base.OnAfterSelect(e);
if (ValueChanged != null)
ValueChanged(this, new
DropDownValueChangedEventArgs(e.Node));
}
protected override void OnDoubleClick(EventArgs e)
{
base.OnDoubleClick(e);
TreeNode node = HitTest(PointToClient(Cursor
.Position)).Node;
if (FinishEditing != null)
FinishEditing(this, new
DropDownValueChangedEventArgs(node));
}
protected override void OnKeyUp(KeyEventArgs e)
{
base.OnKeyUp(e);
if (FinishEditing != null)
{
switch (e.KeyCode)
{
case Keys.Enter:
FinishEditing(this, new
DropDownValueChangedEventArgs(Value));
break;
case Keys.Escape:
FinishEditing(this, new
DropDownValueChangedEventArgs(null));
break;
}
}
}
public event DropDownValueChangedEventHandler FinishEditing;
public event DropDownValueChangedEventHandler ValueChanged;
public object Value
{
get { return base.SelectedNode; }
set
{
if (value is TreeNode)
base.SelectedNode = value as TreeNode;
}
}
}
ILookupItem<T>
Since I like the new object databinding features in .NET 2.0, I use them extensively throughout my projects. So I had this interface already available in my little toolbox, which is why I decided to reuse it here as well.
To keep the control as simple as possible, I set the internal combo's DropDownStyle
to DropDownList
to prevent text editing in the ComboBox
itself and instead allow editing in the drop-down portion of the control only. When DropDownPanel
's value should change after editing your control (i.e., your control fires the FinishEditing
event), the combo's DataSource
has to be cleared and the new value added to the combo's DataSource
in order to display the new value.
Possibly in future versions of this control, text editing features will be added, which could then make this simple interface obsolete. Don't get excited, I am afraid that whatever will replace it will be a lot more complex.
public interface ILookupItem<T> where T: struct
{
T Id { get; }
string Text { get; }
}
We better not populate the DropDownTree
with standard TreeNodes
but with objects derived from it instead. We just add an implementation of our ILookupItem<T>
:
internal class DropDownNode : TreeNode, ILookupItem<long>
{
public DropDownNode() : base()
{
}
public DropDownNode(string Text) : base(Text)
{
}
public DropDownNode(string Text, DropDownNode[]
Children) : base(Text, Children)
{
}
public DropDownNode(string Text, int ImageIndex, int
SelectedImageIndex)
: base(Text, ImageIndex,
SelectedImageIndex)
{
}
public DropDownNode(string Text, int ImageIndex,
int SelectedImageIndex,
DropDownNode[] Children)
: base(Text, ImageIndex,
SelectedImageIndex, Children)
{
}
public long Id
{
get { return 0; }
}
public new string Text
{
get { return base.Text; }
}
}
Having done this, all that is left to do, is to set up the DropDownTree
and to push it into the DropDownPanel
from within our application:
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
treePanel.DropDownControl = CreateTree();
}
private DropDownTree CreateTree()
{
DropDownTree tree = new DropDownTree();
DropDownNode root = new DropDownNode("1");
tree.BorderStyle = BorderStyle.None;
tree.Size = new Size(200, 300);
tree.Nodes.Add(root);
for (int i = 1; i <= 20; i++)
{
DropDownNode node = new DropDownNode("1." +
i.ToString("00"));
root.Nodes.Add(node);
for (int j = 1; j <= 2; j++)
node.Nodes.Add(new DropDownNode("1." +
i.ToString("00") + "." + j.ToString()));
}
root.Expand();
this.treePanel.FinishEditing += new
DropDownValueChangedEventHandler(
treePanel_FinishEditing);
this.treePanel.ValueChanged += new
DropDownValueChangedEventHandler(
treePanel_ValueChanged);
return tree;
}
Of course, it would be better to inherit your own specialized combos from DropDownPanel
, instead of using the DropDownPanel
itself. A sample for such an inherited control is included in the demo project.
History