I maintain a C# .Net 3.5 Visual Studio Windows Form legacy desktop application. In the application, a ListBox’s DataSource utilizes a bounded DataTable filled from a database SQL query.
I want to create a “type-ahead” search for my ListBox. I use a TextBox’s entered text to implement the “type-ahead” search of the ListBox Items and get the index of the first Item that matches the entered text and call method SetSelected to that index. The text to match may be anywhere in the ListBox Items not just at the beginning of the Item.
The ListBox may have thousands of entries therefore, I am trying to use LINQ or some other way to directly find a matching Item instead of iterating through each ListBox item checking if the TextBox.Text occurs anywhere in the ListBox Item. Even when I tried the iteration technique, I did not get the expected results.
private void txtSearch_TextChanged(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(txtSearch.Text) == false)
{
for (int i = 0; i <= lbItems.Items.Count - 1; i )
{
if (lbItems.Items[i].ToString().ToUpper().Contains(txtSearch.Text.ToUpper()))
{
lbItems.SetSelected(i, true);
break;
}
}
}
}
The below works but only matches from the start of the ListBox Item.
private void txtSearch_TextChanged(object sender, EventArgs e)
{
// Ensure we have a string to search.
if (!string.IsNullOrEmpty(txtSearch.Text))
{
// Find the item in the list and store the index to the item.
int index = lbItems.FindString(txtSearch.Text);
// Determine if a valid index is returned. Select the item if it is valid.
if (index != -1)
lbItems.SetSelected(index, true);
else
MessageBox.Show("The search string did not match any items in the ListBox");
}
}
Can I use LINQ or some other way to directly find the index of a ListBox Item that contains a string without iterating through the ListBox Items?
Any other hints as to how to implement a “type-ahead” Item search selection in a ListBox?
CodePudding user response:
You mentioned:
a ListBox’s DataSource utilizes a bounded DataTable filled from a database SQL query.
Accordingly, lbItems.Items[i].ToString()
does not return what you think, the item's text, it returns System.Data.DataRowView
which is the type of the item when the DataSource
is a DataTable
. You need to call the GetItemText
method to get the DisplayMember
.
private void txtSearch_TextChanged(object sender, EventArgs e)
{
var txt = txtSearch.Text.Trim();
for (int i = 0; i < lbItems.Items.Count; i )
{
bool b = false;
if (txt.Length > 0)
{
var itemText = lbItems.GetItemText(lbItems.Items[i]);
b = itemText.IndexOf(txt, StringComparison.InvariantCultureIgnoreCase) >= 0;
}
lbItems.SetSelected(i, b);
}
}
Alternatively and instead of selecting the matched items, you can filter
the view by setting the DataTable.DefaultView.RowFilter
property. You just need to write:
private void txtSearch_TextChanged(object sender, EventArgs e)
{
var filter = txtSearch.Text.Trim().Length > 0
? $"{lbItems.DisplayMember} LIKE '%{txtSearch.Text}%'"
: null;
(lbItems.DataSource as DataTable).DefaultView.RowFilter = filter;
}
CodePudding user response:
If you want to go the indexing route, here's a solution that lets you skip a lot of entries and jump based on the first letter typed. After the first letter, it's a sequential scan. You could refine it if you liked.
First, add a list to hold the words (that list will be bound to the Listbox as a data source) and a dictionary to hold the index:
private List<string> _wordsList = new List<string>();
private Dictionary<string, int> _wordsIndex = new Dictionary<string, int>();
private int _lastIndexFound = 0;
I scrapped a handful of web pages (including this one and some news stuff), copying and pasting to Notepad (to convert the clipboard to text only) and added the file to the project with "Copy Always". At startup, I read that in, throw away everything that isn't a word, and sort it. The result is a list with a couple of thousand unique words, in case-independent sort order:
var fileContents = File.ReadAllText("TextFile1.txt");
var words = fileContents.Split(' ', '\t', '\r', '\n');
const string wordPattern = @"^[a-zA-Z].*";
var wordRegex = new Regex(wordPattern);
foreach (var word in words)
{
if (wordRegex.IsMatch(word))
{
_wordsList.Add(word);
}
}
_wordsList = _wordsList.OrderBy(word => word).Distinct().ToList();
Then I build the index:
var curLetter = "a";
var lastLetter = "z";
var curIndex = 0;
_wordsIndex.Add(curLetter, curIndex);
foreach(var word in _wordsList)
{
curIndex;
if (!word[0].ToString().Equals(curLetter, StringComparison.OrdinalIgnoreCase))
{
var nextCurLetterChar = (char)(curLetter[0] 1);
curLetter = nextCurLetterChar.ToString();
if (string.Compare(curLetter, lastLetter, true) <=0)
{
_wordsIndex.Add(curLetter, curIndex);
}
}
}
Finally, here's my TextEntered event code:
private void textBox1_TextChanged(object sender, EventArgs e)
{
var enteredText = textBox1.Text;
if (string.IsNullOrEmpty(enteredText))
{
listBox1.SelectedIndex = -1;
_lastIndexFound = 0;
return;
}
var firstLetter = (char)enteredText[0];
if (_wordsIndex.TryGetValue(firstLetter.ToString(), out var firstLetterIndex))
{
for(var i = firstLetterIndex; i < _wordsList.Count; i )
{
if (_wordsList[i].StartsWith(enteredText, true, null))
{
_lastIndexFound = i;
break;
}
}
listBox1.SelectedIndex = _lastIndexFound;
}
}
It's pretty quick and dirty code and it's had only light testing. But, it seems to work.