views:

2496

answers:

13

If a user select all items in a .NET 2.0 ListView, the ListView will fire a SelectedIndexChanged event for every item; rather than firing an event to indicate that the selection has changed

If the user then clicks to select just one item in the list, the ListView will fire a SelectedIndexChanged event for every item that is getting unselected, and then an SelectedIndexChanged event for the single newly selected item; rather than firing an event to indicate that the selection has changed.

If you have code in the SelectedIndexChanged event handler, the program will become pretty unresponsive when you begin to have a few hundred/thousand items in the list.

i've thought about dwell timers, etc.

But does anyone have a good solution to avoid thousands of needless ListView.SelectedIndexChange events, when really one event will do?

+1  A: 

You could interpret the event on your own. That is, compare the list of selected items on the postback to the list of selected items before: if different, call your function that used to handle that event.

EndangeredMassa
Problem is that it's different every time.
Ian Boyd
A: 

you can try removing the event handler

Yitzchok
Without any further elaboration this sounds snarky, -1.
Ian Boyd
+1  A: 

I would either try tying the postback to a button to allow the user to submit their changes and unhook the event handler.

Rob
Winforms.ListView
Ian Boyd
A: 

I was just trying to tackle this very problem yesterday. I don't know exactly what you mean by "dwell" timers, but I tried implementing my very own version of waiting until all changes are done. Unfortunately the only way I could think of to do this was in a separate thread and it turns out that when you create a separate thread, your UI elements are inaccessible in that thread. .NET throws an exception stating that the UI elements can only be accessed in the thread where the elements were created! So, I found a way to optimize my response to the SelectedIndexChanged and make it fast enough to where it is bearable - its not a scalable solution though. Lets hope someone has a clever idea to tackle this problem in a single thread.

i created an answer that shows the "dwell" concept. You start a timer during OnChange, and 200ms later all the seletions will be done, and you can then fire the **real** Change event.
Ian Boyd
+2  A: 

This is the dwell timer solution i'm using for now (dwell just means "wait for a little bit"). This code might suffer from a race condition, and perhaps a null reference exception.

Timer changeDelayTimer = null;

private void lvResults_SelectedIndexChanged(object sender, EventArgs e)
{
        if (this.changeDelayTimer == null)
        {
         this.changeDelayTimer = new Timer();
         this.changeDelayTimer.Tick += ChangeDelayTimerTick;
         this.changeDelayTimer.Interval = 200; //200ms is what Explorer uses
        }
        this.changeDelayTimer.Enabled = false;
        this.changeDelayTimer.Enabled = true;
}

private void ChangeDelayTimerTick(object sender, EventArgs e)
{
    this.changeDelayTimer.Enabled = false;
    this.changeDelayTimer.Dispose();
    this.changeDelayTimer = null;

    //Add original SelectedIndexChanged event handler code here
    //todo
}
Ian Boyd
It should be noted that this 'dwell' solution isn't an answer. It's the hack workaround i implemented until i can get a real answer.
Ian Boyd
The Timer class's event runs within the UI thread, so the code should work as expected.
Thanatos
That doesn't mean that the code properly stops the timer when the form closes, or doesn't try to start another timer when the first one is going, or that the timer can't fire after the form has been disposed, or that isn't null before it is referened. "Just because it works doesn't mean it's right."
Ian Boyd
+3  A: 

Good solution from Ian. I took that and made it into a reusable class, making sure to dispose of the timer properly. I also reduced the interval to get a more responsive app. This control also doublebuffers to reduce flicker.

  public class DoublebufferedListView : System.Windows.Forms.ListView
  {
     private Timer m_changeDelayTimer = null;
     public DoublebufferedListView()
        : base()
     {
        // Set common properties for our listviews
        if (!SystemInformation.TerminalServerSession)
        {
           DoubleBuffered = true;
           SetStyle(ControlStyles.ResizeRedraw, true);
        }
     }

     /// <summary>
     /// Make sure to properly dispose of the timer
     /// </summary>
     /// <param name="disposing"></param>
     protected override void Dispose(bool disposing)
     {
        if (disposing && m_changeDelayTimer != null)
        {
           m_changeDelayTimer.Tick -= ChangeDelayTimerTick;
           m_changeDelayTimer.Dispose();
        }
        base.Dispose(disposing);
     }

     /// <summary>
     /// Hack to avoid lots of unnecessary change events by marshaling with a timer:
     /// http://stackoverflow.com/questions/86793/how-to-avoid-thousands-of-needless-listview-selectedindexchanged-events
     /// </summary>
     /// <param name="e"></param>
     protected override void OnSelectedIndexChanged(EventArgs e)
     {
        if (m_changeDelayTimer == null)
        {
           m_changeDelayTimer = new Timer();
           m_changeDelayTimer.Tick += ChangeDelayTimerTick;
           m_changeDelayTimer.Interval = 40;
        }
        m_changeDelayTimer.Enabled = false;
        m_changeDelayTimer.Enabled = true;
     }

     private void ChangeDelayTimerTick(object sender, EventArgs e)
     {
        m_changeDelayTimer.Enabled = false;
        base.OnSelectedIndexChanged(new EventArgs());
     }
  }

Do let me know if this can be improved.

Robert Jeppesen
+1 for only double buffering if not in a terminal session/RDP
Ian Boyd
i'll accept this answer, without testing the code. i **hope** there isn't a crash of some sort.
Ian Boyd
If there is, do let me know. :)
Robert Jeppesen
A: 

Maybe this can help you to accomplish what you need without using timers:

http://www.dotjem.com/archive/2009/06/19/20.aspx

I Don't like the user of timers ect. As i also state in the post...

Hope it helps...

Ohh i forgot to say, it's .NET 3.5, and I am using some of the features in linq to acomplish "Selection Changes Evaluation" if you can call it that o.O...

Anyways, if you are on an older version, this evaluation has to be done with a bit more code... >.<...

You should give a line about that your solution is. Looking at the code i don't really know what you're trying to achieve. You seem to be throwing an event when selection changes...which is the problem i'm having.
Ian Boyd
+1  A: 

The timer is the best overall solution.

A problem with Jens's suggestion is that once the list has a lot of selected items (thousands or more), getting the list of selected items starts to take a long time.

Instead of creating a timer object every time a SelectedIndexChanged event occurs, it's simpler to just put a permanent one on the form with the designer, and have it check a boolean variable in the class to see whether or not it should call the updating function.

For example:

bool timer_event_should_call_update_controls = false;

private void lvwMyListView_SelectedIndexChanged(object sender, EventArgs e) {

  timer_event_should_call_update_controls = true;
}

private void UpdateControlsTimer_Tick(object sender, EventArgs e) {

  if (timer_event_should_call_update_controls) {
    timer_event_should_call_update_controls = false;

    update_controls();
  }
}

This works fine if you're using the information simply for display purposes, such as updating a status bar to say "X out of Y selected".

A: 

I recommend virtualizing your list view if it has a few hundred or thousand items.

Joe Chung
Do virtual listview not let you select items?
Ian Boyd
A: 

Maylon >>>

The aim was never to work with list above a few hundreds items, But... I have tested the Overall user experience with 10.000 items, and selections of 1000-5000 items at one time (and changes of 1000-3000 items in both Selected and Deselected)...

The overall duration of calculating never exceeded 0.1 sec, some of the highest measurements was of 0.04sec, I Found that perfectly acceptable with that many items.

And at 10.000 items, just initializing the list takes over 10 seconds, so at this point I would have thought other things had come in to play, as Virtualization as Joe Chung points out.

That said, it should be clear that the code is not an optimal solution in how it calculates the difference in the selection, if needed this can be improved a lot and in various ways, I focused on the understanding of the concept with the code rather than the performance.

However, if your experiencing degraded performance I am very interested in some of the following:

  • How many items in the list?
  • How many selected/deselected elements at a time?
  • How long does it roughly take for the event to raise?
  • Hardware platform?
  • More about The case of use?
  • Other relevant information you can think of?

Otherwise it ain't easy to help improving the solution.

Jens
Have 10,000 items in the list, and push your "Select All" keyboard shortcut. Then clear the selection.
Ian Boyd
The concept is very simple, you react to user input rather than property changes on elements or alike.You need to use the new events ,because old will work as they always, the new ones will check if selection changes was made to the list view when 1 of 2 things happen. 1. The Mouse key is released. 2. A Keyboard key is release.That way the Event only fires ones as per user interaction, rather than ones per element change.And if so, it will raise a "ListSelectionChanged" event.The "Select All" shortcut does not work by default with a ListView, so that must be something you have added?
Jens
A: 

Leave the ListView and all the old controls.

Make DataGridView your friend, and all will be well :)

leppie
As long as i can use it without databinding
Ian Boyd
A: 

A flag works for the OnLoad event of the windows form / web form / mobile form. In a single select Listview, not multi-select, the following code is simple to implement, and prevents multiple firing of the event.

As the ListView de-selects the first item, the second item it what you need and the collection should only ever contain one item.

The same below was used in a mobile application, therefore some of the collection names might be different as it is using the compact framework, however the same principles apply.

Note: Make sure OnLoad and populate of the listview you set the first item to be selected.

// ################ CODE STARTS HERE ################
//Flag  to create at the form level
System.Boolean lsvLoadFlag = true;

//Make sure to set the flag to true at the begin of the form load and after
private void frmMain_Load(object sender, EventArgs e)
{
    //Prevent the listview from firing crazy in a single click NOT multislect environment
    lsvLoadFlag = true;

    //DO SOME CODE....

    //Enable the listview to process events
    lsvLoadFlag = false;
}

//Populate First then this line of code
lsvMain.Items[0].Selected = true;

//SelectedIndexChanged Event
 private void lsvMain_SelectedIndexChanged(object sender, EventArgs e)
{
    ListViewItem lvi = null;

    if (!lsvLoadFlag)
    {
        if (this.lsvMain.SelectedIndices != null)
        {
            if (this.lsvMain.SelectedIndices.Count == 1)
            {
                lvi = this.lsvMain.Items[this.lsvMain.SelectedIndices[0]];
            }
        }
    }
}
################ CODE END HERE    ################

Ideally, this code should be put into a UserControl for easy re-use and distrbution in a single select ListView. This code would not be much use in a multi-select, as the event works as it should for that behavior.

I hope that helps.

Kind regards,

Anthony N. Urwin http://www.manatix.com

Anthony Urwin
A: 

Raymond Chen has a blog post that (probably) explains why there are thousands of change events, rather than just one:

Why is there an LVN_ODSTATECHANGED notification when there's already a perfectly good LVN_ITEMCHANGED notification?

...
The LVN_ODSTATECHANGED notification tells you that the state of all items in the specified range has changed. It's a shorthand for sending an individual LVN_ITEMCHANGED for all items in the range [iFrom..iTo]. If you have an ownerdata list view with 500,000 items and somebody does a select-all, you'll be glad that you get a single LVN_ODSTATECHANGED notification with iFrom=0 and iTo=499999 instead of a half million individual little LVN_ITEMCHANGED notifications.

i say probably explains why, because there's no guarantee that the .NET list view is a wrapper around the Listview Common Control - that's an implementation detail that is free to change at any time (although almost certainly never will).

The hinted solution is to use the .NET listview in virtual mode, making the control an order of magnitude more difficult to use.

Ian Boyd