.net winforms listview selectedindexchanged

.net - ¿Cómo evitar miles de eventos innecesarios ListView.SelectedIndexChanged?



winforms (14)

Si un usuario selecciona todos los elementos en .NET 2.0 ListView, ListView disparará un evento SelectedIndexChanged para cada elemento, en lugar de disparar un evento para indicar que la selección ha cambiado.

Si el usuario hace clic para seleccionar solo un elemento en la lista, el ListView disparará un evento SelectedIndexChanged para cada elemento que se está deseleccionando, y luego un evento SelectedIndexChanged para el elemento recién seleccionado, en lugar de disparar un evento para indicar que el la selección ha cambiado.

Si tiene el código en el controlador de eventos SelectedIndexChanged , el programa dejará de responder cuando comience a tener unos pocos cientos / miles de elementos en la lista.

He pensado en los tiempos de espera , etc.

Pero, ¿alguien tiene una buena solución para evitar miles de innecesarios ListView. SelectedIndexChange events, cuando realmente un evento va a hacer?


Estaba tratando de abordar este problema ayer. No sé exactamente a qué te refieres con temporizadores "dwell", pero traté de implementar mi propia versión de espera hasta que se hayan realizado todos los cambios. Desafortunadamente, la única forma en que podía pensar para hacer esto era en un hilo separado y resulta que cuando creas un hilo separado, tus elementos de UI son inaccesibles en ese hilo. .NET lanza una excepción que indica que solo se puede acceder a los elementos de la interfaz de usuario en la secuencia donde se crearon los elementos. Entonces, encontré una forma de optimizar mi respuesta al SelectedIndexChanged y hacerlo lo suficientemente rápido como para soportarlo, aunque no es una solución escalable. Esperemos que alguien tenga una idea inteligente para abordar este problema en un solo hilo.


Intentaré vincular la devolución de datos a un botón para permitir que el usuario envíe sus cambios y desenganche el controlador de eventos.


Esta es la solución de temporizador de permanencia que estoy usando por ahora (dwell solo significa "esperar un poco"). Este código puede sufrir una condición de carrera, y quizás una excepción de referencia nula.

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 }


Tal vez esto puede ayudarte a lograr lo que necesitas sin usar temporizadores:

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

No me gusta el usuario de temporizadores ect. Como también declaro en el post ...

Espero eso ayude...

Ohh, me olvidé de decir que es .NET 3.5 y estoy usando algunas de las funciones de linq para realizar la "Evaluación de cambios de selección" si puede llamarlo así.

De todos modos, si estás en una versión anterior, esta evaluación debe hacerse con un poco más de código ...>. <...


Deje ListView y todos los controles anteriores.

Haz que DataGridView tu amigo, y todo estará bien :)


El temporizador es la mejor solución general.

Un problema con la sugerencia de Jens es que una vez que la lista tiene una gran cantidad de elementos seleccionados (miles o más), obtener la lista de elementos seleccionados comienza a tomar mucho tiempo.

En lugar de crear un objeto de temporizador cada vez que ocurre un evento SelectedIndexChanged, es más simple simplemente poner uno permanente en el formulario con el diseñador, y hacer que verifique una variable booleana en la clase para ver si debe o no llamar a la función de actualización.

Por ejemplo:

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(); } }

Esto funciona bien si está utilizando la información simplemente para fines de visualización, como actualizar una barra de estado para decir "X de Y seleccionado".


Maylon >>>

El objetivo nunca fue trabajar con la lista de cientos de elementos, pero ... He probado la experiencia general del usuario con 10.000 elementos, y selecciones de 1000-5000 elementos a la vez (y cambios de 1000-3000 elementos en ambos seleccionados). y Deselected) ...

La duración total del cálculo nunca excedió 0.1 segundos, algunas de las mediciones más altas fueron de 0.04 segundos, encontré eso perfectamente aceptable con tantos artículos.

Y con 10.000 elementos, solo inicializar la lista lleva más de 10 segundos, por lo que en este momento habría pensado que habían entrado otras cosas para jugar, como señala Virtualización como Joe Chung.

Dicho esto, debe quedar claro que el código no es una solución óptima en cuanto a cómo se calcula la diferencia en la selección, si es necesario, se puede mejorar mucho y de varias maneras, me centré en la comprensión del concepto con el código en vez que el rendimiento.

Sin embargo, si experimenta un rendimiento degradado, estoy muy interesado en algunos de los siguientes:

  • ¿Cuántos elementos hay en la lista?
  • ¿Cuántos elementos seleccionados / deseleccionados a la vez?
  • ¿Cuánto tiempo dura más o menos para que suba el evento?
  • Plataforma de hardware?
  • Más sobre El caso de uso?
  • ¿Otra información relevante que pueda pensar?

De lo contrario, no es fácil ayudar a mejorar la solución.


Recomiendo virtualizar su vista de lista si tiene algunos cientos o miles de elementos.


Un indicador funciona para el evento OnLoad del formulario de Windows / formulario web / formulario móvil. En una vista de lista de selección única, no de selección múltiple, el siguiente código es fácil de implementar e impide la activación múltiple del evento.

A medida que ListView deselecciona el primer elemento, el segundo elemento es lo que necesita y la colección solo debe contener un elemento.

Lo mismo a continuación se utilizó en una aplicación móvil, por lo tanto, algunos de los nombres de las colecciones pueden ser diferentes, ya que se utiliza el marco compacto, sin embargo, se aplican los mismos principios.

Nota: asegúrese de que OnLoad y de la vista de lista configure el primer elemento que se seleccionará.

// ################ 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 ################

Idealmente, este código debería colocarse en un UserControl para facilitar su reutilización y distribución en un solo ListView. Este código no sería de mucha utilidad en una selección múltiple, ya que el evento funciona como debería para ese comportamiento.

Espero que eso ayude.

Saludos cordiales,

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


Raymond Chen tiene una publicación en el blog que (probablemente) explica por qué hay miles de eventos de cambio , en lugar de solo uno:

¿Por qué hay una notificación LVN_ODSTATECHANGED cuando ya hay una notificación LVN_ITEMCHANGED perfectamente buena?

...
La notificación LVN_ODSTATECHANGED le informa que el estado de todos los artículos en el rango especificado ha cambiado. Es una forma abreviada de enviar un LVN_ITEMCHANGED individual para todos los elementos del rango [iFrom..iTo] . Si tiene una vista de lista de datos de propietario con 500,000 elementos y alguien selecciona todo, se alegrará de recibir una sola notificación iFrom=0 con iFrom=0 e iTo=499999 lugar de medio millón de pequeñas notificaciones LVN_ITEMCHANGED individuales.

Probablemente explique por qué, porque no hay garantía de que la vista de lista .NET sea una envoltura alrededor del control común de Listview: es un detalle de implementación que se puede cambiar libremente en cualquier momento (aunque casi seguro que nunca lo hará).

La solución sugerida es usar la vista de lista .NET en modo virtual, lo que hace que el control sea de un orden de magnitud más difícil de usar.


Puedo tener una mejor solución.

Mi situación:

  • Vista de lista de selección única (en lugar de selección múltiple)
  • Quiero evitar el procesamiento del evento cuando se activa para anular la selección del elemento seleccionado previamente.

Mi solución:

  • Registre en qué elemento hizo clic el usuario en MouseDown
  • Ignore el evento SelectedIndexChanged si este elemento no es nulo y SelectedIndexes.Count == 0

Código:

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(); }


Buena solución de Ian. Lo tomé y lo convertí en una clase reutilizable, asegurándome de deshacerme del temporizador correctamente. También reduje el intervalo para obtener una aplicación más receptiva. Este control también duplica los bloqueos para reducir el parpadeo.

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://.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()); } }

Háganme saber si esto puede mejorarse.


La vieja pregunta que sé, pero esto todavía parece ser un problema.

Aquí está mi solución sin usar temporizadores.

Espera el evento MouseUp o KeyUp antes de activar el evento SelectionChanged. Si está cambiando la selección programáticamente, esto no funcionará, el evento no se disparará, pero podría agregar fácilmente un evento FinishedChanging o algo para activar el evento.

(También tiene algunas cosas para detener el parpadeo que no es relevante para esta pregunta).

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); } }


Puedes usar async y 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; }