-1

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.

Sub grid closed enter image description here

Sub grid open enter image description here

  • Selecting an account from the ComboBox populates the main grid with transactions. Clicking the [Sub] button opens the sub grid and shows the subtransactions for that transaction.
  • If the user enters values in the transaction (main row) and presses 'Enter', the transaction values are successfully committed; i.e., the Transaction ValdationStep.CommittedValue is triggered.
  • If the user enters values in the main row (doesn't press 'Enter') and just clicks the [Subs] button to open the sub grid, then enters values in the subtransaction and presses 'Enter', the subtransaction values are successfully committed; i.e., the SubtransactionValidation ValdationStep.CommittedValue is triggered.
  • But if the user then clicks out of the sub grid without ever pressing 'Enter' on the main row, the values in the transaction are not committed; i.e., 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;
    }
}
IronRod
  • 874
  • 10
  • 21

1 Answers1

2

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)
IronRod
  • 874
  • 10
  • 21