How to avoid thousands of needless ListView.SelectedIndexChanged events?
Asked Answered
F

14

18

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?

Far answered 17/9, 2008 at 19:38 Comment(0)
S
13

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:
     /// https://mcmap.net/q/679628/-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;
        }
        // When a new SelectedIndexChanged event arrives, disable, then enable the
        // timer, effectively resetting it, so that after the last one in a batch
        // arrives, there is at least 40 ms before we react, plenty of time 
        // to wait any other selection events in the same batch.
        m_changeDelayTimer.Enabled = false;
        m_changeDelayTimer.Enabled = true;
     }

     private void ChangeDelayTimerTick(object sender, EventArgs e)
     {
        m_changeDelayTimer.Enabled = false;
        base.OnSelectedIndexChanged(new EventArgs());
     }
  }
Scilicet answered 11/6, 2009 at 13:11 Comment(6)
+1 for only double buffering if not in a terminal session/RDPFar
i'll accept this answer, without testing the code. i hope there isn't a crash of some sort.Far
If there is, do let me know. :)Scilicet
I was still clueless why in OnSelectedIndexChanged Event is having the timer that is being toggled its enabled condition to false and true simultaneously with out any region? code m_changeDelayTimer.Enabled = false; m_changeDelayTimer.Enabled = true;codeMyrica
@Myrica I added some comments to the code. Hopefully this explains it.Scilicet
@RobertJeppesen the listview constructor is causing some heavy flickering of the listview items for me. I've been using this separate Listview class and it is good in one winform and is flickering heavily in other winform wherein it contains listview and treeview in side by side in separate panels. It is like whenever we hover over the row then only the listview item is visible and it is still disappearing again. I've almost 2000 rows in this listview. When I remove the constructor then it is fine but minimal flicker is occuring which is not that much distracting but ok.Myrica
F
3

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
}
Far answered 17/9, 2008 at 20:21 Comment(3)
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.Far
The Timer class's event runs within the UI thread, so the code should work as expected.Neurophysiology
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."Far
O
2

Here is my solution not using timers.

It waits for the MouseUp or KeyUp event before firing the SelectionChanged event. If you are changing the selection programatically, then this will not work, the event won't fire, but you could easily add a FinishedChanging event or something to trigger the event.

(It also has some stuff to stop flickering which isn't relevant to this question).

public class ListViewNF : ListView
{
    bool SelectedIndexChanging = false;
    
    public ListViewNF()
    {
        this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
        this.SetStyle(ControlStyles.EnableNotifyMessage, true);
    }
    
    protected override void OnNotifyMessage(Message m)
    {
        if(m.Msg != 0x14)
            base.OnNotifyMessage(m);
    }
    
    protected override void OnSelectedIndexChanged(EventArgs e)
    {
        SelectedIndexChanging = true;
        //base.OnSelectedIndexChanged(e);
    }
    
    protected override void OnMouseUp(MouseEventArgs e)
    {
        if (SelectedIndexChanging)
        {
            base.OnSelectedIndexChanged(EventArgs.Empty);
            SelectedIndexChanging = false;
        }
        
        base.OnMouseUp(e);
    }
    
    protected override void OnKeyUp(KeyEventArgs e)
    {
        if (SelectedIndexChanging)
        {
            base.OnSelectedIndexChanged(EventArgs.Empty);
            SelectedIndexChanging = false;
        }

        base.OnKeyUp(e);
    }
}
Oldfashioned answered 15/1, 2014 at 11:25 Comment(1)
Works like a charm. Thanks. This should be the accepted answer as it doesn't rely on timers.Unchurch
V
2

You can use async & await:

private bool waitForUpdateControls = false;

private async void listView_SelectedIndexChanged(object sender, EventArgs e)
{
    // To avoid thousands of needless ListView.SelectedIndexChanged events.

    if (waitForUpdateControls)
    {
        return;
    }

    waitForUpdateControls = true;

    await Task.Delay(100);

    waitForUpdateControls = false;

    UpdateControls();

    return;
}
Vertu answered 21/2, 2017 at 18:44 Comment(0)
S
1

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".

Staceystaci answered 7/7, 2009 at 2:6 Comment(0)
R
1

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.

Ruttger answered 6/9, 2010 at 11:53 Comment(0)
R
0

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

Redingote answered 17/9, 2008 at 19:47 Comment(0)
C
0

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.

Cohabit answered 17/9, 2008 at 19:48 Comment(1)
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.Far
P
0

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

Phanerozoic answered 7/7, 2009 at 2:14 Comment(1)
Do virtual listview not let you select items?Far
S
0

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.

Sorrows answered 7/7, 2009 at 8:27 Comment(2)
Have 10,000 items in the list, and push your "Select All" keyboard shortcut. Then clear the selection.Far
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?Sorrows
T
0

Leave the ListView and all the old controls.

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

Tuckie answered 7/7, 2009 at 8:32 Comment(1)
As long as i can use it without databindingFar
F
0

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.

Far answered 28/10, 2010 at 18:10 Comment(0)
S
0

I may have a better solution.

My situation:

  • Single select list view (rather than multi-select)
  • I want to avoid processing the event when it fires for deselection of the previously selected item.

My solution:

  • Record what item the user clicked on MouseDown
  • Ignore the SelectedIndexChanged event if this item is not null and SelectedIndexes.Count == 0

Code:

ListViewItem ItemOnMouseDown = null;
private void lvTransactions_MouseDown(object sender, MouseEventArgs e)
{
    ItemOnMouseDown = lvTransactions.GetItemAt(e.X, e.Y);
}
private void lvTransactions_SelectedIndexChanged(object sender, EventArgs e)
{
    if (ItemOnMouseDown != null && lvTransactions.SelectedIndices.Count == 0)
        return;

    SelectedIndexDidReallyChange();

}
Selia answered 10/8, 2011 at 3:49 Comment(0)
H
-1

What worked for me was just using the OnClick event.

I just needed to get a single value and get out, and the first choice was fine, whether it was the same original value or a new one.

The click seems to occur after all of the selection changes are done, like the timer would do.

Click ensures a real click occurred rather than just mouse up. Though in practice probably makes no difference unless they slid into the dropdown with the mouse down and released.

This worked for me because, click seems to only fire in the list item bearing client area. And I had no headers to click on.

I just had a plain single control popup dropdown. And I didn't have to worry about key movements selecting items. Any key movements on a property grid dropdown cancel the dropdown.

Trying to close in the middle of SelectedIndexChanged would many times cause a crash also. But closing during Click is fine.

The crashing thing was what caused me to look for alternatives and find this post.

    void OnClick(object sender, EventArgs e)
    {
        if (this.isInitialize) // kind of pedantic
            return;

        if (this.SelectedIndices.Count > 0)
        {
            string value = this.SelectedItems[0].Tag;
            if (value != null)
            {
                this.OutValue = value;
            }
        }

        //NOTE: if this close is done in SelectedIndexChanged, will crash
        //  with corrupted memory error if an item was already selected

        // Tell property grid to close the wrapper Form
        var editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
        if ((object)editorService != null)
        {
            editorService.CloseDropDown();
        }
    }
Humidor answered 23/9, 2022 at 2:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.