How to Fix WPF DataGrid 'Refresh' Not Allowed During AddNew/EditItem Transaction in MVVM: Exit Edit Mode When Searching or Checking Checkboxes

The WPF DataGrid is a powerful control for displaying and editing tabular data, but it can throw a frustrating exception when you least expect it: "Refresh is not allowed during an AddNew or EditItem transaction." This error typically occurs when a user is editing a row (or adding a new item) and then triggers an action that refreshes the DataGrid—such as searching, filtering, or checking a checkbox that updates the underlying data.

In Model-View-ViewModel (MVVM) architectures, fixing this requires careful coordination between the View (where the DataGrid lives) and the ViewModel (where business logic resides), without violating MVVM’s separation of concerns. This blog will demystify the root cause of the error, explain key DataGrid concepts, and provide a step-by-step MVVM-friendly solution to exit edit mode before refreshing.

Table of Contents#

  1. Understanding the Problem
  2. Why Does This Happen?
  3. Key Concepts: DataGrid Edit Modes and Transactions
  4. The MVVM Challenge
  5. Solution: Exit Edit Mode Before Refreshing
  6. Step-by-Step Implementation
  7. Handling Edge Cases
  8. Testing the Solution
  9. Conclusion
  10. References

1. Understanding the Problem#

Let’s set the scene:

  • A user is editing an existing row in a DataGrid (e.g., updating a "Name" field).
  • While still in edit mode, they click a "Search" button to filter the grid or check a checkbox that triggers a data refresh.
  • The DataGrid throws an exception with the message: "Refresh is not allowed during an AddNew or EditItem transaction."

This error occurs because the DataGrid is in the middle of an edit or add transaction and cannot safely refresh its data without corrupting the ongoing operation.

2. Why Does This Happen?#

The DataGrid maintains internal state to ensure data integrity during edits. When you start editing a row (EditItem) or adding a new row (AddNew), the DataGrid enters a transaction. During this transaction, critical operations like refreshing the data source (e.g., changing ItemsSource, sorting, or filtering) are blocked to prevent data loss or inconsistency.

The DataGrid relies on the IEditableCollectionView interface (implemented by collections like ObservableCollection<T>) to manage these transactions. Until the transaction is committed (via CommitEdit/CommitNew) or canceled (via CancelEdit/CancelNew), the DataGrid blocks refreshes to avoid disrupting the in-progress operation.

3. Key Concepts: DataGrid Edit Modes and Transactions#

To fix the issue, it’s essential to understand the DataGrid’s edit modes and transaction lifecycle:

EditItem Mode#

  • Triggered when a user double-clicks a row or presses F2 to edit existing data.
  • The DataGrid enters an edit transaction, locking the row for modifications.

AddNew Mode#

  • Triggered when a user clicks the "New Item" row (enabled via CanUserAddRows="True") or calls DataGrid.BeginAddNew().
  • The DataGrid creates a temporary new item and enters an add transaction.

Transaction Lifecycle#

  1. Start: Edit/Add begins (e.g., user starts typing in a cell).
  2. Active: Transaction is in progress; DataGrid.IsEditingItem or DataGrid.IsAddingNew returns true.
  3. End: Transaction is committed (changes saved) or canceled (changes discarded) via user action (e.g., pressing Enter or Esc) or code.

4. The MVVM Challenge#

In MVVM, the ViewModel should never reference the View (e.g., the DataGrid control directly). This separation ensures testability and maintainability. However, exiting edit mode requires interacting with the DataGrid’s state—creating a dilemma:

  • Traditional Fix: Use code-behind in the View to check DataGrid.IsEditingItem and call CancelEdit() before refreshing. But this violates MVVM’s "no View logic in code-behind" principle.
  • MVVM Fix: Bridge the View and ViewModel without tight coupling. The solution lies in Attached Behaviors—reusable components that encapsulate View logic and expose properties/commands for ViewModel binding.

5. Solution: Exit Edit Mode Before Refreshing#

The core solution is to exit edit/add mode before triggering a refresh. Here’s the step-by-step plan:

  1. Detect Refresh Triggers: Identify actions that cause refreshes (e.g., SearchCommand, FilterCommand, or checkbox clicks).
  2. Exit Edit Mode: Before executing the refresh logic, ensure the DataGrid exits edit/add mode.
  3. MVVM Coordination: Use an Attached Behavior to handle the DataGrid’s edit state, exposing a command/property the ViewModel can trigger.

6. Step-by-Step Implementation#

Let’s build the solution with an Attached Behavior, ensuring MVVM compliance.

Step 1: Create the Attached Behavior#

An Attached Behavior will encapsulate the logic to exit edit/add mode. It will expose a dependency property the ViewModel can bind to, triggering the exit.

// DataGridEditModeBehavior.cs
using System.Windows;
using System.Windows.Controls;
 
namespace YourApp.Behaviors
{
    public static class DataGridEditModeBehavior
    {
        // Dependency property to trigger exit edit mode
        public static readonly DependencyProperty ExitEditModeRequestedProperty =
            DependencyProperty.RegisterAttached(
                "ExitEditModeRequested",
                typeof(bool),
                typeof(DataGridEditModeBehavior),
                new PropertyMetadata(false, OnExitEditModeRequestedChanged));
 
        // Getter for ExitEditModeRequested
        public static bool GetExitEditModeRequested(DependencyObject obj) =>
            (bool)obj.GetValue(ExitEditModeRequestedProperty);
 
        // Setter for ExitEditModeRequested
        public static void SetExitEditModeRequested(DependencyObject obj, bool value) =>
            obj.SetValue(ExitEditModeRequestedProperty, value);
 
        // Handle property changes to trigger exit edit mode
        private static void OnExitEditModeRequestedChanged(
            DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is DataGrid dataGrid && (bool)e.NewValue)
            {
                ExitEditAddMode(dataGrid);
                // Reset the property to allow future triggers
                SetExitEditModeRequested(dataGrid, false);
            }
        }
 
        // Exit edit/add mode by canceling transactions
        private static void ExitEditAddMode(DataGrid dataGrid)
        {
            // Cancel edit mode if active
            if (dataGrid.IsEditingItem)
            {
                dataGrid.CancelEdit(DataGridEditingUnit.Row); // Cancel row-level edit
            }
 
            // Cancel add mode if active
            if (dataGrid.IsAddingNew)
            {
                dataGrid.CancelNew(); // Cancel new item transaction
            }
        }
    }
}

Step 2: Attach the Behavior to the DataGrid#

In your View’s XAML, attach the behavior to the DataGrid and bind its ExitEditModeRequested property to a ViewModel property:

<!-- YourView.xaml -->
<Window
    ...
    xmlns:behaviors="clr-namespace:YourApp.Behaviors">
 
    <DataGrid
        x:Name="MyDataGrid"
        ItemsSource="{Binding Items}"
        behaviors:DataGridEditModeBehavior.ExitEditModeRequested="{Binding ExitEditModeRequested, Mode=TwoWay}">
        <!-- Columns defined here -->
    </DataGrid>
 
    <!-- Example: Search TextBox and Button -->
    <TextBox Text="{Binding SearchQuery}" />
    <Button Command="{Binding SearchCommand}" Content="Search" />
</Window>

Step 3: Update the ViewModel#

Add a property to trigger exit edit mode and modify refresh commands (e.g., SearchCommand) to request exit before refreshing:

// YourViewModel.cs
using System.ComponentModel;
using System.Windows.Input;
using YourApp.Commands; // Assume RelayCommand is a basic ICommand implementation
 
public class YourViewModel : INotifyPropertyChanged
{
    private bool _exitEditModeRequested;
    private string _searchQuery;
    private ObservableCollection<YourItem> _items;
 
    // Property to trigger exit edit mode (bound to the behavior)
    public bool ExitEditModeRequested
    {
        get => _exitEditModeRequested;
        set
        {
            _exitEditModeRequested = value;
            OnPropertyChanged();
        }
    }
 
    // Search command (refresh trigger)
    public ICommand SearchCommand => new RelayCommand(Search);
 
    // Implement INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
 
    private void Search()
    {
        // Step 1: Request exit edit mode (triggers the behavior)
        ExitEditModeRequested = true;
 
        // Step 2: Refresh data (e.g., filter items based on SearchQuery)
        _items = new ObservableCollection<YourItem>(
            _originalItems.Where(item => item.Name.Contains(SearchQuery)));
        OnPropertyChanged(nameof(Items));
    }
 
    // Other ViewModel logic...
}

7. Handling Edge Cases#

Unsaved Changes#

If the user has unsaved edits, CancelEdit()/CancelNew() will discard them. To preserve changes:

  • Use dataGrid.CommitEdit() instead of CancelEdit() to save edits before refreshing.
  • Add a confirmation dialog (via a service) to prompt the user: "Save changes before refreshing?"

Example modified ExitEditAddMode:

private static void ExitEditAddMode(DataGrid dataGrid)
{
    if (dataGrid.IsEditingItem)
    {
        // Commit if the item is valid; adjust validation logic as needed
        if (dataGrid.CommitEdit(DataGridEditingUnit.Row))
        {
            // Commit succeeded
        }
        else
        {
            dataGrid.CancelEdit(DataGridEditingUnit.Row); // Rollback invalid edits
        }
    }
    if (dataGrid.IsAddingNew)
    {
        dataGrid.CancelNew(); // New items are often temporary; adjust if needed
    }
}

Multiple DataGrids#

If your View has multiple DataGrids, attach the behavior to each and bind to separate ViewModel properties (e.g., ExitEditModeRequestedForGrid1).

8. Testing the Solution#

Verify the fix with these scenarios:

  1. Edit Mode Refresh:

    • Start editing a row.
    • Click "Search"—no exception should occur, and edits are canceled/committed.
  2. AddNew Mode Refresh:

    • Click the "New Item" row and start typing.
    • Check a filter checkbox—no exception, and the new item is discarded.
  3. Unsaved Changes:

    • Modify a cell, then trigger a refresh. Confirm changes are either saved (if using CommitEdit) or discarded (if using CancelEdit).

9. Conclusion#

The "Refresh not allowed during AddNew/EditItem transaction" error arises because the DataGrid blocks refreshes during active edits. In MVVM, we solve this by using an Attached Behavior to exit edit mode before refreshing, keeping the ViewModel decoupled from the View.

This approach ensures compliance with MVVM principles while maintaining data integrity. Adjust the behavior to commit/cancel edits based on your app’s requirements for unsaved changes.

10. References#