Search This Blog

Custom ListBox control with Items Disable/Enable feature

I run once on a case where I have to show on a System.Windows.Forms.ListBox control some items that end user can not select, but still they have to be shown anyway since end user was expecting them. The point is that System.Windows.Forms.ListBox does not provide such feature, so I have decided to create my own user control and share it.

Here are some snapshots of the control at run time:










Disable item3 [index 2]











Disable Item5 [Index4]












Enabling Item3[index2]

Explaining the code:

This custom ListBox derives from System.Windows.Forms.ListBox, it overrides some methods, mainly drawing items, and selected indices management.

We need first to track the list of disabled items so we display them accordingly [grayed on screen], for this we need a collection like the one used to track the selectedindices.
This will have exactly the same structure.

public class DisabledIndexCollection : IList, ICollection, IEnumerable

public DisabledIndexCollection(ListBox owner)
{
    this.owner = owner;
}

This is needed to have a reference to the owner listbox, wich is passed as a "this" parameter when initializing a Netdev.Windows.Forms.ListBox as shown here:


public ListBox()
{
    DrawMode = System.Windows.Forms.DrawMode.OwnerDrawFixed;
    disabledIndices = new DisabledIndexCollection(this);
}



Drawing ListBox Items:

Painting Listbox items is done by overriding OnDrawItem method in the custom control.

protected override void OnDrawItem(System.Windows.Forms.DrawItemEventArgs e)
{
    base.OnDrawItem(e);
    if (DesignMode && Items.Count == 0)
    {
        if (e.Index == 0)
        {
            // Draw Control name as an item is design mode.
            e.Graphics.FillRectangle(SystemBrushes.Window, e.Bounds);
            e.Graphics.DrawString(this.Name, e.Font, SystemBrushes.WindowText, e.Bounds);
        }
        return;
    }


    if (e.Index != ListBox.NoMatches)
    {
        object item = this.Items[e.Index];
        // Get the text to display, this depends if the list is bound to data and 
        // DisplayMember, EnableFormating and FormatString are set.
        string displayValue = GetItemText(item);


        if (disabledIndices.Contains(e.Index))
        {
            e.Graphics.FillRectangle(SystemBrushes.InactiveBorder, e.Bounds);
            e.Graphics.DrawString(displayValue, e.Font, SystemBrushes.GrayText, e.Bounds);
        }
        else
        {
            if (SelectionMode == System.Windows.Forms.SelectionMode.None)
            {
                e.Graphics.FillRectangle(SystemBrushes.Window, e.Bounds);
                e.Graphics.DrawString(displayValue, e.Font, SystemBrushes.WindowText, e.Bounds);
            }
            else
            {
                if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
                {
                    e.Graphics.FillRectangle(SystemBrushes.Highlight, e.Bounds);
                    e.DrawFocusRectangle();
                    e.Graphics.DrawString(displayValue, e.Font, SystemBrushes.HighlightText, e.Bounds);
                }
                else
                {
                    e.Graphics.FillRectangle(SystemBrushes.Window, e.Bounds);
                    e.Graphics.DrawString(displayValue, e.Font, SystemBrushes.WindowText, e.Bounds);
                }
            }
        }
    }
}



 Enabling and Disabling items is done through two methods:

EnableItem(int index) and DisableItem(int index), where index is the index if the item we want to enable or disable.

Overriding OnSelectedIndexChanged method was required, to prevent the event from being propagated to end users if end user tries to select a disabled item. In such case the event is swallowed.


protected override void OnSelectedIndexChanged(EventArgs e)
{
    // Retreive new value of selected index.
    int currentSelectedIndex = SelectedIndex;
    List selectedDisabledIndices = new List();


    for (int i = 0; i < SelectedIndices.Count; i++)
    {
        // unselect items that are already disabled.
        if (disabledIndices.Contains(SelectedIndices[i]))
        {
            selectedDisabledIndices.Add(SelectedIndices[i]);
            SelectedIndices.Remove(SelectedIndices[i]);
        }
    }
    foreach (int index in selectedDisabledIndices)
    {
        // Fire DisabledItemSelected event for each disabled item that has been selected.
        IndexEventArgs args = new IndexEventArgs(index);
        OnDisabledItemSelected(this, args);
    }
    // if updated selected index is equal to the original one then bubble up the event
    if (currentSelectedIndex == SelectedIndex)
        base.OnSelectedIndexChanged(e);
}

public event EventHandler DisabledItemSelected;

protected virtual void OnDisabledItemSelected(object sender, IndexEventArgs e)
{
    if (DisabledItemSelected != null)
    {
        DisabledItemSelected(sender, e);
    }
}



2 comments:

  1. if the control is bound to a data source and DisplayMember property is set then items are not displaying the right value, rather they display the class name.

    ReplyDelete
  2. that's right, we are calling item.Tostring() method to get the text to display for each item, unless listbox items are strings or some basic types calling Tostring() will return the object class name and namespace if not overridden by user.

    I have posted a fix for this by calling the base class method "GetItemText(int index)" that will return the right text to display.

    ReplyDelete