Build a Custom Self-Expanding MultiSelect
WinForms ListBox
By Peter A. Bromberg, Ph.D.

Peter Bromberg

One of the biggest problems when designing screens and forms for surveys and assessments that have a large number of questions is "real estate". I have a healthcare assessment module I'm completing for a project that I've put into a tabbed interface. Each "Tab" represents a section of a very long healthcare assessment, and one of the speciification's requirements is to have a number of multi-select dropdown - type controls on a single tab.


The nice thing about a combobox is that it is very compact - it can occupy basically one "line" of vertical space, and when you click on the arrow at the right, all the items in it drop down and then when you are done selecting, it, contracts again. Thus, you can have many of these on a single form or "Tab".

Unfortunately, the Windows Forms ComboBox class doesn't support multiple selections. Only the ListBox does. But the problem with the ListBox is that if you set its height to a single item, you have to use the little up/down arrows at the right to move through the list of items, and that is clumsy and unproductive because you only get to see one of the items at a time. And if you set its height big enough to show all the items, well -- there goes your real estate.

I quickly figured out that it would be a lot easier to make a ListBox -- which already supports multiple selections -- behave the way I wanted, than to try to make a ComboBox support multiple selections.

My solution, I believe, is quite elegant and compact, and serves as an excellent illustration of the incredible power and flexibility of the .NET Framework. What I did, in essence, is to create a CustomListBox control derived from ListBox, and override its OnMouseEnter and OnMouseLeave events to automatically expand the height to exactly show all the items, allowing multiple selections, while also automatically sending all other controls to the back so that they didn't interfere with my "view". Then, when you are finished selecting and OnMouseLeave, everything is restored to its original compact, one-line height.

Let's breeze through the code, which I've commented in the appropriate places, and you will see that this is not at all a complex control to write:

using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Windows.Forms;

namespace PAB.CustomControls
{
 public class CustomListBox : System.Windows.Forms.ListBox
 {  
    private int LastHeight=0;
  private int LastParentHeight=0;
  private System.ComponentModel.Container components = null;

  public CustomListBox()
  {
   InitializeComponent();
  }

  protected override void OnMouseEnter(EventArgs e)
  {  
   // store the last height of this control
   this.LastHeight =this.Height;
   // use the ItemHeight to compute needed height
   int tmpHeight=this.GetItemHeight(0);     
   this.Height=this.Items.Count *(tmpHeight+1);
   // if parent is GroupBox or Panel, we also need to
   // expand its height temporarily so it will accomodate us!
   if(this.Parent is GroupBox || this.Parent is Panel)
   {
    // store the parent's height so we can put it back
    this.LastParentHeight=Parent.Height;
    this.Parent.Height=this.Height +tmpHeight;
                this.Parent.SendToBack();
    this.Parent.Refresh();
   }
  // all the other guys to the back of the line...
   Form frm = this.FindForm();
   foreach( Control c in frm.Controls)
   {   
    c.SendToBack();   
   }
 // we go to the front, everybody else behind us
   this.BringToFront();
   this.Refresh();
   base.OnMouseEnter (e);
  }
  protected override void OnMouseLeave(EventArgs e)
  {
   // restore original height
   this.Height =this.LastHeight ;
   // if we did the daddy, fix him too...
   if(this.LastParentHeight!=0)
    this.Parent.Height=this.LastParentHeight;
   base.OnMouseLeave (e);
  }
  protected override void Dispose( bool disposing )
  {
   if( disposing )
   {
    if( components != null )
     components.Dispose();
   }
   base.Dispose( disposing );
  }
  #region Component Designer generated code
   private void InitializeComponent()
  {
   this.Name = "CustomListBox1";
   this.Size = new System.Drawing.Size(150, 24);
  }
  #endregion
 }
}

Here is how the final control appears in its original and Expanded states:


No focus

On MouseEnter

You can download the solution below, which includes both the control project and a Windows Forms "Test Harness" just like the above.

Download the Source Code that accompanies this article

 

 

Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform. Pete's samples at GotDotNet.com have been downloaded over 41,000 times. You can read Peter's UnBlog Here.  --><--NOTE: Post QUESTIONS on FORUMS!
Do you have a question or comment about this article? Have a programming problem you need to solve? Post it at eggheadcafe.com forums and receive immediate email notification of responses.