2010-08-04 7 views
5

J'aimerais pouvoir filtrer une liste contenant 1000 chaînes de 50 à 4000 caractères chacune, car l'utilisateur saisit sans délai dans la zone de texte.Filtrage en temps réel de la listbox

J'utilise actuellement un temporisateur qui met à jour la liste après que l'événement TextChanged de la zone de texte n'a pas été déclenché en 300ms. Cependant, c'est assez saccadé et l'interface utilisateur se fige parfois momentanément.

Quelle est la manière normale d'implémenter des fonctionnalités similaires?

Editer: J'utilise winforms et .net2.

Merci

Voici une version allégée du code Je suis actuellement en utilisant:

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 ou WinForms ou autre chose? – kbrimington

+0

J'utilise des winforms. – Ryan

Répondre

1

Vous pouvez faire deux choses:

  1. Assurez-vous l'interface utilisateur plus réactive avec un deuxième fil qui prend soin du filtrage. Une très grande nouvelle technologie est Reactive Extensions (Rx) qui fera exactement ce dont vous avez besoin.

    Je peux donner un exemple. Je suppose que vous utilisez WinForms? Une partie de votre code aiderait.

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

    Voici un petit teaser:

    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. Faites votre algorithme de filtrage plus efficace. Est-ce que vous filtrez avec quelque chose comme StartWith ou quelque chose comme Contient?

    Vous pouvez utiliser quelque chose comme un arbre de suffixe ou tous les préfixes des éléments de liste et faire une recherche. Mais décrivez ce dont vous avez besoin précisément et je trouverai quelque chose de simple - mais assez efficace. L'interface utilisateur est assez lourde si vous voulez afficher 100.000 éléments dans le ListBox mais si vous ne prenez que 100, disons 100, c'est rapide (décommentez la ligne .Take (100)). Cela peut aussi être amélioré si la recherche est faite dans un autre thread. Ca devrait être facile avec Rx mais je ne l'ai pas essayé.

Mise à jour

Essayez quelque chose comme ça. Cela fonctionne bien ici avec 100.000 éléments qui ont ~ 10 caractères. Il utilise des extensions réactives (le lien avant).

En outre, l'algorithme est naïf et peut être rendu beaucoup plus rapide si vous le souhaitez.

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

Merci pour la réponse. J'ai mis à jour ma question avec une version condensée de mon code. – Ryan

+0

Merci beaucoup pour le code d'exemple. Cependant, autant que je sache, les extensions réactives ne sont disponibles que pour .net3.5 et plus. Y a-t-il des équivalents .net2 que je pourrais utiliser? – Ryan

+0

Hhm à peu près tout doit être réécrit: D LINQ n'est pas en .Net framework 2.0 non plus. Puis-je voir l'ancien retardateur que vous avez écrit? Si cela vous suffit, vous pouvez couper les 100 premiers résultats de votre liste de résultats à Items.AddRange - ce sera bien plus rapide que si vous voulez afficher 1000 éléments. –

1

Tout d'abord, grâce à @lasseespeholt, pour obtenir de moi cette idée a commencé, très nouveau pour moi. Mais en effet Rx est très intéressant à faire, cela rend la vie beaucoup plus facile :)

J'ai dû implémenter une chose similaire avec un arbre contenant des nœuds (seul le niveau parent) étant filtré par l'événement text changed dans WinForms.

L'application gardé de s'écraser sur moi, pour une raison étrange.

J'ai trouvé un fichier PDF sur le site MSDN @MSDN Rx (PDF download link - voir page 25) qui traitait un problème similaire et avait décrit un problème d'accès au thread croisé.

Voici le correctif il prévu a fonctionné pour moi, la solution est d'utiliser également ObserveOn avant de vous abonner.

est un exemple de code ici, qui utilise la dernière version de Rx - Réponse de 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

Azerax est la bonne avec la nouvelle version du RX.

Lorsque vous voulez séparer le code des éléments de l'interface utilisateur, vous pouvez avoir:

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

Cela fera la notification au thread d'interface utilisateur. Sinon, la commande des gaz (*) n'aura aucun effet.

0

Sans-jack haut ce fil mais tout le monde a suggéré des développements ou des ressources LINQ style supplémentaires pour ajouter à la tête de la bibliothèque de l'application. Ce que j'ai fait était de définir une collection List (Of) pour contenir la liste originale d'informations que je finis de charger dans le ListBox et une collection Filtered List (Of) pour contenir le sous-ensemble filtré résultant.

j'ai fait utiliser l'espace de noms RegEx pour faire la filtration, mais vous pouvez utiliser le système de modèle inhérent au cadre de chaîne. Voici le code que j'ai utilisé pour faire le travail.

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