How can I force a commit when a datagrid row loses focus as opposed to requiring the user to press return to commit?
I have a MVVM project with a main DataGrid
using a RowDetailsTemplate
that contains another sub DataGrid
. It uses the default behavior of pressing 'Enter' on the row commits the value changes. I have a ValidatonRule
implemented for both the main row and the sub row.
Transaction ValdationStep.CommittedValue
is triggered.SubtransactionValidation ValdationStep.CommittedValue
is triggered.Transaction ValdationStep.CommittedValue
is never triggered. Then, when a new account is selected (which results in a change in the ItemsSource
collection) a CollectionChanged
event is triggered on ItemsSource
to remove the source item (the uncommitted transaction) as if the user hit 'Esc'.I assume the CollectionChanged
event is triggered by the DataGrid
deciding the row hadn't been committed and so removes it from the collection.
This SO: Cancel collection changed event on an observable collection may be a way to cancel the CollectionChanged
and removal of the item but it seems forcing the commit of the changes is more what is needed. Plus, I'd have to figure out when to enable/disable when to ignore the event.
I thought this SO: wpf Datagrid force datagrid row evaluation might be a solution (though overkill) and implemented it in the button code (see below) but it still triggered the CollectionChanged
event to remove the item event when I select a different account and the ItemsSource
collection is changed.
private void ToggleSubs_Click(object sender, RoutedEventArgs e)
{
...
foreach (var item in BankDataGrid.ItemContainerGenerator.Items)
{
var container = BankDataGrid.ItemContainerGenerator.ContainerFromItem(item);
if (container != null && container is DataGridRow dgr)
{
dgr.BindingGroup.CommitEdit();
}
}
}
I then tried catching the ValidationStep.ConvertedProposedValue
(which I found is triggered after clicking [Subs] button) and forcing BindingGroup.CommitEdit()
on that row. But when I selected a different account, once again the CollectionChanged
event was triggered to remove the item.
XAML
<Grid>
<Grid x:Name="MainPanel">
<Grid.RowDefinitions>
<RowDefinition Height="26" />
<RowDefinition Height="30" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
...
<Grid x:Name="Controls" Grid.Row="1" >
...
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Left">
<Label Content="Account" VerticalAlignment="Center" />
<ComboBox Name="ControlsAccount" VerticalAlignment="Center" Width="210"
ItemsSource="{Binding AccountList}"
SelectedItem="{Binding SelectedAccount}"
IsEnabled="{Binding HasAccounts}" >
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"
Foreground="{Binding ItemForeColor}"
Background="{Binding ItemBackColor}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
...
</Grid>
<DataGrid x:Name="BankDataGrid" Grid.Row="2"
ItemsSource="{Binding SelectedAccount.Transactions, UpdateSourceTrigger=LostFocus}"
SelectedItem="{Binding SelectedAccount.SelectedTransaction, Converter={StaticResource TransConverter}}" >
...
<DataGrid.RowValidationRules>
<valid:BankTransactionValidation ValidationStep="UpdatedValue" />
<valid:BankTransactionValidation ValidationStep="CommittedValue" />
</DataGrid.RowValidationRules>
...
<DataGrid.Columns>
...
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate x:Name="SubDataTemplate">
<DataGrid x:Name="SubDataGrid"
ItemsSource="{Binding Subtransactions}"
SelectedItem="{Binding SelectedSubtransaction, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource SubtransConverter}}" >
...
<DataGrid.RowValidationRules>
<valid:SubtransactionValidation ValidationStep="UpdatedValue" />
<valid:SubtransactionValidation ValidationStep="CommittedValue" />
</DataGrid.RowValidationRules>
...
<DataGrid.Columns>
...
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
</Grid>
...
</Grid>
MainVM
partial class MainVM : BaseVM
{
public MainVM()
{
LoadSettings();
InitMainPanel();
InitTransactionSearch();
InitLookupMaintenance();
InitLookupReplacement();
InitAccountDetails();
InitBusyPanel();
}
...
private ObservableCollection<AccountVM> accountList;
public ObservableCollection<AccountVM> AccountList
{
get => accountList;
set
{
accountList = value;
NotifyPropertyChanged();
}
}
private AccountVM selectedAccount;
public AccountVM SelectedAccount
{
get => selectedAccount;
set
{
selectedAccount = value;
NotifyPropertyChanged();
if (selectedAccount != null)
{
WindowTitle = $"{selectedAccount.Name} - {AppName}";
BankDataGridVisibility = VISIBILITY_SHOW;
BackgroundImageVisibility = VISIBILITY_HIDE;
SubtransactionsVisibility = VISIBILITY_COLLAPSE;
SubtransactionVM.XferAccountRemove(selectedAccount.Name, AccountList);
}
else
{
HideTransactions();
}
HasAccounts = (AccountList != null && AccountList.Count > 0) ? LITERAL_TRUE : LITERAL_FALSE;
}
}
...
}
AccountVM CollectionChanged handler for Transactions collection
public class AccountVM : BaseVM
{
private ObservableCollection<BankTransactionVM> transactions;
public ObservableCollection<BankTransactionVM> Transactions
{
get
{
if (transactions == null)
{
ReadTransactions();
}
return transactions;
}
}
private BankTransactionVM selectedTransaction;
public BankTransactionVM SelectedTransaction
{
get => selectedTransaction;
set
{
selectedTransaction = value;
NotifyPropertyChanged();
}
}
...
private void BankTransactions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var action = e.Action;
if (action == NotifyCollectionChangedAction.Remove)
{
foreach (BankTransactionVM item in e.OldItems)
{
// Notifiy parent of change for properties dependent upon collection items
Transaction_PropertyChanged(item, new PropertyChangedEventArgs(nameof(TransactionsCount)));
Transaction_PropertyChanged(item, new PropertyChangedEventArgs(nameof(TransactionsTotal)));
// Remove the item
item.Delete();
}
}
else if (action == NotifyCollectionChangedAction.Add)
{
foreach (BankTransactionVM item in e.NewItems)
{
// Add the item
item.AccountId = AccountId;
item.Insert();
// Add the event handlers
item.PropertyChanged += new PropertyChangedEventHandler(Transaction_PropertyChanged);
item.TransferCreated += new TransferCreatedEventHandler(Transaction_TransferCreated);
item.TransferDeleted += new TransferDeletedEventHandler(Transaction_TransferDeleted);
// Notifiy parent of change for properties dependent upon collection items
Transaction_PropertyChanged(item, new PropertyChangedEventArgs(nameof(TransactionsCount)));
Transaction_PropertyChanged(item, new PropertyChangedEventArgs(nameof(TransactionsTotal)));
}
}
}
}
BankTransactionValidation
public class BankTransactionValidation : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
var result = ValidationResult.ValidResult;
var bg = (BindingGroup)value;
var row = (DataGridRow)bg.Owner;
if (row.Item is BankTransactionVM item)
{
switch (ValidationStep)
{
case ValidationStep.RawProposedValue:
break;
case ValidationStep.ConvertedProposedValue:
break;
case ValidationStep.UpdatedValue:
break;
case ValidationStep.CommittedValue:
if (item.TransactionId > 0)
{
item.Update();
if (item.Subtransactions.Count == 0)
{
item.Subtransactions.Add(new SubtransactionVM(item.TransactionId));
}
}
break;
}
}
return result;
}
}
I found the key was using DataGrid.CommitEdit() instead of BindingGroup.CommitEdit(). By using the following line in the ToggleSubs_Click()
method, before reading in the data for the subgrid, it forces the same sequence as the user pressing [Enter] on the row.
DataGrid.CommitEdit(DataGridEditingUnit.Row, true)