ListBox with Disableable Items






3.50/5 (4 votes)
Provides code for a ListBox with items that can be disabled

Introduction
Recently I needed to create an application where certain features were turned on or off depending on the customer. A group of these features were contained in a pair of ListBoxes, but I couldn't find any way to disable certain items in the lists. I decided to make my own class which derives from ListBox and here's what I ended up with.
Now with databinding! It actually wasn't as complicated as I thought it would be. More information below.
Using the Code
The DisableListBox contains a list of booleans, ListEnables
, which defines which items (in Items
) are enabled and disabled. ListEnables[i]
being true meaning Items[i]
is enabled. I've provided functions to add/insert/remove/removeAt and clear items, so you don't have to worry about keeping the two lists synchronized.
public void AddItem(object item, bool enabled);
public void InsertItem(int index, object item, bool enabled);
public void RemoveItem(object item);
public void RemoveItemAt(int index);
public void ClearItems();
Now if you want to enable or disable items after adding them, I created a few simple methods for that:
public void EnableItem(object item);
public void EnableItemAt(int index);
public void DisableItem(object item);
public void DisableItemAt(int index);
I've also left ItemEnables
exposed in case you want to mess around with the Items
and ItemEnables
lists yourself.
Now for the fun part. Version 1.1 allows databinding. The control exposes a property EnableMember which works just like DisplayMember and ValueMember. EnableMember will take the value in the property and use it to determine whether the item is enabled. It uses Convert.ToBoolean
so it can handle numeric types and strings. If you don't set EnableMember when databinding, all of the items will default to enabled. Here's the code which runs when EnableMember
or DataSource
change:
private void RefreshItemEnables()
{
// Get enable property
GetEnableProperty();
// Clear enable list
ItemEnables.Clear();
// Fill enable list
for (int i = 0; i < Items.Count; i++)
ItemEnables.Add(ProduceEnable(i));
// Ensure disabled items are not selected
for (int i = SelectedItems.Count - 1; i >= 0; i--)
if (!ItemEnables[SelectedIndices[i]])
SelectedItems.Remove(SelectedItems[i]);
}
private void GetEnableProperty()
{
// If it should be bound to a property
if (DataSource != null && EnableMember != string.Empty)
{
// Clear property
enableProperty = null;
// Find property
foreach (PropertyDescriptor property in DataManager.GetItemProperties())
if (property.Name == enableMember)
enableProperty = property;
}
}
private bool ProduceEnable(int i)
{
// If databound and enable property is set
if (DataSource != null && enableProperty != null)
try
{
// Convert property to boolean
return Convert.ToBoolean(enableProperty.GetValue(Items[i]));
}
// Object couldn't be converted to boolean
catch (InvalidCastException)
{
return false;
}
else
return true;
}
I also had to handle when items in the data source changed, which is done by registering some of the ListBox
's DataManager
(which is actually a CurrencyManager
) events.
void DataManager_ListChanged(object sender, ListChangedEventArgs e)
{
switch (e.ListChangedType)
{
// Handle items being added
case ListChangedType.ItemAdded:
ItemEnables.Insert(e.NewIndex, ProduceEnable(e.NewIndex));
break;
// Handle items being deleted
case ListChangedType.ItemDeleted:
ItemEnables.RemoveAt(e.NewIndex);
break;
}
}
void DataManager_ItemChanged(object sender, ItemChangedEventArgs e)
{
// Handle items changing
if (e.Index > -1)
SetEnabledAt(e.Index, ProduceEnable(e.Index));
}
A lot of the work went into making sure the disabled items couldn't be selected. For this I had to override WndProc
and catch the following messages:
// Page Up/Down
private const int VK_PRIOR = 0x21;
private const int VK_NEXT = 0x22;
// End/Home
private const int VK_END = 0x23;
private const int VK_HOME = 0x24;
// Arrow keys
private const int VK_LEFT = 0x25;
private const int VK_UP = 0x26;
private const int VK_RIGHT = 0x27;
private const int VK_DOWN = 0x28;
private const int WM_KEYDOWN = 0x100;
private const int WM_MOUSEMOVE = 0x200;
private const int WM_LBUTTONDOWN = 0x201;
First I handled mouse selection:
// Intercept mouse selection
if (m.Msg == WM_MOUSEMOVE || m.Msg == WM_LBUTTONDOWN)
{
// Get mouse location
Point clickedPt = new Point();
clickedPt.X = lParam & 0x0000FFFF;
clickedPt.Y = lParam >> 16;
// If point is on a disabled item, ignore mouse
for (int i = 0; i < Items.Count; i++)
if (!ItemEnables[i] && GetItemRectangle(i).Contains(clickedPt))
return;
}
Then keyboard selection (for brevity I've only shown half the code):
// Intercept keyboard selection
if (m.Msg == WM_KEYDOWN)
// Handle single down
if (wParam == VK_DOWN || wParam == VK_RIGHT)
{
// Select next enabled item
for (int i = SelectedIndex + 1; i < Items.Count; i++)
if (ItemEnables[i])
{
SelectedIndex = i;
break;
}
return;
}
// Handle single up
else if (wParam == VK_UP || wParam == VK_LEFT)
{
...
}
// Handle page up
else if (wParam == VK_PRIOR)
{
// Ignore if empty
if (ItemEnables.Count == 0)
return;
// Get current selected index
int currentIndex = Math.Max(0, SelectedIndex);
// Get number of items to jump
int toJump = NumVisibleItems() - 1;
// Check if there are enough items to jump a full page
if (currentIndex >= toJump)
{
// Jump at least a full page if possible
for (int i = currentIndex - toJump; i >= 0; i--)
if (ItemEnables[i])
{
SelectedIndex = i;
return;
}
}
// If there aren't enough items, try to jump as far as possible
else
toJump = currentIndex;
// Jump as far as possible without ending on a disabled item
for (int i = currentIndex - toJump; i <= currentIndex; i++)
if (ItemEnables[i])
{
SelectedIndex = i;
break;
}
return;
}
// Handle page down
else if (wParam == VK_NEXT)
{
...
}
// Handle end
else if (wParam == VK_END)
{
// Select closest enabled item to end
for (int i = ItemEnables.Count - 1; i >= 0; i--)
if (ItemEnables[i])
{
SelectedIndex = i;
break;
}
return;
}
// Handle home
else if (wParam == VK_HOME)
{
...
}
The final task was to handle the drawing of the disabled items. This was done by overriding OnDrawItem
after first setting DrawMode = DrawMode.OwnerDrawFixed;
in the constructor. I decided to expose two properties, EnabledItemColor
and DisabledItemColor
to set the text color of the items. These are defaulted to Black and Gray respectively in the constructor. I also added code to handle RightToLeft
being set and making sure it displays exactly like a ListBox.
protected override void OnDrawItem(DrawItemEventArgs e)
{
// Stops control from throwing errors if empty or in design mode
if (e.Index > -1 && !suspendDraw && !IsDesignMode())
{
// Draw the background
e.DrawBackground();
// Select color to use
Color color;
if (Enabled && ItemEnables[e.Index])
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
color = Color.White;
else
color = EnabledItemColor;
else
color = DisabledItemColor;
// Align text
Rectangle shiftedBounds;
TextFormatFlags alignment;
if (base.RightToLeft == RightToLeft.No)
{
// To look the same as ListBox, the bounds have to be shifted
shiftedBounds = new Rectangle(e.Bounds.X - 1, e.Bounds.Y, e.Bounds.Width,
e.Bounds.Height);
alignment = TextFormatFlags.Left;
}
else
{
// To look the same as ListBox, the bounds have to be shifted
shiftedBounds = new Rectangle(e.Bounds.X + 2, e.Bounds.Y, e.Bounds.Width,
e.Bounds.Height);
alignment = TextFormatFlags.Right;
}
// Get string to display
string displayString = GetItemText(Items[e.Index]);
// Draw the string
TextRenderer.DrawText(e.Graphics, displayString, e.Font, shiftedBounds, color,
alignment);
// Draw the focus rectangle
e.DrawFocusRectangle();
}
// Call base OnDrawItem
base.OnDrawItem(e);
}
Points of Interest
I had a fun time trying to get the name of the DisableListBox to show up in design mode, just like ListBox does it. Basically what this entailed was having the DrawMode
set to Normal
in the designer but OwnerDrawFixed
otherwise. Here's all the code that handles this:
// Set to normal so name shows up in design mode
private DrawMode drawMode = DrawMode.Normal;
/// <summary>
/// Gets or sets the drawing mode for the control.
/// </summary>
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public override DrawMode DrawMode
{
get { return drawMode; }
set
{
drawMode = value;
// Keeps base.DrawMode set to Normal so name shows up in the designer
if (!IsDesignMode()) base.DrawMode = value;
}
}
/// <summary>
/// Initializes a new instance of the DisableListBox class.
/// </summary>
public DisableListBox()
{
DrawMode = DrawMode.OwnerDrawFixed;
}
private bool IsDesignMode()
{
return DesignMode || LicenseManager.UsageMode == LicenseUsageMode.Designtime;
}
The IsDesignMode()
method is there because DesignMode
doesn't like to work all the time. You can read more about that here.
Special thanks go to Hans Passant (nobugz) and Nishant Sivakumar on the MSDN forums for helping me out with this.
Known Bug(s)
Changing the EnableMember
while data is bound causes enabled items to become disabled. If a previously enabled item was the selected item, it is unselected. Unfortunately, CurrencyManager
(which handles data binding) has no "unselected" position. If the data source changes before you select something else, the list will refresh and the disabled item will now be selected.
I don't know how to solve this because there's always the case where all the items are disabled so I can't just set it to another item instead of removing the selection. Also, in this case the OnSelectedIndexChanged
event doesn't even fire when the item is reselected. I'm clueless how to fix this so any suggestions are welcome.
The other "bugs" are that the DrawMode
, SelectionMode
and Sorted
properties have been hidden. DrawMode
has been hidden because changing it to Normal
doesn't show the disabled items, and if you're going to change it to OwnerDrawVariable
you'll have to change the OnDrawItem
code anyways, so at that point you can unhide it if you want to be able to switch between OwnerDrawFixed
and OwnerDrawVariable
.
Sorted
isn't implemented because to keep the two lists (Items
and ItemEnables
) synchronized I'll have to write a custom sort in the class, which just seems kind of wasteful. Plus I doubt anyone would use it all that much. If anyone would like to see it added just comment and we'll see.
SelectionMode
is hidden because the multi-select options really screw things up. I don't know how to handle shift-clicking when there are disabled items in the middle, so like Sorted
I won't be implementing this unless I get a request for it.
History
- July 29, 2009
- Updated to v1.2
- Added
EnableMemberError
event which fires if anEnableMember
value can't be converted to boolean, giving the exception and the index of the item - Bug Fix: Adding empty data source after the constructor threw an exception
- Bug Fix: Updating bound data source threw an exception
- Bug Fix: Page Up/Down and Home/End would let you select disabled items
- Bug Prevention: Disallows changing
SelectionMode
andSorted
until those features are implemented - July 17, 2009
- Updated to v1.1
- Added data binding and better drawing
- July 10, 2009
- Submitted article