Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A multi-selection Drop Down List using a generic Abstract PopUp class

0.00/5 (No votes)
11 Jun 2012 1  
A Generic Abstract class providing a true Pop-up control, implemented in a multiple selection drop down list.

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!

  1. Create a new UserControl in the normal way.
  2. Edit the code file.
  3. 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!
  4. Go to the end of your file, and just inside the namespace, but outside your usercontrol, add the lines:
           /// <summary>
           /// This class exists solely to provide a non-abstract layer
           /// </summary>
           [ToolboxItem(false)] public class MyClassMyControl : PopUpControl<MyControl> { }
    replacing MyClass with your UserControl name and MyControl with the control you want to pop up.
  5. 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.
  6. 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
    {
    /// <summary>
    /// Demo of a simple class for use in MultiSelectDropList
    /// </summary>
    public class DemoClass
        {
        #region Properties
        /// <summary>
        /// A numeric value
        /// </summary>
        public int Number { get; set; }
        /// <summary>
        /// A string value
        /// </summary>
        public string Text { get; set; }
        #endregion
 
        #region Constructors
        /// <summary>
        /// Default constructor
        /// </summary>
        public DemoClass(string text, int number)
            {
            Text = text;
            Number = number;
            }
        #endregion
 
        #region Overrides
        /// <summary>
        /// Provide a human readable version
        /// </summary>
        /// <returns></returns>
        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
    {
    /// <summary>
    /// Demonstrate the MultiSelectDropList
    /// </summary>
    public partial class frmDemo : Form
        {
        /// <summary>
        /// Default constructor
        /// </summary>
        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);
            }
        /// <summary>
        /// Selection changed, update the display list.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        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:

        /// <summary>
        /// Clicked - open the drop down
        /// It will close when the user clicks away from it.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        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 

        /// <summary>
        /// The items for the list
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public ListBox.ObjectCollection Items
            {
            get { return content.Items; }
            }
        /// <summary>
        /// The selected items
        /// </summary>
        [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:

        /// <summary>
        /// Combo selection changed
        /// Update drop down text, and pass event on up.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        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.
        /// <summary>
        /// Set the control text, fitting it as needed.
        /// </summary>
        /// <param name="text" />
        private void SetDropDownText(string text)
            {
            // Tooltip: the list itself
            toolTip.SetToolTip(theButton, text.Replace(&quot;; &quot;, &quot;\n&quot;));
            // The button gets an abbreviated version.
            text = StaticMethods.FitTextToSpace(text,
                                                Width - 25,
                                                2,
                                                string.Format(&quot;++ {0} Items ++&quot;, content.SelectedItems.Count),
                                                Font);
            theButton.Text = text;
            }
Again, pretty obvious, it just uses a library method to fit the text to the space:
        /// <summary>
        /// Fit the text in the space
        /// If there is not enough space, first truncate and add an elipsis,
        /// Then replace with the substitution string when it exceeds the limit
        /// </summary>
        /// <param name="text" />
        /// <param name="availableSpace" />
        /// <param name="limitMultiplier" />
        /// <param name="substituteText" />
        /// <param name="font" />
        /// <returns />
        public static string FitTextToSpace(string text, int availableSpace, int limitMultiplier, string substituteText, Font font)
            {
            Size s = TextRenderer.MeasureText(text, font);

            if (s.Width &gt; availableSpace)
                {
                if (s.Width &gt; availableSpace * limitMultiplier &amp;&amp; !string.IsNullOrWhiteSpace(substituteText))
                    {
                    text = substituteText;
                    }
                else
                    {
                    while (s.Width &gt; availableSpace)
                        {
                        text = text.Substring(0, text.Length - 1);
                        s = TextRenderer.MeasureText(text + &quot;...&quot;, font);
                        }
                    text += &quot;...&quot;;
                    }
                }
            return text;
            }

Again, not complex.

Constructor 

All that is left is the constructor which sets up the ListBox parameters, and the ToolTip:

        /// <summary>
        /// Default constructor
        /// </summary>
        public MultiSelectDropList()
            {
            InitializeComponent();
            content.SelectionMode = SelectionMode.MultiSimple;
            content.SelectedIndexChanged += new EventHandler(content_SelectedIndexChanged);
            // Set up the delays for the ToolTip.
            toolTip.AutoPopDelay = 5000;
            toolTip.InitialDelay = 1000;
            toolTip.ReshowDelay = 500;
            // Force the ToolTip text to be displayed whether or not the form is active.
            toolTip.ShowAlways = true;
            // Set up the ToolTip text for the Button and Checkbox.
            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

        /// <summary>
        /// Milliseonds before reopen of drop down
        /// </summary>
        /// <seealso cref="ignoreOpenUntil"/>
        const int reopenDelay = 200;

Private Fields

        /// <summary>
        /// The actual drop down that shows the ListBox
        /// </summary>
        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 
        /// <summary>
        /// The Toolstrip Host
        /// (This can't be set until the control is ready)
        /// </summary>
        protected ToolStripControlHost host;
This supports the ToolStripDropDown control and forms a bridge to the target control.
        /// <summary>
        /// The content itself
        /// </summary>
        protected T content = new T();
The Target control itself.
        /// <summary>
        /// Used to prevent re-open of dropdown if the old one has just closed.
        /// For example, if this is a drop down list, then clicking on the
        /// list control (button with a drop triangle) will open the list.
        /// Clicking on it again should close the drop list, not re-open
        /// it again.
        /// </summary>
        private DateTime ignoreOpenUntil = DateTime.MinValue;
The description says it all.

Constructor

        /// <summary>
        /// Default constructor
        /// </summary>
        public PopUpControl()
            {
            InitializeComponent();
            // Prepare the dropdown for action
            host = new ToolStripControlHost(content);
            dropDown.Items.Add(host);
            // Add event to prevent re-open when expecting close.
            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

        /// <summary>
        /// Dropdown Visible changed.
        /// If it is now hidden, prevent immediate re-open
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        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 

        /// <summary>
        /// Open the Pop up
        /// </summary>
        protected void Open()
            {
            // Prevent re-open of dropdown if the old one has just closed.
            // For example, if this is a drop down list, then clicking on the
            // list control (button with a drop triangle) will open the list.
            // Clicking on it again should close the drop list, not re-open
            // it again.
            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

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