2010-08-04 14 views
5

Me gustaría poder filtrar un listbox que contiene 1000 cadenas, cada una de 50 a 4000 caracteres de longitud, ya que el usuario escribe en el cuadro de texto sin demora.Filtrado en tiempo real de listbox

Actualmente estoy usando un temporizador que actualiza el listbox después de que el evento TextChanged del cuadro de texto no se haya activado en 300ms. Sin embargo, esto es bastante desigual y el ui a veces se congela momentáneamente.

¿Cuál es la forma normal de implementar una funcionalidad similar a esta?

Editar: Estoy usando winforms y .net2.

Gracias

que aquí hay una versión simplificada del código que estoy usando actualmente:

string separatedSearchString = this.filterTextBox.Text; 

List<string> searchStrings = new List<string>(separatedSearchString.Split(new char[] { ';' }, 
               StringSplitOptions.RemoveEmptyEntries)); 

//this is a member variable which is cleared when new data is loaded into the listbox 
if (this.unfilteredItems.Count == 0) 
{ 
    foreach (IMessage line in this.logMessagesListBox.Items) 
    { 
     this.unfilteredItems.Add(line); 
    } 
} 

StringComparison comp = this.IsCaseInsensitive 
         ? StringComparison.OrdinalIgnoreCase 
         : StringComparison.Ordinal; 

List<IMessage> resultingFilteredItems = new List<IMessage>(); 

foreach (IMessage line in this.unfilteredItems) 
{ 
    string message = line.ToString(); 
    if(searchStrings.TrueForAll(delegate(string item) { return message.IndexOf(item, comp) >= 0; })) 
    { 
     resultingFilteredItems.Add(line); 
    } 
} 

this.logMessagesListBox.BeginUpdate(); 
this.logMessagesListBox.Items.Clear(); 
this.logMessagesListBox.Items.AddRange(resultingFilteredItems.ToArray()); 
this.logMessagesListBox.EndUpdate(); 
+0

ASP.NET o WinForms o algo más? – kbrimington

+0

Estoy usando winforms. – Ryan

Respuesta

1

Puede hacer dos cosas:

  1. Hacer que la interfaz de usuario más sensible con una segundo hilo que se ocupa del filtrado. Una tecnología realmente nueva es Reactive Extensions (Rx) que hará exactamente lo que necesita.

    Puedo dar un ejemplo. Supongo que usas WinForms? Una parte de tu código ayudaría.

    http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx

    aquí es un poco bromista:

    Observable.Context = SynchronizationContext.Current; 
    var textchanged = Observable.FromEvent<EventArgs>(textBox1, "TextChanged"); 
    
    textchanged.Throttle(300).Subscribe(ea => 
    { 
        //Here 300 milisec. is gone without TextChanged fired. Do the filtering 
    }); 
    
  2. Haga su algoritmo de filtrado más eficiente. ¿Filtras con algo como StartWith o algo así como Contiene?

    Puede usar algo así como un árbol de sufijos o todos los prefijos de los elementos de la lista y hacer una búsqueda. Pero describe lo que necesitas con precisión y encontraré algo simple, pero lo suficientemente eficiente. La interfaz de usuario es bastante pesada si quiere mostrar 100.000 elementos en el ListBox, pero si solo toma, digamos 100, es rápido (elimine la línea .Take (100)). También se puede mejorar un poco si la búsqueda se realiza en otro hilo. Debería ser fácil con Rx pero no lo he probado.

actualización

intentar algo como esto. Funciona bien aquí con 100.000 elementos que tiene ~ 10 caracteres de largo. Utiliza Reactive Extensions (el enlace anterior).

Además, el algoritmo es ingenuo y puede hacerse mucho más rápido si así lo desea.

private void Form1_Load(object sender, EventArgs e) 
{ 
    Observable.Context = SynchronizationContext.Current; 
    var textchanged = Observable.FromEvent<EventArgs>(textBox1, "TextChanged"); 

    //You can change 300 to something lower to make it more responsive 
    textchanged.Throttle(300).Subscribe(filter); 
} 

private void filter(IEvent<EventArgs> e) 
{ 
    var searchStrings = textBox1.Text.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); 

    //my randStrings is your unfiltered messages 

    StringComparison comp = StringComparison.CurrentCulture; //Do what you want here 

    var resultList = from line in randStrings 
        where searchStrings.All(item => line.IndexOf(item, comp) >= 0) 
        select line; 

    //A lot faster but only gives you first 100 finds then uncomment: 
    //resultList = resultList.Take(100); 

    listBox1.BeginUpdate(); 
    listBox1.Items.Clear(); 
    listBox1.Items.AddRange(resultList.ToArray()); 
    listBox1.EndUpdate(); 
} 
+0

Gracias por la respuesta. He actualizado mi pregunta con una versión condensada de mi código. – Ryan

+0

Muchas gracias por el código de ejemplo. Sin embargo, hasta donde puedo decir, las extensiones reactivas solo están disponibles para .net3.5 y superiores. ¿Hay algún equivalente .net2 que pueda usar? – Ryan

+0

Hhm casi todo tiene que ser reescrito: D LINQ tampoco está en .NET Framework 2.0. ¿Puedo ver el viejo temporizador de retraso que escribiste? Si es suficiente, puede cortar, de modo que solo los primeros 100 en su lista de resultados se entreguen a Items.AddRange: será mucho más rápido que si quiere mostrar 1000 elementos. –

1

En primer lugar, gracias a @lasseespeholt, por iniciarme en esta idea, muy nuevo para mí. Pero de hecho Rx es muy interesante de hacer, hace la vida mucho más fácil :)

Tuve que implementar algo similar con una vista de árbol que contiene nodos (solo nivel principal) que se filtra por el evento de texto modificado en WinForms.

La aplicación siguió chocando conmigo, por alguna extraña razón.

Encontré un PDF en el sitio de MSDN @MSDN Rx (PDF download link - consulte la página 25) que estaba abordando un problema similar y que describía un problema de acceso a hilos cruzados.

Aquí está la solución que proporcionó que funcionó para mí, la solución es utilizar también ObserveOn antes de suscribirse.

Aquí es código de ejemplo, que utiliza la versión posterior de Rx - 1.0.10605.1

/// <summary> 
    /// Attach an event handler for the text changed event 
    /// </summary> 
    private void attachTextChangedEventHandler() 
    {    
    var input = (from evt in Observable.FromEventPattern<EventArgs>(textBox1,"TextChanged") 
    .select ((TextBox)evt.Sender).Text) 
    .DistinctUntilChanged() 
    .Throttle(TimeSpan.FromSeconds(1)); 
    input.ObserveOn(treeView1).Subscribe(filterHandler, errorMsg); 
    } 
    private void filterHandler(string filterText) 
    { 
     Loadtreeview(filterText); 
    } 
2

respuesta de Azerax es la correcta con la nueva versión del RX.

Cuando se desea separar el código de los elementos de la interfaz, se puede tener:

input.ObserveOn(SynchronizationContext.Current).Subscribe(filterHandler, errorMsg); 

Esto hará que la notificación al hilo de interfaz de usuario. De lo contrario, el acelerador (*) no tendrá efecto.

0

No hay que resaltar este hilo, pero todos sugirieron desarrollos con estilo LINQ o recursos adicionales para agregar a la sobrecarga de la biblioteca de la aplicación.

Lo que hice fue definir una colección List (Of) para mantener la lista original de información que terminé cargando en el ListBox y una colección de lista filtrada (Of) para contener el subconjunto filtrado resultante.

Utilicé el espacio de nombres RegEx para hacer la filtración, pero podría usar el sistema de patrones inherente al marco de trabajo String. Aquí está el código que usé para hacer el trabajo.

Private Sub txtNetRegex_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles txtNetRegex.TextChanged 
     If String.IsNullOrEmpty(txtNetRegex.Text) Then 
     btnNetALLToDB.Enabled = False 
     Else 
     btnNetALLToDB.Enabled = True 

     Dim reg As New Regex(txtNetRegex.Text, RegexOptions.IgnoreCase) 

     Me._netFilteredNames = New List(Of String) 

     For Each s As String In Me._netNames 
      On Error Resume Next 
      If (reg.IsMatch(s)) Then 
       Me._netFilteredNames.Add(s) 
      End If 
     Next 

     LoadNetBox() 
     End If 
    End Sub 
    Private Sub LoadNetBox() 
     lbxNetwork.Items.Clear() 
     lbxNetwork.Refresh() 

     Dim lst As List(Of String) 
     If Me.chkEnableNetFilter.Checked And (Me._netFilteredNames IsNot Nothing) Then 
     lst = Me._netFilteredNames 
     Else 
     lst = Me._netNames 
     End If 

     If lst IsNot Nothing Then 
     For Each s As String In lst 
      lbxNetwork.Items.Add(s) 
     Next 
     End If 

     lbxNetwork.Refresh() 
    End Sub