问题
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:
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;
}
// 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());
}
}
Do let me know if this can be improved.
回答2:
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
}
回答3:
Old question I know, but this still seems to be an issue.
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);
}
}
回答4:
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".
回答5:
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
回答6:
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;
}
回答7:
I would either try tying the postback to a button to allow the user to submit their changes and unhook the event handler.
回答8:
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.
回答9:
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... >.<...
回答10:
I recommend virtualizing your list view if it has a few hundred or thousand items.
回答11:
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.
回答12:
Leave the ListView
and all the old controls.
Make DataGridView
your friend, and all will be well :)
回答13:
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?
...
TheLVN_ODSTATECHANGED
notification tells you that the state of all items in the specified range has changed. It's a shorthand for sending an individualLVN_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 singleLVN_ODSTATECHANGED
notification withiFrom=0
andiTo=499999
instead of a half million individual littleLVN_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.
回答14:
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();
}
来源:https://stackoverflow.com/questions/86793/how-to-avoid-thousands-of-needless-listview-selectedindexchanged-events