Search listview items using textbox - c#

I am working on a simple application (phonebook) in C# and in my project I have got a listview filled with contacts. I have been trying to implement the possibility to automatically (instantly) search through a listview using a textbox. I have managed to make it work, but not in the desired way. You will realise the actual problem if I give you an example. Let's say that I have got a contact named Bill Gates and when I try searching for it - it gets found and that part is OK. But, the problem is when I try to search for another contact. In that case, I have to clear the textbox before I type another name, but it is possible to remove only letter by letter. When I start removing the whole name, after removing a first letter it acts like I have just entered the name - it selects the item (and focuses as well) - actually there is no time to remove the whole name before it finds a contact again. I have to remove a first letter, then switch back to the textbox, remove another letter etc. Is there any solution for searching to be automatic - as it is now, but on the other hand for removing (clearing the textbox) without selecting contacts to be possible.
Take a look at the code:
private void txt_Search_TextChanged(object sender, System.EventArgs e)
{
if (txt_Search.Text != "")
{
foreach (ListViewItem item in listView1.Items)
{
if (item.Text.ToLower().Contains(txt_Search.Text.ToLower()))
{
item.Selected = true;
}
else
{
listView1.Items.Remove(item);
}
}
if (listView1.SelectedItems.Count == 1)
{
listView1.Focus();
}
}
else
{
LoadContacts();
RefreshAll();
}
}

There are some things wrong in your code, firstly when modifying a collection in a loop through it, we should not use foreach although in some case it seems to work but not really, it will surely be strange in future and confuse you. We should use a for loop instead and loop in the reverse order. The second wrong thing is you set the Selected to true which may cause your textBox lose focus to the listView. The solution is we have to use some other way to indicate that the item is selected, such as by using BackColor instead:
private void txt_Search_TextChanged(object sender, System.EventArgs e)
{
if (txt_Search.Text != "") {
for(int i = listView1.Items.Count - 1; i >= 0; i--) {
var item = listView1.Items[i];
if (item.Text.ToLower().Contains(txt_Search.Text.ToLower())) {
item.BackColor = SystemColors.Highlight;
item.ForeColor = SystemColors.HighlightText;
}
else {
listView1.Items.Remove(item);
}
}
if (listView1.SelectedItems.Count == 1) {
listView1.Focus();
}
}
else
LoadContacts();
RefreshAll();
}
}
Also after user focusing the ListView, all the BackColor and ForeColor should be reset, we can handle the Enter event of ListView:
//Enter event handler for listView1
private void listView1_Enter(object sender, EventArgs e){
foreach(ListViewItem item in listView1.Items){
item.BackColor = SystemColors.Window;
item.ForeColor = SystemColors.WindowText;
}
}

EDIT
you better not use Text_Changed, rather try Key_Down method as follows
private void txt_Search_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter) //apply your search only when pressing ENTER key
{
// you do your search as it was before
// i personally don't have suggestions here
if (!txt_Search.AutoCompleteCustomSource.Contains(txt_Search.Text)) txt_Search.AutoCompleteCustomSource.Add(txt_Search.Text);
//the line above will save all your searched contacts and display it in a beautiful format
}
else if (txt_Search.Text == "")
{
LoadContacts();
RefreshAll();
}
}
Of course don't forget to set the properties of txt_Search
AutoCompleteMode = SuggestAppend and AutoCompleteSource = CustomSource

This kind of feels like a hack, but you could track the length of text that has been typed into the textbox, and only perform your searching and focus logic if the length of text is greater than the previous time the event was called. That way if someone deletes a letter, the searching and focusing won't occur. Something like:
// declare lastSearchLength as a int outside of your TextChanged delegate
if (!String.IsNullOrEmpty(txt_Search.Text) && txt_Search.Text.Length > lastSearchLength)
{
foreach (ListViewItem item in listView1.Items)
{
if (item.Text.ToLower().Contains(txt_Search.Text.ToLower()))
{
item.Selected = true;
}
else
{
listView1.Items.Remove(item);
}
}
if (listView1.SelectedItems.Count == 1)
{
listView1.Focus();
}
lastSearchLength = txt_Search.Text.Length;
}
else
{
LoadContacts();
RefreshAll();
}
}

You are doing a postback with every key press. When the page reloads, it will not retain focus where you would expect. I recommend implementing this in JavaScript on the client, or using a search button instead of TextChanged event.

Related

C# ListView search item without clear list

I have winform project on C# platform. I have listview and textbox as you see in pic below.
I want to reorder the list according to the text value entered by the user.
I researched before ask here, I generally saw solutions based on removing and re-adding all units to listview again. I don't want to do that because my listview has too many items with pictures so removing and re-adding items causes listview to work slowly.
What I want is that, when the user enters characters in the textbox, the items which starts with this characters, bring this items the top of the list something similar google search system.
I tried the codes below but this send the item at the end of the list even though i chose index 0.
Thanx.
private void txt_search_TextChanged(object sender, EventArgs e)
{
string text = txt_search.Text;
var item = listView1.FindItemWithText(text);
if (item != null)
{
int index = listView1.Items.IndexOf(item);
if (index > 0)
{
listView1.Items.RemoveAt(index);
listView1.Items.Insert(0, item);
}
}
}
ListView is sorted using the .Sort() function, not sure what the default behaviour is, but I think you need a custom comparer.
Here is an example implementation by (ab)using the ListViewItem.Tag.
Custom Comparer:
private class SearchCompare : Comparer<ListViewItem>
{
public override int Compare(ListViewItem x, ListViewItem y)
{
if (x?.Tag != null && y?.Tag != null)
{
return x.Tag.ToString().CompareTo(y.Tag.ToString());
}
return 0;
}
}
Initializing the ListView:
var items = new[]
{
"1 no",
"2 yes",
"3 no",
"4 yes"
};
foreach (var item in items)
{
listView1.Items.Add(item);
}
listView1.ListViewItemSorter = new SearchCompare(); // custom sorting
And ofcourse the text changed event handler:
private void textBox1_TextChanged(object sender, EventArgs e)
{
string text = textBox1.Text;
foreach (ListViewItem item in listView1.Items)
{
if (item.Text.IndexOf(text, StringComparison.InvariantCultureIgnoreCase) > -1)
{
item.Tag = "a"; // a is sorted before b
}
else
{
item.Tag = "b"; // b is sorted after a
}
}
listView1.Sort();
}
Typing "yes" in the search textbox will sort items 2 and 4 in front of items 1 and 3.

CheckedListBox Only One Checked But Keep Last

I am having some difficulty getting a CheckedListBox to behave the way I want it to. What I am trying to accomplish is getting a CheckedListBox with the first box checked on PageLoad. I want only one checkbox checked at any given time but I also don't want the user to uncheck the last checked box. I can do one or the other but I can't seem to do both.
Here is some code snippets that I have used to accomplish the task of having only one checkbox checked. The problem with these are the last selection can be unchecked where there are no checkboxes checked.
1st Snippet:
if(e.NewValue == CheckState.Checked)
{
// Uncheck the other items
for (int i = 0; i < defCheckedListBox.Items.Count; i++)
{
if (e.Index != i)
{
this.defCheckedListBox.SetItemChecked(i, false);
}
}
}
2nd Snippet
// Ensure that we are checking an item
if (e.NewValue != CheckState.Checked)
{
return;
}
// Get the items that are selected
CheckedListBox.CheckedIndexCollection selectedItems = this.defCheckedListBox.CheckedIndices;
// Check that we have at least 1 item selected
if (selectedItems.Count > 0)
{
// Uncheck the other item
this.defCheckedListBox.SetItemChecked(selectedItems[0], false);
}
Here is what I have used to prevent the last checked box to be "unchecked"
if (laborLevelDefCheckedListBox.CheckedItems.Count == 1)
{
if (e.CurrentValue == CheckState.Checked)
{
e.NewValue = CheckState.Checked;
}
}
I know this has got to be simple but I think because I've had a long week and I have looked at this too long it is just not coming to me. Any help with this is super appreciated! If I solve this over the weekend I will be sure to post my solution. BTW Happy Holidays to those here in the States :)
Chris makes a good point in the comments that this feels like you are re-inventing radio buttons but you are almost there with the code you have posted if you really want it to work with a CheckedListBox. I have adapted the code from your 1st Snippet which I think does the trick:
//remove the event handler so when we change the state of other items the event
//isn't fired again.
defCheckedListBox.ItemCheck -= defCheckedListBox_ItemCheck;
if (e.NewValue == CheckState.Checked)
{
// Uncheck the other items
for (int i = 0; i < defCheckedListBox.Items.Count; i++)
{
if (e.Index != i)
{
this.defCheckedListBox.SetItemChecked(i, false);
}
}
}
else
{
//the state was not checked.
//as only one item can ever be Checked inside the event
//handler the state of not checked is invalid for us; set the state back to Checked.
e.NewValue = CheckState.Checked;
}
//re-add the event handler
defCheckedListBox.ItemCheck += defCheckedListBox_ItemCheck;
Essentially the only new parts are the else where we reset the state if the state was not Checked and the removing and re-adding of the event to prevent it firing again when we manually set the state of other items (this could be handled with a global bool if you prefer).
// Use CheckBoxList Event
private void checkedListBox1_ItemCheck(object sender, ItemCheckEventArgs e) {
// Stops DeInitialize Event
checkedListBox1.ItemCheck -= checkedListBox1_ItemCheck;
// Get the new value old value my be in Indeterminate State
checkedListBox1.SetItemCheckState(e.Index, e.NewValue);
// Start ReInitialize Event
checkedListBox1.ItemCheck += checkedListBox1_ItemCheck;
}

How can I check for a selectedIndex among dozens of list boxes in order to edit/delete?

I have dozens of list boxes on my form. I want to be able to edit/delete items from those list boxes using only one edit button and only one delete button. Should a loop be created so I don't have to code an if statement for each list box? Maybe a custom method? Kind of lost here.
Thanks for the input.
This is the code for one of the list boxes to for edit:
private void btnEdit_Click(object sender, EventArgs e)
{
//Edit an item in the list box
//If there are no appointments OR no appointment is selected, inform the user and cancel the operation
if ((!(appointmentList.Count > 0)) || lstDayView.SelectedIndex == -1)
{
MessageBox.Show("Error! You need to select an appointment!");
return;
}
else
{
int index = lstDayView.SelectedIndex;
var myForm = new Form2(appointmentList[index] as Appointment);
if (myForm.ShowDialog() == DialogResult.OK && myForm.Tag is Appointment)
{
Appointment appoint = myForm.Tag as Appointment;
//lstDayView.Items.RemoveAt(index);
appointmentList.RemoveAt(index);
appointmentList.Insert(index, appoint);
//appoint.toListBox(lstDayView, index);
this.setCal();
}
}
And this is for delete:
private void btnDeleteApp_Click_1(object sender, EventArgs e)
{
if ((!(appointmentList.Count > 0)) || lstDayView.SelectedIndex == -1)
{
MessageBox.Show("Error! You need to make and/or select an appointment!");
return;
}
else
{
if (MessageBox.Show("Are you sure you wish to delete?", "Confirm Delete", MessageBoxButtons.OKCancel) == DialogResult.OK)
{
appointmentList.RemoveAt(lstDayView.SelectedIndex); //Issue this is removed the index number from the list not the appointmentList
this.setCal();
}
}
As ThunderGr suggests, you can attach the same event handler to all Listboxes so that the same logic can be applied. When it comes to iterating (both to assign the event handlers initially, and to subsequently work on the collection as a whole), I would set the Tag property of each control to something relavent (e.g. "List"), and then you can do:
foreach(var control in Controls.Where(c => c.Tag == "List"))
{
// work with control here (casting to ListBox appropriately).
}
You could even make this a method of your form that returns IEnumerable<ListBox> like this:
public IEnumerable<ListBox> ListBoxes
{
get
{
return Controls.Where(c => c.Tag == "List").Cast<ListBox>();
}
}
You are removing another ListItem (lstDayView) from appointmentList List.
Replace This :
appointmentList.RemoveAt(lstDayView.SelectedIndex);
With following :
appointmentList.RemoveAt(appointmentList.SelectedIndex);
You can use the Form.Controls collection in a foreach loop to check for and manipulate the listboxes.
Another thing you can do is to assign the same selectedindexchanged event to all the listboxes you want to manipulate. The "sender" parameter holds the object that caused the event to fire. You can cast it to a listbox and manipulate it.
EDIT:Your basic problem is that you do not know which listbox was last selected. You need to define a variable that will keep the last listbox entered by the user(using the Enter event) and use that variable to delete the item from the listbox.
Example:
ListBox lastEnteredListbox = null;
private void aListboxName_Enter(object sender, EventArgs e)
{
lastEnteredListbox=(ListBox)sender;
}
private void theButton_Click(object sender, EventArgs e)
{
if(lastEnteredListbox == null || lastEnteredListbox.SelectedIndex == -1)
{
MessageBox.Show("You need to select an Item");
return;
}
lastEnteredListbox.RemoveAt(lastEnteredListbox.SelectedIndex);
}
You need to set the Enter event of all listboxes you wish to manipulate to the aListBoxName_Enter method.
In case that you want to simply delete the same index from all listboxes, you just loop through the controls collection of the form and do an if(control is ListBox) ((ListBox)control).RemoveAt(index);

ListPicker SelectionChanged gets called multiple times

the ListPicker is a Control from the WP8 Toolkit.
Code:
private void field_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Make sure we don't handle the event during initiation.
if (e.RemovedItems != null && e.RemovedItems.Count > 0)
{
if (this.field.SelectedItems != null)
{
if (this.field.SelectedIndex != -1)
{
ListPicker_SelectionChanged(sender, e);
//Make needed proffesions visable:
profls.Clear();
foreach (ListPickItem item in field.SelectedItems)
switch (item.Tag)
{
default:
foreach (ListPickItem iitem in profl[9])
profls.Add(iitem);
break;
case 90017:
foreach (ListPickItem iitem in profl[0])
profls.Add(iitem);
break;
case 9000:
foreach (ListPickItem iitem in profl[1])
profls.Add(iitem);
break;
}
}
}
}
}
Please notice that a profession ListPicker's ItemsSource is Data Binded to the profls var.
I modified the Listpicker so that I can also set the SelectedItems property and not only read from it (following this guide) and it works great.
Problem:
The field_SelectionChanged event gets called multiple times whenever I change the field listpicker's selecteditems. (i want it to be called only once..) Another wierd thing is that on one of the last calls the field_SelectedItems is equal to the old selectedItems (the ones before the "change")..
Is it a bug or my problem? (How do I fix it?)
EDIT:
I checked and it appears that it gets called only once if the are no selected items in the listpicker before I select items. (I mean that SelectedItems is empty before I select new items)
Fixed :)
I used the code suggested in this answer: listPicker not updating selection in full mode
if (MyListPicker.SelectedIndex != -1)
{
//Code..
}
I had the same issue of the selectedchange event being called twice.At the end of the listPicker_selectedchange event, set the listpicker selected index to -1.
private void listpicker_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//Code
listpicker.SelectedIndex = -1;
}
Thank you Dan Barzilay!!

WPF: Dropdown of a Combobox highlightes the text

When I type in the combobox I automatically opens enables the dropdown list
searchComboBox.IsDropDownOpen = true;
The problem here is - the text gets highlighted and the next keystrock overwrites the previous text.
How can I disable the text highlighting when ComboBox DropDown opens up?
I had this very same issue and like some of the users being new to WPF, struggled to get the solution given by Einar Guðsteinsson to work. So in support of his answer I'm pasting here the steps to get this to work. (Or more accurately how I got this to work).
First create a custom combobox class which inherits from the Combobox class. See code below for full implementation. You can change the code in OnDropSelectionChanged to suit your individual requirements.
namespace MyCustomComboBoxApp
{
using System.Windows.Controls;
public class MyCustomComboBox : ComboBox
{
private int caretPosition;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var element = GetTemplateChild("PART_EditableTextBox");
if (element != null)
{
var textBox = (TextBox)element;
textBox.SelectionChanged += OnDropSelectionChanged;
}
}
private void OnDropSelectionChanged(object sender, System.Windows.RoutedEventArgs e)
{
TextBox txt = (TextBox)sender;
if (base.IsDropDownOpen && txt.SelectionLength > 0)
{
caretPosition = txt.SelectionLength; // caretPosition must be set to TextBox's SelectionLength
txt.CaretIndex = caretPosition;
}
if (txt.SelectionLength == 0 && txt.CaretIndex != 0)
{
caretPosition = txt.CaretIndex;
}
}
}
Ensure that this custom combo class exists in the same project. THen you can use the code below to reference this combo in your UI.
<Window x:Class="MyCustomComboBoxApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:MyCustomComboBoxApp"
Title="MainWindow" Height="350" Width="525" FocusManager.FocusedElement="{Binding ElementName=cb}">
<Grid>
<StackPanel Orientation="Vertical">
<cc:MyCustomComboBox x:Name="cb" IsEditable="True" Height="20" Margin="10" IsTextSearchEnabled="False" KeyUp="cb_KeyUp">
<ComboBoxItem>Toyota</ComboBoxItem>
<ComboBoxItem>Honda</ComboBoxItem>
<ComboBoxItem>Suzuki</ComboBoxItem>
<ComboBoxItem>Vauxhall</ComboBoxItem>
</cc:MyCustomComboBox>
</StackPanel>
</Grid>
</Window>
Thats it! Any questions, please ask! I'll do my best to help.
THanks to Einar Guðsteinsson for his solution!
Better late than never and if some one else hit this proplem he might use this.
There is away todo this if you override combobox.
First get handle on the textbox that is used in the template and register to selectionchanged event.
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var element = GetTemplateChild("PART_EditableTextBox");
if (element != null)
{
var textBox = (TextBox)element;
textBox.SelectionChanged += OnDropSelectionChanged;
}
}
private void OnDropSelectionChanged(object sender, RoutedEventArgs e)
{
// Your code
}
Then in the event handler you can set the selection again like you want it to be. In my case I was calling IsDropDownOpen in code. Saved selection there then put it back in the event handler. Ugly but did the trick.
Further to clsturgeon's answer, I have solved the issue by setting the selection when DropDownOpened event occurred:
private void ItemCBox_DropDownOpened(object sender, EventArgs e)
{
TextBox textBox = (TextBox)((ComboBox)sender).Template.FindName("PART_EditableTextBox", (ComboBox)sender);
textBox.SelectionStart = ((ComboBox)sender).Text.Length;
textBox.SelectionLength = 0;
}
I think in the Solution provided by Andrew N there is something missing as when I tried it out the Selection Changed event of the TextBox was placing the caret at the wrong place. So I made this change to solve that.
namespace MyCustomComboBoxApp { using System.Windows.Controls;
public class MyCustomComboBox : ComboBox
{
private int caretPosition;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var element = GetTemplateChild("PART_EditableTextBox");
if (element != null)
{
var textBox = (TextBox)element;
textBox.SelectionChanged += OnDropSelectionChanged;
}
}
private void OnDropSelectionChanged(object sender, System.Windows.RoutedEventArgs e)
{
TextBox txt = (TextBox)sender;
if (base.IsDropDownOpen && txt.SelectionLength > 0)
{
caretPosition = txt.SelectionLength; // caretPosition must be set to TextBox's SelectionLength
txt.CaretIndex = caretPosition;
}
if (txt.SelectionLength == 0 && txt.CaretIndex != 0)
{
caretPosition = txt.CaretIndex;
}
}
}
When a comboxbox gains focus you can disable the text highlighting (i.e. by selecting no text upon the GotFocus event). However, when you pulldown the combobox the system is going to locate the item in the list and make that the selected item. This in turn automatically highlights the text. If I understand the behaviour you are looking for, I do not believe it is fully possible.
I was able to fix it using a modified answer from Jun Xie. Assuming you are using the keyUp event for your combobox search, I found an edge case in my custom use case that would still overwrite the text:
Type in the combobox for the first time. Text is fine.
Use up and down arrow keys to select an item in the list, but not "committing" the change (pressing enter, for example, and closing the dropDown selections. Note the text is highlighted at this point like clsturgeon points out.
Try to type in the textbox again. In this case the text will be over-ridden because the dropdown was still open hence the event never fired to clear the highlight.
The solution is to check if an item is selected. Here's the working code:
XAML:
<ComboBox x:Name="SearchBox" IsEditable="True" KeyUp="SearchBox_KeyUp"
PreviewMouseDown="SearchBox_PreviewMouseDown" IsTextSearchEnabled="False"
DropDownOpened="SearchBox_DropDownOpened">
</ComboBox>
Code:
private void SearchBox_KeyUp(object sender, KeyEventArgs e)
{
SearchBox.IsDropDownOpen = true;
if (e.Key == Key.Down || e.Key == Key.Up)
{
e.Handled = true;
//if trying to navigate but there's noting selected, then select one
if(SearchBox.Items.Count > 0 && SearchBox.SelectedIndex == -1)
{
SearchBox.SelectedIndex = 0;
}
}
else if (e.Key == Key.Enter)
{
//commit to selection
}
else if (string.IsNullOrWhiteSpace(SearchBox.Text))
{
SearchBox.Items.Clear();
SearchBox.IsDropDownOpen = false;
SearchBox.SelectedIndex = -1;
}
else if (SearchBox.Text.Length > 1)
{
//if something is currently selected, then changing the selected index later will loose
//focus on textbox part of combobox and cause the text to
//highlight in the middle of typing. this will "eat" the first letter or two of the user's search
if(SearchBox.SelectedIndex != -1)
{
TextBox textBox = (TextBox)((ComboBox)sender).Template.FindName("PART_EditableTextBox", (ComboBox)sender);
//backup what the user was typing
string temp = SearchBox.Text;
//set the selected index to nothing. sets focus to dropdown
SearchBox.SelectedIndex = -1;
//restore the text. sets focus and highlights the combobox text
SearchBox.Text = temp;
//set the selection to the end (remove selection)
textBox.SelectionStart = ((ComboBox)sender).Text.Length;
textBox.SelectionLength = 0;
}
//your search logic
}
}
private void SearchBox_DropDownOpened(object sender, EventArgs e)
{
TextBox textBox = (TextBox)((ComboBox)sender).Template.FindName("PART_EditableTextBox", (ComboBox)sender);
textBox.SelectionStart = ((ComboBox)sender).Text.Length;
textBox.SelectionLength = 0;
}
An alternative. Prevent the framework from messing with selection.
public class ReflectionPreventSelectAllOnDropDown
{
private static readonly PropertyInfo edtbPropertyInfo;
static ReflectionPreventSelectAllOnDropDown()
{
edtbPropertyInfo = typeof(ComboBox).GetProperty("EditableTextBoxSite", BindingFlags.NonPublic | BindingFlags.Instance);
}
public void DropDown(ComboBox comboBox)
{
if (!comboBox.IsDropDownOpen)
{
var edtb = edtbPropertyInfo.GetValue(comboBox);
edtbPropertyInfo.SetValue(comboBox, null);
comboBox.IsDropDownOpen = true;
edtbPropertyInfo.SetValue(comboBox, edtb);
}
}
}

Categories