Why does the ComboBox in my DataGrid column reset itself to null? - wpf

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.DataContext is an instance of MyDataViewModel.
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 ComboBox named cboNo in btnSave_Click, but I don't see that in the xaml. This ComboBox's ItemSource appears to be bound to MyDataViewModel.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 is the data source for the main combo box on the page
model.ComboSource = repo.GetData();
This fires PropertyChanged, and every ComboBox bound to this property will be updated. That means not only cboNo but also every ComboBox in the DataGrid.
It is expected behavior that, when ComboBox.ItemsSource changes, if ComboBox.SelectedItem is not contained within the items source, that SelectedItem is nulled out.
I just spun up a prototype (501+) and it appears that if the IEnumerable that the ComboBox is bound to changes, but the elements in the IEnumerable do not, then SelectedItem is not nulled out.
var temp = combo.ItemsSource.OfType<object>().ToArray();
combo.ItemsSource = temp;
So, within the btnSave_Click event 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 update cboNo'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...

Related

Need help: DataGridCheckBoxColumn two-way works only one-way

I (..still learning wpf..) made a DataGrid with the first column showing that a signal is enabled or not.
In the xaml:
<DataGrid.Columns> ...
<DataGridCheckBoxColumn Width ="30" Header="" IsReadOnly="False" Binding="{Binding IsEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataGrid.Columns>
The ItemSouce of DataGrid is correctly set and bound with the data in list of ObservableCollection<Signal> signalList. Everything is correctly shown in DataGrid. So the binding from signalList to DataGrid here is working fine.
On the other side, I want that with every change of signalList, the DataGrid can update itself automatically. However, if I do
signalList[0].IsEnabled = true;
The DataGrid doesn't get updated. I searched a lot but still can't find the answer.
Did I miss something? Thx.
Edit1:
DataGrid do get updated, only if I click another row, and then draw the scroller out of sight. Then if I draw the scroller back, the row is correctly shown. I think I definitively missed something, can someone give me a hint?
I solved the problem with help from Aybe and gavin. For record, I add my code here:
class Signal : INotifyPropertyChanged
{
...
private bool isEnabled;
public bool IsEnabled
{
get { return isEnabled; }
set { isEnabled = value; OnPropertyChanged(new PropertyChangedEventArgs("IsEnabled"));}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
}

Populate a second DataGrid once I double click on a row WPF

I am still a little new to wpf and MVVM. I am trying to code a solution without breaking that pattern. I have two (well three, but for the scope of this question just two) DataGrids. I want to double click on the row of one, and from that load data Into the second DataGrid (ideally I would spin up a second thread that would load the data). So far I can get a window to pop up when I double click on a row. I throw the code for the event into the code behind for the xaml. To me that seems very windows formish. Somehow or the other I feel like that breaks the pattern a great deal.
private void DataGrid_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) {
if (popDataGrid.SelectedItem == null) {
return;
}
var selectedPopulation = popDataGrid.SelectedItem as PopulationModel;
MessageBox.Show(string.Format("The Population you double clicked on has this ID - {0}, Name - {1}, and Description {2}", selectedPopulation.populationID, selectedPopulation.PopName, selectedPopulation.description));
}
That is the code for the event in the code behind and here is the grids definition in the xaml:
<DataGrid ItemsSource="{Binding PopulationCollection}" Name="popDataGrid"
AutoGenerateColumns="False" RowDetailsVisibilityMode="VisibleWhenSelected"
CanUserAddRows="False" Margin="296,120,0,587" HorizontalAlignment="Left" Width="503" Grid.Column="1"
MouseDoubleClick="DataGrid_MouseDoubleClick">
</DataGrid>
I am thinking this code should go in the MainWindowViewModel. So I am attempting to create a command:
public ICommand DoubleClickPopRow { get { return new DelegateCommand(OnDoubleClickPopRow); }}
and the same event handler:
private void OnDoubleClickPopRow(object sender, MouseButtonEventArgs e) {
}
But the ICommand is throwing an exception when it returns the DelegateCommand(OnDoubleClickPopRow).
Well, one can plainly see that the number of arguments doesn't match. I know I am doing something wrong, but I am not quite sure what it is. I will continue to research this but any help you guys can give would be greatly appreciated.
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
<DataGrid ItemsSource="{Binding PopulationCollection}" Name="popDataGrid"
AutoGenerateColumns="False" RowDetailsVisibilityMode="VisibleWhenSelected"
CanUserAddRows="False" Margin="296,120,0,587" HorizontalAlignment="Left" Width="503" Grid.Column="1" SelectedItem="{Binding ItemInViewModel}"></DataGrid>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding Save_Bid}" />
</i:EventTrigger>
</i:Interaction.Triggers>
You can add this to your DataGrid and add your code in your viewmodel.
Now that we have a selected item bound to an item in our view model we can use that item to know when we can fire the even we want as well as what item to use when the event is fired When the event can be fired
bool Can_Fire_Event()
{
if(ItemInViewModel != null)
{ return true; } else { return false; }
}
private RelayCommand _saveBid;
public ICommand SaveBid
{
get
{
if (_saveBid == null)
{
_saveBid = new RelayCommand(param => Save_Bid(), param => Can_Fire_Event());
}
return _saveBid;
}
}
public void Save_Bid()
{
//Open your new Window here, using your "ItemInViewModel" because this event couldn't be fired from your datagrid unless the "ItemInViewModel" had a value assigned to it
}

Passing the value of an item selected in a Gridview to the ViewModel of a different Usercontrol

After researching a solution to this for three days, finally I gave up. Now I need your help to solve this.
Scenario:
I've a GridView in a Usercontrol (Lets say WLMSLogs.xaml) and My GridView ItemSource is binded to a List from the ViewModel (WMLSLogsViewModel.cs)
Lets say the List has 4 items (EventID, Name, Request and Response). BothRequest and Responses are XML Strings.
GridView needs to display some of the List items in RowDetailsTemplate under different tab items. So I'm displaying Request and Response under respective TabItems.
<GridView x:Name="WMLSLogGrid" ItemsSource="{Binding WMLSLogModelList}">
<GridView.Columns>
<GridViewDataColumn DataMemberBinding="{Binding ID}" Header="ID"/>
<GridViewDataColumn DataMemberBinding="{Binding UserName}" Header="UserName"/>
</GridView.Columns>
<GridView.RowDetailsTemplate>
<DataTemplate >
<TabControl>
<TabItem Header="Request Xml">
<TextBlock text="{Binding Request}"/>
</TabItem>
<TabItem Header="Response Xml">
<TextBlock text="{Binding Response}"/>
</TabItem>
<TabItem Header="EventLogs" Tag="{Binding .}">
<views:LogEvents />
</TabItem>
</TabControl>
</Grid>
</DataTemplate>
</GridView.RowDetailsTemplate>
WMLSLogModelList is a Observable collection in WMLSLogsViewModel.cs
So far everything works fine and Grid is displaying data as expected.
Now When User expands any row, he can see two TabItems with request and response.
Here I need to add one more TabItem (LogEvents) besides Request and Response tabs.
This LogEvents tab is going have one more GridView to display (so I added a new View <views:LogEvents /> in the tab). Here comes the tricky part.
This LogEvents GridView needs to get the data based on the corresponding Selecteditem (which is EventId), and pass this EventId to a different ViewModel (LogEventViewModel.cs) and binds the data to the Inner GridView dynamically. All this has to happen either as I expand the RowDetails section or if I select the Tab LogEvents.
There is no relation between the data items of these two Grids, except getting the Selected EventId of the main GridView and passing this to a different ViewModel then to Domain service to get the inner GridView Data.
What I did so far
As I mentioned I created a new View UserControl for LogEvents, Placed it under new TabItem(EventLogs) inside row details template of main GridView.
LogEvent UserControl contains a Grid View binded to LogEventsViewModel to get the Collection based on Selected row EventId.
How do I assign the Selected EventId to a new ViewModel and Get the data dynamically?
One Way: As I showed you, I called LogEvents by placing it in side TabItem. whenever I expanded any row, Then It is Initializing the LogEvents page, During that I tried to bind the Datacontext to LogEventsViewModel. But I'm unable to get the Seleted row EventId dynamically. If I get that then I can easily pass it to the LogEventsViewModel constructor.
var _viewModel = new LogEventsViewModel(EventId);
this.DataContext = _viewModel;
InitializeComponent();
Other way:
Pass the selected EventId directly from xaml View binding to that page initialization and then to LogEventsViewModel
Something like this
<views:LogEvents something="{Binding EventId}"/>
Is there any other way to do this?
Details:
LogEvents.xaml
<UserControl.Resources>
<viewModel:LogEventsViewModel x:Key="ViewModel"/>
</UserControl.Resources>
<Grid DataContext="{Binding Source={StaticResource ViewModel}}">
<GridView x:Name="LogEventsGrid" ItemsSource="{Binding View}"
<GridView.Columns>
<telerik:GridViewToggleRowDetailsColumn />
<telerik:GridViewDataColumn DataMemberBinding="{Binding EventId}" Header="LogEventId"/>
<telerik:GridViewDataColumn DataMemberBinding="{Binding Exception}" Header="Exception" />
</GridView.Columns>
</telerik:RadGridView>
</Grid>
LogEvents.xaml.cs
public int EventId
{
get { return (int)GetValue(EventIdProperty); }
set { SetValue(EventIdProperty, value); }
}
public static readonly DependencyProperty EventIdProperty =
DependencyProperty.Register("EventId", typeof(int), typeof(ApplicationLog),
new PropertyMetadata(0, new PropertyChangedCallback(OnEventIdChanged)));
private static void OnEventIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
int LogEventId1 = (int)e.NewValue;
// Need to assign propery in LogEventsViewModel
}
LogEventsViewModel.cs
WMLCDomainContext _Context = new WMLCDomainContext();
private QueryableDomainServiceCollectionView<LogEvents> view;
public int _eventid;
public ApplicationLogsViewModel()
{
EntityQuery<LogEvents> getLogEventsQuery = _Context.GetApplicationLogListQuery(EventId);
this.view = new QueryableDomainServiceCollectionView<ApplicationLog>(_Context, getLogEventsQuery );
}
public IEnumerable View
{get {return this.view;}}
public int EventId
{
get{return this._eventid;}
set{_eventid = value;}
}
I would create a dependency property on your LogEventsViewModel and then set up a binding on your LogEvents view, something like this:
<views:LogEvents EventId="{Binding EventId}" />
Then in LogEvents.xaml.cs you could create your dependency property:
private LogEvents_ViewModel _viewModel
{
get { return this.DataContext as LogEvents_ViewModel; }
}
public string EventId
{
get { return (string)GetValue(EventIdProperty); }
set { SetValue(EventIdProperty, value); }
}
public static readonly DependencyProperty EventIdProperty =
DependencyProperty.Register("EventId", typeof(string), typeof(LogEvents),
new PropertyMetadata(string.Empty, new PropertyChangedCallback(OnEventIdChanged)));
private static void OnEventIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((LogEvents)d).OnTrackerInstanceChanged(e);
}
protected virtual void OnEventIdChanged(DependencyPropertyChangedEventArgs e)
{
this._viewModel.EventId = e.NewValue;
}
KodeKreachor is correct, though it may be necessary to set a Bindable attribute on your exposed property. Without it, a property might not always show in the bindable properties of the control, even if it does still work.

ListView with checkboxes

I have a list of items displayed as a ListBox.
<ListView ItemsSource="{Binding ListOfSomeItems}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn Header="Status">
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsReceived}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
Now there is 2 user cases I need to implement:
1) When user mark an Item as received (the CheckBox get checked) I need to update the item. How should I bind the checked event to ICommand in my ViewModel?
2) When user tries to remove received flag (un-checks the checkBox) a pop up should be provided with an option to cancel the operation (if someone clicked the checkBox by accident) or to provide a note for a reason. That note along with the Item that been unchecked should be sent to different ICommand in VM.
Any suggestions?
Thanks in advance
UPDATE:
My viewModel do implements INoftiyPropertyChanged but I do not have property for the single item. The property is nested in complex class: something like Account.Holders[x].Requirements[y].IsReceived.
Why just not bind a command which should be exposed by ViewModel:
<CheckBox
Command="{Binding ReceivedStatusChangedCommand}"
CommandParameter="{Binding ...IsReceived}"
/>
Then in command handler you can analyze passed in value of the IsReceived as command parameter.
Since you're binding to an IsChecked property in your object, you can simply listen to the PropertyChanged event for that object and call whatever method you need when it changes.
For example,
// Wireup CollectionChanged in Constructor
public MyVMConstructor()
{
ListOfSomeItems = new List<SomeItem>();
ListOfSomeItems.CollectionChanged += ListOfSomeItems_CollectionChanged;
}
// In CollectionChanged event, wire up PropertyChanged event on items
void ListOfSomeItems_CollectionChanged(object sender, CollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach(SomeItem item in e.NewItems)
item.PropertyChanged += SomeItem_PropertyChanged;
}
if (e.OldItems != null)
{
foreach(SomeItem item in e.OldItems)
item.PropertyChanged -= SomeItem_PropertyChanged;
}
}
// In PropertyChanged, if property was IsReceived then verify and update
void SomeItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsReceived")
{
if (MessageBox.Show("Are you sure?", "Confirm", MessageBoxButtons.YesNo) == DialogResult.Yes)
{
var item = sender as SomeItem;
UpdateSomeItem(item.Id, item.IsReceived);
}
}
}
Your ViewModel should implement INoftiyPropertyChanged. Just listen to the PropertyChanged event and respond to it.
Edit
Based on the structure of your ViewModel, if Account needs to respond the a Requirement, it might be best to use something like Prism's EventAggregator or MVVM Light's Messenger.
MVVM solution would be:
Since bindings are two way you don't need to do anything to have your object updated from UI. But if you want your UI to be updated on changes in model, your model classes need to implement INotifyPropertyChanged
I imagine this would be done in IsReceived setter. Add two fields to your model class.
public string ValidationError
{ get; set; }
public bool HasValidationError
{ get; set; }
Then create a PopUp with error which is hidden by default. Implement INotifyPropertyChanged in your class, and bind PopUp visibility to HasValidationError, and message in inner TextBlock to ValidationError.

Silverlight NumericUpDown inside Datagrid strange behaviour [duplicate]

I have a DataGrid. One of the columns is a template with a CheckBox in it. When the Checked or Unchecked events trigger, (it happens with both) the CheckBox's DataContext is sometimes null, which causes my code to error. It seems to be null most often if the mouse is moving while you press and release the button quickly (it's intermittent).
I listened for changes to the DataContext of the CheckBox by making views:ListenCheckBox (extends CheckBox) and attaching a binding, and it's never set to null, but it is set from null to a Task at times I wouldn't expect, i.e. after the DataGrid has been totally generated and you're checking/unchecking boxes. Immediately after the [un]checked event runs with a null DataContext, I get the notification that shows the DataContext changed from null to a Task, so it appears that when I get a null DataContext, it's because it hadn't actually set the DataContext by the time it ran the Checked/Unchecked event.
Also, I added Tag="{Binding}" to the CheckBox for debugging. The Tag is not null (i.e. it has the proper object) more often than the DataContext, but still not all the time.
Here are the relevant bits of the XAML code:
<navigation:Page.Resources>
<sdk:DataGridTemplateColumn x:Key="DeleteOrPrintSelect" Header="Delete Or Print Notes Selection">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<views:ListenCheckBox IsChecked="{Binding DeleteOrPrintNotesSelection, Mode=TwoWay}" Checked="DeletePrintNotesCheckBox_Changed" Unchecked="DeletePrintNotesCheckBox_Changed" HorizontalAlignment="Center" VerticalAlignment="Center" Tag="{Binding}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
</navigation:Page.Resources>
<sdk:DataGrid x:Name="dataGrid1" Grid.Column="1" Grid.Row="2" AutoGeneratingColumn="dataGrid1_AutoGeneratingColumn">
<sdk:DataGrid.RowGroupHeaderStyles>
[removed]
</sdk:DataGrid.RowGroupHeaderStyles>
</sdk:DataGrid>
And the relevant code behind:
// Create a collection to store task data.
ObservableCollection<Task> taskList = new ObservableCollection<Task>();
[code adding Tasks to taskList removed]
PagedCollectionView panelListView = new PagedCollectionView(taskList);
this.dataGrid1.ItemsSource = panelListView;
}
private void dataGrid1_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.PropertyName == "DeleteOrPrintNotesSelection")
{
e.Column = Resources["DeleteOrPrintSelect"] as DataGridTemplateColumn;
}
else
{
e.Column.IsReadOnly = true;
}
}
private void DeletePrintNotesCheckBox_Changed(object sender, RoutedEventArgs e)
{
try
{
var cb = sender as CheckBox;
var t = cb.DataContext as Task;
t.DeleteOrPrintNotesSelection = cb.IsChecked == true;
PagedCollectionView pcv = this.dataGrid1.ItemsSource as PagedCollectionView;
ObservableCollection<Task> taskList = pcv.SourceCollection as ObservableCollection<Task>;
bool anySelected = taskList.Any(x => x.DeleteOrPrintNotesSelection);
this.btnPrint.IsEnabled = anySelected;
this.btnDelete.IsEnabled = anySelected;
}
catch (Exception ex)
{
ErrorMessageBox.Show("recheck", ex, this);
}
}
Any ideas? Thanks in advance.
I found that the problem happened when you double click on the cell and it moved it to the cell editing template. In my case, I didn't have a cell editing template defined, so it used the same cell template, but instead of not changing anything, it apparently decided to make a new check box. I set the column's IsReadOnly property to true, and it fixed it. An alternate solution:
DataContext="{Binding}" (in XAML, or the code equivalent:)
cb.SetBinding(FrameworkElement.DataContextProperty, new Binding());
I'm not sure why this one fixes it, since I thought the default DataContext is {Binding}. Perhaps it's a Silverlight bug, and it gets set in a different order if you define it explicitly instead of leaving it the default.

Resources