binding to a combobox getting different results when using using SelectedValue - c#

I have bound to my combobox this simple class:
public class Company
{
public Guid CorporationId { set; get; }
public Guid TokenId { set; get; }
public string Name { set; get; }
}
And this is my binding:
private void FillCompaniesComboBox()
{
_doneLoadingComboBox = false;
comboBox_Companies.Items.Clear();
if (CurrentSettings.AllCompanies.Count == 0)
{
return;
}
bindingSource1.DataSource = CurrentSettings.AllCompanies;
comboBox_Companies.DataSource = bindingSource1.DataSource;
comboBox_Companies.DisplayMember = "Name";
comboBox_Companies.ValueMember = "CorporationId";
comboBox_Companies.SelectedIndex = 1;
_doneLoadingComboBox = true;
}
When I attempt to get the value of the selected item, I'm getting different results. Here is the code I am using to get my value:
private void comboBox_Companies_SelectedIndexChanged(object sender, EventArgs e)
{
if (!_doneLoadingComboBox && comboBox_Companies.SelectedIndex == -1)
{
return;
}
var value = (Company)comboBox_Companies.SelectedValue;
Console.WriteLine("Value: " + value.CorporationId);
}
Here is what is happening:
This one works at intended:
And this is were it is causing an issue:
Am I not retrieving the data correctly? I need the Company information that it is bound to.

Okay so here's what you need to do...
Assuming that your CurrentSettings.AllCompanies is an IList<Company> that you've already populated with data, here's what your code should look like:
public class ComboBoxItem {
// your class
private Company Comp;
}
private readonly BindingSource _bsSelectedCompany = new BindingSource();
private readonly ComboBoxItem _comboBoxItem = new ComboBoxItem();
// your main form method
public MainForm() {
// initialization code...
InitializeComponent();
// prevents errors in case your data binding objects are empty
ResetComboBox(comboBox1);
comboBox1.DataBindings.Add(new Binding(
"SelectedItem",
_bsSelectedCompany,
"Comp",
false,
DataSourceUpdateMode.OnPropertyChanged
));
comboBox1.DataSource = CurrentSettings.AllCompanies;
comboBox1.DisplayMember = "Name";
}
// simple method for resetting a given combo box to a default state
private static void ResetComboBox(ComboBox comboBox) {
comboBox.Items.Clear();
comboBox.Items.Add("Select a method...");
comboBox.SelectedItem = comboBox.Items[0];
}
By doing this, you're able to just use _comboBoxItem to safely get the information about your selected item without having to potentially Invoke it (in the case of accessing it on a separate thread).

Related

Removing item from ListBox - unhandled exception in WPF?

I have this code in my MainWindow.
The user enters Name, Phone and Email in the fields provided, selects Location and then the Name appears in the listbox, lstClients.
I'm trying to write a method to remove a selected name from the listbox.
namespace WpfApp_Employment_Help
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
// client list
List<Client> ClientList = new List<Client>();
public MainWindow()
{
InitializeComponent();
}
// method to select location via radio button
public string GetSelectedLocation()
{
string selected = string.Empty;
if (RBLocE.IsChecked == true) { selected = "Edinburgh"; }
else if (RBLocG.IsChecked == true) { selected = "Glasgow"; }
else if (RBLocO.IsChecked == true) { selected = "Other"; }
return selected;
}
// method to create a new client on click
private void newClient(object sender, RoutedEventArgs e)
{
Client c = new Client(boxClientName.Text, boxClientPhone.Text, boxClientEmail.Text, GetSelectedLocation());
boxClientName.Clear();
boxClientPhone.Clear();
boxClientEmail.Clear();
ClientList.Add(c);
lstClients.ItemsSource = null;
lstClients.ItemsSource = ClientList;
}
// method to id selected client
private void AssignID(object sender, RoutedEventArgs e)
{
Client c = lstClients.SelectedItem as Client;
if (c != null)
{
c.AssignID();
}
lstClients.ItemsSource = null;
lstClients.ItemsSource = ClientList;
}
// method to remove selected client
private void RemoveClient(object sender, RoutedEventArgs e)
{
lstClients.Items.Remove(lstClients.SelectedItem);
}
}
}
When I run this code, I get Unhandled Exception:
System.InvalidOperationException: 'Operation is not valid while ItemsSource is in use. Access and modify elements with ItemsControl.ItemsSource instead.'
how can I rewrite my RemoveClient method?
my code for the Client class is this:
public partial class Client
{
public string Name { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public string Location { get; }
public bool IDed { get; private set; }
public Client(string n, string p, string e, string l)
{
Name = n;
Phone = p;
Email = e;
Location = l;
}
}
I have Visual Studio 2022 which has been recently updated.
I have also tried the following solution but it gives me another unhandled error?
It looks like I need to change List </string/> and string to something else. but what?
private void RemoveClient(object sender, EventArgs e)
{
if (lstClients.Items.Count >= 1)
{
if (lstClients.SelectedValue != null)
{
var items = (List<string>)lstClients.ItemsSource;
var item = (string)lstClients.SelectedValue;
lstClients.ItemsSource = null;
lstClients.Items.Clear();
items.Remove(item);
lstClients.ItemsSource = items;
}
}
else
{
System.Windows.Forms.MessageBox.Show("No ITEMS Found");
}
}
System.InvalidCastException: 'Unable to cast object of type 'System.Collections.Generic.List`1[WpfApp_Employment_Help.Client]' to type 'System.Collections.Generic.List`1[System.String]'.'
As the error message suggests, you can't modify the ItemsControl via the collection view returned from the ItemsControl.Items property.
WPF is generally designed to work on the data sources instead of handling the data related controls (data presentation). This way data and data presentation (GUI) are cleanly separated and code will become a lot simpler to write.
In case of the ListView (or ItemsControl in general), simply modify the source collection.
To improve performance, the source collection should be a INotifyCollectionChanged implementation, for example ObservableCollection<T>, especially when you expect to modify the source collection.
This makes invalidating the ItemsSource e.g. by assigning null, just to set it again redundant and significantly improves the performance.
public partial class MainWindow : Window
{
// client list
public ObservableCollection<Client> ClientList { get; } = new ObservableCollection<Client>();
// method to create a new client on click
private void newClient(object sender, RoutedEventArgs e)
{
Client c = new Client(boxClientName.Text, boxClientPhone.Text, boxClientEmail.Text, GetSelectedLocation());
boxClientName.Clear();
boxClientPhone.Clear();
boxClientEmail.Clear();
ClientList.Add(c);
// The following lines are no longer needed
// as the GUI is now notified about the collection changes (by the INotifyCollectionChanged collection)
//lstClients.ItemsSource = null;
//lstClients.ItemsSource = ClientList;
}
// method to id selected client
private void AssignID(object sender, RoutedEventArgs e)
{
Client c = lstClients.SelectedItem as Client;
// Same as the if-statement below
c?.AssignID();
//if (c != null)
//{
// c.AssignID();
//}
// The following lines are no longer needed
// as the GUI is now notified about the collection changes (by the INotifyCollectionChanged collection)
//lstClients.ItemsSource = null;
//lstClients.ItemsSource = ClientList;
}
// method to remove selected client
private void RemoveClient(object sender, RoutedEventArgs e)
{
var clientToRemove = lstClients.SelectedItem as Client;
this.ClientList.Remove(clientToRemove);
}
}
If you change the type of ClientList from List<Client> to ObservableCollection<Client>, you could simply remove the item directly from the source collection:
public partial class MainWindow : Window
{
ObservableCollection<Client> ClientList = new ObservableCollection<Client>();
public MainWindow()
{
InitializeComponent();
}
...
private void RemoveClient(object sender, RoutedEventArgs e)
{
ClientList.Remove(lstClients.SelectedItem as Client);
}
}

CurrentPosition and current Index are out of sync when navigating items in System.Windows.Data.ListCollectionView

I found out that the CurrentPosition property and the IndexOf method in the ListCollectionView are out of sync when the NewItemPlaceholderPosition property = AtEnd;
When navigating to NewItemPlaceholder, the CurrentPosition property does not change although MoveCurrentTo returns true.
Here is test code:
public class Customer
{
public string CustomerID { get; set; }
public string CompanyName { get; set; }
}
private void ButtonTest_Click(object sender, RoutedEventArgs e)
{
List<Customer> list = new List<Customer>();
list.Add(new Customer() { CustomerID = "ALFKI", CompanyName = "Alfreds Futterkiste" });
list.Add(new Customer() { CustomerID = "ANATR", CompanyName = "Ana Trujillo Emparedados y helados" });
ListCollectionView collView = new ListCollectionView(list);
collView.NewItemPlaceholderPosition = System.ComponentModel.NewItemPlaceholderPosition.AtEnd;
Debug.WriteLine($" collView.Count = {collView.Count}");
foreach (var item in collView)
{
bool moved = collView.MoveCurrentTo(item);
int index = collView.IndexOf(item);
Debug.WriteLine($" moved = {moved}; CurrentPosition = {collView.CurrentPosition}; collView.IndexOf(item) = {index}");
}
}
Result is:
collView.Count = 3
moved = True; CurrentPosition = 0; collView.IndexOf(item) = 0
moved = True; CurrentPosition = 1; collView.IndexOf(item) = 1
moved = True; CurrentPosition = 1; collView.IndexOf(item) = 2
Last line - CurrentPosition = 1 but collView.IndexOf(item) = 2?
Can't figure out if this is a bug in the ListCollectionView or a feature.
But your collection only contains two items. The third is just a placeholder, which is not a real item and does not participate in collection view navigation.
If you would've consulted the docs, you would learn that:
"Methods that perform relative navigation, such as the MoveCurrentToNext, skip the NewItemPlaceholder."
and
"Methods that perform absolute navigation, such as MoveCurrentToPosition, do nothing if the NewItemPlaceholder would be the CurrentItem."
The last quote applies to your situation: since navigating to the next/last item would set the NewItemPlaceholder as CurrentItem, the operation MoveCurrentTo does nothing. It returns true as the operation has not failed by definition.
To make that selecting rows in the DataGrid will move the record pointer of the CollectionView to the SelectedItem you have to explicitly enable this behavior by setting IsSynchronizedWithCurrentItem to true:
<DataGrid IsSynchronizedWithCurrentItem="True" />
To detect if the currently selected item is the CollectionView.NewItemPlaceholder you can handle the DataGrid.SelectionChanged event:
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Contains(CollectionView.NewItemPlaceholder))
{
// Clear TextBox e.g. by setting SelectedItem to null
(sender as Selector).SelectedItem = null;
}
}
The DataGrid uses the same logic internally to handle the blank line.
You can use this class to replace the default DataGrid. It exposes a CurrentActiveRow property. Everything else works as usual. If you want to edit the place holder row using an external TextBox, simply bind to the CurrentActiveRowProperty (which also reflects the SelectedItem property):
Usage Example
<TextBox Text="{Binding ElementName=DataGrid, Path=CurrentActiveRow.CustomerID, UpdateSourceTrigger=PropertyChanged}" />
<TextBoxEditDataGrid x:Name="DataGrid"
ItemsSource="{Binding Items}"
IsSynchronizedWithCurrentItem="True" />
TextBoxEditDataGrid.cs
public class TextBoxEditDataGrid : DataGrid
{
public object CurrentActiveRow
{
get { return (object)GetValue(CurrentActiveRowProperty); }
set { SetValue(CurrentActiveRowProperty, value); }
}
public static readonly DependencyProperty CurrentActiveRowProperty = DependencyProperty.Register(
"CurrentActiveRow",
typeof(object),
typeof(TextBoxEditDataGrid),
new PropertyMetadata(default));
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
if (e.AddedItems.Contains(CollectionView.NewItemPlaceholder)
&& this.HasItems
&& this.SelectedItems.Count == 1) // Only execute on single item select
{
var editableView = this.Items as IEditableCollectionView;
object newEditItem = editableView.AddNew();
editableView.CancelNew(); // Don't add here. Only use instance and add it on edit started
if (!TryLazyAddNewEditItem(newEditItem))
{
AddNewEditItem(newEditItem);
}
this.CurrentActiveRow = newEditItem;
}
else if (!ReferenceEquals(this.SelectedItem, this.CurrentActiveRow))
{
this.CurrentActiveRow = this.SelectedItem;
}
}
private bool TryLazyAddNewEditItem(object newEditItem)
{
if (newEditItem is not INotifyPropertyChanged propertyChangedSource)
{
return false;
}
PropertyChangedEventManager.AddHandler(propertyChangedSource, AddNewEditItem_OnPropertyChanged, string.Empty);
return true;
}
private void AddNewEditItem(object newEditItem)
{
InsertEditItem(newEditItem);
this.SelectedItem = newEditItem;
}
private void InsertEditItem(object newEditItem)
{
if (CollectionViewSource.GetDefaultView(this.ItemsSource) is ListCollectionView listView)
{
int insertIndex = listView.NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning
? 0
: listView.Count - 1;
(listView.SourceCollection as IList).Insert(insertIndex, newEditItem);
}
else
{
var editableView = this.Items as IEditableCollectionViewAddNewItem;
editableView.AddNewItem(newEditItem);
editableView.CommitNew();
}
}
private void AddNewEditItem_OnPropertyChanged(object sender, EventArgs e)
{
PropertyChangedEventManager.RemoveHandler(sender as INotifyPropertyChanged, AddNewEditItem_OnPropertyChanged, string.Empty);
AddNewEditItem(sender);
}
}

How to keep variable through an event?

I have a method to populate a combobox with some strings. At the end of the method I assign to the SelectedIndexChanged event. Here's how that method looks
public ComboBox PopulateComboBox()
{
Worksheet sheetWithTemplateNames = _iReader.GetWorksheetByName("Templates");
int lastRowOfTemplates = _iReader.GetLastRow(sheetWithTemplateNames);
var templateNames = _iHandler.GetTemplateNames(sheetWithTemplateNames, lastRowOfTemplates);
foreach (var template in templateNames)
{
Box.Items.Add(template);
}
Box.SelectedIndexChanged += Box_SelectedIndexChanged;
return Box;
}
and it works as I want to. My problem is that I need to use this templateNames list in the actual event and that's causing trouble. Here's how my event looks like now but ain't functioning.
private void Box_SelectedIndexChanged(object sender, EventArgs e)
{
ComboBox cmb = (ComboBox)sender;
var chosenObject = cmb.SelectedIndex;
MessageBox.Show(templateNames[chosenObject]);
}
but my list is now empty. It's instantiated in the constructor so I'd assume it'd keep it's state but that isn't the situation. Here's the top of the class
public class TemplateListCombobox
{
public ComboBox Box { get; set; }
private IDataReader _iReader;
private IDataHandler _iHandler;
private List<string> templateNames;
public TemplateListCombobox()
{
Box = new ComboBox();
_iReader = new DataReader();
_iHandler = new DataHandler();
templateNames = new List<string>();
}
}
so how could I possibly keep the state of my list through the event?
UPDATE:
MY class that calls this:
public static class GroupBoxHolder
{
private static GroupBox _thisGroupBox;
public static GroupBox GetGroupBox()
{
PopulateGroupBox();
return _thisGroupBox;
}
public static void PopulateGroupBox()
{
_thisGroupBox = new GroupBox();
TemplateListCombobox combo = new TemplateListCombobox();
ComboBox box = combo.GetComboBox();
_thisGroupBox.Controls.Add(box);
ConfigureGroupBox();
}
public static void ConfigureGroupBox()
{
_thisGroupBox.Location = new Point { X = 75, Y = 15 };
_thisGroupBox.Height = 150;
_thisGroupBox.Width = 400;
}
}
and my updated class
public class TemplateListCombobox
{
private ComboBox _box;
private readonly IDataReader _iReader;
private readonly IDataHandler _iHandler;
private readonly Worksheet _sheetWithTemplateNames;
public TemplateListCombobox()
{
_box = new ComboBox();
_iReader = new DataReader();
_iHandler = new DataHandler();
_sheetWithTemplateNames = _iReader.GetWorksheetByName("Templates");
PopulateComboBox();
}
public void PopulateComboBox()
{
int lastRowOfTemplates = _iReader.GetLastRow(_sheetWithTemplateNames);
var templateNames = _iHandler.GetTemplateNames(_sheetWithTemplateNames, lastRowOfTemplates);
foreach (var template in templateNames)
{
_box.Items.Add(template);
}
_box.SelectedIndexChanged += Box_SelectedIndexChanged;
}
public ComboBox GetComboBox()
{
return _box;
}
private void Box_SelectedIndexChanged(object sender, EventArgs e)
{
ComboBox cmb = (ComboBox)sender;
var chosenObject = cmb.SelectedItem.ToString();
var firstRowForTemplate = _iReader.GetFirstRowForTemplate(_sheetWithTemplateNames, chosenObject.ToString());
var attributes = _iReader.GetTemplateAttributes(_sheetWithTemplateNames, chosenObject, firstRowForTemplate);
}
}
A. Two lists?
var templateNames = creates a new local variable.
Do you want to set a member field instead? If so, remove var.
B. Read from comobox
Since the combo box contains the names read directly from it.
The flow of the code is something like the following:
var templateNames = _iHandler.GetTemplateNames(sheetWithTemplateNames, lastRowOfTemplates);
foreach (var template in templateNames)
{
Box.Items.Add(template);
}
... time passes
var chosenObject = cmb.SelectedIndex;
MessageBox.Show(templateNames[chosenObject]);
The combobox contains all the information you need. (You could use SelectedItem).
Note on two sources.
Since you're not clearing the combobox's items in the populate method, if it was called twice the combobox would gain new elements. That's not great but more importantly templateNames[chosenObject] would not work anymore because the combobox and the list would be out of sync.
In addition to the var templateNames mistake that doesn't cause the problem, you don't call PopulateComboBox in the code you provide, so the list is empty...
I don't understand why this method returns the Box since there is no parameter and it is a member field that you access directly in the code.
You only need to assign the event in the constructor, not each time you call the populate method that you need to call somewhere.
You should improve your class design because there is several clumsiness. For example, what is TemplateListCombobox without parent and how do you intend to use it? How are initialized _iReader and _iHandler? Do you need to keep templateNames? And so on...

How to implement cascading ComboBoxes in DataGridView control?

I need to implement cascading ComboBoxes in few DataGridViews. As a proof of concept I have put together the code below. 3 Columns (Customer, Country, City) When selecting Country, City should populate but it doesn't work.
Is there a better way to achieve this and fix what I am doing wrong?
public partial class Form1 : Form
{
private List<Customer> customers;
private List<Country> countries;
private List<City> cities;
private ComboBox cboCountry;
private ComboBox cboCity;
public Form1()
{
InitializeComponent();
countries = GetCountries();
customers = GetCustomers();
SetupDataGridView();
}
private List<Customer> GetCustomers()
{
var customerList = new List<Customer>
{
new Customer {Id=1,Name = "Jo",Surname = "Smith"},
new Customer {Id=2,Name = "Mary",Surname = "Glog"},
new Customer {Id=3,Name = "Mark",Surname = "Bloggs"}
};
return customerList;
}
private List<Country> GetCountries()
{
var countryList = new List<Country>
{
new Country {Id=1,Name = "England"},
new Country {Id=2,Name = "Spain"},
new Country {Id=3,Name = "Germany"}
};
return countryList;
}
private List<City> GetCities(string countryName)
{
var cityList = new List<City>();
if (countryName == "England") cityList.Add(new City { Id = 1, Name = "London" });
if (countryName == "Spain") cityList.Add(new City { Id = 2, Name = "Madrid" });
if (countryName == "Germany") cityList.Add(new City { Id = 3, Name = "Berlin" });
return cityList;
}
private void SetupDataGridView()
{
dataGridView1.CellLeave += dataGridView1_CellLeave;
dataGridView1.EditingControlShowing += dataGridView1_EditingControlShowing;
DataGridViewTextBoxColumn colCustomer = new DataGridViewTextBoxColumn();
colCustomer.Name = "colCustomer";
colCustomer.HeaderText = "CustomerName";
DataGridViewComboBoxColumn colCountry = new DataGridViewComboBoxColumn();
colCountry.Name = "colCountry";
colCountry.HeaderText = "Country";
DataGridViewComboBoxColumn colCity = new DataGridViewComboBoxColumn();
colCity.Name = "colCity";
colCity.HeaderText = "City";
dataGridView1.Columns.Add(colCustomer);
dataGridView1.Columns.Add(colCountry);
dataGridView1.Columns.Add(colCity);
//Databind gridview columns
((DataGridViewComboBoxColumn)dataGridView1.Columns["colCountry"]).DisplayMember = "Name";
((DataGridViewComboBoxColumn)dataGridView1.Columns["colCountry"]).ValueMember = "Id";
((DataGridViewComboBoxColumn)dataGridView1.Columns["colCountry"]).DataSource = countries;
((DataGridViewComboBoxColumn)dataGridView1.Columns["colCity"]).DisplayMember = "Name";
((DataGridViewComboBoxColumn)dataGridView1.Columns["colCity"]).ValueMember = "Id";
((DataGridViewComboBoxColumn)dataGridView1.Columns["colCity"]).DataSource = cities;
foreach (Customer cust in customers)
{
dataGridView1.Rows.Add(cust.Name + " " + cust.Surname);
}
}
private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
//register a event to filter displaying value of items column.
if (dataGridView1.CurrentRow != null && dataGridView1.CurrentCell.ColumnIndex == 2)
{
cboCity = e.Control as ComboBox;
if (cboCity != null)
{
cboCity.DropDown += cboCity_DropDown;
}
}
//Register SelectedValueChanged event and reset item comboBox to default if category changes
if (dataGridView1.CurrentRow != null && dataGridView1.CurrentCell.ColumnIndex == 1)
{
cboCountry = e.Control as ComboBox;
if (cboCountry != null)
{
cboCountry.SelectedValueChanged += cboCountry_SelectedValueChanged;
}
}
}
void cboCountry_SelectedValueChanged(object sender, EventArgs e)
{
//If category value changed then reset item to default.
dataGridView1.CurrentRow.Cells[2].Value = 0;
}
void cboCity_DropDown(object sender, EventArgs e)
{
string countryName = dataGridView1.CurrentRow.Cells[1].Value.ToString();
List<City> cities = new List<City>();
cities = GetCities(countryName);
cboCity.DataSource = cities;
cboCity.DisplayMember = "Name";
cboCity.ValueMember = "Id";
}
private void dataGridView1_CellLeave(object sender, DataGridViewCellEventArgs e)
{
if (cboCity != null) cboCity.DropDown -= cboCity_DropDown;
if (cboCountry != null)
{
cboCountry.SelectedValueChanged -= cboCountry_SelectedValueChanged;
}
}
}
public class Country
{
public int Id { get; set; }
public string Name { get; set; }
}
public class City
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
}
}
I wish I could easily give you a coded solution in a few lines, but I would probably have to post an entire Visual Studio project to demonstrate in code.
The idea here is that you should never try to control this kind of scenario by acting through Controls' events. Rather, you should aim to use Windows Forms' data binding mechanism. By binding the controls to a data source that is capable of letting the UI know when its state changes, you only have to modify the underlying data, and the UI will update itself accordingly.
What you need is to setup what is usually known as a ViewModel to hold the state of the various controls involved, and whatever business logic (such as setting the list of cities based on the country) should be taken care of within this ViewModel object in reaction to setting properties on it.
I invite you to search information on data binding as well as on the various .NET interfaces that participate in it. The first one is definitely INotifyPropertyChanged, which your ViewModel will need to implement to trigger changes in the UI when its state changes.
Judicious use of the BindingSource component will also facilitate your job, for example to fill the various ComboBoxes with the desired values.
Get familiar with Windows Form's data binding, and you will have much less pain in handling such scenarios.
Like I said, I wish I could demonstrate this in just a few lines of codes, and I hope that what I wrote will point you in the right direction.
Cheers
As explained in the comments above, DataGridViewComboBox related issues might become tricky (you are basically adding a different control inside an-already-pretty-complex one); and what you aim does bring this configuration to its limits. DataGridView is a control expected to ease the management of medium-complexity, data-related issues; you can get the best performance with its most defining features (e.g., textbox-based cells, events triggered after the cell has been validated, etc.). Thus, including comboboxes (or checkboxes or equivalent) cells is OK as far as you don't bring its performance to the limits. To get the best result possible for what you want (coordinating different comboboxes), I suggest you to not rely on a DataGridView control (or, at least, not for the combobox coordination part) as far as the implemention is problematic, the final result not as reliable as it can get and, in any case, the overall structure much more rigid than the one resulting from a DGV-independent approach (i.e., individual ComboBox controls).
In any case, I have felt curious about this implementation (mainly after seeing quite a few problems in my preliminary tests) and decided to write this code to answer your concern.
private void Form1_Load(object sender, EventArgs e)
{
dataGridView1.EditingControlShowing +=new DataGridViewEditingControlShowingEventHandler(dataGridView1_EditingControlShowing);
DataGridViewComboBoxColumn curCol1 = new DataGridViewComboBoxColumn();
List<string> source1 = new List<string>() { "val1", "val2", "val3" };
curCol1.DataSource = source1;
DataGridViewComboBoxColumn curCol2 = new DataGridViewComboBoxColumn();
dataGridView1.Columns.Add(curCol1);
dataGridView1.Columns.Add(curCol2);
for (int i = 0; i <= 5; i++)
{
dataGridView1.Rows.Add();
dataGridView1[0, i].Value = source1[0];
changeSourceCol2((string)dataGridView1[0, i].Value, (DataGridViewComboBoxCell)dataGridView1[1, i]);
}
}
private void changeSourceCol2(string col1Val, DataGridViewComboBoxCell cellToChange)
{
if (col1Val != null)
{
List<string> source2 = new List<string>() { col1Val + "1", col1Val + "2", col1Val + "3" };
cellToChange.DataSource = source2;
cellToChange.Value = source2[0];
}
}
private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
if (dataGridView1.CurrentRow != null)
{
ComboBox col1Combo = e.Control as ComboBox;
if (col1Combo != null)
{
if (dataGridView1.CurrentCell.ColumnIndex == 0)
{
col1Combo.SelectedIndexChanged += col1Combo_SelectedIndexChanged;
}
}
}
}
private void col1Combo_SelectedIndexChanged(object sender, EventArgs e)
{
if (dataGridView1.CurrentCell.ColumnIndex == 0)
{
dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit);
changeSourceCol2(dataGridView1.CurrentCell.Value.ToString(), (DataGridViewComboBoxCell)dataGridView1[1, dataGridView1.CurrentCell.RowIndex]);
}
}
This code works fine with one limitation: when you change the index of the first combobox, the value is not immediately committed (and thus the second combobox cannot be updated). After doing some tests, I have confirmed that the proposed configuration (i.e., just writing dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit); before populating the second-combobox source delivers the best performance). Despite that, note that this code does not work perfectly on this front: it starts working (updating automatically the second combobox every time a new item is selected in the first one) from the second selection onwards; not sure about the exact reason of this but, as said, any other alternative I tried delivers a still worse performance. I haven't worked too much on this front because of my aforementioned comments (actually, doing this is not even recommendable) and because of feeling that you have to do part of the work.....

CheckedListBox with search function not checking correctly

I'm having strange issues with the check box control in C# .Net
My code below shows all logic that is required - _itemsChecked is a private dictionary containing all of the _fixtures and whether they are true or false (checked or un checked)
What I want is to be able to search my check list whilst retaining those which have been checked previously. If a checked item is included in the search results I want it to be checked.
The code nearly works! But for some reason boxes are randomly checked here and there, and it appears to work through debug but when the screen returns to the control it then hasn't worked.
Sure I'm missing something very simple.
My logic is:
DataSource includes those which match the typed search query,
Iterate through this list and check if the Guid is true in the dictionary.
If it is true then we set it as checked.
Hope I have provided adequate information.
Many thanks in advance.
private void searchTextBox_KeyUp(object sender, EventArgs e)
{
lst.DataSource = _fixtures
.OrderBy(f =>
f.Description)
.Where(f =>
f.Description.ToLower().Contains(searchFixturesTextBox.Text.ToLower()))
.ToList();
lst.DisplayMember = "Description";
for (var i = 0; i < lst.Items.Count; i++)
if(_itemsChecked.Contains(new KeyValuePair<Guid, bool>(((Fixture)lst.Items[i]).Guid, true)))
lst.SetItemChecked(i, true);
}
void lst_ItemCheck(object sender, ItemCheckEventArgs e)
{
var selectedItem = ((ListBox) sender).SelectedItem as Fixture;
if (selectedFixtureItem != null)
_itemsChecked[selectedItem.Guid] = e.CurrentValue == CheckState.Unchecked;
}
So I put this together from a few examples I found. The majority of the work came from How do I make a ListBox refresh its item text?
public class Employee
{
public string Name { get; set; }
public int Id { get; set; }
public bool IsChecked { get; set; }
public override string ToString()
{
return Name;
}
}
public partial class Form1 : Form
{
// Keep a bindable list of employees
private BindingList<Employee> _employees;
public Form1()
{
InitializeComponent();
// Load some fake employees on load
this.Load += new EventHandler(Form1_Load);
// Click once to trigger checkbox changes
checkedListBox1.CheckOnClick = true;
// Look for item check change events (to update there check property)
checkedListBox1.ItemCheck +=
new ItemCheckEventHandler(CheckedListBox_ItemCheck);
}
// Load some fake data
private void Form1_Load(object sender, EventArgs e)
{
_employees = new BindingList<Employee>();
for (int i = 0; i < 10; i++)
{
_employees.Add(new Employee()
{ Id = i, Name = "Employee " + i.ToString() });
}
// Display member doesnt seem to work, so using ToString override instead
//checkedListBox1.DisplayMember = "Name";
//checkedListBox1.ValueMember = "Name";
checkedListBox1.DataSource = _employees;
// Another example databind to show selection changes
txtId.DataBindings.Add("Text", _employees, "Id");
txtName.DataBindings.Add("Text", _employees, "Name");
}
// Item check changed, update the Employee IsChecked property
private void CheckedListBox_ItemCheck(object sender, ItemCheckEventArgs e)
{
CheckedListBox clb = sender as CheckedListBox;
if (clb != null)
{
Employee checked_employee = clb.Items[e.Index] as Employee;
if (checked_employee != null)
{
checked_employee.IsChecked = (e.NewValue == CheckState.Checked);
}
}
}
// Just a simple test that removes an item from the list, rebinds it
// and updates the selected values
private void btnChangeList_Click(object sender, EventArgs e)
{
_employees.RemoveAt(1);
checkedListBox1.DataSource = _employees;
for (var i = 0; i < checkedListBox1.Items.Count; i++)
{
Employee employee_to_check = checkedListBox1.Items[i] as Employee;
if (employee_to_check != null)
{
checkedListBox1.SetItemChecked(i, employee_to_check.IsChecked);
}
}
}
}

Categories