SelectedIndexChanged Not Firing in WinForms for Controls Generated at Runtime - c#

My forehead is bruised from banging my head against my desk on this one.
I have a Form that generates a bunch of custom UserControls (FieldMapper objects) based on parameters passed to it. The FieldMapper UserControl is essentially just a few labels, a BindingSource, and a ComboBox (bound to the BindingSource) organized on a TableLayoutPanel. The ComboBox on this UserControl is set to DropDownList and is bound to the BindingSource, which gets populated with a IList passed as an argument at construction time.
The FieldMapper has an event called MappingChanged, which gets fired whenever the underlying SelectedIndexChanged event is fired and some logic determines the arguments for the MappingChangedEventArgs that are fired with the event. This is used to remove/add items from other comboboxes so nothing can be selected twice in the series of FieldMapping controls
The problem is that ONLY ONE control fires the underlying SelectedIndexChanged event. This control is whatever control I FIRST change it's ComboBox's selected value/index. It doesn't matter which one I select, but only that control will fire subsequent events, although other control's combobox's were changed.
Here is the constructor for the FieldMapper (removed parameters not concerning to the issue)
public FieldMapper(IList<string> fields)
{
InitializeComponent();
fields = fields.OrderBy(s => s).ToList();
string[] copyBuffer = new string[fields.Count + 1];
fields.CopyTo(copyBuffer, 1);
copyBuffer[0] = "NONE";
AvailableMappings = new BindingList<string>(copyBuffer.ToList());
AvailableMappings.RaiseListChangedEvents = true;
bindingSource = new BindingSource();
mapSelector.DataSource = bindingSource;
bindingSource.DataSource = AvailableMappings;
mapSelector.SelectedItem = "NONE";
mapSelector.SelectedIndexChanged += mapSelector_SelectedIndexChanged;
}
The parent Form of these FieldMapper controls generates them as such:
private void BuildMappingTable(IEnumerable<string> fieldNames)
{
int row = 0;
foreach (var field in fbProject.Fields)
{
FieldMapper mapper = new FieldMapper(field.Key, field.Value.FieldName, fieldNames.ToList());
mapper.EnableMappingField = false;
mapper.Anchor = AnchorStyles.Right | AnchorStyles.Left;
mapper.MappingChanged += MappingChanged;
mappingTable.Controls.Add(mapper, 0, row);
row++;
}
if (!built && !original)
SetInitialValues()
built = true;
}
When I check the breakpoints when debugging this issue the event handlers are always populated for both the FieldMapper and the FieldMapper's comobobox's SelectedIndexChanged event, so I'm not sure why they aren't firing.
EDIT: As requested here is the mapSelector_SelectedIndexChanged method
private void mapSelector_SelectedIndexChanged(object sender, EventArgs e)
{
if (currentValue == null || currentValue.Equals("NONE"))
{
currentValue = (string)mapSelector.SelectedItem;
OnMappingChanged(new MappingSelectedEventArgs(MappingSelectedEventArgs.MappingSelectedAction.SET, currentValue));
}
else if (mapSelector.SelectedItem.Equals("NONE"))
{
OnMappingChanged(new MappingSelectedEventArgs(MappingSelectedEventArgs.MappingSelectedAction.REMOVED, currentValue));
currentValue = "NONE";
}
else if (!mapSelector.SelectedItem.Equals(currentValue))
{
OnMappingChanged(new MappingSelectedEventArgs(MappingSelectedEventArgs.MappingSelectedAction.CHANGED, currentValue,
(string)mapSelector.SelectedItem));
currentValue = (string)mapSelector.SelectedItem;
}
}
protected void OnMappingChanged(MappingSelectedEventArgs args)
{
if (MappingChanged != null)
MappingChanged(this, args);
}

Replace:
private void mapSelector_SelectedIndexChanged(object sender, EventArgs e)
by:
private void bindingSource_CurrentChanged(object sender, EventArgs e)

Related

C# Fail to set DatagridComboboxCell value to null

I am trying to make a datagridview in which there exists a combobox column, and this column is binded a list of options. My logic is that, if the user selects the wrong option, the program could detect and deselect this wrong option(which means comboboxCell.value = null).
The psuedo code for this logic is as follows:
//use CurrentCellDirtyStateChanged and CellValueChanged as event listeners
this.DGV.CurrentCellDirtyStateChanged += DGV_CurrentCellDirtyStateChanged;
this.DGV.CellValueChanged += DGV_CellValueChanged;
//CurrentCellDirtyStateChanged event
private void DGV_CurrentCellDirtyStateChanged(object sender, EventArgs e)
{
if (DGV.IsCurrentCellDirty)
{
DGV.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
}
//CellValueChanged event
private void DGV_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
if (!CheckingCorrect(DGV.CurrentCell))
{
// set selected value null
DGV.CurrentCell.Value = null
}
}
However, after the code sets currentcell value to null, the datagridview cell still kept the selected value shown.
I also tried may other ways like DGV.ClearSelection(), but none of these works.
There are two things that will make working with the DataGridViewComboBoxColumn/Cell easier...
Make a Class specifically for the combo box column
Use the proper event to help in casting the DataGridViewComboBoxCell to a
regular ComboBox.
In this example I made a simple Class called ComboValues that has two properties for the column ValueMember and DisplayMember. In this example the ValueMember is an int, however you could leave it out or use some other type that fits your needs. This class will simplify things as we will have the properties for both the ValueMember and DisplayMember for the combo box column.
This ComboValues Class may look something like…
public class ComboValues {
public int Value { get; set; }
public string Display { get; set; }
}
Then a method to get a List<ComboValues> from the DB that we will use as a DataSource for the combo box column.
private List<ComboValues> GetComboValues() {
List<ComboValues> items = new List<ComboValues>();
items.Add(new ComboValues { Value = 0, Display = null });
items.Add(new ComboValues { Value = 1, Display = "Option 1" });
items.Add(new ComboValues { Value = 2, Display = "Option 2" });
items.Add(new ComboValues { Value = 3, Display = "Option 3" });
items.Add(new ComboValues { Value = 4, Display = "Option 4" });
return items;
}
And setting up the combo box column for the grid…
private DataGridViewComboBoxColumn GetComboCol() {
DataGridViewComboBoxColumn col = new DataGridViewComboBoxColumn();
col.HeaderText = "Combo Items";
col.ValueMember = "Value";
col.DisplayMember = "Display";
col.DataSource = GetComboValues();
return col;
}
We will call the above method in the forms Load event to add the column to the Grid…
private void Form1_Load(object sender, EventArgs e) {
DGV.Columns.Add(GetComboCol());
}
Next, we want to set up the edited DataGridViewComboBoxCell to act like a REGULAR ComboBox. We can do this using the grids EditingControlShowing event. This event will fire when the user clicks into a cell and in this case if the user clicks into a combo box cell, then we want to cast that cell to a globally defined ComboBox. So, we will need a global ComboBox variable called something like… EditedComboBox.
ComboBox EditedComboBox = null;
Is what we will do is that when the grids EditingControlShowing event fires and we check to see if it is the combo box cell, then we simply cast that cell to our global EditedComboBox and wire up its SelectedIndexChanged event. Then we can capture “when” the user “changes” a value in the combo box cell. The EditingControlShowing event may look something like…
private void DGV_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e) {
if (DGV.CurrentCell.ColumnIndex == 0) {
EditedComboBox = (ComboBox)e.Control;
if (EditedComboBox != null) {
EditedComboBox.SelectedIndexChanged += new EventHandler(ComboBox_SelectedIndexChanged);
}
}
}
Above, column index 0 is the target combo box column. All we need to do is implement and wire up the SelectedIndexChanged event and wait for the user to change the combo box value.
When the user does indeed change the combo boxes value, then our SelectedIndexChanged event will fire and it is in this event where you would “CHECK” if the selected value needs adjustments on some parameters. In this example, the condition is if the user selects “Option 3”, then that is an invalid value and we will set the cells value to null. This event may look something like…
private void ComboBox_SelectedIndexChanged(object sender, EventArgs e) {
//Debug.WriteLine("CB_SelectedIndexChanged");
if (EditedComboBox != null) {
ComboValues cv = (ComboValues)EditedComboBox.SelectedItem;
if (cv != null && !CheckingCorrect(cv.Display)) {
EditedComboBox.SelectedIndexChanged -= new EventHandler(ComboBox_SelectedIndexChanged);
EditedComboBox.SelectedIndex = 0;
EditedComboBox.SelectedIndexChanged += new EventHandler(ComboBox_SelectedIndexChanged);//DGV.CurrentCell.Value = 0;
}
}
}
In this event, we grab (cast) the combo boxes SelectedItem to a ComboValues object. With it we can easily check what value is selected and in turn set its value to null if needed. Note “item” 0 in our List<ComboValues> is the null value, so we set the combo boxes SelectedIndex to zero (0) to make the grid display a null value.
Also, since we are “changing” the combo boxes Index, we do NOT want this event to re-fire.
Lastly, we need one more event to wire up to TURN OFF the SelectedIndex changed event to prevent it from firing more than we want. In this case we will wire up the grids CellLeave event. In that event, the user is leaving the cell and if the EditedComboBox is not null... then we know we need to un-wire the event. It may look something like…
private void DGV_CellLeave(object sender, DataGridViewCellEventArgs e) {
if (EditedComboBox != null) {
EditedComboBox.SelectedIndexChanged -= new EventHandler(ComboBox_SelectedIndexChanged);
EditedComboBox = null;
}
}
I altered the checking code to check for a string and not necessarily a cell. The condition checker code…
private bool CheckingCorrect(string cellValue) {
if (cellValue != null && cellValue == "Option 3") {
return false;
}
else {
return true;
}
}
I hope this helps and makes sense. If you have questions, then feel free to ask.

How to change RepositoryItem without using CustomRowCellEdit?

I would like to change RepositoryItem in diffrent way than shown in the code bellow. Motivation to do this is described in obsolete field CustomRowCellEditEventArgs.RowHandle.
private void GridView_CustomRowCellEdit(object sender, CustomRowCellEditEventArgs e)
{
GridView view = sender as GridView;
if (e.Column.FieldName == CONSTS_FIELD_NAME)
{
var val = (VAL_TYPE) view.GetRowCellValue(e.RowHandle, CONSTS_FIELD_NAME);
if (val == VAL_VALUE)
e.RepositoryItem = new RepositoryItem(); // setting new Repository Item
}
}
So I decided to use this code:
private void GridView_CustomRowCellEdit(object sender, CustomRowCellEditEventArgs e)
{
GridView view = sender as GridView;
if (e.Column.FieldName == CONSTS_FIELD_NAME)
{
var result = view.GetSelectedRows();
var val = (VAL_TYPE) view.GetRowCellValue(result.First(), CONSTS_FIELD_NAME);
if (val == VAL_VALUE)
e.RepositoryItem = new RepositoryItem(); // setting new Repository Item
}
}
Is there any other way to change RepositoryItem using some events?
The situation with CustomColumnDataEventArgs you mentioned(providing unbound data for the specific column) have no relation with GridView's editing process. When the CustomRowCellEdit event is fired all the row handles are already calculated. Thus there is no motivation to avoid the first approach.
The only recommendation I can suggest is to use the predefined repository item instead of creating a new one every time:
void GridView_CustomRowCellEdit(object sender, CustomRowCellEditEventArgs e) {
GridView view = sender as GridView;
if(e.Column.FieldName == CONSTS_FIELD_NAME) {
var val = (VAL_TYPE)view.GetRowCellValue(e.RowHandle, CONSTS_VAL);
if(val == VAL_VALUE)
e.RepositoryItem = repositoryItemForVAL_VALUE;
}
}
To use a specific editor for inplace-editing only, you should handle the CustomRowCellEditForEditing event.
Please also carefully read the Remarks section of the GridView.CustomRowCellEdit event documentation which clearly describes how this event works.

ListBox deletion

I have a listbox, with 2 buttons, new and delete. new adds an item into the list box, and the delete button should delete the item out of the list box. The list box items are tied to a class that stores user entered data from text boxes below.
private void AddListBox()
{
lstCondition.BeginUpdate();
Condition cond = new Condition("");
cond.Name = string.Format("Condition {0}", _selection.NetConditions.Count + 1);
_selection.NetConditions.Add(cond);
lstCondition.EndUpdate();
lstCondition.SelectedItem = cond;
cboNetCondition.Properties.Items.Clear();
cboNetCondition.Properties.Items.AddRange(NetCondition);
cboControlType.Properties.Items.Clear();
cboControlType.Properties.Items.AddRange(ControlType);
cboFlowRate.Properties.Items.Clear();
cboFlowRate.Properties.Items.AddRange(FlowRate);
}
private void btnNew_Click(object sender, EventArgs e)
{
AddListBox();
}
the cbo items are comboboxes, whose data gets tied in the condition class to each instance of the list box.
public frmNetConditions(Condition condo, Selection selection)
{
InitializeComponent();
_selection = selection;
lstCondition.DataSource = _selection.NetConditions;
condition = _selection.NetConditions.Count;
}
private void btnDelete_Click(object sender, EventArgs e)
{
selectedCondition = (Condition)lstCondition.SelectedItem;
cboControlType.SelectedIndex = -1;
cboNetCondition.SelectedIndex = -1;
cboFlowRate.SelectedIndex = -1;
txtFlowRate.Text = string.Empty;
txtStatPressure.Text = string.Empty;
txtDampOpening.Text = string.Empty;
txtDensity.Text = string.Empty;
cboDensity.SelectedIndex = -1;
lstCondition.Items.Remove(lstCondition.SelectedItem);
lstCondition.Refresh();
}
After pressing this delete button, the listbox, still contains the item i wish to delete, im unsure why thats the case?
Update with datasource
public List<Condition> NetConditions { get { return _netconditions; } }
As already suggested, you should bind to a BindingList<Condition> instead of a List<Condition>. This allows you to change the datasource and the control (ListBox) to get notified by your changes. The code should look like this:
lstCondition.ValueMember = "ConditionId";
lstCondition.DisplayMember = "Name";
lstCondition.DataSource = NetConditions;
After defining the binding, the correct way of operating on the ListBox items is to remove from the datasource, not the ListBox itself:
// SelectedItem should be checked for null (no selection is an option)
NetCondition.Remove((Condition)lstCondition.SelectedItem);
However, if you plan to change properties from an element (so, not the list itself), the control is notified only if your element (Condition) implements INotifyPropertyChanged interface.

Update DataGrid's ItemsSource on SelectionChanged event of a ComboBox

I subscribed to a SelectionChangedEvent on a ComboBox in a DataGrid with the following code:
public static DataGridTemplateColumn CreateComboboxColumn(string colName, Binding textBinding, SelectionChangedEventHandler selChangedHandler = null)
{
var cboColumn = new DataGridTemplateColumn {Header = colName};
...
if (selChangedHandler != null)
cboFactory.AddHandler(Selector.SelectionChangedEvent, selChangedHandler);
...
return cboColumn;
}
The handler I actually register contains:
private void ComboBoxSelectionChangedHandler(object sender, SelectionChangedEventArgs e)
{
Console.WriteLine(#"selectHandler");
var cboBox = sender as ComboBox;
if (cboBox == null)
return;
if (cboBox.IsDropDownOpen) // a selection in combobox was made
{
CommitEdit();
}
else // trigger the combobox to show its list
cboBox.IsDropDownOpen = true;
}
... and is located in my custom DataGrid class.
If I select an item in the ComboBox, e.AddedItems and cboBox.SelectedItem contains the selected value, but nothing is changed on CommitEdit().
What I want is to force a commit to directly update the DataGrid's ItemsSource, when the user selects an item in the drop-down-list. Normally this is raised if the control looses focus...
The link in the solution found in this thread is not available any more and I don't know how to use this code.
I created a tricky, but working, solution for my problem. Here's the modified handler from above:
private void ComboBoxSelectionChangedHandler(object sender, SelectionChangedEventArgs e)
{
Console.WriteLine(#"selectHandler");
var cboBox = sender as ComboBox;
if (cboBox == null)
return;
if (cboBox.IsDropDownOpen) // a selection in combobox was made
{
cboBox.Text = cboBox.SelectedValue as string;
cboBox.MoveFocus(new TraversalRequest(FocusNavigationDirection.Right));
}
else // user wants to open the combobox
cboBox.IsDropDownOpen = true;
}
Because my ComboBoxColumn is a custom DataGridTemplateColumn I force it to show its list, when the user first selects the cell.
To change the bound items value I manually overwrite the displayed text with recently selected item and force the UI to select another item (in this case the control to the right) to make an implicit call to CellEditEnding event, which (in my case) commits the whole row:
private bool _isManualEditCommit = false;
private void _CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
// commit a manual edit
// this if-clause prevents double execution of the EditEnding event
if (!_isManualEditCommit)
{
Console.WriteLine(#"_CellEditEnding() manualeditcommit");
_isManualEditCommit = true;
CommitEdit(DataGridEditingUnit.Row, true);
_isManualEditCommit = false;
checkRow(e.Row);
}
}
Maybe I could help somebody with this "dirty" solution ;-)

C# WinForm Datagrid doubleclick event

Is there a doubleclick event for a datagrid? I'm trying to use this code to open a details form when the user doubleclicks on a row.
http://www.codeproject.com/KB/grid/usingdatagrid.aspx
I tried adding it by doubleclicking on the control, but it gives dataGrid1_Navigate instead.
What you get when you double click a control in design mode is the event the designers of the control thought would be the most used, in this case it's Navigate.
But yes, this control has two double click events:
public partial class Form1 : Form
{
DataGrid grid = new DataGrid();
public Form1()
{
InitializeComponent();
grid.DoubleClick += new EventHandler(grid_DoubleClick);
grid.MouseDoubleClick += new MouseEventHandler(grid_MouseDoubleClick);
grid.Dock = DockStyle.Fill;
this.Controls.Add(grid);
}
void grid_MouseDoubleClick(object sender, MouseEventArgs e)
{
}
void grid_DoubleClick(object sender, EventArgs e)
{
}
}
However, both of these events run when you double click anywhere on the control and they don't directly give you information on what row was selected. You might be able to retrieve the row double clicked in the grid_MouseDoubleClick handler by getting it from the control based on the point being clicked (e.Location), that's how it works in the TreeView control for example. At a quick glance I didn't see if the control has such a method. You might want to consider using DataGridView instead, if you don't have a particular reason to use this control.
Sounds like you need a way to get a list of all the events for a given control, rather than finding the default event (which is what VS gives you when you double click a control in the designer)
There are a few ways of doing this:
One way Select the grid.
Then click the events icon to turn the properties window into a list of events, then doubel click the event you want to strart coding the event.
Alternatively, switch to code view, select the grid in the drop down list of objects at the top left of the code window, then select the event you want from the list of all the events for that control in the event list (top right of the code window)
I tried #steve76's code, but had to tweak it slightly to work in a Windows Embedded CE 6.0 system. Here is what worked for me.
private void dataGrid1_DoubleClick(object sender, EventArgs e)
{
Point pt = dataGrid1.PointToClient(Control.MousePosition);
DataGrid.HitTestInfo info = dataGrid1.HitTest(pt.X, pt.Y);
int row;
int col;
if (info.Column < 0)
col = 0;
else
col = info.Column;
if (info.Row < 0)
row = 0;
else
row = info.Row;
object cellData = dataGrid1[row, col];
string cellString = "(null)";
if (cellData != null)
cellString = cellData.ToString();
MessageBox.Show(cellString, "Cell Contents");
}
Perhaps you can use the DataGridView.CellContentDoubleClick event.
Example:
private void DataGridView1_CellContentDoubleClick(Object sender, DataGridViewCellEventArgs e) {
System.Text.StringBuilder messageBoxCS = new System.Text.StringBuilder();
messageBoxCS.AppendFormat("{0} = {1}", "ColumnIndex", e.ColumnIndex );
messageBoxCS.AppendLine();
messageBoxCS.AppendFormat("{0} = {1}", "RowIndex", e.RowIndex );
messageBoxCS.AppendLine();
MessageBox.Show(messageBoxCS.ToString(), "CellContentDoubleClick Event" );
}
If that is not what you are looking for, you can find other events in the reference:
http://msdn.microsoft.com/en-us/library/system.windows.forms.datagridview_events.aspx

Categories