Introduction
In my current project I am a bit short of screen space for a list filter, so I wanted to be able to let the user select multiple items from a list, without taking up too much initial screen real estate. The obvious solution was to use a DropDownList styled ComboBox, but that only allows a single selection. So I went looking on the internet for a suitable control...
Background
I found this: CheckBox ComboBox Extending the ComboBox Class and Its Items[^]
But it has a number of problems: it needs a couple of clicks before it changes a checkbox.Checked state, the spacing on the checkboxes themselves is way too big (easy to solve) and frankly it looks a bit clumsy. The code itself is also hard to work out, and seemed a lot more complex than it needed to be.
So I took inspiration from this, and looked at the ToolStripDropDown on MSDN: ToolStripDropDown Class[^]. This gave an example that was a lot simpler and cleaner, but still had some oddities.
So, I built my own. It was surprisingly easy. Then I saw how the same thing could be used to provide a pop-up from any control - so I started again and made a Abstract Generic class that provided Popup facilities to any control.
Using the code
In this discussion I need to discuss two separate controls which can be displayed.
The first I will call the Base control - it sits on your form and causes the popup to open when the user interacts with it. The control that is displayed in the popup I will call the Target control.
PopUpControl
Derive your Base control from PopUpControl, specifying which control you want to "PopUp" (the Target control). The Base control you derive will be the one you add to your form, and you will provide a means to open the popup and display the target control.
This is slightly more complex than it needs to be. Thanks Microsoft!
- Create a new UserControl in the normal way.
- Edit the code file.
- STOP! Do not derive your control from
PopUpControl
yet!
PopUpControl
is Abstract, so you can't accidentally use it. If you derive directly from it, you will get a designer problem because it refuses to display controls derived from abstract (and has done since at least VS2008). It will however display controls derived from controls derived from abstract.
So... a bodge! - Go to the end of your file, and just inside the namespace, but outside your usercontrol, add the lines:
[ToolboxItem(false)] public class MyClassMyControl : PopUpControl<MyControl> { }
replacing MyClass
with your UserControl
name and MyControl
with the control you want to pop up. - Now, go back to the top, and derive your class from the new class you created in the last step.
You could use #if DEBUG
to select which class you derive from, but that means that the code you test is not the same code you release, so I prefer not to. - All you have to do now is design your Base control, and call the
Open
method when you want to pop up your Target.
That's it! Your selected Target control will pop up when you tell it to.
It exposes no public properties, methods, or events, and requires no methods to be implemented. The only needs it has are the Target control, and that you should call the Open
method when you want the target to pop up.
MultiSelectDropList
This is a Base control that look like a ComboBox set to use the DropDownList style, that contains a Multi-select ListBox as its Target. Just drop it on your form! It is pretty simple, it doesn't have a lot of properties / methods / events.
Properties:
Items
(ListBox.ObjectCollection
) Collection of items to display in the drop down list when open. These can be any object, provided it overrides ToString
to supply a human readable content. Getter only, setter not supplied.
SelectedItems
(ListBox.ObjectCollection
) Collection of items from the Items property that have been selected by the user. Getter only, setter not supplied.
Events:
SelectionChanged
signalled when the user selects or deselects one or more items.
Methods:
None.
You cannot add items to the drop down in the designer, the Items collection must be set at run time.
The MultiSelectDownDownList contains a single control - a Button docked to fill the control, and with its TextAlign set to the Left, and an image of a drop triangle set to align to the right. The click event calls the Open method to display the Target ListBox.
The Demo Application
The Demo app shows a MultiSelectDropList in action. It consists of two projects - the demo itself, and a UtilityControls class library that contains the PopUpControl and the MultiSelectDropList.
The Demo app itself is pretty simple: a demo class that just takes a string and a number, and overrides ToString to provide a human readable string:
namespace Demo
{
public class DemoClass
{
#region Properties
public int Number { get; set; }
public string Text { get; set; }
#endregion
#region Constructors
public DemoClass(string text, int number)
{
Text = text;
Number = number;
}
#endregion
#region Overrides
public override string ToString()
{
return string.Format("{0}: {1}", Number, Text);
}
#endregion
}
}
And a demo form containing three controls, a label, a MultiSelectDropList and a ListBox to display the current selection. The Form loads the MultiSelectDropList items in the constructor, and handles the MultiSelectDropList.SelectionChanged event to update the Listbox:
using System;
using System.Windows.Forms;
namespace Demo
{
public partial class frmDemo : Form
{
public frmDemo()
{
InitializeComponent();
DemoClass[] demo = new DemoClass[] {
new DemoClass("The first string", 1),
new DemoClass("Select me!",76),
new DemoClass("No, me!", 14),
new DemoClass("PICK ME", 42),
new DemoClass("You try thinking up",3),
new DemoClass("Seven strings for this.",-423),
new DemoClass("The last string", 666)};
dropList.Items.AddRange(demo);
}
private void dropList_SelectionChanged(object sender, EventArgs e)
{
showSelected.Items.Clear();
foreach (object o in dropList.SelectedItems)
{
showSelected.Items.Add(o);
}
}
}
}
The results are easy to see.
The drop list is in the top right, and shows that no items are selected.
Click the the drop down...
And the list pops up. Select an item...
And the list remains, but the list box shows the selected item - as does the drop list itself.
Select some more...
The list box is updated, but the drop list doesn't have room, so it automatically inserts an ellipsis. A tool tip will will show the complete list if you hover the mouse over the control when the list is closed.
Select some more...
Notice the drop list text has changed to reflect the number of items instead of showing any text. This is just a way of saying "there is a lot here - don't be fooled by a short string!"
This happens automatically when the combined text length exceeds twice the space available.
How it works, part 1
First, lets look at the MultiSelectDropList
class. Surprisingly, there is very little code here that is related directly to the task of dropping down the list: a single event handler containing a single line of code:
private void theButton_Click(object sender, EventArgs e)
{
Open();
}
That's it. All the rest of the code is concerned with supporting the list box and it's selections. If your target control need no support, then you need no more code.
Properties
[EditorBrowsable(EditorBrowsableState.Never)]
public ListBox.ObjectCollection Items
{
get { return content.Items; }
}
[EditorBrowsable(EditorBrowsableState.Never)]
public ListBox.SelectedObjectCollection SelectedItems
{
get { return content.SelectedItems; }
}
The first line of each property ensures that it does not appear in the designer property sheet, the getter just passes the property through.
Events
The SelectionChanged
event is constructed in the normal way (I use a Visual Studio snippet to automatically create them : A simple code snippet to add an event[^]
The only other event handler is the SelectedIndexChanged
event handler for the Popup ListBox:
private void content_SelectedIndexChanged(object sender, EventArgs e)
{
if (content.SelectedItems.Count == 0)
{
SetDropDownText("--None--");
}
else
{
StringBuilder sb = new StringBuilder();
string sep = "";
foreach (object o in content.SelectedItems)
{
sb.AppendFormat("{0}{1}", sep, o.ToString());
sep = "; ";
}
SetDropDownText(sb.ToString());
}
OnSelectionChanged(null);
}
Which is pretty obvious: it builds a string and uses a private method to output it to the text of the button that is the visible surface of the control. It then signals the event on up to the form handler.
private void SetDropDownText(string text)
{
toolTip.SetToolTip(theButton, text.Replace("; ", "\n"));
text = StaticMethods.FitTextToSpace(text,
Width - 25,
2,
string.Format("++ {0} Items ++", content.SelectedItems.Count),
Font);
theButton.Text = text;
}
Again, pretty obvious, it just uses a library method to fit the text to the space:
public static string FitTextToSpace(string text, int availableSpace, int limitMultiplier, string substituteText, Font font)
{
Size s = TextRenderer.MeasureText(text, font);
if (s.Width > availableSpace)
{
if (s.Width > availableSpace * limitMultiplier && !string.IsNullOrWhiteSpace(substituteText))
{
text = substituteText;
}
else
{
while (s.Width > availableSpace)
{
text = text.Substring(0, text.Length - 1);
s = TextRenderer.MeasureText(text + "...", font);
}
text += "...";
}
}
return text;
}
Again, not complex.
Constructor
All that is left is the constructor which sets up the ListBox parameters, and the ToolTip:
public MultiSelectDropList()
{
InitializeComponent();
content.SelectionMode = SelectionMode.MultiSimple;
content.SelectedIndexChanged += new EventHandler(content_SelectedIndexChanged);
toolTip.AutoPopDelay = 5000;
toolTip.InitialDelay = 1000;
toolTip.ReshowDelay = 500;
toolTip.ShowAlways = true;
SetDropDownText("--None--");
}
How it works, part 2
Now for the hideously complicated bit: the PopUpControl
.
What do you mean, there is almost no code there either?
It uses a ToolStripDropDown
control, which does all the work for you...
public partial class PopUpControl<T> : UserControl where T: Control, new()
Declares the control as derived from
Usercontrol
, and specifies that it needs a single Type parameter, which must be derived from a
Control
, and must have a constructor that takes no parameters.
Constants
const int reopenDelay = 200;
Private Fields
protected ToolStripDropDown dropDown = new ToolStripDropDown();
This is the control that does the work - it handles the drop down and its display, positioning, closing and everything else. It needs to be hooked to the Target control via the
ToolStripControlHost
protected ToolStripControlHost host;
This supports the
ToolStripDropDown
control and forms a bridge to the target control.
protected T content = new T();
The Target control itself.
private DateTime ignoreOpenUntil = DateTime.MinValue;
The description says it all.
Constructor
public PopUpControl()
{
InitializeComponent();
host = new ToolStripControlHost(content);
dropDown.Items.Add(host);
dropDown.VisibleChanged += new EventHandler(DropDown_VisibleChanged);
}
Just link the three controls together, and set the event handler for the
VisibleChanged
event - we use this to prevent ourselves from opening the drop down again if he clicks on the base control to close it.
Events
private void DropDown_VisibleChanged(object sender, EventArgs e)
{
Control c = sender as Control;
if (c != null)
{
if (!c.Visible)
{
ignoreOpenUntil = DateTime.Now.AddMilliseconds(reopenDelay);
}
}
}
Just set a time so that we won't open it again immediately (in case he does click on the base control to close the drop down) - currently set at 200ms - two tenths of a second is not noticeable in human terms.
Methods
protected void Open()
{
if (DateTime.Now > ignoreOpenUntil)
{
host.Width = content.Width;
host.Height = content.Height;
dropDown.Show(this, 0, this.Height);
}
}
And that is that. All you have to do is call this method, and everything happens behind the scenes!
History
First version