Repository: MudBlazor/MudBlazor (2026-03-27T14:35:41Z)

Issue:
MudDataGrid: Add event for AFTER CommittedItemChanges
### Describe your idea

When `EditMode` is `DataGridEditMode.Form` and `CommittedItemChanges` fires, the entity passed to the event handler is a copy of the bound item, and the edits are not applied to the bound item until after the event handler returns.

In my use case, I have an object with a collection property bound to the grid. When the object is edited in my UI, a copy of the original object is made, and after any property of the object changes, the current object is checked for equality against the original copy to determine if the object has changed (in order to enable a save button for example). When an item in the object's collection property is edited via `DataGridEditMode.Form`, the only way to know when the editing is complete is via the `CommittedItemChanges` event, but because the edits have not yet been applied, comparing the original object to the current object results in no changes even when an item in the data grid has been changed.

There should be a final event such as `CommittedItemChanged` that fires after changes have been applied so there any equality checks on objects that reference the edited item will operate on the actual changed object and not a copy of the object.

### Example code and screenshots

_No response_


Discussion:

Patch:
diff --git a/src/MudBlazor.Docs/Pages/Components/DataGrid/DataGridPage.razor b/src/MudBlazor.Docs/Pages/Components/DataGrid/DataGridPage.razor
index d4f16617a3fc..bd16888737de 100644
--- a/src/MudBlazor.Docs/Pages/Components/DataGrid/DataGridPage.razor
+++ b/src/MudBlazor.Docs/Pages/Components/DataGrid/DataGridPage.razor
@@ -81,50 +81,28 @@
         <DocsPageSection>
             <SectionHeader Title="Editing">
                 <Description>
-                    The <CodeInline Tag>MudDataGrid</CodeInline> supports data editing. To enable it, set
-                    <CodeInline>ReadOnly</CodeInline> to <CodeInline>false</CodeInline> and choose an
-                    <CodeInline>EditMode</CodeInline>:
-                    <ul>
-                        <li><CodeInline>DataGridEditMode.Form</CodeInline> - Opens a popup form for editing</li>
-                        <li><CodeInline>DataGridEditMode.Cell</CodeInline> - Enables Excel-style inline editing</li>
-                    </ul>
+                    Enable editing with <CodeInline>ReadOnly="false"</CodeInline>. Use <CodeInline>DataGridEditMode.Form</CodeInline> for dialog editing or <CodeInline>DataGridEditMode.Cell</CodeInline> for inline editing.
+                    <br /><br />
 
-                    <strong>Saving Changes</strong>
-                    <br />
-                    When a user saves changes, the <CodeInline>CommittedItemChanges</CodeInline> callback is invoked
-                    with the edited values. This callback should return:
+                    Edit events:
                     <ul>
-                        <li><CodeInline>DataGridEditFormAction.Close</CodeInline> - Accepts the changes and closes the edit form</li>
-                        <li><CodeInline>DataGridEditFormAction.KeepOpen</CodeInline> - Rejects the changes and keeps the form open (useful for validation failures)</li>
+                        <li><span class="mr-2">&bull;</span><CodeInline>StartedEditingItem</CodeInline>: editing begins</li>
+                        <li><span class="mr-2">&bull;</span><CodeInline>FormFieldChanged</CodeInline>: a field changes in form mode</li>
+                        <li><span class="mr-2">&bull;</span><CodeInline>CommittedItemChanges</CodeInline>: runs before applying changes</li>
+                        <li><span class="mr-2">&bull;</span><CodeInline>CommittedItemChanged</CodeInline>: runs after a successful commit</li>
+                        <li><span class="mr-2">&bull;</span><CodeInline>CanceledEditingItem</CodeInline>: editing is canceled</li>
                     </ul>
-                    In the example below, the form remains open when Molar mass is negative, allowing the user to correct the error.
+
+                    In form mode, <CodeInline>CommittedItemChanges</CodeInline> receives the edited copy and returns <CodeInline>Close</CodeInline> to save or <CodeInline>KeepOpen</CodeInline> to keep the dialog open. In this example, a negative Molar mass keeps the form open.
                     <br /><br />
 
-                    <strong>Column Configuration</strong>
-                    <br />
-                    To disable editing for a specific column, set <CodeInline>Editable</CodeInline> to <CodeInline>false</CodeInline>.
-                    To make a field optional, set <CodeInline>Required</CodeInline> to <CodeInline>false</CodeInline>.
+                    Set <CodeInline>Editable</CodeInline> to <CodeInline>false</CodeInline> to disable editing for a column. Set <CodeInline>Required</CodeInline> to <CodeInline>false</CodeInline> to make a field optional.
                     <br /><br />
 
-                    <strong>Custom Input Components</strong>
-                    <br />
-                    By default, each editable cell uses an appropriate input component based on the data type.
-                    You can provide custom editing UI using an <CodeInline Tag>EditTemplate</CodeInline>
-                    (see the <CodeInline>Position</CodeInline> column in the example below).
-                    In <CodeInline>Form</CodeInline> mode the built-in Save button commits changes automatically.
-                    In <CodeInline>Cell</CodeInline> mode, standard columns automatically invoke
-                    <CodeInline>CommittedItemChanges</CodeInline> when the value changes, but custom controls inside
-                    an <CodeInline Tag>EditTemplate</CodeInline> do not. Track changes directly in the value-changed
-                    callback instead.
+                    Editable cells use a matching input by default. Use an <CodeInline Tag>EditTemplate</CodeInline> for custom editors. In cell mode, standard columns commit automatically, but controls inside an <CodeInline Tag>EditTemplate</CodeInline> must handle commits in their value-changed callback.
                     <br /><br />
 
-                    <strong>Programmatic Editing</strong>
-                    <br />
-                    To add and edit a new row programmatically:
-                    <ul>
-                        <li><strong>Form mode:</strong> Call <CodeInline>SetEditingItemAsync</CodeInline> with a new instance of type <CodeInline>T</CodeInline></li>
-                        <li><strong>Cell mode:</strong> Add the new item directly to <CodeInline>Items</CodeInline></li>
-                    </ul>
+                    To add and edit a new row programmatically, call <CodeInline>SetEditingItemAsync</CodeInline> with a new <CodeInline>T</CodeInline> instance in form mode. In cell mode, add the new item directly to <CodeInline>Items</CodeInline>.
                 </Description>
             </SectionHeader>
             <SectionContent DarkenBackground="true" Code="@nameof(DataGridEditingExample)" ShowCode="false" Block="true" FullWidth="true">
diff --git a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs
index c6a6e3110b91..82a8a632e6f5 100644
--- a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs
+++ b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs
@@ -316,7 +316,9 @@ private Task ItemUpdatedAsync(MudItemDropInfo<Column<T>> dropItem)
         /// Occurs when edit mode begins for an item.
         /// </summary>
         /// <remarks>
-        /// If changes are committed, the <see cref="CommittedItemChanges"/> delegate is called. If editing is canceled, the <see cref="CanceledEditingItem"/> occurs.
+        /// Use this event to react when editing starts. For form field changes, use <see cref="FormFieldChanged"/>.
+        /// For save and cancel notifications, use <see cref="CommittedItemChanges"/>, <see cref="CommittedItemChanged"/>,
+        /// or <see cref="CanceledEditingItem"/>.
         /// </remarks>
         [Parameter]
         public EventCallback<T> StartedEditingItem { get; set; }
@@ -324,36 +326,42 @@ private Task ItemUpdatedAsync(MudItemDropInfo<Column<T>> dropItem)
         /// <summary>
         /// Occurs when editing of an item has been canceled.
         /// </summary>
+        /// <remarks>
+        /// This event is raised instead of <see cref="CommittedItemChanges"/> and <see cref="CommittedItemChanged"/>
+        /// when the edit operation is canceled.
+        /// </remarks>
         [Parameter]
         public EventCallback<T> CanceledEditingItem { get; set; }
 
         /// <summary>
-        /// Invoked when the user saves changes to an item, allowing for validation, 
-        /// persistence, or other processing.
+        /// Invoked when the user saves changes to an item, allowing validation, persistence, or other pre-commit work.
         /// </summary>
         /// <remarks>
-        /// <para>
-        /// The callback receives a separate instance of the item with the committed values, 
-        /// preventing changes from being applied to the underlying data source before 
-        /// validation or processing completes.
-        /// </para>
-        /// <para>
-        /// Return a <see cref="DataGridEditFormAction"/> value to control the edit form behavior. 
-        /// When <see cref="EditMode"/> is <see cref="DataGridEditMode.Form"/>:
-        /// </para>
-        /// <list type="bullet">
-        /// <item><see cref="DataGridEditFormAction.Close"/> - Accepts changes and closes the form</item>
-        /// <item><see cref="DataGridEditFormAction.KeepOpen"/> - Rejects changes and keeps the form open for corrections</item>
-        /// </list>
+        /// In <see cref="DataGridEditMode.Form"/>, this callback receives the edited copy before values are applied to
+        /// the source item. Return <see cref="DataGridEditFormAction.Close"/> to continue or
+        /// <see cref="DataGridEditFormAction.KeepOpen"/> to leave the form open for corrections.
+        /// Use <see cref="CommittedItemChanged"/> to react after the source item has been updated.
         /// </remarks>
         [Parameter]
         public Func<T, Task<DataGridEditFormAction>>? CommittedItemChanges { get; set; }
 
+        /// <summary>
+        /// Occurs after committed changes have been applied to the underlying item.
+        /// </summary>
+        /// <remarks>
+        /// Use this callback for post-commit logic. In <see cref="DataGridEditMode.Form"/>, it runs after
+        /// <see cref="CommittedItemChanges"/> completes and after values are copied to the source item.
+        /// In <see cref="DataGridEditMode.Cell"/>, it runs after <see cref="CommittedItemChanges"/> completes,
+        /// with the already-updated item.
+        /// </remarks>
+        [Parameter]
+        public EventCallback<T> CommittedItemChanged { get; set; }
+
         /// <summary>
         /// Occurs when a field changes in the edit dialog.
         /// </summary>
         /// <remarks>
-        /// This event only occurs when <see cref="EditMode"/> is <see cref="DataGridEditMode.Form"/>.
+        /// This event only occurs in <see cref="DataGridEditMode.Form"/> and fires for individual field changes before save.
         /// </remarks>
         [Parameter]
         public EventCallback<FormFieldChangedEventArgs> FormFieldChanged { get; set; }
@@ -1966,6 +1974,8 @@ internal async Task CommitItemChangesAsync(T item)
             // Here, we need to validate at the cellular level...
             if (CommittedItemChanges != null)
                 _ = await CommittedItemChanges(item); // ignore return value in cell edit mode
+
+            await CommittedItemChanged.InvokeAsync(item);
         }
 
         /// <summary>
@@ -1997,6 +2007,8 @@ internal async Task CommitItemChangesAsync()
                 foreach (var property in _properties.Where(p => p.CanWrite))
                     property.SetValue(_editingSourceItem, property.GetValue(_editingItem));
 
+                await CommittedItemChanged.InvokeAsync(_editingSourceItem);
+
                 ClearEditingItem();
                 _isEditFormOpen = false;
             }


Test patch:
diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridCommittedItemChangedTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridCommittedItemChangedTest.razor
new file mode 100644
index 000000000000..e133b0a5adee
--- /dev/null
+++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridCommittedItemChangedTest.razor
@@ -0,0 +1,48 @@
+<MudDialogProvider />
+
+<MudDataGrid T="Model" Items="@_items" ReadOnly="false"
+             EditMode="@DataGridEditMode.Form" EditTrigger="@DataGridEditTrigger.OnRowClick"
+             CommittedItemChanges="OnCommittedItemChangesAsync"
+             CommittedItemChanged="OnCommittedItemChangedAsync">
+    <Columns>
+        <PropertyColumn Property="x => x.Name" />
+        <PropertyColumn Property="x => x.Age" />
+    </Columns>
+</MudDataGrid>
+
+@code {
+    private readonly List<Model> _items = new()
+    {
+        new Model { Name = "John", Age = 45 },
+        new Model { Name = "Johanna", Age = 23 }
+    };
+
+    public string? SourceNameWhenCommittedItemChanges { get; private set; }
+
+    public string? EditedCopyNameWhenCommittedItemChanges { get; private set; }
+
+    public string? SourceNameWhenCommittedItemChanged { get; private set; }
+
+    public bool CommittedItemChangedCalled { get; private set; }
+
+    public class Model
+    {
+        public required string Name { get; set; }
+
+        public required int Age { get; set; }
+    }
+
+    private Task<DataGridEditFormAction> OnCommittedItemChangesAsync(Model item)
+    {
+        EditedCopyNameWhenCommittedItemChanges = item.Name;
+        SourceNameWhenCommittedItemChanges = _items[1].Name;
+        return Task.FromResult(DataGridEditFormAction.Close);
+    }
+
+    private Task OnCommittedItemChangedAsync(Model item)
+    {
+        CommittedItemChangedCalled = true;
+        SourceNameWhenCommittedItemChanged = _items[1].Name;
+        return Task.CompletedTask;
+    }
+}
diff --git a/src/MudBlazor.UnitTests/Components/DataGridTests.cs b/src/MudBlazor.UnitTests/Components/DataGridTests.cs
index ef4a68eae815..43b4c492586c 100644
--- a/src/MudBlazor.UnitTests/Components/DataGridTests.cs
+++ b/src/MudBlazor.UnitTests/Components/DataGridTests.cs
@@ -1345,6 +1345,29 @@ await dataGrid.SetParametersAndRenderAsync(parameters => parameters
             comp.Instance.CanceledEditingItem.Should().Be(true);
         }
 
+        [Test]
+        public async Task DataGridCommittedItemChangedOccursAfterSourceItemUpdateInFormEdit()
+        {
+            var comp = Context.Render<DataGridCommittedItemChangedTest>();
+            var dataGrid = comp.FindComponent<MudDataGrid<DataGridCommittedItemChangedTest.Model>>();
+
+            await dataGrid.FindAll("tbody tr")[1].ClickAsync();
+
+            await comp.FindAll("div input")[0].ChangeAsync("Galadriel");
+            await comp.FindAll("div input")[1].ChangeAsync(12);
+            await comp.Find(".mud-dialog-actions .mud-button-filled-primary").ClickAsync();
+
+            await comp.WaitForAssertionAsync(() =>
+            {
+                comp.Instance.EditedCopyNameWhenCommittedItemChanges.Should().Be("Galadriel");
+                comp.Instance.SourceNameWhenCommittedItemChanges.Should().Be("Johanna");
+                comp.Instance.CommittedItemChangedCalled.Should().BeTrue();
+                comp.Instance.SourceNameWhenCommittedItemChanged.Should().Be("Galadriel");
+                dataGrid.FindAll("td")[2].TextContent.Trim().Should().Be("Galadriel");
+                dataGrid.FindAll("td")[3].TextContent.Trim().Should().Be("12");
+            });
+        }
+
         [Test]
         public async Task DataGridEditComplexPropertyExpression()
         {