SelectedIndexChanged annoyance... what am I doing wrong?

SaintJimmy

Member
Joined
Jul 7, 2006
Messages
24
Programming Experience
5-10
I've got this application I'm working on, and there's a little annoyance I'm having with the damned ListView control. Here's basically what I've got:

The application is a client program that downloads data from SQL Server and stores it in a local DataSet. I query a customer table and populate a ListView control (lvCustomerList in the code below) with that data, and I have a panel on the form with several controls that display data related to the customer that is currently selected in the ListView control. The user can make changes to the data, and those changes are stored in the local DataSet.

Now... when the user selects a new customer from the ListView, I do some validation in the SelectedIndexChanged event (which I set up using AddHandler in the Form's Load event) to determine if any data was changed, and if it was, I prompt the user to save or reject the changes... and there's an option to cancel and remain on the customer that was changed so they can double-check their entry if they need to.

My problem comes in when the user selects "Cancel" from the prompt. I get the prompt twice when Cancel is selected, and I think it may have something to do with my changing the selection in the cancel routine, even though I remove the handler before changing selection and then add it back after changing.

And to make matters more confusing... when I use the debugger and step through the event handler, I only get the prompt once. Go figure.

Any thoughts on this? I've put the entire SelectedIndexChanged event below for you guys to check out. It's commented out the ass, so you should be able to make sense of what I'm doing. Let me know if I'm doing something wrong here... I've been racking my brain on this for days.

VB.NET:
    Private Sub lvCustomerList_SelectedIndexChanged(ByVal sender As System.Object, _
        ByVal e As System.EventArgs)
        
        ' Dump some data to the Output window to determine what the **** is going
        ' on here
        Debug.WriteLine("Entered handler...")
        Debug.IndentLevel += 1
        Debug.WriteLine("Selected Index Changed... ")
        Debug.IndentLevel += 1
        If lvCustomerList.SelectedItems.Count > 0 Then
            Debug.WriteLine("Customer Selected: " & _
                DirectCast(lvCustomerList.SelectedItems(0), _
                CustomerListViewItem).GetDataRow.Item("CustomerName").ToString.Trim())
            If Not _currentcustomerrecord Is Nothing Then
                Debug.WriteLine("Previous Customer: " & _
                    _currentcustomerrecord.Item("CustomerName").ToString.Trim())
            Else
                Debug.WriteLine("First Selection after Execution")
            End If
        Else
            Debug.WriteLine("No items selected")
        End If
        Debug.IndentLevel -= 1
        ' There should only be one item selected, since the MultiSelect property
        ' has been turned off.  However, events are also fired when deselecting an item,
        ' so, check that there is a selected item before proceeding
        If lvCustomerList.SelectedItems.Count > 0 Then
            ' We need to check the current customer record for a value before
            ' attempting to validate.  We only need to validate if this is a 
            ' non-Nothing value, since otherwise it is the first selection made
            ' upon running the application.
            If Not _currentcustomerrecord Is Nothing Then
                ' If we make it here, then a customer was already selected before
                ' the index change.  Let's verify that the new selection is actually
                ' a different customer
                ' We need to be able to gain access to the DataRow associated with
                ' the newly selected item, so cast the currently selected item to the
                ' necessary type.
                Dim newitem As CustomerListViewItem = _
                    DirectCast(lvCustomerList.SelectedItems(0), CustomerListViewItem)
                ' Get the customer number for both the previously selected item
                ' and the newly selected item
                Dim PrevItem As String = _
                    _currentcustomerrecord.Item("CustomerNumber").ToString
                Dim NextItem As String = _
                    newitem.GetDataRow.Item("CustomerNumber").ToString
                ' If both items refer to the same customer, then we do not need to
                ' do anything further.  The associated data is still shown in the
                ' customer data panel, and no save prompt is required.
                If PrevItem = NextItem Then
                    Debug.IndentLevel -= 1
                    Debug.WriteLine("Exiting hander...")
                    Exit Sub
                End If
                ' However, if the newly selected item is different, we need to determine
                ' if any data associated with the previously selected customer has
                ' changed.  If it has, let's prompt the user for some saving instructions.
                If CLIDDataSet.HasChanges Then
                    Debug.WriteLine("Records were changed... prompting.")
                    ' We have some changes to deal with.  We need to ask the user what
                    ' to do with them
                    Dim dresult As DialogResult = _
                        MessageBox.Show("Save changes to " & _
                        _currentcustomerrecord.Item("CustomerName").ToString.Trim() & _
                        "?", "Save changes?", MessageBoxButtons.YesNoCancel, _
                        MessageBoxIcon.Question)
                    Select Case dresult
                        Case DialogResult.OK
                            ' The user wants to save the data, use DataAdapters to
                            ' merge changes to the database on the server.
                            Debug.WriteLine("Saving changes...")
                        Case DialogResult.No
                            ' The user wants to discard changes.
                            Debug.WriteLine("Rejecting changes...")
                            CLIDDataSet.RejectChanges()
                        Case DialogResult.Cancel
                            ' The user wants to cancel the prompt.  In this case, we
                            ' want to move the selection back to the previous one.
                            ' This is where it gets tricky.
                            Debug.WriteLine("Cancelled... remain on initial customer")
                            ' We want to make sure that when we move the selection, we
                            ' don't raise another index change event, so temporarily
                            ' remove the SelectedIndexChanged handler
                            RemoveHandler lvCustomerList.SelectedIndexChanged, _
                                          AddressOf lvCustomerList_SelectedIndexChanged
                            Debug.WriteLine("Killed selection handler...")
                            ' Now we cancel the selection on the newly selected item
                            Debug.Write("Deselected new item...")
                            Debug.WriteLine(newitem.GetDataRow.Item("CustomerName").ToString.Trim)
                            newitem.Selected = False
                            ' And set selection back to the previously selected item.
                            ' To do this, we need to iterate through all items until
                            ' we find the one with an underlying DataRow that matches
                            ' the current customer record.
                            For Each itm As ListViewItem In lvCustomerList.Items
                                ' We need to cast to our inherited item class so we
                                ' can access the DataRow
                                Dim castitm As CustomerListViewItem = _
                                    DirectCast(itm, CustomerListViewItem)
                                If castitm.GetDataRow.Equals(_currentcustomerrecord) Then
                                    ' We've found our item.  Select it, and make sure
                                    ' the list is scrolled so that it's visible
                                    Debug.Write("Select initial item... ")
                                    Debug.WriteLine(castitm.GetDataRow.Item("CustomerName").ToString.Trim)
                                    castitm.Selected = True
                                    castitm.EnsureVisible()
                                    ' There will only be one, so no need to check the
                                    ' others.
                                    Exit For
                                End If
                            Next
                            ' Now our selections should be correct.  Add the handler
                            ' back so that subsequent selection changes will be handled
                            Debug.WriteLine("Reapplying handler...")
                            AddHandler lvCustomerList.SelectedIndexChanged, _
                                       AddressOf lvCustomerList_SelectedIndexChanged
                            ' The data panel should still be populated with data for
                            ' the customer that is now re-selected. No other processing
                            ' needed.
                            Debug.IndentLevel -= 1
                            Debug.WriteLine("Exiting handler...")
                            Exit Sub
                    End Select
                End If
            End If
 
            btnIPAddRemove.Enabled = True
            Dim custitm As CustomerListViewItem
            custitm = DirectCast(lvCustomerList.SelectedItems(0), CustomerListViewItem)
            _currentcustomerrecord = custitm.GetDataRow()
            AppStatus.Text = "Current selection:  " & _
                custitm.GetDataRow.Item("CustomerName").ToString
            Debug.WriteLine("Populating customer data for " & _
                custitm.GetDataRow.Item("CustomerName").ToString.Trim)
            ' Populate the "Network Info" (Notes) textbox
            UpdateNotes(custitm.GetDataRow)
            ' Update the contents of the Contact Info panel
            UpdateContacts(custitm.GetDataRow)
            ' Update the contents of the IP Info panel
            UpdateIPInfo(custitm.GetDataRow)
            ' Update the contents of the Antenna Info panel
            UpdateAntennaInfo(custitm.GetDataRow)
        Else
            ' If there is no customer selected, we need to disable the add and 
            ' remove buttons for IP address and MAC address.
            If _currentcustomerrecord Is Nothing Then
                btnIPAddRemove.Enabled = False
                btnAddMAC.Enabled = False
                btnRemoveMAC.Enabled = False
            End If
        End If
        Debug.IndentLevel -= 1
        Debug.WriteLine("Exiting handler...")
    End Sub
 
Btw

I almost forgot... you'll see some casting in that code of a ListViewItem to CustomerListViewItem. That's a class that I derived from ListViewItem and added a reference to a DataRow object to it. That makes it a little easier to determine which item is selected.

Also, _currentcustomerrecord is a form-level DataRow object that I use to store the current customer's record. You can see in the code where this variable gets assigned.
 
Honestly.... the list view was never intended to be a data grid in this manner.... you may want to consider using the FlexGrid, then you can trap events like BeforeRowChange.... at least then you can set the Cancel property of the eventArgs to true and it will prevent the selection from changing in the first place.

-tg
 
To put it more simply

Ok, I'll have to be more general in my question though, while my problem is very specifc.... but let's try this....

I want to display a prompt when the user selects a different item on a ListView if there is any data associated with that item that has been changed and not yet saved. Basically just a simple "You've made changes, do you want to save them? Yes? No? Cancel?"

If the user opts to cancel, I want to basically be able to cancel the selection change so that the selected item remains the one that has changes.

Following me so far? Good.

The method I'm using to do this works fine except that when the user selects cancel and I change the selection back to the previous one, the SelectedItemChanged event fires yet again trying to move the selection to the newly selected item from the one I'm trying to keep selected.

Why would this happen?
 
I'm not actually changing the ListView itself.

TechGnome said:
Honestly.... the list view was never intended to be a data grid in this manner.... you may want to consider using the FlexGrid, then you can trap events like BeforeRowChange.... at least then you can set the Cancel property of the eventArgs to true and it will prevent the selection from changing in the first place.

The .NET DataGrid would work in the same way in that case, but it's kind of overkill. And besides, all I'm doing with the ListView is displaying data... which I thought was exactly what it was intended to do. I'm not looking to prompt to save changes based on the ListView's contents, rather I want to save changes based on whether or not a separate DataSet has changes or not.

I probably will end up using a DataGrid before its all over because this is becoming too much of a pain in the ass.
 
If you don't mind zip the project and post it and or e-mail it to me. I'll help you step through it.
 
Last edited:
Too big

ImDaFrEaK said:
If you don't mind zip the project and post it and or e-mail it to me. I'll help you step through it.

The project is way too big. Besides I've pretty much come to the conclusion that there's just a bug in the .NET 1.1 ListView control that causes it to fire the initial event a second time when you change the selection from within the event handler. Because when I step through the process using the debugger, everything works as it should.

I'm just going to substitute a DataGrid for the ListView and move on.
 
Actualy it's proly not a bug, but just the way it works. When you re-set the selection, I would certanly expect the event to refire since that is in fact what is going on, the selection is changing.... how ever, when stepping through in the debugger, you do get slightly different results, and a lot of it has to do with focusing... not sure if I can explain it, but I've seen that kind of behavior in all version of VB, not just .NET and not with just the ListView either, but with other controls. It's rather infuriating, but I've come to the conclusion that when you step through the code sometimes, the focus changes from the app to the VB IDE and that can and does change the course of things (dang that event based programming.)

-tg
 
It actually fires recursively

I go through a certain process when I run the application where I select one customer, make a change to it, and then select a different customer before I save the change.... and I've tried this with the handler I posted above, and with 1 variation on that handler, and here's what the results in the output window tell me:

When run with the handler I posted, RemoveHandler actually does prevent my resetting action from firing another change event. But oddly enough, the debugger also shows that the event that fired when I selected a different customer after changing data in the first one, actually happens again, exactly as it did previously... as if I didn't reset the selection at all.

In the second scenario... I comment out the RemoveHandler and AddHandler calls and run through the same sequence of operations. This way, the output shows that when I reset the selection, the event fires recursively. It runs the handler immediately, and waits for it to return, then continues execution. But again, after all should be finished and processed correctly, the initial event fires again. This is the strangest behavior I've ever seen.

I'm actually not where I can paste the output, but as soon as I can, I will show you what I'm talking about.
 
Best I could find at the moment for confirm and allow to cancel selection change in ListView:
VB.NET:
Dim current As Integer
 
Private Sub ListView1_MouseUp(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles ListView1.MouseUp
  Dim lvi As ListViewItem = ListView1.GetItemAt(e.X, e.Y)
  If Not lvi Is Nothing Then
    Dim ix As Integer = lvi.Index
    If ix <> current Then
      Dim dr As DialogResult = MessageBox.Show("ok?", "confirm change", MessageBoxButtons.OKCancel)
      If dr = Windows.Forms.DialogResult.OK Then
        current = ix
      Else
        ListView1.Items(current).Selected = True
      End If
    End If
  End If
End Sub
 
That might work

That could work, but I'd also have to make sure to handle keyboard events as well as the mouse event in case the user decides to change selection with the keyboard. Moving to a DataGrid still seems the best option.
 
SaintJimmy said:
When run with the handler I posted, RemoveHandler actually does prevent my resetting action from firing another change event. But oddly enough, the debugger also shows that the event that fired when I selected a different customer after changing data in the first one, actually happens again, exactly as it did previously... as if I didn't reset the selection at all.
.

Apps do behave differntly under debug conditions and sometimes not all events fire.. It can happen thorugh process flow issues like the fact that a breakpoint hits means your app loses focus - some events will no longer fire and as an example, menus dont stay open..

If youre wondering why selecteditem changes twice, i suggest you dump in the contents of the event args to the trace if it contains any interesting data, or at the very least dump in the index of the list - it might explain what the list is firing twice. Oh, and one thing i nearly forgot.. ensure your logic cannot add the event handler twice - because then it really will fire twice even though the event bubble is only being started once. If you AddHandler the same Sub to the same control 5 times, then it looks like the event fires 5 times - its actually jsut a side effect of the way the delegate system works to fire events. For this reason I usually prefer NOT adding evet handlers programmatically, and using boolean values to control flow as in my example
 
Check it out

Maybe this will give you guys a more visual explanation of the problem. Here's EXACTLY what I'm doing.

1.) I run the app.

2.) I select a customer from the list... ACA Corporation in this example.

3.) I change some of their data.

4.) I select a different customer from the list.

5.) I get prompted to save changes... I select "Cancel"

6.) I get the prompt again, and again select "Cancel"

7.) Now ACA Corporation is correctly selected in the ListView. I close the application and check the output window to see what happened when changing selection... results are below.

One other thing to note... the code that produced this output is the same as what I posted in my initial thread, only I commented out the AddHandler and RemoveHandler calls, so you can see by the output that the handler fires again when I programatically change the selection.

VB.NET:
Entered handler...
    Selected Index Changed... 
        Customer Selected: ACA Corporation
        First Selection after Execution
    Populating customer data for ACA Corporation
Exiting handler...
 
Entered handler...
    Selected Index Changed... 
        No items selected
Exiting handler...
Entered handler...
    Selected Index Changed... 
        Customer Selected: Abe Lavalais
        Previous Customer: ACA Corporation
    Records were changed... prompting.
    Cancelled... remain on initial customer
    Deselected new item...Abe Lavalais
    Entered handler...
        Selected Index Changed... 
            No items selected
    Exiting handler...
    Select initial item... ACA Corporation
    Entered handler...
        Selected Index Changed... 
            Customer Selected: ACA Corporation
            Previous Customer: ACA Corporation
    Exiting hander...
Exiting handler...
 
Entered handler...
    Selected Index Changed... 
        No items selected
Exiting handler...
Entered handler...
    Selected Index Changed... 
        Customer Selected: Abe Lavalais
        Previous Customer: ACA Corporation
    Records were changed... prompting.
    Cancelled... remain on initial customer
    Deselected new item...Abe Lavalais
    Entered handler...
        Selected Index Changed... 
            No items selected
    Exiting handler...
    Select initial item... ACA Corporation
    Entered handler...
        Selected Index Changed... 
            Customer Selected: ACA Corporation
            Previous Customer: ACA Corporation
    Exiting hander...
Exiting handler...

As you can see, when I opt to cancel the prompt, the event actually fires a total of 4 times (not counting the 2 times in each event that occurs when I programatically change selection). And that's what's confusing the crap out of me.
 
It is as you have already said yourself, the SelectedIndexChanged event fires both when item is selected and when it is un-selected, in addition it seems unstopable by RemoveHandler because it is not the event that changes the selection but what caused it. You may see this more clearly when investigating the ItemSelectionChanged event, which also provides some more info in the e parameter with properties Item/ItemIndex/IsSelected, using this event also allows you to ignore all those 'un-select' events easily. Still the ListView seems reluctant to allow forcing a selection change when it is in the process of doing this itself. The crazy code below is the second attempt, it does cancel and reset selection, but I'm sure someone finds a shorter path.
VB.NET:
Dim cur As Integer
Dim bIgnoreThisChange As Boolean = False
Dim bIgnoreThisChangeToo As Boolean = False
 
Private Sub ListView1_ItemSelectionChanged(ByVal sender As Object, ByVal e As System.Windows.Forms.ListViewItemSelectionChangedEventArgs) _
Handles ListView1.ItemSelectionChanged
  'ignoring 'un-selects'
  If e.IsSelected = True Then 
    If bIgnoreThisChange = True Then
      bIgnoreThisChange = False
      bIgnoreThisChangeToo = True
    ElseIf bIgnoreThisChangeToo = True Then
      ListView1.Items(cur).Selected = True
      bIgnoreThisChangeToo = False
    Else
      Dim dr As DialogResult = MessageBox.Show("ok?", "confirm change", MessageBoxButtons.OKCancel)
      If dr = Windows.Forms.DialogResult.OK Then
        cur = e.ItemIndex
      Else '=Cancel
        bIgnoreThisChange = True
        ListView1.Items(cur).Selected = True
      End If
    End If
  End If
End Sub
Here is the event log, also pretty crazy. Item index 1 is initially selected, I click item index 2 but cancel. You see that ItemSelectionChange happens immediately after setting the Selected property of the item to True (before the code line after executes, which is the 'after' log write instruction), this is also why you see two 'second setting' in log in contradiction to all logic sense (event happened again before boolean bIgnoreThisChangeToo was set).
VB.NET:
10.07.2006 19:06:07 MouseDown - Selected index 1
10.07.2006 19:06:07 ItemSelectionChanged index 2 is selected - Selected index 2
10.07.2006 19:06:07 before confirm - Selected index 2
10.07.2006 19:06:08 after confirm - Selected index 2
10.07.2006 19:06:08 before first setting index 1 - Selected index 2
10.07.2006 19:06:08 ItemSelectionChanged index 1 is selected - Selected index 1
10.07.2006 19:06:08 after first setting index 1 - Selected index 1
10.07.2006 19:06:08 ItemSelectionChanged index 2 is selected - Selected index 2
10.07.2006 19:06:08 before second setting index 1 - Selected index 2
10.07.2006 19:06:08 ItemSelectionChanged index 1 is selected - Selected index 1
10.07.2006 19:06:08 before second setting index 1 - Selected index 1
10.07.2006 19:06:08 after second setting index 1 - Selected index 1
10.07.2006 19:06:08 after second setting index 1 - Selected index 1
10.07.2006 19:06:08 MouseUp - Selected index 1
Funny thing there is no MouseClick logged.
 
Back
Top