0

I implemented a SearchTextBox and it works finally however it is pretty slow (Autosuggestions show up after 5-7 seconds). I read from a CSV file and create an object type Observable Collection and dump that into SuggestionSource then filter the results as SearchBox text change. Altogether there are about 700 suggestions and each suggestion has 50-100 characters. Is it normal to be slow under these circumstances or what I am doing wrong?

XAML :

<controls:SearchTextBox x:Name="SearchLayersBox" Height="23" Margin="5,5,10,10" VerticalAlignment="Top" InfoText="Search" SearchMode="Auto" ShowHistory="True" Search="SearchTextBox_Search" Initialized="SearchLayersBox_Initialized" TextChanged="SearchLayersBox_TextChanged" SuggestionListMax="15" />

C#

public void LayersListSearchBox()
{
    LayerDict.Clear();
    DataListBoxSource.Clear();
    var path = @"\\nrdsmnt6\mine maps\DMP_Database\Layer_Properties_spc_CSV.csv";

    using (TextFieldParser csvParser = new TextFieldParser(path))
    {
       csvParser.SetDelimiters(new string[] { "*%&" });
       csvParser.HasFieldsEnclosedInQuotes = false;
       //Skip the row with the column names
       csvParser.ReadLine();

       while (!csvParser.EndOfData)
          {
             string[] fields = csvParser.ReadFields();
 
             string LayerDisplayName = fields[3].Substring(1);

             SearchSuggestCollection.Add(LayerDisplayName);
          }
    }
 
    SearchLayersBox.SuggestionSource = SearchSuggestCollection;
 }

 

private void SearchLayersBox_TextChanged(object sender, TextChangedEventArgs e)
{
    string searchString = SearchLayersBox.Text;

    ObservableCollection<object> filteredCollection = new ObservableCollection<object>(from objectL in SearchSuggestCollection where objectL.ToString().Contains(searchString) select objectL);

    SearchLayersBox.SuggestionSource = filteredCollection;

}

 

private void SearchLayersBox_Initialized(object sender, EventArgs e)
{
   LayersListSearchBox();
}

UPDATE 1

After reading comments I switched to ICollectionView to filter the ObservableCollection

public ICollectionView SearchView
        {
            get { return CollectionViewSource.GetDefaultView(SearchSuggestCollection); }
        }

and inserted binding to SearchView as @Bandook and @ASh suggest by following ASh's answer

<controls:SearchTextBox x:Name="SearchLayersBox" SuggestionSource="{Binding SearchView, UpdateSourceTrigger=PropertyChanged}" Height="23" Margin="5,5,10,10" VerticalAlignment="Top" InfoText="Search" SearchMode="Auto" ShowHistory="True" Search="SearchTextBox_Search" Initialized="SearchLayersBox_Initialized" TextChanged="SearchLayersBox_TextChanged" SuggestionListMax="15" />

Also, I required at least three letters entered before autosuggestion show up.

private void SearchLayersBox_TextChanged(object sender, TextChangedEventArgs e)
        {
 
            string searchString = SearchLayersBox.Text;

            if (searchString.Length >= 3)
            {
                SearchView.Filter = item =>
                {
                    return item.ToString().Contains(searchString);
                };
            }

           
        }

I think the binding to SearchView does not work. Is this not right way to create the binding?

SuggestionSource="{Binding SearchView, UpdateSourceTrigger=PropertyChanged}"

UPDATE 2

After trying too many attempts much frustration I could not find the solution in the proper way however I make it pretty fast after removing initial SuggestSource assignments and requiring at least 3 letters before firing the suggestions

XAML:

<controls:SearchTextBox x:Name="SearchLayersBox" Height="23" 
Margin="5,5,10,10" VerticalAlignment="Top" InfoText="Search here" 
SearchMode="Auto" ShowHistory="True" Search="SearchTextBox_Search" 
Initialized="SearchLayersBox_Initialized" 
TextChanged="SearchLayersBox_TextChanged" SuggestionListMax="15" />

C#

   private ObservableCollection<object> _searchSuggestionCollection;
        public ObservableCollection<object> SearchSuggestCollection
        {
            get { return _searchSuggestionCollection; }
            set
            {
                if (_searchSuggestionCollection != value)
                {
                    _searchSuggestionCollection = value;
                    OnPropertyChanged();
                }
            }
        }

void LayersListSearchBox()
        {
            LayerDict.Clear();
            DataListBoxSource.Clear();
            var path = @"\\nrdsmnt6\mine maps\DMP_Database\Layer_Properties_spc_CSV.csv";
            SearchSuggestCollection = new ObservableCollection<object>();

            using (TextFieldParser csvParser = new TextFieldParser(path))
            {
                csvParser.SetDelimiters(new string[] { "*%&" });
                csvParser.HasFieldsEnclosedInQuotes = false;
                //Skip the row with the column names
                csvParser.ReadLine();

                while (!csvParser.EndOfData)
                {
                    string[] fields = csvParser.ReadFields();
     
                    string LayerDisplayName = fields[3].Substring(1);

                    SearchSuggestCollection.Add(LayerDisplayName);
                }
            }
        }

private void SearchLayersBox_Initialized(object sender, EventArgs e)
        {
            LayersListSearchBox();
        }

private void SearchLayersBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            string searchString = SearchLayersBox.Text;
            SearchLayersBox.SuggestionSource = null;

            if (searchString.Length >= 3)
            {
                ObservableCollection<object> filteredCollection = new ObservableCollection<object>(from objectL in SearchSuggestCollection where objectL.ToString().Contains(searchString) select objectL);

                SearchLayersBox.SuggestionSource = filteredCollection;
            }
    }
Amadeus
  • 157
  • 10
  • Have you looked at some other event - e.g. PreviewKeyDown rather than TextChanged event - please test that and get back to me. – Bandook Oct 21 '20 at 06:18
  • Also I would move your execution of the method "LayersListSearchBox" to your window constructor - to ensure the file source is read to your "SearchSuggestCollection" even before the search box is initialized. Btw, all your logic is within the view - it should follow mvvm pattern - but i guess you need a quick solution for now so change these 2 things and let me know if its working faster. – Bandook Oct 21 '20 at 06:39
  • better use ICollectionView filter instead of rebuilding collection every time: https://stackoverflow.com/questions/61209535/filtering-icollectionview-binded-to-itemscontrol. also add delay to optimize search – ASh Oct 21 '20 at 08:01
  • It's correct to filter using the `ICollectionView` of the suggetsion collection. But you don't need to bind to it. You can bind to the original collection as long as you finally bind it to an `ItemsControl` to display them. `ItemsControl` does automatically use the `ICollectionsView` to handle its items. – BionicCode Oct 22 '20 at 08:59
  • @Bandook thank you for your time and suggestions using a different event did not make any difference neither moving the execution method. I dont know how to bind that is why I wrote it in that way in the first place. I have tried a lot to make the binding but I need to learn more to do that. – Amadeus Oct 22 '20 at 20:01
  • @ASh I did not find much difference between LINQ filtering and ICollectionView after testing them out although the reason might be the collection size as it is pretty small amount. Also, I tried to use your answer partially lack of binding knowledge prevented for me to use your answer completely. Thank you! – Amadeus Oct 22 '20 at 20:06

1 Answers1

1

You can improve lookup time by replacing string.Contains with string.StartsWith. This should significantly improve string parsing, because Contains will touch every entry.
In case you want to allow different search patterns like starts with, ends with or contains, you should make your search configurable in order to restrict the required behavior to achieve a minimal performance impact.

Also the searching should be implemented inside your custom SearchTextBox control. You should override TextBoxBase.OnTextChanged for this purpose:

class SearchTextBox : TextBox
{
  // This MUST be a DependencyProperty to allow being set via data binding
  public IEnumerable SuggestionSource { get; set; }

  protected override void OnTextChanged(TextChangedEventArgs e)
  {
    string searchString = this.Text;
    if (this.SuggestionSource == null || searchString.Length < 3)
    {
      return;
    }

    ICollectionView suggestionsView = CollectionViewSource.GetDefaultView(this.SuggestionSource);
    suggestionsView.Filter = item => (item as string).StartsWith(searchString);           
  }
}

You can even further improve lookup speed by implementing a lookup tree, which you create dynamically, by storing each search key and its associated results in a dictionary or hash table.

You can prepare the lookup dictionary in a background thread or during application startup (while showing a splash screen) and store it in a database of file for the next time the application starts.
I suggest to prepare all three letter up to e.g. five letter combinations by iterating over your file input, when creating the suggestions source collection. Then in background you complete the lookup dictionary until it contains a key that completely matches a result i.e. the complete input source is indexed.
This kind of lookup dictionary eliminates reoccurring and expensive string comparisons.

Example:

Input: "Treasure", "Treasurer".
Index:
Entry1: "Tre" : "Treasure", "Treasurer"
Entry2: "Trea" : "Treasure", "Treasurer"
Entry3: "Treas" : "Treasure", "Treasurer"
...
Entry6: "Treasure" : "Treasure", "Treasurer"
Entry7: "Treasurer" : "Treasurer"

As suggested by ASh, you should consider to implement a delay e.g. 500ms before executing the filter, to avoid filtering on every keystroke. You can make this delay optional and enable it in case the performance degrades too much.

BionicCode
  • 1
  • 4
  • 28
  • 44
  • I did not find much perf difference between string.Startswith and string.Contains I think because I don't have many items in the collection. Unfortunately, I don't know much about data binding and using ItemsControl. I have tried to do it but could not figure out. I ma always up to make it proper and I will learn about these ASAP. However, I was able to make it pretty fast with couple of tweaks in my original way without using binding or ICollectionview. I will post it as an update. I love the idea of creating a dictionary. I will definitely keep it in my mind for larger collectios. Thank you! – Amadeus Oct 22 '20 at 20:11