Performance Boost with Virtual RadMultiColumnComboBox - Quick Loading and Fast Filtering using Trie
| Product Version | Product | Author | Last modified |
|---|---|---|---|
| Q2 2013 | RadMultiColumnComboBox for WinForms | Georgi Georgiev | 07/11/2013 |
PROBLEM
You want to display to your users thousands of rows into with **RadMultiColumnComboBox **and still be able to filter them quickly, load almost instantaneously and have control over your data.
SOLUTION
Create a custom RadMultiColumnComboBox which will fit into your needs.
I will try to explain only the most important methods/data structures of this example. The full source code can be downloaded below.
When you open the solution you will see four projects:

-
Trie – This is the data structure used by our **MultiColumnComboBox **to perform the searching operations. Although it is highly modified the main idea still reminds of a Trie. More information about it can be found here
-
Trie.Tests – These are the unit tests for the Trie, they are not needed for the project to function properly, however if you decide to modify something in the **Trie **class you would want to run these unit tests in order to ensure that the functionality is still intact.
-
VirtualMultiColumnComboBox – This is the test project which contains one form. In this test project I am filling a data source of 100 000 objects and binding it to the MultiColumnComboBox
-
VirtualMultiColumnComboBox.Implementation – The project on which we will focus during this article. Contains the VirtualMultiColumnComboBox and VirtualMultiColumnComboBoxElement classes.
First of all I think we should start from the Trie because it is a class completely separated from theMultiColumnComboBox and can work independently.
The **Trie **itself consists of nodes. Each **TrieNode **has a **Value **and **Children **properties. The value property is char and the Children are Dictionary with Char for Key and TrieNode as a Value. Each node also has an "**IsWord" **property which indicates whether this node marks the start of a new word.
The Trie has 4 main functionalities:
-
Insert words into the Trie
-
Check if a word is contained inside the Trie
-
Search for List of words. The search operation is performed with Contains or StartsWith filter
-
Ignore words in the Trie. The words are simply ignored and not deleted from the Trie since it would require it to rebuild itself, which will cost time
Every method has comments added above them with the corresponding algorithm (if any).
Now I am going to skip the **VirtualMultiColumnComboBox **and go straight to the VirtualMultiColumnComboBoxElementclass.
Let’s get started from the constructor –
public VirtualMultiColumnComboBoxElement()
: base()
{
this.EditorControl.LoadElementTree();
}
In the body of the constructor the LoadElementTree method is used to speed up the loading of the dropdown, otherwise you will see a significant slowdown when openning the dropdown for the first time.
The next important step is to initialize the data source. The data source is initialized when you set the DataSourceproperty or the **ValueMember **property. The order does not matter. The VirtualMultiColumnComboBox requires onlyValueMember for its filtering operations. The **DisplayMember **property on the other hand specifies which value to be displayed in the textbox when a row is selected.
The DataSourcecan also be loaded asynchronously in a separate from the UI thread. This can be controlled from the**LoadDataSourceAsync **property. Once the **DataSource **is loaded the **DataSourceLoaded **event is being fired. Here it is important to say that the DataSource property provides the dynamically updated data source. You can access all objects from your actual data source from the **AllDataSource **property.
Basically after these operations are finished the ComboBoxis ready to roll. With the code below we can initialize ourComboBox:
this.virtualRadMultiColumnComboBox1.LoadDataSourceAsync = true;
this.virtualRadMultiColumnComboBox1.ValueMember = "DummysDummy.Name";
this.virtualRadMultiColumnComboBox1.DisplayMember = "DummysDummy";
this.virtualRadMultiColumnComboBox1.DataSource = this.ds;
this.virtualRadMultiColumnComboBox1.AutoFilter = true;
this.virtualRadMultiColumnComboBox1.AutoShowHidePopup = true;
this.virtualRadMultiColumnComboBox1.SearchType = TrieImplementation.SearchType.Contains;
this.virtualRadMultiColumnComboBox1.EditorControl.AutoSizeColumnsMode = Telerik.WinControls.UI.GridViewAutoSizeColumnsMode.Fill;
this.virtualRadMultiColumnComboBox1.SearchCompleted += radMultiColumnComboBox1_SearchCompleted;
this.virtualRadMultiColumnComboBox1.SearchStarting += radMultiColumnComboBox1_SearchStarting;
this.virtualRadMultiColumnComboBox1.EditorControlCellValueNeeded += virtualRadMultiColumnComboBox1_EditorControlCellValueNeeded;
this.virtualRadMultiColumnComboBox1.DataSourceLoaded += virtualRadMultiColumnComboBox1_DataSourceLoaded;
It also has 3 additional events – SearchCompleted, SearchStarting(cancelable) and EditorControlCellValueNeeded. Please note that if you do not set the AutoFilter property the PerformSearch method will have to be manually called:
this.virtualRadMultiColumnComboBox1.PerformSearch("test", SearchType(optional));
The SearchTypesets the default search type for AutoFiltering. You can explicitly specify the **SearchType **when searching manually.
Now let’s take a look at what exactly the Search method does. No matter where it is called from – from the control, from the element or internally from the AutoFiltering, the end point is always the PerformSearchCoremethod inside the VirtualMultiColumnComboBoxElement.
Basically this method starts a new thread which performs the search operation within the Trie, updates the data source and sets the rows count of the grid control since it is in Virtual Mode.
protected virtual void PerformSearchCore(string text, SearchType searchType)
{
if (!this.OnSearchStarting(text))
{
return;
}
if (!this.searching)
{
Thread searchThread = this.CreateSearchThread(searchType, text);
searchThread.IsBackground = true;
searchThread.Start();
}
else
{
this.enqueuedSearchType = searchType;
this.enqueuedSearchText = text;
}
}
private Thread CreateSearchThread(SearchType searchType, string text)
{
Thread searchThread = new Thread(() =>
{
if (this.searching)
{
this.enqueuedSearchType = searchType;
this.enqueuedSearchText = text;
return;
}
Thread.Sleep(this.startSearchInterval);
if (this.enqueuedSearchText != null)
{
this.PerformNewSearchFromQueue();
return;
}
this.searching = true;
ICollection<string> results = this.trie.Search(this.Text, searchType);
if (!(this.virtualDataSource is List<object>))
{
this.virtualDataSource = new List<object>();
}
List<object> dataSource = this.virtualDataSource as List<object>;
dataSource.Clear();
foreach (string result in results)
{
if (this.actualDataSource.ContainsKey(result))
{
foreach (object item in this.actualDataSource[result])
{
dataSource.Add(item);
}
}
}
searching = false;
this.EditorControl.Invoke(new InvokeDelegate(() =>
{
if (this.EditorControl.RowCount != results.Count)
{
this.EditorControl.RowCount = results.Count;
}
this.SetCurrentState(PopupEditorState.Ready);
this.OnSearchCompleted(text, results);
}));
});
return searchThread;
}
The complete examples in C# and VB can be downloaded by clicking the following link.