In my WPF application, I am developing a fairly straightforward page that allows either creating a new object or choosing one from a combo box, then editing the object.
One of the parts of the object that is editable is a related database table in a one-to-many relationship, so for that piece I used a DataGrid. The DataGrid itself has a data-bound ComboBox column, as you can see here:
<DataGrid AutoGenerateColumns="False" EnableRowVirtualization="True"
CanUserAddRows="False" CanUserDeleteRows="True"
ItemsSource="{Binding Path=No.Lower_Assy}"
DataGridCell.Selected="dgAssy_GotFocus">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Number & Type">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path=DataContext.ComboSource, RelativeSource={RelativeSource AncestorType=Page}}"
SelectedValuePath="bwk_No"
SelectedValue="{Binding Path=fwf_Higher_N, ValidatesOnDataErrors=True, ValidatesOnExceptions=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Number}"/>
<TextBlock Text="{Binding Path=Type}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- other text columns omitted -->
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Delete" Click="btnDeleteHigherAssy_Click" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Code behind:
private void dgAssy_GotFocus(object sender, RoutedEventArgs e)
{
if (e.OriginalSource.GetType() == typeof(DataGridCell))
{
// Starts the edit on the row
DataGrid grd = (DataGrid)sender;
grd.BeginEdit(e);
}
}
And for the save button:
private void btnSave_Click(object sender, RoutedEventArgs e)
{
if (CanUserEdit())
{
if (string.IsNullOrWhiteSpace(model.Data.Error))
{
repo.Save(model.Data);
StatusText = STATUS_SAVED;
model.CanSave = false;
// This is the data source for the main combo box on the page
model.ComboSource = repo.GetData();
// Set the combo box's selected item, in case this is a new object.
// cboNo is the main combo box on the page which allows selecting
// an object to edit
// Apparently setting SelectedItem directly doesn't work on a databound combo box
int index = model.ComboSource.ToList().FindIndex(x => x.bwk_No == model.Data.bwk_No);
cboNo.SelectedIndex = index;
}
else
{
MessageBox.Show("Invalid data:\n" + model.Data.Error, "Cannot save");
}
}
}
The problem
When I choose an item from the combo box in the data grid, it seems to work until I click on the save button. Then two things happen:
- The combo box’s selected item is set to
null, blanking out the combo box. - As a result of (1), the save button is re-enabled because the data has changed. (The save button is bound to
model.CanSave, which as you can see is set to false in the button handler; it is set to true by a property change event handler if there are no data errors.)
Why is it being reset? I’ve followed the code flow closely and can see the property change event for the combo box’s backing field (fwf_Higher_N) being handled, and it appears to somehow come from the line model.ComboSource = repo.GetData();, but the stack only shows [external code] and I don’t see why that line would modify an existing object.
The model class
// Names have been changed to protect the innocent
private class MyDataViewModel : INotifyPropertyChanged
{
private DbData _Data;
public DbData Data
{
get { return _Data; }
set
{
_Data = value;
OnPropertyChanged("Data");
}
}
private IQueryable<MyComboModel> _ComboSource;
public IQueryable<MyComboModel> ComboSource {
get { return _ComboSource; }
set
{
_ComboSource = value;
OnPropertyChanged("ComboSource");
}
}
private bool _CanSave;
public bool CanSave
{
get { return _CanSave; }
set
{
_CanSave = value;
OnPropertyChanged("CanSave");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
Your description of what is going on and your markup doesn’t quite match. I’m going to make some assumptions, such as that
Page.DataContextis an instance ofMyDataViewModel.I’m sorry to say it, but a SSCCE would do wonders here. I strongly suggest when anyone gets into situations where they are elbow deep in code they don’t quite understand that they break out what they are attempting to do and create a minimal prototype that either exhibits the same behavior, or that helps you learn what’s going wrong. I’ve made 500+ prototypes in the past five years.
As for this situation, you refer to a
ComboBoxnamed cboNo inbtnSave_Click, but I don’t see that in the xaml. This ComboBox’s ItemSource appears to be bound toMyDataViewModel.ComboSource.In addition, all ComboBoxes in the DataGrid also appear to be bound to the model’s ComboSource. And, in the button handler event, you change what is in the property:
This fires
PropertyChanged, and every ComboBox bound to this property will be updated. That means not onlycboNobut also every ComboBox in the DataGrid.It is expected behavior that, when
ComboBox.ItemsSourcechanges, ifComboBox.SelectedItemis not contained within the items source, thatSelectedItemis nulled out.I just spun up a prototype (501+) and it appears that if the
IEnumerablethat the ComboBox is bound to changes, but the elements in the IEnumerable do not, thenSelectedItemis not nulled out.So, within the
btnSave_Clickevent handler, you change this ItemsSource, which probably does not have the same instances that are already in the combo, thus nulling out SelectedItem for all ComboBoxes bound to this property, and then only updatecboNo‘s SelectedIndex.Now, as for what to do about it…
Well, not sure. From the rest of your code, it appears you need to do some more codebehind work to make sure only the necessary ComboBoxes have their sources updated…