C# ListView search item without clear list - c#

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.

Related

Text box does not update after changing after selected item in ComboBox (ComboBox gets the list from a text file)

I am confronting with an issue with the ComboBox in a WinForms app. The combo box contains a list with items that are grabbed from a TXT file. To read and update the list I added the following code to Form_Load.
string[] lineOfContents = File.ReadAllLines(Application.StartupPath + #"Items.txt");
foreach(var line in lineOfContents)
{
string[] tokens = line.Split(',');
ComboBox.Items.Add(tokens[0]);
}
All nice and good, the list does update. However, I also have some TextBoxes that are changing their text based on the string of the selected item in the ComboBox. For example, when the selected item in the ComboBox is "Example", the text in the first text box should change from empty to "I am an example!", but it doesn't and remains empty. The code for it is:
if(ComboBox.SelectedItem == "Example")
{
TextBox.Text = "I am an example!";
}
At first I though it's a conversion issue so I tried to use Convert.ToString(tokens[0]); but it did not work as well.
Any suggestions on what I should try next to fix this issue?
You are describing bound behavior so the first thing to check is whether the TextBox is properly connected to the event fired by the ComboBox. Something like this would be the first step to getting the behavior you describe.
public MainForm()
{
InitializeComponent();
// Subscribe to the event here OR using the Designer
comboBox1.SelectedIndexChanged += comboBox1_SelectedIndexChanged;
}
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
if (comboBox1.Text == "Example")
{
textBox1.Text = "I am an example!";
}
else textBox1.Text = comboBox1.Text;
}
But the missing step in the code you posted is actually selecting a value in the combo box after the tokens are loaded into it. This leaves the text box blank as demonstrated here:
private void buttonLoad_Click(object sender, EventArgs e)
{
loadMockFile();
}
private void loadMockFile()
{
string[] lineOfContents = MockFile;
foreach (var line in lineOfContents)
{
var token =
line.Split(',')
.Select(_ => _.Trim())
.Distinct()
.First();
if (!comboBox1.Items.Contains(token))
{
comboBox1.Items.Add(token);
}
}
}
string[] MockFile = new string []
{
"Example, A, B,",
"Other, C, D,",
};
So the solution is to make sure that you make a selection after the tokens are loaded into the ComboBox, for example as shown in the handler for the [Load + Select] button:
private void buttonLoadPlusSelect_Click(object sender, EventArgs e)
{
loadMockFile();
var foundIndex = comboBox1.FindStringExact("Example");
comboBox1.SelectedIndex = foundIndex;
}

Changing SelectedItem property of a ListView programatically

I have a 2 ListViews with same items in both of them. What I want to do is that when a selection is made in one ListView, the same selection should be reflected in the other ListView also. The two ListViews are bound to two different ViewModels but both the ViewModels implement the same interface.
I've overridden the Equals methods in both ViewModels.
The two ListViews are on different XAML pages. The first ListView say LV1 is in Page1.xaml and LV2 is in Page2.xaml. What I want is that when I am changing the selection in LV2 the selection in LV1 should also change( one way only ). I've set x:FieldModifier="public" on LV1 and exposing through a static property of Page1 like this:
public sealed partial class Page1 : Page
{
public static Page1 page1 { get; private set; }
}
And on Page2, I have this :
private async void LV2_ItemClick(object sender, ItemClickEventArgs e)
{
var selected = e.ClickedItem as ISomeCommonInterface;
//Comparision is successful --> Contains() always returns corect value;
if (Page1.page1.LV1.Items.ToList().Contains(selected))
{
Page1.page1.LV1.SelectedItem = null; // this works
Page1.page1.LV1.SelectedItem = selected; // this doesn't work
}
}
I've found that inside the if condition, assignment to null changes the SelectedItem of LV1 to null but the next line doesn't change it to selected ( it remains null ).
add after assignment:
Page1.page1.LV1.Select();
This works for me:
private void LV1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selected = (sender as ListView).SelectedItem as string;
int index = -1;
for (int i = 0; i < LV2.Items.Count(); i++)
{
if (LV2.Items[i] as string == selected){
index = i;
break;
}
}
// The if becomes obsolete here, it could be replaced by
// if(index >= 0)
if (LV2.Items.ToList().Contains(selected))
{
LV2.SelectedIndex = index;
}
}
There is probably an easier way of getting the index of LV1's SelectedItem in LV2, but it should be enough to get you on the right track.
You can check out the minimal testing app I created that shows that SelectedItem works too.
Method 1 - SelectionMode="Multiple" - both ListViews in sync
You should subscribe the SelectionChanged event on both ListViews - item may not get selected only by click - and there (when selection is changed) you should sync the selection.
private void SyncSelection(object sender, SelectionChangedEventArgs e)
{
ListView listViewToAdd = ReferenceEquals(sender, firstListView) ? secondListView : firstListView;
foreach (var item in e.AddedItems)
{
if (!listViewToAdd.SelectedItems.Contains(item))
{
listViewToAdd.SelectedItems.Add(item);
}
}
foreach (var item in e.RemovedItems)
{
listViewToAdd.SelectedItems.Remove(item);
}
}
Method 2 - SelectionMode="Multiple" - update one after selecting in the other
You should subscribe the SelectionChanged event only on the ListView where items could be selected.
private void SyncSelection(object sender, SelectionChangedEventArgs e)
{
foreach (var item in e.AddedItems)
{
secondListView.SelectedItems.Add(item);
}
foreach (var item in e.RemovedItems)
{
secondListView.SelectedItems.Remove(item);
}
}
Method 3 - SelectionMode="Single"
Subscribe the SelectionChanged event on both if you want to make them be in sync or only on the selectable one if you only want to update the second based on the first.
private void SyncSelection(object sender, SelectionChangedEventArgs e)
{
ListView senderListView = (ListView)sender;
ListView listViewToAdd = ReferenceEquals(sender, firstListView) ? secondListView : firstListView;
listViewToAdd.SelectedItem = senderListView.SelectedItem;
}
You may need to replace var with your interface to make it work.

Remove() function seems only removes a couple of controls at a time

I'm trying to remove multiple controls at a time (textboxes and checkboxes) in a windows form application. Basically, I have a row of textboxes and a corresponding checkbox. When the "delete rows" button is clicked, it should remove all rows that have been selected. But it only seems to remove two or three at a time (the same two or three in each row, but there doesn't seem to be any reason it selects those same two or three). I've attached a couple of screenshots showing what is happening.
Here I've selected a couple of rows:
After hitting delete once:
After hitting delete twice:
This just shows the names of each element. As you can see, the names are the same in each row:
Here is the relevant code:
//Gets a list of all ticked checkboxes
public List<string> checkForChecked()
{
var allCheckboxes = tabPage1.GetAllControlsOfType<CheckBox>();
int count = allCheckboxes.Count<CheckBox>();
List<string> checkedChecks = new List<string>();
foreach(Control c in tabPage1.Controls)
{
if(c is CheckBox && ((CheckBox)c).Checked)
{
checkedChecks.Add(c.Name.ToString());
}
}
return checkedChecks;
}
//The button click. Loop through elements and remove ones with the right name
private void button2_Click(object sender, System.EventArgs e)
{
List<string> toDelete = checkForChecked();
foreach (var val in toDelete)
{
foreach (Control item in tabPage1.Controls.OfType<Control>())
{
if (item.Name == val.ToString())
{
tabPage1.Controls.Remove(item);
}
}
}
}
I'm using a windows form app, no asp or other web technology.
It's a common error of removing items from a collection while enumerating it. For example:
foreach (Control c in Controls)
Controls.Remove(c);
will remove only half of the controls and leave every second control.
Some of the common solutions to the general problem are:
removing items in reverse order starting from the end of the collection
removing the first found item until no items found
enumerating a copy of the collection
In your case, both methods can be combined to something like this ( .Find returns Control[] ) :
foreach (var cb in tabPage1.Controls.OfType<CheckBox>())
if (cb.Checked)
foreach (var c in tabPage1.Controls.Find(cb.Name, false))
tabPage1.Controls.Remove(c)
You could try something more like:
private void button2_Click(object sender, EventArgs e)
{
List<CheckBox> CheckedBoxes = new List<CheckBox>();
foreach (CheckBox cb in tabPage1.Controls.OfType<CheckBox>())
{
if (cb.Checked)
{
CheckedBoxes.Add(cb);
}
}
foreach (CheckBox cb in CheckedBoxes)
{
string cbName = cb.Name;
cb.Dispose();
// ... probably more code in here to find the other controls
// ... in the "row" based on "cbName"
}
}
You are iterating the controls collection and removing a control, which effects the index of every item in the collection every time you call the Remove() method. This probably messes with your iterator in the foreach, thus the 'skipping' behavior. When removing from any controls collection it is best not to be iterating it at the same time. Moreover, when looping on a Controls collection where you are removing items, code your loops as iterators working backwards.
for (var i=Parent.Controls.Count-1; i >=0; i--)
{
if (someCondition) Parent.Controls.Remove(Parent.Controls[i]);
}
Here is a bit cleaner version of your code. The list returned from the CheckForChecked method is now a list of controls which is easier to use than a list of the controls names.
In the remove, we are iterating the list of things to delete, not the controls collection from which we are deleting.
//Gets a list of all ticked checkboxes
public List<Control> CheckForChecked()
{
var tabPage1 = new TabPage();
var results = new List<Control>(0);
results.AddRange(from Control c in tabPage1.Controls
where c is CheckBox && (c as CheckBox).Checked
select c);
return results;
}
//The button click. Loop through elements and remove ones with the right name
private void button2_Click(object sender, System.EventArgs e)
{
var toDelete = CheckForChecked();
var tabPage1 = new TabPage();
foreach (var val in toDelete.Where(val => tabPage1.Controls.Contains(val)))
{
tabPage1.Controls.Remove(val);
}
}
Hope this helps someone.

C# 2 Comboboxes with identical, not twice-selectable content

I have a dialog form where the user has to selected which colums from a textfile he wants to use for drawing a graph.
If someone doesn't quite understand what I mean, please look at the following example:
The dialog opens
The user selects e.g. that the x-values of his graph shall be from the second column of the textfile
The user selects e.g. that the y-values of his graph shall be from the third column of the textfile
The user clicks "OK"
The problem I have is the following:
I want to prevent the user from selecting the same column for x and y values, which would result in a line in an angle of probably 45 degrees and make the graph useless.
Both comboboxes are filled with the same array of strings, which contains the headlines of the columns in the textfile. Getting those strings into the comboboxes works great, but:
I tried removing the item selected in one combobox from the other combobox and otherwise.
Before that, the currently selected item is stored in a variable and the items are reset to the default state, which means all headlines from the textfile.
But, as I programmatically set the index to where it was before, so that the user doesn't have to, the SelectedIndexChanged event fires and traps my code in an infinite loop.
public void setComboboxText()
{
cbX.Items.Clear();
cbY.Items.Clear();
cbX.Items.AddRange(cbText);
cbY.Items.AddRange(cbText);
}
void CbXSelectedIndexChanged(object sender, EventArgs e)
{
var item = cbX.SelectedItem;
setComboboxText();
cbX.SelectedItem = item;
cbY.Items.Remove(cbX.SelectedItem);
}
void CbYSelectedIndexChanged(object sender, EventArgs e)
{
var item = cbY.SelectedItem;
setComboboxText();
cbY.SelectedItem = item;
cbX.Items.Remove(cbY.SelectedItem);
}
The code does the following:
The currently selected item is temporarily stored
The items of the combobox are reset
The currently selected item is set to be the item stores before
The item selected in the changed box disappears from the other combobox
Any help appreciated, especially if someone could tell me if I can do what I want with another event or even without events.
Thanks in advance
I think this is what you are trying to achieve.
public partial class Form1 : Form
{
List<string> source1 = new List<string>();
List<string> source2 = new List<string>();
public Form1()
{
InitializeComponent();
for (int i = 0; i < 10; i++)
{
source1.Add("item" + i);
source2.Add("item" + i);
}
comboBox1.Items.AddRange(source1.ToArray());
comboBox2.Items.AddRange(source2.ToArray());
}
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
if (comboBox2.Items.Contains(comboBox1.SelectedItem))
{
comboBox2.Items.Clear();
List<string> updatedList = new List<string>();
updatedList = (from x in source2
where !x.Equals(comboBox1.SelectedItem)
select x).ToList<string>();
comboBox2.Items.AddRange(updatedList.ToArray());
}
}
private void comboBox2_SelectedIndexChanged(object sender, EventArgs e)
{
if (comboBox1.Items.Contains(comboBox2.SelectedItem))
{
comboBox1.Items.Clear();
List<string> updatedList = new List<string>();
updatedList = (from x in source1
where !x.Equals(comboBox2.SelectedItem)
select x).ToList<string>();
comboBox1.Items.AddRange(updatedList.ToArray());
}
}
}
Make the source collections available avaiable to each combobox SelectedIndexChanged handlers
On each selection change update the source of the other combobox only if the newly selected item exists in the other combobox Items.

Search listview items using textbox

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.

Categories