2008-09-17 20 views
16

Si un usuario selecciona todos los elementos en .NET 2.0 ListView, el ListView disparará un evento SelectedIndexChanged para cada elemento, en lugar de activar un evento para indicar que la selección ha cambiado.¿Cómo evitar miles de eventos innecesarios ListView.SelectedIndexChanged?

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

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

He pensado en temporizadores de permanencia, etc.

Pero ¿alguien tiene una buena solución para evitar miles de ListView innecesaria. SelectedIndexChange eventos, cuando realmente un evento hará?

Respuesta

11

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

Háganme saber si esto se puede mejorar.

+0

+1 para solo doble almacenamiento en búfer si no está en una sesión de terminal/RDP –

+0

Aceptaré esta respuesta, sin probar el código. Espero ** que no haya un colapso de algún tipo. –

+0

Si es así, házmelo saber. :) –

0

Podría 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.

+0

Winforms.ListView –

0

Ayer intenté resolver este problema. 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. Lamentablemente, 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.

+0

creé una respuesta que muestra el concepto de "habitar". Usted inicia un temporizador durante OnChange, y 200ms después se realizarán todas las selecciones, y luego podrá disparar el evento de cambio ** real **. –

2

Esta es la solución del temporizador de permanencia que estoy usando por ahora (dwell 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 
} 
+0

Debe tenerse en cuenta que esta solución de "permanencia" no es una respuesta. Es la solución de hack que implementé hasta que puedo obtener una respuesta real. –

+0

El evento de la clase del temporizador se ejecuta dentro del subproceso de interfaz de usuario, por lo que el código debería funcionar como se esperaba. – Thanatos

+0

Eso no significa que el código detiene correctamente el temporizador cuando el formulario se cierra, o no intenta iniciar otro temporizador cuando el primero está en marcha, o que el temporizador no puede disparar una vez que se ha eliminado el formulario, o eso no es nulo antes de que sea evaluado. "El hecho de que funcione no significa que sea correcto". –

0

Tal vez esto le puede ayudar a lograr lo que necesita sin necesidad de utilizar temporizadores:

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

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

creo que sirve ...

Ohh me olvidó decir, es .NET 3.5, y estoy usando algunas de las funciones en linq para realizar "Selection Changes Evaluation" si puede llamarlo así ...

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

+0

Deberías dar una línea sobre la solución. Al mirar el código, realmente no sé lo que estás tratando de lograr. Parece que estás lanzando un evento cuando cambia la selección ... que es el problema que estoy teniendo. –

1

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 sencillo 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 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 usted está utilizando la información sólo con fines de exhibición, tales como la actualización de una barra de estado que decir "X de Y seleccionado".

0

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

+1

¿La vista de lista virtual no le permite seleccionar elementos? –

0

Maylon >>>

El objetivo era no volver a trabajar con lista anterior algunos artículos cientos, pero ... He probado la experiencia del usuario con 10.000 artículos, y las selecciones de artículos en 1000-5000 una vez (y cambios de 1000-3000 elementos tanto en Seleccionado como Deseleccionado) ...

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

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

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 lugar del rendimiento.

Sin embargo, si su rendimiento sea menor que experimenta Estoy muy interesado en algunos de los siguientes:

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

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

+0

Tiene 10.000 elementos en la lista y presiona el atajo de teclado "Seleccionar todo". Luego borre la selección. –

+0

El concepto es muy simple, usted reacciona ante la entrada del usuario en lugar de los cambios de propiedad en los elementos o por igual. Debe usar los eventos nuevos, porque los antiguos funcionarán como siempre, los nuevos verificarán si se realizaron cambios de selección en la vista de lista cuando ocurren 1 de 2 cosas. 1. Se suelta la tecla del mouse. 2. Se libera una tecla del teclado. De esta forma, el evento solo activa los uno según la interacción del usuario, en lugar de los cambios de uno por elemento. Y si es así, generará un evento "ListSelectionChanged". El acceso directo "Seleccionar todo" no funciona de manera predeterminada con un ListView, ¿debe ser algo que haya agregado? – Jens

0

Deje el ListView y todos los controles anteriores.

Hacer DataGridView su amigo, y todo estará bien :)

+1

Mientras lo pueda usar sin enlace de datos –

1

Una bandera trabaja para el evento OnLoad del/formulario web Formulario de ventanas/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.

Como ListView selecciona 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 debe ser puesto en un control de usuario para una fácil reutilización y distrbution en un solo ListView de selección. 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 ayude.

Saludos cordiales,

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

0

Raymond Chen has a blog post that (probably) explains why there are thousands of change events, en lugar de sólo uno:

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

...
El LVN_ODSTATECHANGED notificación te dice th en el estado de todos los artículos en el rango especificado ha cambiado. Es una forma abreviada de enviar un individual LVN_ITEMCHANGED para todos los artículos en el rango [iFrom..iTo]. Si tiene una vista de lista ownerdata con 500.000 artículos y alguien hace un seleccione todo, se le alegra que usted obtener una sola notificación LVN_ODSTATECHANGED con iFrom=0 y iTo=499999 en lugar de un medio millón individuo poco LVN_ITEMCHANGED notificaciones

digo probablemente explica por qué, porque no hay garantía de que la vista de lista .NET es una envoltura alrededor del Control Común Listview - eso es un detalle de implementación que es libre de cambiar en cualquier momento (aunque casi seguro que nunca será).

La solución sugerida es utilizar 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.

0

Quizás tenga una mejor solución.

Mi situación:

  • Individual seleccione Vista de lista (en vez de selección múltiple)
  • quiero evitar procesar el evento cuando se despide por anulación de la selección de la opción seleccionada previamente.

Mi solución:

  • Grabar lo que el tema que el usuario ha hecho clic en MouseDown
  • Ignorar el evento SelectedIndexChanged si este artículo no es nulo y == 0 SelectedIndexes.Count

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

} 
1

Old que Lo 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 activará, 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); 
    } 
} 
+0

Funciona como un encanto. Gracias. Esta debería ser la respuesta aceptada ya que no depende de temporizadores. –

1

Puede utilizar 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; 
} 
Cuestiones relacionadas