views:

1159

answers:

2

I have a text box where I want to limit the number of selected items to MaxSelection. The desired behaviour is that once MaxSelection items are selected any futher selections are ignored. (Thus this question is different from "limit selections in a listbox in vb.net").

I have an event handler for the SelectedIndexChanged event for the list box that attempts to accomplish this. If the user uses Ctrl-click to select the (MaxSelection+1)th item, the selection is reverted to the previous selection.

The problem is when the user selects an item and then Shift-clicks an item down the list that is MaxSelection+1 items further down the list. In this case, more than one SelectedIndexChanged event is raised: one for the Shift-click which selects the item that was Shift-clicked, and one to select all the items between the original selection and the Shift-clicked selection. The first of these events allows the user to select the Shift-clicked item (which is technically correct), then the second event reverts the selection to the selection as it was after the first event (which will be the originally selected item and the Shift-clicked item). What is desired is that the code would revert the selection to the selection before the first event (which is only the originally selected item).

Is there any way to retain the selection before the Shift-click?

Thanks, Rob

Here's the SelectedIndexChanged event handler:

    void ChildSelectionChanged(object sender, EventArgs e)
    {
        ListBox listBox = sender as ListBox;

        //If the number of selected items is greater than the number the user is allowed to select
        if ((this.MaxSelection != null) && (listBox.SelectedItems.Count > this.MaxSelection))
        {
            //Prevent this method from running while reverting the selection
            listBox.SelectedIndexChanged -= ChildSelectionChanged;

            //Revert the selection to the previous selection
            try
            {
                for (int index = 0; index < listBox.Items.Count; index++)
                {
                    if (listBox.SelectedIndices.Contains(index) && !this.previousSelection.Contains(index))
                    {
                        listBox.SetSelected(index, false);
                    }
                }
            }
            finally
            {
                //Re-enable this method as an event handler for the selection change event
                listBox.SelectedIndexChanged += ChildSelectionChanged;
            }
        }
        else
        {
            //Store the current selection
            this.previousSelection.Clear();
            foreach (int selectedIndex in listBox.SelectedIndices)
            {
                this.previousSelection.Add(selectedIndex);
            }

            //Let any interested code know the selection has changed.
            //(We do not do this in the case where the selection would put
            //the selected count above max since we revert the selection;
            //there is no net effect in that case.)
            RaiseSelectionChangedEvent();
        }

    }
+1  A: 

Some 3rd-party components have cancelable events such as BeforeSelectedIndexChanged.

But when using the MS default component, I think that your approach is basically what you need. You could also store the selection in other events (such as MouseDown or KeyDown) which are known to be triggered before a change.

Lucero
Unfortunately, I'm finding that MouseDown and KeyDown are firing after the SelectedValueChanged event. You have, however, inspired a solution using MouseUp which I'll post shortly. Thanks so much.
Robert Gowland
I didn't test it, but rather assumed that they should get fired before. Sorry for that, and glad to hear that you found a workaround. However, in the "worst" case, just create your own control inherited from ListBox and hook into the windows message handling by overriding the WndProc method.
Lucero
A: 

Thanks to Lucero's insight that I could put the code to store the selection in another event, I was able to create a solution using MouseUp. As stated in the comments to Lucero's question, MouseDown fires after the SelectedValueChange event, so I has to use MouseUp instead. Here is the code:

    /// <summary>
    /// Handle the ListBox's SelectedValueChanged event, revert the selection if there are too many selected
    /// </summary>
    /// <param name="sender">the sending object</param>
    /// <param name="e">the event args</param>
    void ChildSelectionChanged(object sender, EventArgs e)
    {
        ListBox listBox = sender as ListBox;

        //If the number of selected items is greater than the number the user is allowed to select
        if ((this.MaxSelection != null) && (listBox.SelectedItems.Count > this.MaxSelection))
        {
            //Prevent this method from running while reverting the selection
            listBox.SelectedIndexChanged -= ChildSelectionChanged;

            //Revert the selection to the previously stored selection
            try
            {
                for (int index = 0; index < listBox.Items.Count; index++)
                {
                    if (listBox.SelectedIndices.Contains(index) && !this.previousSelection.Contains(index))
                    {
                        listBox.SetSelected(index, false);
                    }
                }
            }
            catch (ArgumentOutOfRangeException ex)
            {
            }
            catch (InvalidOperationException ex)
            {
            }
            finally
            {
                //Re-enable this method as an event handler for the selection change event
                listBox.SelectedIndexChanged += ChildSelectionChanged;
            }
        }
        else
        {
            RaiseSelectionChangedEvent();
        }
    }

    /// <summary>
    /// Handle the ListBox's MouseUp event, store the selection state.
    /// </summary>
    /// <param name="sender">the sending object</param>
    /// <param name="e">the event args</param>
    /// <remarks>This method saves the state of selection of the list box into a class member.
    /// This is used by the SelectedValueChanged handler such that when the user selects more 
    /// items than they are allowed to, it will revert the selection to the state saved here 
    /// in this MouseUp handler, which is the state of the selection at the end of the previous
    /// mouse click.  
    /// We have to use the MouseUp event since:
    /// a) the SelectedValueChanged event is called multiple times when a Shift-click is made;
    /// the first time it fires the item that was Shift-clicked is selected, the next time it
    /// fires, the rest of the items intended by the Shift-click are selected.  Thus using the
    /// SelectedValueChanged handler to store the selection state would fail in the following
    /// scenario:
    ///   i)   the user is allowed to select 2 items max
    ///   ii)  the user clicks Line1
    ///   iii) the SelectedValueChanged fires, the max has not been exceeded, selection stored
    ///        let's call it Selection_A which contains Line1
    ///   iii) the user Shift-clicks and item 2 lines down from the first selection called Line3
    ///   iv)  the SelectedValueChanged fires, the selection shows that only Line1 and Line3 are
    ///        selected, hence the max has not been exceeded, selection stored let's call it 
    ///        Selection_B which contains Line1, Line3
    ///   v)   the SelectedValueChanged fires again, this time Line1, Line2, and Line3 are selected,
    ///        hence the max has been exceeded so we revert to the previously stored selection
    ///        which is Selection_B, what we wanted was to revert to Selection_A
    /// b) the MouseDown event fires after the first SelectedValueChanged event, hence saving the 
    /// state in MouseDown also stores the state at the wrong time.</remarks>
    private void valuesListBox_MouseUp(object sender, MouseEventArgs e)
    {
        if (this.MaxSelection == null)
        {
            return;
        }

        ListBox listBox = sender as ListBox;

        //Store the current selection
        this.previousSelection.Clear();
        foreach (int selectedIndex in listBox.SelectedIndices)
        {
            this.previousSelection.Add(selectedIndex);
        }
    }
Robert Gowland