From 6176f595c18f3b1ee197d30582dea8f045f50035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 14 Jul 2026 22:01:07 +0200 Subject: [PATCH 01/13] Refresh programmatic cell validation --- .../Validation/DataGridValidationTests.cs | 42 +++++++++++++++++-- .../DataGrid.Validation.cs | 23 ++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs index 868cd81b..f767aff1 100644 --- a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs +++ b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs @@ -447,6 +447,37 @@ public void INotifyDataErrorInfo_validation_restores_on_row_recycle() } } + [AvaloniaFact] + public void INotifyDataErrorInfo_programmatic_change_refreshes_realized_cell_validation() + { + var (grid, root, item, column) = CreateNotifyValidationGrid(initialCode: "Valid"); + + try + { + var cell = FindCell(grid, item, column.Index); + Assert.Equal(DataGridValidationSeverity.None, cell.ValidationSeverity); + Assert.False(DataValidationErrors.GetHasErrors(cell)); + + item.Code = "X"; + grid.UpdateLayout(); + + Assert.Equal(DataGridValidationSeverity.Warning, cell.ValidationSeverity); + Assert.True(((IPseudoClasses)cell.Classes).Contains(":warning")); + Assert.True(DataValidationErrors.GetHasErrors(cell)); + + item.Code = "Valid"; + grid.UpdateLayout(); + + Assert.Equal(DataGridValidationSeverity.None, cell.ValidationSeverity); + Assert.False(((IPseudoClasses)cell.Classes).Contains(":warning")); + Assert.False(DataValidationErrors.GetHasErrors(cell)); + } + finally + { + root.Close(); + } + } + [AvaloniaFact] public void Grid_invalid_when_offscreen_item_has_error() { @@ -830,14 +861,19 @@ private static (DataGrid grid, Window root, InfoValidationItem item, DataGridTex private static (DataGrid grid, Window root, NotifyValidationItem item, DataGridTextColumn column) CreateNotifyValidationGrid() { - return CreateNotifyValidationGrid(TwoWayBinding(nameof(NotifyValidationItem.Code))); + return CreateNotifyValidationGrid(TwoWayBinding(nameof(NotifyValidationItem.Code)), "X"); } - private static (DataGrid grid, Window root, NotifyValidationItem item, DataGridTextColumn column) CreateNotifyValidationGrid(BindingBase binding) + private static (DataGrid grid, Window root, NotifyValidationItem item, DataGridTextColumn column) CreateNotifyValidationGrid(string initialCode) + { + return CreateNotifyValidationGrid(TwoWayBinding(nameof(NotifyValidationItem.Code)), initialCode); + } + + private static (DataGrid grid, Window root, NotifyValidationItem item, DataGridTextColumn column) CreateNotifyValidationGrid(BindingBase binding, string initialCode = "X") { var item = new NotifyValidationItem { - Code = "X" + Code = initialCode }; var items = new ObservableCollection { item }; diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs index 58032cf9..c80d35ac 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs @@ -478,9 +478,32 @@ private void CollectionValidationItem_ErrorsChanged(object sender, DataErrorsCha } UpdateTrackedCollectionValidationItemState(notifyDataErrorInfo); + RefreshRealizedRowValidationState(notifyDataErrorInfo); UpdateGridValidationState(); } + private void RefreshRealizedRowValidationState(INotifyDataErrorInfo notifyDataErrorInfo) + { + if (DisplayData == null) + { + return; + } + + for (int slot = DisplayData.FirstScrollingSlot; + slot > -1 && slot <= DisplayData.LastScrollingSlot; + slot++) + { + if (DisplayData.GetDisplayedElement(slot) is not DataGridRow row || + ReferenceEquals(row, EditingRow) || + !ReferenceEquals(row.DataContext, notifyDataErrorInfo)) + { + continue; + } + + RestoreRowValidationState(row, notifyDataErrorInfo); + } + } + private void DetachCollectionValidationTracking() { foreach (var notifyDataErrorInfo in _collectionValidationTrackedItems) From 7725e9db76028225462958d08b1137481137b691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 02:29:38 +0200 Subject: [PATCH 02/13] Preserve unrelated cell validation errors --- .../Validation/DataGridValidationTests.cs | 40 +++++++++++++++++++ .../DataGrid.Validation.cs | 20 ++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs index f767aff1..8fb5403b 100644 --- a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs +++ b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs @@ -478,6 +478,46 @@ public void INotifyDataErrorInfo_programmatic_change_refreshes_realized_cell_val } } + [AvaloniaFact] + public void INotifyDataErrorInfo_change_preserves_binding_validation_on_other_property() + { + var (grid, root, item, _, warningColumn) = CreateMixedValidationGrid(); + + try + { + int slot = grid.SlotFromRowIndex(0); + Assert.True(grid.UpdateSelectionAndCurrency( + warningColumn.Index, + slot, + DataGridSelectionAction.SelectCurrent, + scrollIntoView: false)); + grid.UpdateLayout(); + + Assert.True(grid.BeginEdit()); + grid.UpdateLayout(); + + DataGridCell warningCell = FindCell(grid, item, warningColumn.Index); + TextBox textBox = Assert.IsType(warningCell.Content); + textBox.Text = "X"; + UpdateEditingElementSource(textBox); + Assert.True(grid.CommitEdit(DataGridEditingUnit.Cell, exitEditingMode: true)); + grid.UpdateLayout(); + + Assert.Equal(DataGridValidationSeverity.Warning, warningCell.ValidationSeverity); + Assert.True(DataValidationErrors.GetHasErrors(warningCell)); + + item.ErrorValue = "Valid"; + grid.UpdateLayout(); + + Assert.Equal(DataGridValidationSeverity.Warning, warningCell.ValidationSeverity); + Assert.True(DataValidationErrors.GetHasErrors(warningCell)); + } + finally + { + root.Close(); + } + } + [AvaloniaFact] public void Grid_invalid_when_offscreen_item_has_error() { diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs index c80d35ac..26f92251 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs @@ -68,7 +68,11 @@ private static void ClearCellValidation(DataGridCell cell) DataValidationErrors.ClearErrors(cell); } - private void RestoreRowValidationState(DataGridRow row, object item, bool clearIfNoIndei = true) + private void RestoreRowValidationState( + DataGridRow row, + object item, + bool clearIfNoIndei = true, + string propertyName = null) { if (row is null) { @@ -105,6 +109,12 @@ private void RestoreRowValidationState(DataGridRow row, object item, bool clearI continue; } + if (!string.IsNullOrEmpty(propertyName) && + !string.Equals(bindingPath, propertyName, StringComparison.Ordinal)) + { + continue; + } + var errors = notifyDataErrorInfo.GetErrors(bindingPath); if (errors is null) { @@ -478,11 +488,13 @@ private void CollectionValidationItem_ErrorsChanged(object sender, DataErrorsCha } UpdateTrackedCollectionValidationItemState(notifyDataErrorInfo); - RefreshRealizedRowValidationState(notifyDataErrorInfo); + RefreshRealizedRowValidationState(notifyDataErrorInfo, e.PropertyName); UpdateGridValidationState(); } - private void RefreshRealizedRowValidationState(INotifyDataErrorInfo notifyDataErrorInfo) + private void RefreshRealizedRowValidationState( + INotifyDataErrorInfo notifyDataErrorInfo, + string propertyName) { if (DisplayData == null) { @@ -500,7 +512,7 @@ private void RefreshRealizedRowValidationState(INotifyDataErrorInfo notifyDataEr continue; } - RestoreRowValidationState(row, notifyDataErrorInfo); + RestoreRowValidationState(row, notifyDataErrorInfo, propertyName: propertyName); } } From 061ec3849f5c32138750756b5f1c2cf1617ae161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 07:37:33 +0200 Subject: [PATCH 03/13] Preserve validation error provenance --- .../Validation/DataGridValidationTests.cs | 40 ++++++++++++++- .../DataGrid.Validation.cs | 49 +++++++++++++++++-- 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs index 8fb5403b..8d451d8a 100644 --- a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs +++ b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs @@ -479,12 +479,14 @@ public void INotifyDataErrorInfo_programmatic_change_refreshes_realized_cell_val } [AvaloniaFact] - public void INotifyDataErrorInfo_change_preserves_binding_validation_on_other_property() + public void INotifyDataErrorInfo_all_property_change_preserves_binding_validation() { var (grid, root, item, _, warningColumn) = CreateMixedValidationGrid(); try { + item.ErrorValue = "Valid"; + grid.UpdateLayout(); int slot = grid.SlotFromRowIndex(0); Assert.True(grid.UpdateSelectionAndCurrency( warningColumn.Index, @@ -501,12 +503,13 @@ public void INotifyDataErrorInfo_change_preserves_binding_validation_on_other_pr textBox.Text = "X"; UpdateEditingElementSource(textBox); Assert.True(grid.CommitEdit(DataGridEditingUnit.Cell, exitEditingMode: true)); + Assert.True(grid.CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)); grid.UpdateLayout(); Assert.Equal(DataGridValidationSeverity.Warning, warningCell.ValidationSeverity); Assert.True(DataValidationErrors.GetHasErrors(warningCell)); - item.ErrorValue = "Valid"; + item.NotifyAllPropertiesChanged(); grid.UpdateLayout(); Assert.Equal(DataGridValidationSeverity.Warning, warningCell.ValidationSeverity); @@ -518,6 +521,28 @@ public void INotifyDataErrorInfo_change_preserves_binding_validation_on_other_pr } } + [AvaloniaFact] + public void INotifyDataErrorInfo_all_property_change_clears_removed_indei_error() + { + var (grid, root, item, errorColumn, _) = CreateMixedValidationGrid(); + + try + { + DataGridCell errorCell = FindCell(grid, item, errorColumn.Index); + Assert.Equal(DataGridValidationSeverity.Error, errorCell.ValidationSeverity); + + item.ClearErrorsAndNotifyAllProperties(); + grid.UpdateLayout(); + + Assert.Equal(DataGridValidationSeverity.None, errorCell.ValidationSeverity); + Assert.False(DataValidationErrors.GetHasErrors(errorCell)); + } + finally + { + root.Close(); + } + } + [AvaloniaFact] public void Grid_invalid_when_offscreen_item_has_error() { @@ -1885,6 +1910,17 @@ public IEnumerable GetErrors(string? propertyName) return Array.Empty(); } + public void ClearErrorsAndNotifyAllProperties() + { + _errors.Clear(); + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(null)); + } + + public void NotifyAllPropertiesChanged() + { + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(null)); + } + private void SetError(string propertyName, DataGridValidationResult? error) { if (error == null) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs index 26f92251..5001ea5a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs @@ -56,7 +56,7 @@ private void ResetValidationStatus(DataGridCell editingCell = null) _validationSubscription = null; } - private static void ClearCellValidation(DataGridCell cell) + private void ClearCellValidation(DataGridCell cell) { if (!cell.IsValid || cell.ValidationSeverity != DataGridValidationSeverity.None) { @@ -118,14 +118,14 @@ private void RestoreRowValidationState( var errors = notifyDataErrorInfo.GetErrors(bindingPath); if (errors is null) { - ClearCellValidation(cell); + ClearNotifyDataErrorInfoValidation(cell); continue; } var exceptions = CreateValidationExceptions(errors); if (exceptions.Count == 0) { - ClearCellValidation(cell); + ClearNotifyDataErrorInfoValidation(cell); continue; } @@ -138,12 +138,53 @@ private void RestoreRowValidationState( ? exceptions[0] : new AggregateException(exceptions); DataValidationErrors.SetError(cell, errorException); + IEnumerable displayedErrors = DataValidationErrors.GetErrors(cell); + if (displayedErrors is not null) + { + var trackedErrors = new List(); + foreach (object displayedError in displayedErrors) + { + trackedErrors.Add(displayedError); + } + + _notifyDataErrorInfoCellErrors[cell] = trackedErrors.ToArray(); + } } UpdateRowValidationStateFromCells(row); UpdateGridValidationState(); } + private void ClearNotifyDataErrorInfoValidation(DataGridCell cell) + { + if (!_notifyDataErrorInfoCellErrors.Remove(cell, out object[] expectedErrors)) + { + return; + } + + IEnumerable currentErrors = DataValidationErrors.GetErrors(cell); + if (currentErrors is null) + { + return; + } + + int index = 0; + foreach (object currentError in currentErrors) + { + if (index >= expectedErrors.Length || !Equals(currentError, expectedErrors[index])) + { + return; + } + + index++; + } + + if (index == expectedErrors.Length) + { + ClearCellValidation(cell); + } + } + private void ClearRowValidation(DataGridRow row) { if (row is null) @@ -156,6 +197,7 @@ private void ClearRowValidation(DataGridRow row) foreach (DataGridCell cell in row.Cells) { + _notifyDataErrorInfoCellErrors.Remove(cell); ClearCellValidation(cell); } @@ -585,6 +627,7 @@ private static List CreateValidationExceptions(IEnumerable errors) private bool _isValid = true; private readonly HashSet _collectionValidationTrackedItems = new(ReferenceEqualityComparer.Instance); private readonly HashSet _collectionValidationItemsWithError = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary _notifyDataErrorInfoCellErrors = new(); private bool _collectionValidationStateInitialized; private bool _collectionValidationStateInvalidated = true; From c3e71a5b74a83f0b4a738d3fe0f15a642c64dd1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 07:53:19 +0200 Subject: [PATCH 04/13] Refresh non-editing cells during row edit --- .../Validation/DataGridValidationTests.cs | 32 +++++++++++++++++++ .../DataGrid.Validation.cs | 18 +++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs index 8d451d8a..62dacb06 100644 --- a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs +++ b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs @@ -543,6 +543,38 @@ public void INotifyDataErrorInfo_all_property_change_clears_removed_indei_error( } } + [AvaloniaFact] + public void INotifyDataErrorInfo_change_refreshes_non_editing_cell_on_editing_row() + { + var (grid, root, item, errorColumn, warningColumn) = CreateMixedValidationGrid(); + + try + { + int slot = grid.SlotFromRowIndex(0); + Assert.True(grid.UpdateSelectionAndCurrency( + warningColumn.Index, + slot, + DataGridSelectionAction.SelectCurrent, + scrollIntoView: false)); + Assert.True(grid.BeginEdit()); + grid.UpdateLayout(); + + DataGridCell errorCell = FindCell(grid, item, errorColumn.Index); + Assert.Equal(DataGridValidationSeverity.Error, errorCell.ValidationSeverity); + + item.ErrorValue = "Valid"; + grid.UpdateLayout(); + + Assert.Equal(DataGridValidationSeverity.None, errorCell.ValidationSeverity); + Assert.False(DataValidationErrors.GetHasErrors(errorCell)); + Assert.Equal(warningColumn.Index, grid.EditingColumnIndex); + } + finally + { + root.Close(); + } + } + [AvaloniaFact] public void Grid_invalid_when_offscreen_item_has_error() { diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs index 5001ea5a..45c58a4c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs @@ -72,7 +72,8 @@ private void RestoreRowValidationState( DataGridRow row, object item, bool clearIfNoIndei = true, - string propertyName = null) + string propertyName = null, + int excludedColumnIndex = -1) { if (row is null) { @@ -101,6 +102,11 @@ private void RestoreRowValidationState( continue; } + if (column.Index == excludedColumnIndex) + { + continue; + } + var cell = row.Cells[column.Index]; var bindingPath = GetColumnBindingPath(column); @@ -548,13 +554,19 @@ private void RefreshRealizedRowValidationState( slot++) { if (DisplayData.GetDisplayedElement(slot) is not DataGridRow row || - ReferenceEquals(row, EditingRow) || !ReferenceEquals(row.DataContext, notifyDataErrorInfo)) { continue; } - RestoreRowValidationState(row, notifyDataErrorInfo, propertyName: propertyName); + int excludedColumnIndex = ReferenceEquals(row, EditingRow) + ? _editingColumnIndex + : -1; + RestoreRowValidationState( + row, + notifyDataErrorInfo, + propertyName: propertyName, + excludedColumnIndex: excludedColumnIndex); } } From 496f4e8c3c3096e6077d7cc777aa13c4caef19a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 08:04:51 +0200 Subject: [PATCH 05/13] Preserve binding errors during INDEI refresh --- .../Validation/DataGridValidationTests.cs | 35 ++++++ .../DataGrid.Validation.cs | 111 ++++++++++++++---- 2 files changed, 124 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs index 62dacb06..1923341f 100644 --- a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs +++ b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs @@ -478,6 +478,41 @@ public void INotifyDataErrorInfo_programmatic_change_refreshes_realized_cell_val } } + [AvaloniaFact] + public void INotifyDataErrorInfo_refresh_preserves_same_cell_binding_error() + { + var (grid, root, item, column) = CreateNotifyValidationGrid(initialCode: "Valid"); + + try + { + DataGridCell cell = FindCell(grid, item, column.Index); + var bindingError = new DataValidationException( + new DataGridValidationResult("Binding error", DataGridValidationSeverity.Error)); + DataValidationErrors.SetError(cell, bindingError); + object displayedBindingError = Assert.Single(DataValidationErrors.GetErrors(cell)); + cell.IsValid = false; + cell.ValidationSeverity = DataGridValidationSeverity.Error; + cell.UpdatePseudoClasses(); + + item.Code = "X"; + grid.UpdateLayout(); + + Assert.Contains(displayedBindingError, DataValidationErrors.GetErrors(cell)); + Assert.True(DataValidationErrors.GetHasErrors(cell)); + + item.Code = "Valid"; + grid.UpdateLayout(); + + Assert.Same(displayedBindingError, Assert.Single(DataValidationErrors.GetErrors(cell))); + Assert.Equal(DataGridValidationSeverity.Error, cell.ValidationSeverity); + Assert.False(cell.IsValid); + } + finally + { + root.Close(); + } + } + [AvaloniaFact] public void INotifyDataErrorInfo_all_property_change_preserves_binding_validation() { diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs index 45c58a4c..07844b57 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs @@ -58,6 +58,8 @@ private void ResetValidationStatus(DataGridCell editingCell = null) private void ClearCellValidation(DataGridCell cell) { + _notifyDataErrorInfoCellErrors.Remove(cell); + if (!cell.IsValid || cell.ValidationSeverity != DataGridValidationSeverity.None) { cell.IsValid = true; @@ -135,7 +137,10 @@ private void RestoreRowValidationState( continue; } - var severity = ValidationUtil.GetValidationSeverity(exceptions); + var preservedErrors = GetErrorsWithoutOwnedNotifyDataErrorInfoErrors(cell); + var severity = MaxSeverity( + ValidationUtil.GetValidationSeverity(exceptions), + GetDisplayedValidationSeverity(preservedErrors)); cell.IsValid = severity != DataGridValidationSeverity.Error; cell.ValidationSeverity = severity; cell.UpdatePseudoClasses(); @@ -143,18 +148,9 @@ private void RestoreRowValidationState( var errorException = exceptions.Count == 1 ? exceptions[0] : new AggregateException(exceptions); - DataValidationErrors.SetError(cell, errorException); - IEnumerable displayedErrors = DataValidationErrors.GetErrors(cell); - if (displayedErrors is not null) - { - var trackedErrors = new List(); - foreach (object displayedError in displayedErrors) - { - trackedErrors.Add(displayedError); - } - - _notifyDataErrorInfoCellErrors[cell] = trackedErrors.ToArray(); - } + preservedErrors.Add(errorException); + DataValidationErrors.SetErrors(cell, preservedErrors); + _notifyDataErrorInfoCellErrors[cell] = new object[] { errorException }; } UpdateRowValidationStateFromCells(row); @@ -168,27 +164,98 @@ private void ClearNotifyDataErrorInfoValidation(DataGridCell cell) return; } - IEnumerable currentErrors = DataValidationErrors.GetErrors(cell); - if (currentErrors is null) + List preservedErrors = GetErrorsWithoutOwnedErrors(cell, expectedErrors); + if (preservedErrors.Count == 0) { + ClearCellValidation(cell); return; } - int index = 0; - foreach (object currentError in currentErrors) + DataValidationErrors.SetErrors(cell, preservedErrors); + DataGridValidationSeverity severity = GetDisplayedValidationSeverity(preservedErrors); + cell.IsValid = severity != DataGridValidationSeverity.Error; + cell.ValidationSeverity = severity; + cell.UpdatePseudoClasses(); + } + + private List GetErrorsWithoutOwnedNotifyDataErrorInfoErrors(DataGridCell cell) + { + return _notifyDataErrorInfoCellErrors.TryGetValue(cell, out object[] ownedErrors) + ? GetErrorsWithoutOwnedErrors(cell, ownedErrors) + : GetDisplayedErrors(cell); + } + + private static List GetErrorsWithoutOwnedErrors(DataGridCell cell, object[] ownedErrors) + { + List errors = GetDisplayedErrors(cell); + foreach (object ownedError in ownedErrors) { - if (index >= expectedErrors.Length || !Equals(currentError, expectedErrors[index])) + for (int index = 0; index < errors.Count; index++) { - return; + if (ReferenceEquals(errors[index], ownedError)) + { + errors.RemoveAt(index); + break; + } + } + } + + return errors; + } + + private static List GetDisplayedErrors(DataGridCell cell) + { + var errors = new List(); + IEnumerable displayedErrors = DataValidationErrors.GetErrors(cell); + if (displayedErrors is null) + { + return errors; + } + + foreach (object error in displayedErrors) + { + if (error is not null) + { + errors.Add(error); } + } + + return errors; + } - index++; + private static DataGridValidationSeverity GetDisplayedValidationSeverity(List errors) + { + DataGridValidationSeverity severity = DataGridValidationSeverity.None; + foreach (object error in errors) + { + severity = MaxSeverity(severity, GetDisplayedValidationSeverity(error)); } - if (index == expectedErrors.Length) + return severity; + } + + private static DataGridValidationSeverity GetDisplayedValidationSeverity(object error) + { + if (error is AggregateException aggregateException) { - ClearCellValidation(cell); + DataGridValidationSeverity severity = DataGridValidationSeverity.None; + foreach (Exception innerException in aggregateException.InnerExceptions) + { + severity = MaxSeverity(severity, GetDisplayedValidationSeverity(innerException)); + } + + return severity; } + + return ValidationUtil.GetValidationSeverity( + error as Exception ?? new DataValidationException(error)); + } + + private static DataGridValidationSeverity MaxSeverity( + DataGridValidationSeverity first, + DataGridValidationSeverity second) + { + return first >= second ? first : second; } private void ClearRowValidation(DataGridRow row) From 8eb61ab8e7e6d82fec2f170841c161444eb4cb68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 08:16:59 +0200 Subject: [PATCH 06/13] Refresh INDEI ownership after cell edits --- .../Validation/DataGridValidationTests.cs | 33 +++++++++++++++++++ .../DataGrid.Editing.cs | 33 +++++++++++++++++-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs index 1923341f..a770ed2b 100644 --- a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs +++ b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs @@ -412,6 +412,39 @@ public void INotifyDataErrorInfo_edit_clears_cell_errors_while_editing() } } + [AvaloniaFact] + public void INotifyDataErrorInfo_committed_warning_is_cleared_by_later_model_change() + { + var (grid, root, item, column) = CreateNotifyValidationGrid(); + + try + { + int slot = grid.SlotFromRowIndex(0); + Assert.True(grid.UpdateSelectionAndCurrency( + column.Index, + slot, + DataGridSelectionAction.SelectCurrent, + scrollIntoView: false)); + Assert.True(grid.BeginEdit()); + grid.UpdateLayout(); + + Assert.True(grid.CommitEdit(DataGridEditingUnit.Cell, exitEditingMode: true)); + DataGridCell cell = FindCell(grid, item, column.Index); + Assert.Equal(DataGridValidationSeverity.Warning, cell.ValidationSeverity); + Assert.True(DataValidationErrors.GetHasErrors(cell)); + + item.Code = "Valid"; + grid.UpdateLayout(); + + Assert.Equal(DataGridValidationSeverity.None, cell.ValidationSeverity); + Assert.False(DataValidationErrors.GetHasErrors(cell)); + } + finally + { + root.Close(); + } + } + [AvaloniaFact] public void INotifyDataErrorInfo_validation_restores_on_row_recycle() { diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs index 43313568..f53ed69e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs @@ -346,6 +346,12 @@ private bool BeginCellEdit(RoutedEventArgs editingEventArgs) // Finally, we can prepare the cell for editing _editingCellValidationSnapshot = CellValidationSnapshot.Capture(dataGridCell); + _editingCellHadNotifyDataErrorInfoValidation = + _notifyDataErrorInfoCellErrors.ContainsKey(dataGridCell); + _editingCellPreservedValidationErrors = _editingCellHadNotifyDataErrorInfoValidation + ? GetErrorsWithoutOwnedNotifyDataErrorInfoErrors(dataGridCell).ToArray() + : null; + _notifyDataErrorInfoCellErrors.Remove(dataGridCell); // Hide existing cell errors while editing to avoid duplicate validation visuals. DataValidationErrors.ClearErrors(dataGridCell); _editingColumnIndex = CurrentColumnIndex; @@ -526,8 +532,25 @@ void SetValidationStatus(ICellEditBinding binding) if (editingCell != null) { - DataValidationErrors.SetError(editingCell, - new AggregateException(binding.ValidationErrors)); + var aggregateError = new AggregateException(binding.ValidationErrors); + if (_editingCellHadNotifyDataErrorInfoValidation) + { + var displayedErrors = new List(); + if (_editingCellPreservedValidationErrors is not null) + { + displayedErrors.AddRange(_editingCellPreservedValidationErrors); + } + + displayedErrors.Add(aggregateError); + DataValidationErrors.SetErrors(editingCell, displayedErrors); + _notifyDataErrorInfoCellErrors[editingCell] = + new object[] { aggregateError }; + } + else + { + _notifyDataErrorInfoCellErrors.Remove(editingCell); + DataValidationErrors.SetError(editingCell, aggregateError); + } } } } @@ -586,6 +609,8 @@ void SetValidationStatus(ICellEditBinding binding) } } _editingCellValidationSnapshot = null; + _editingCellHadNotifyDataErrorInfoValidation = false; + _editingCellPreservedValidationErrors = null; if (exitEditingMode) { @@ -1061,6 +1086,8 @@ private void ResetEditingRow() EditingRow = null; _editingRowValidationSnapshot = null; _editingCellValidationSnapshot = null; + _editingCellHadNotifyDataErrorInfoValidation = false; + _editingCellPreservedValidationErrors = null; } @@ -1415,6 +1442,8 @@ virtual void OnRowEditEnding(DataGridRowEditEndingEventArgs e) private bool _focusEditingControl; private List _editingRowValidationSnapshot; private CellValidationSnapshot _editingCellValidationSnapshot; + private bool _editingCellHadNotifyDataErrorInfoValidation; + private object[] _editingCellPreservedValidationErrors; /// From c32849572ee0c6fca786803f3937a42a20ffd874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 08:36:37 +0200 Subject: [PATCH 07/13] Track INDEI warnings introduced by edits --- .../Validation/DataGridValidationTests.cs | 36 +++++++++++++++++++ .../DataGrid.Editing.cs | 7 +++- .../DataGrid.Validation.cs | 30 ++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs index a770ed2b..ec2a35a3 100644 --- a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs +++ b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs @@ -445,6 +445,42 @@ public void INotifyDataErrorInfo_committed_warning_is_cleared_by_later_model_cha } } + [AvaloniaFact] + public void INotifyDataErrorInfo_warning_introduced_by_edit_is_later_cleared() + { + var (grid, root, item, column) = CreateNotifyValidationGrid(initialCode: "Valid"); + + try + { + int slot = grid.SlotFromRowIndex(0); + Assert.True(grid.UpdateSelectionAndCurrency( + column.Index, + slot, + DataGridSelectionAction.SelectCurrent, + scrollIntoView: false)); + Assert.True(grid.BeginEdit()); + grid.UpdateLayout(); + + DataGridCell cell = FindCell(grid, item, column.Index); + TextBox textBox = Assert.IsType(cell.Content); + textBox.Text = "X"; + UpdateEditingElementSource(textBox); + Assert.True(grid.CommitEdit(DataGridEditingUnit.Cell, exitEditingMode: true)); + Assert.Equal(DataGridValidationSeverity.Warning, cell.ValidationSeverity); + Assert.True(DataValidationErrors.GetHasErrors(cell)); + + item.Code = "Valid"; + grid.UpdateLayout(); + + Assert.Equal(DataGridValidationSeverity.None, cell.ValidationSeverity); + Assert.False(DataValidationErrors.GetHasErrors(cell)); + } + finally + { + root.Close(); + } + } + [AvaloniaFact] public void INotifyDataErrorInfo_validation_restores_on_row_recycle() { diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs index f53ed69e..a8103ea4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs @@ -533,7 +533,12 @@ void SetValidationStatus(ICellEditBinding binding) if (editingCell != null) { var aggregateError = new AggregateException(binding.ValidationErrors); - if (_editingCellHadNotifyDataErrorInfoValidation) + bool isNotifyDataErrorInfoValidation = + _editingCellHadNotifyDataErrorInfoValidation || + HasNotifyDataErrorInfoValidation( + editingRow?.DataContext, + editingCell.OwningColumn); + if (isNotifyDataErrorInfoValidation) { var displayedErrors = new List(); if (_editingCellPreservedValidationErrors is not null) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs index 07844b57..688638ec 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs @@ -699,6 +699,36 @@ private static List CreateValidationExceptions(IEnumerable errors) return exceptions; } + private static bool HasNotifyDataErrorInfoValidation(object item, DataGridColumn column) + { + if (item is not INotifyDataErrorInfo notifyDataErrorInfo) + { + return false; + } + + string bindingPath = GetColumnBindingPath(column); + if (string.IsNullOrWhiteSpace(bindingPath)) + { + return false; + } + + IEnumerable errors = notifyDataErrorInfo.GetErrors(bindingPath); + if (errors is null) + { + return false; + } + + foreach (object error in errors) + { + if (error is not null) + { + return true; + } + } + + return false; + } + private List _bindingValidationErrors; private IDisposable _validationSubscription; From 149af8748a20d87ac1cdd46f5afc88d39bb1d1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 08:59:03 +0200 Subject: [PATCH 08/13] Preserve non-INDEI edit validation errors --- .../Validation/DataGridValidationTests.cs | 140 ++++++++++++++++++ .../DataGrid.Editing.cs | 29 ++-- .../DataGrid.Validation.cs | 50 +++++-- 3 files changed, 198 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs index ec2a35a3..0c114ed8 100644 --- a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs +++ b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs @@ -12,6 +12,7 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Data.Core.Plugins; using Avalonia.Headless.XUnit; @@ -647,6 +648,71 @@ public void INotifyDataErrorInfo_all_property_change_clears_removed_indei_error( } } + [AvaloniaFact] + public void INotifyDataErrorInfo_clear_preserves_same_cell_setter_warning_after_edit() + { + var item = new MixedValidationItem + { + WarningValue = "Okay" + }; + item.SetWarningValueError(); + var root = new Window + { + Width = 600, + Height = 300 + }; + root.SetThemeStyles(DataGridTheme.Simple); + var grid = new DataGrid + { + ItemsSource = new ObservableCollection { item }, + AutoGenerateColumns = false + }; + var warningColumn = new MixedSourceValidationColumn(); + grid.ColumnsInternal.Add(warningColumn); + root.Content = grid; + root.Show(); + grid.ApplyTemplate(); + grid.UpdateLayout(); + + try + { + int slot = grid.SlotFromRowIndex(0); + Assert.True(grid.UpdateSelectionAndCurrency( + warningColumn.Index, + slot, + DataGridSelectionAction.SelectCurrent, + scrollIntoView: false)); + Assert.True(grid.BeginEdit()); + grid.UpdateLayout(); + + DataGridCell warningCell = FindCell(grid, item, warningColumn.Index); + Assert.True(grid.CommitEdit(DataGridEditingUnit.Cell, exitEditingMode: true)); + grid.UpdateLayout(); + + Assert.Contains( + DataValidationErrors.GetErrors(warningCell), + error => ErrorContainsMessage(error, "Model warning")); + Assert.Contains( + DataValidationErrors.GetErrors(warningCell), + error => ErrorContainsMessage(error, "Warning value should")); + + item.ClearWarningValueError(); + grid.UpdateLayout(); + + Assert.DoesNotContain( + DataValidationErrors.GetErrors(warningCell), + error => ErrorContainsMessage(error, "Model warning")); + Assert.Contains( + DataValidationErrors.GetErrors(warningCell), + error => ErrorContainsMessage(error, "Warning value should")); + Assert.Equal(DataGridValidationSeverity.Warning, warningCell.ValidationSeverity); + } + finally + { + root.Close(); + } + } + [AvaloniaFact] public void INotifyDataErrorInfo_change_refreshes_non_editing_cell_on_editing_row() { @@ -2057,6 +2123,18 @@ public void NotifyAllPropertiesChanged() ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(null)); } + public void SetWarningValueError() + { + SetError( + nameof(WarningValue), + new DataGridValidationResult("Model warning.", DataGridValidationSeverity.Warning)); + } + + public void ClearWarningValueError() + { + SetError(nameof(WarningValue), null); + } + private void SetError(string propertyName, DataGridValidationResult? error) { if (error == null) @@ -2094,6 +2172,68 @@ private bool SetProperty(ref T field, T value, [CallerMemberName] string? pro } } + private sealed class MixedSourceValidationColumn : DataGridComboBoxColumn + { + public MixedSourceValidationColumn() + { + Header = "Warning"; + SelectedValueBinding = TwoWayBinding(nameof(MixedValidationItem.WarningValue)); + } + + protected override Control GenerateEditingElement( + DataGridCell cell, + object dataItem, + out ICellEditBinding editBinding) + { + editBinding = new MixedSourceEditBinding(); + return new TextBox(); + } + + protected override object PrepareCellForEdit( + Control editingElement, + Avalonia.Interactivity.RoutedEventArgs editingEventArgs) + { + return null!; + } + } + + private sealed class MixedSourceEditBinding : ICellEditBinding + { + public bool IsValid => true; + + public IEnumerable ValidationErrors => new Exception[] + { + new DataValidationException( + new DataGridValidationResult("Model warning.", DataGridValidationSeverity.Warning)), + new DataValidationException( + new DataGridValidationResult( + "Warning value should be at least 3 characters.", + DataGridValidationSeverity.Warning)) + }; + + public IObservable ValidationChanged { get; } = new EmptyValidationObservable(); + + public bool CommitEdit() => true; + + public void Dispose() + { + } + } + + private sealed class EmptyValidationObservable : IObservable + { + public IDisposable Subscribe(IObserver observer) => EmptyValidationDisposable.Instance; + } + + private sealed class EmptyValidationDisposable : IDisposable + { + public static EmptyValidationDisposable Instance { get; } = new(); + + public void Dispose() + { + } + } + private sealed class ExceptionNotifyValidationItem : INotifyPropertyChanged, INotifyDataErrorInfo { private string _code = string.Empty; diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs index a8103ea4..427325d9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs @@ -532,13 +532,11 @@ void SetValidationStatus(ICellEditBinding binding) if (editingCell != null) { - var aggregateError = new AggregateException(binding.ValidationErrors); - bool isNotifyDataErrorInfoValidation = - _editingCellHadNotifyDataErrorInfoValidation || - HasNotifyDataErrorInfoValidation( - editingRow?.DataContext, - editingCell.OwningColumn); - if (isNotifyDataErrorInfoValidation) + var bindingErrors = binding.ValidationErrors.ToList(); + var notifyDataErrorInfoErrors = GetNotifyDataErrorInfoValidationExceptions( + editingRow?.DataContext, + editingCell.OwningColumn); + if (notifyDataErrorInfoErrors.Count > 0) { var displayedErrors = new List(); if (_editingCellPreservedValidationErrors is not null) @@ -546,15 +544,26 @@ void SetValidationStatus(ICellEditBinding binding) displayedErrors.AddRange(_editingCellPreservedValidationErrors); } - displayedErrors.Add(aggregateError); + RemoveMatchingValidationErrors(bindingErrors, notifyDataErrorInfoErrors); + if (bindingErrors.Count > 0) + { + displayedErrors.Add(new AggregateException(bindingErrors)); + } + + object ownedError = notifyDataErrorInfoErrors.Count == 1 + ? notifyDataErrorInfoErrors[0] + : new AggregateException(notifyDataErrorInfoErrors); + displayedErrors.Add(ownedError); DataValidationErrors.SetErrors(editingCell, displayedErrors); _notifyDataErrorInfoCellErrors[editingCell] = - new object[] { aggregateError }; + new object[] { ownedError }; } else { _notifyDataErrorInfoCellErrors.Remove(editingCell); - DataValidationErrors.SetError(editingCell, aggregateError); + DataValidationErrors.SetError( + editingCell, + new AggregateException(bindingErrors)); } } } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs index 688638ec..5df5091b 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs @@ -699,34 +699,62 @@ private static List CreateValidationExceptions(IEnumerable errors) return exceptions; } - private static bool HasNotifyDataErrorInfoValidation(object item, DataGridColumn column) + private static List GetNotifyDataErrorInfoValidationExceptions( + object item, + DataGridColumn column) { if (item is not INotifyDataErrorInfo notifyDataErrorInfo) { - return false; + return new List(); } string bindingPath = GetColumnBindingPath(column); if (string.IsNullOrWhiteSpace(bindingPath)) { - return false; + return new List(); } IEnumerable errors = notifyDataErrorInfo.GetErrors(bindingPath); - if (errors is null) - { - return false; - } + return errors is null + ? new List() + : CreateValidationExceptions(errors); + } - foreach (object error in errors) + private static void RemoveMatchingValidationErrors( + List bindingErrors, + List notifyDataErrorInfoErrors) + { + foreach (Exception notifyDataErrorInfoError in notifyDataErrorInfoErrors) { - if (error is not null) + for (int index = 0; index < bindingErrors.Count; index++) { - return true; + if (ValidationErrorsMatch(bindingErrors[index], notifyDataErrorInfoError)) + { + bindingErrors.RemoveAt(index); + break; + } } } + } - return false; + private static bool ValidationErrorsMatch(Exception first, Exception second) + { + object firstData = ValidationUtil.UnpackDataValidationException(first); + object secondData = ValidationUtil.UnpackDataValidationException(second); + if (ReferenceEquals(firstData, secondData) || Equals(firstData, secondData)) + { + return true; + } + + if (firstData is DataGridValidationResult firstResult && + secondData is DataGridValidationResult secondResult) + { + return firstResult.Severity == secondResult.Severity && + string.Equals(firstResult.Message, secondResult.Message, StringComparison.Ordinal); + } + + return first.GetType() == second.GetType() && + string.Equals(first.Message, second.Message, StringComparison.Ordinal); } private List _bindingValidationErrors; From f14046cb9a122f6cc15faf4b94fba3a03ca65861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 09:15:58 +0200 Subject: [PATCH 09/13] Release validation ownership for removed cells --- .../LeakTests.cs | 64 +++++++++++++++++++ .../DataGrid.Validation.cs | 8 +++ src/Avalonia.Controls.DataGrid/DataGridRow.cs | 1 + 3 files changed, 73 insertions(+) diff --git a/src/Avalonia.Controls.DataGrid.LeakTests/LeakTests.cs b/src/Avalonia.Controls.DataGrid.LeakTests/LeakTests.cs index ee0df05e..9b0bffa1 100644 --- a/src/Avalonia.Controls.DataGrid.LeakTests/LeakTests.cs +++ b/src/Avalonia.Controls.DataGrid.LeakTests/LeakTests.cs @@ -1015,6 +1015,24 @@ WeakReference Run() GC.KeepAlive(items); } + [ReleaseFact] + public void DataGrid_RemovedNotifyDataErrorInfoCell_DoesNotLeak() + { + var items = new ObservableCollection + { + new NotifyDataErrorRowItem("A") + }; + items[0].SetErrors(new[] { "Invalid" }); + + var (grid, cellRef) = RunInSession(() => + RunRemovedNotifyDataErrorInfoCell(items)); + + AssertCollected(cellRef); + + GC.KeepAlive(grid); + GC.KeepAlive(items); + } + [ReleaseFact] public void DataGrid_NotifyDataErrorInfo_ErrorsChanged_DoesNotLeak() { @@ -1817,6 +1835,52 @@ private static WeakReference RunColumnDefinitionsSourceSwap( return gridRef; } + [MethodImpl(MethodImplOptions.NoInlining)] + private static (DataGrid Grid, WeakReference CellRef) RunRemovedNotifyDataErrorInfoCell( + ObservableCollection items) + { + var grid = new DataGrid + { + AutoGenerateColumns = false, + ItemsSource = items + }; + grid.Columns.Add(new DataGridTextColumn + { + Header = "Name", + Binding = new Binding(nameof(NotifyDataErrorRowItem.Name)) + }); + + var window = new Window + { + Width = 600, + Height = 300, + Content = grid + }; + window.SetThemeStyles(); + ShowWindow(window); + grid.ScrollIntoView(items[0], grid.Columns[0]); + Dispatcher.UIThread.RunJobs(); + PumpLayout(grid); + + grid.SelectedItem = items[0]; + grid.CurrentCell = new DataGridCellInfo(items[0], grid.Columns[0], 0, 0); + Dispatcher.UIThread.RunJobs(); + Assert.True(grid.BeginEdit()); + Dispatcher.UIThread.RunJobs(); + var row = Assert.IsType(grid.EditingRow); + DataGridCell cell = row.Cells[0]; + Assert.True(grid.CancelEdit(DataGridEditingUnit.Cell)); + Dispatcher.UIThread.RunJobs(); + Assert.True(DataValidationErrors.GetHasErrors(cell)); + var cellRef = new WeakReference(cell); + + grid.Columns.RemoveAt(0); + Dispatcher.UIThread.RunJobs(); + CleanupWindow(window); + + return (grid, cellRef); + } + [MethodImpl(MethodImplOptions.NoInlining)] private static ( WeakReference GridRef, diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs index 5df5091b..3a24ac65 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Validation.cs @@ -70,6 +70,14 @@ private void ClearCellValidation(DataGridCell cell) DataValidationErrors.ClearErrors(cell); } + internal void OnCellRemovedForValidation(DataGridCell cell) + { + if (cell is not null) + { + _notifyDataErrorInfoCellErrors.Remove(cell); + } + } + private void RestoreRowValidationState( DataGridRow row, object item, diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index 501bfc72..36133b80 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -647,6 +647,7 @@ private void DataGridCellCollection_CellAdded(object sender, DataGridCellEventAr private void DataGridCellCollection_CellRemoved(object sender, DataGridCellEventArgs e) { + OwningGrid?.OnCellRemovedForValidation(e.Cell); _cellsElement?.Children.Remove(e.Cell); } From 35f64b27d1aa59517bef4e72ff03ce204a37e89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 09:34:29 +0200 Subject: [PATCH 10/13] Preserve identical setter validation errors --- .../Validation/DataGridValidationTests.cs | 115 ++++++++++++++++++ .../DataGrid.Editing.cs | 5 +- .../Utils/CellEditBinding.cs | 26 +++- 3 files changed, 143 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs index 0c114ed8..965e8384 100644 --- a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs +++ b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs @@ -713,6 +713,69 @@ public void INotifyDataErrorInfo_clear_preserves_same_cell_setter_warning_after_ } } + [AvaloniaFact] + public void INotifyDataErrorInfo_clear_preserves_identical_setter_warning_after_edit() + { + var item = new SameMessageValidationItem(); + var root = new Window + { + Width = 600, + Height = 300 + }; + root.SetThemeStyles(DataGridTheme.Simple); + var grid = new DataGrid + { + ItemsSource = new ObservableCollection { item }, + AutoGenerateColumns = false + }; + var column = new DataGridTextColumn + { + Header = "Value", + Binding = TwoWayBinding(nameof(SameMessageValidationItem.Value)) + }; + grid.ColumnsInternal.Add(column); + root.Content = grid; + root.Show(); + grid.ApplyTemplate(); + grid.UpdateLayout(); + + try + { + int slot = grid.SlotFromRowIndex(0); + Assert.True(grid.UpdateSelectionAndCurrency( + column.Index, + slot, + DataGridSelectionAction.SelectCurrent, + scrollIntoView: false)); + Assert.True(grid.BeginEdit()); + grid.UpdateLayout(); + + DataGridCell cell = FindCell(grid, item, column.Index); + TextBox textBox = Assert.IsType(cell.Content); + textBox.Text = "Rejected"; + Assert.True(grid.CommitEdit(DataGridEditingUnit.Cell, exitEditingMode: true)); + grid.UpdateLayout(); + + Assert.Equal( + 2, + DataValidationErrors.GetErrors(cell).Count( + error => ErrorContainsMessage(error, SameMessageValidationItem.ErrorMessage))); + + item.ClearModelError(); + grid.UpdateLayout(); + + Assert.Single( + DataValidationErrors.GetErrors(cell), + error => ErrorContainsMessage(error, SameMessageValidationItem.ErrorMessage)); + Assert.Equal(DataGridValidationSeverity.Warning, cell.ValidationSeverity); + Assert.True(cell.IsValid); + } + finally + { + root.Close(); + } + } + [AvaloniaFact] public void INotifyDataErrorInfo_change_refreshes_non_editing_cell_on_editing_row() { @@ -2172,6 +2235,58 @@ private bool SetProperty(ref T field, T value, [CallerMemberName] string? pro } } + private sealed class SameMessageValidationItem : INotifyPropertyChanged, INotifyDataErrorInfo + { + public const string ErrorMessage = "Value is rejected."; + + private string _value = "Initial"; + private bool _hasModelError = true; + + public event PropertyChangedEventHandler? PropertyChanged; + public event EventHandler? ErrorsChanged; + + public string Value + { + get => _value; + set + { + if (string.Equals(value, "Rejected", StringComparison.Ordinal)) + { + throw new DataValidationException( + new DataGridValidationResult(ErrorMessage, DataGridValidationSeverity.Warning)); + } + + if (!string.Equals(_value, value, StringComparison.Ordinal)) + { + _value = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); + } + } + } + + public bool HasErrors => _hasModelError; + + public IEnumerable GetErrors(string? propertyName) + { + if (_hasModelError && + (string.IsNullOrEmpty(propertyName) || propertyName == nameof(Value))) + { + return new[] + { + new DataGridValidationResult(ErrorMessage, DataGridValidationSeverity.Warning) + }; + } + + return Array.Empty(); + } + + public void ClearModelError() + { + _hasModelError = false; + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value))); + } + } + private sealed class MixedSourceValidationColumn : DataGridComboBoxColumn { public MixedSourceValidationColumn() diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs index 427325d9..1df6b2b5 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs @@ -544,7 +544,10 @@ void SetValidationStatus(ICellEditBinding binding) displayedErrors.AddRange(_editingCellPreservedValidationErrors); } - RemoveMatchingValidationErrors(bindingErrors, notifyDataErrorInfoErrors); + if (binding is not ICellEditBindingValidationSource { HasSourceWriteError: true }) + { + RemoveMatchingValidationErrors(bindingErrors, notifyDataErrorInfoErrors); + } if (bindingErrors.Count > 0) { displayedErrors.Add(new AggregateException(bindingErrors)); diff --git a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs index bae4560e..43b125fb 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs @@ -7,6 +7,7 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Data; +using Avalonia.Data.Core; using Avalonia.PropertyStore; using Avalonia.Reactive; @@ -26,12 +27,18 @@ interface ICellEditBinding bool CommitEdit(); } - internal abstract class CellEditBindingBase : ICellEditBinding + internal interface ICellEditBindingValidationSource + { + bool HasSourceWriteError { get; } + } + + internal abstract class CellEditBindingBase : ICellEditBinding, ICellEditBindingValidationSource { private readonly AvaloniaObject _target; private readonly AvaloniaProperty _property; private readonly string _bindingPath; private readonly bool _supportsDirectSourceWriteFallback; + private readonly bool _supportsSourceWriteResult; private readonly LightweightSubject _changedSubject = new(); private readonly List _validationErrors = new(); private DataGridValidationSeverity _validationSeverity = DataGridValidationSeverity.None; @@ -42,6 +49,8 @@ protected CellEditBindingBase(AvaloniaObject target, AvaloniaProperty property, _target = target ?? throw new ArgumentNullException(nameof(target)); _property = property ?? throw new ArgumentNullException(nameof(property)); _supportsDirectSourceWriteFallback = BindingCloneHelper.SupportsDirectDataContextMemberWrite(binding); + _supportsSourceWriteResult = BindingCloneHelper.GetMode(binding) is + BindingMode.TwoWay or BindingMode.OneWayToSource; _bindingPath = _supportsDirectSourceWriteFallback && binding != null ? BindingCloneHelper.GetPath(binding) : null; @@ -52,9 +61,11 @@ protected CellEditBindingBase(AvaloniaObject target, AvaloniaProperty property, public bool IsValid => _validationSeverity != DataGridValidationSeverity.Error; public IEnumerable ValidationErrors => _validationErrors; public IObservable ValidationChanged => _changedSubject; + public bool HasSourceWriteError { get; private set; } public bool CommitEdit() { + HasSourceWriteError = false; Exception commitError = null; var expression = BindingOperations.GetBindingExpressionBase(_target, _property); var preCommitValidationErrors = CaptureValidationErrors(expression); @@ -66,11 +77,21 @@ expression is IValueEntry preCommitValueEntry && { try { - expression.UpdateSource(); + if (_supportsSourceWriteResult && + expression is UntypedBindingExpressionBase untypedExpression) + { + HasSourceWriteError = !untypedExpression.WriteValueToSource( + _target.GetValue(_property)); + } + else + { + expression.UpdateSource(); + } } catch (Exception ex) { commitError = ex; + HasSourceWriteError = true; } } @@ -96,6 +117,7 @@ expression is IValueEntry writeValueEntry && usedToggleSwitchFallback = true; if (writeError != null) { + HasSourceWriteError = true; UpdateValidationErrorsFromException(writeError); } else From c8c520cac70ec289e6738d2f1523f2f193aa0067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 09:51:13 +0200 Subject: [PATCH 11/13] Expose edit validation source provenance --- .../Validation/DataGridValidationTests.cs | 105 ++++++++++++++++++ .../DataGrid.Editing.cs | 2 +- .../Utils/CellEditBinding.cs | 18 ++- 3 files changed, 118 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs index 965e8384..7cd68b43 100644 --- a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs +++ b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs @@ -776,6 +776,63 @@ public void INotifyDataErrorInfo_clear_preserves_identical_setter_warning_after_ } } + [AvaloniaFact] + public void INotifyDataErrorInfo_clear_preserves_identical_custom_binding_warning_after_edit() + { + var item = new SameMessageValidationItem(); + var root = new Window + { + Width = 600, + Height = 300 + }; + root.SetThemeStyles(DataGridTheme.Simple); + var grid = new DataGrid + { + ItemsSource = new ObservableCollection { item }, + AutoGenerateColumns = false + }; + var column = new SameMessageCustomValidationColumn(); + grid.ColumnsInternal.Add(column); + root.Content = grid; + root.Show(); + grid.ApplyTemplate(); + grid.UpdateLayout(); + + try + { + int slot = grid.SlotFromRowIndex(0); + Assert.True(grid.UpdateSelectionAndCurrency( + column.Index, + slot, + DataGridSelectionAction.SelectCurrent, + scrollIntoView: false)); + Assert.True(grid.BeginEdit()); + grid.UpdateLayout(); + + DataGridCell cell = FindCell(grid, item, column.Index); + Assert.True(grid.CommitEdit(DataGridEditingUnit.Cell, exitEditingMode: true)); + grid.UpdateLayout(); + + Assert.Equal( + 2, + DataValidationErrors.GetErrors(cell).Count( + error => ErrorContainsMessage(error, SameMessageValidationItem.ErrorMessage))); + + item.ClearModelError(); + grid.UpdateLayout(); + + Assert.Single( + DataValidationErrors.GetErrors(cell), + error => ErrorContainsMessage(error, SameMessageValidationItem.ErrorMessage)); + Assert.Equal(DataGridValidationSeverity.Warning, cell.ValidationSeverity); + Assert.True(cell.IsValid); + } + finally + { + root.Close(); + } + } + [AvaloniaFact] public void INotifyDataErrorInfo_change_refreshes_non_editing_cell_on_editing_row() { @@ -2287,6 +2344,54 @@ public void ClearModelError() } } + private sealed class SameMessageCustomValidationColumn : DataGridComboBoxColumn + { + public SameMessageCustomValidationColumn() + { + Header = "Value"; + SelectedValueBinding = TwoWayBinding(nameof(SameMessageValidationItem.Value)); + } + + protected override Control GenerateEditingElement( + DataGridCell cell, + object dataItem, + out ICellEditBinding editBinding) + { + editBinding = new SameMessageCustomEditBinding(); + return new TextBox(); + } + + protected override object PrepareCellForEdit( + Control editingElement, + Avalonia.Interactivity.RoutedEventArgs editingEventArgs) + { + return null!; + } + } + + private sealed class SameMessageCustomEditBinding : ICellEditBinding + { + public bool IsValid => true; + + public IEnumerable ValidationErrors => new[] + { + new DataValidationException( + new DataGridValidationResult( + SameMessageValidationItem.ErrorMessage, + DataGridValidationSeverity.Warning)) + }; + + public IObservable ValidationChanged { get; } = new EmptyValidationObservable(); + + public bool HasSourceWriteError => true; + + public bool CommitEdit() => true; + + public void Dispose() + { + } + } + private sealed class MixedSourceValidationColumn : DataGridComboBoxColumn { public MixedSourceValidationColumn() diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs index 1df6b2b5..f0fcff33 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs @@ -544,7 +544,7 @@ void SetValidationStatus(ICellEditBinding binding) displayedErrors.AddRange(_editingCellPreservedValidationErrors); } - if (binding is not ICellEditBindingValidationSource { HasSourceWriteError: true }) + if (!binding.HasSourceWriteError) { RemoveMatchingValidationErrors(bindingErrors, notifyDataErrorInfoErrors); } diff --git a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs index 43b125fb..e6644460 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs @@ -24,15 +24,21 @@ interface ICellEditBinding bool IsValid { get; } IEnumerable ValidationErrors { get; } IObservable ValidationChanged { get; } - bool CommitEdit(); - } - internal interface ICellEditBindingValidationSource - { - bool HasSourceWriteError { get; } + /// + /// Gets a value indicating whether the current validation errors were produced by a + /// failed write to the binding source. + /// + /// + /// Custom edit bindings should return while exposing source-write + /// errors so the grid does not conflate them with equivalent model validation errors. + /// + bool HasSourceWriteError => false; + + bool CommitEdit(); } - internal abstract class CellEditBindingBase : ICellEditBinding, ICellEditBindingValidationSource + internal abstract class CellEditBindingBase : ICellEditBinding { private readonly AvaloniaObject _target; private readonly AvaloniaProperty _property; From 36843ba84e0823099629124a81b1ace71823a0f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 09:59:23 +0200 Subject: [PATCH 12/13] Cover default edit binding validation provenance --- .../Validation/DataGridValidationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs index 7cd68b43..eb490798 100644 --- a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs +++ b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs @@ -731,7 +731,7 @@ public void INotifyDataErrorInfo_clear_preserves_identical_setter_warning_after_ var column = new DataGridTextColumn { Header = "Value", - Binding = TwoWayBinding(nameof(SameMessageValidationItem.Value)) + Binding = new Binding(nameof(SameMessageValidationItem.Value)) }; grid.ColumnsInternal.Add(column); root.Content = grid; From 3572a59fefeb0ef9df50bfb56b510c6527efd823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 10:16:59 +0200 Subject: [PATCH 13/13] Merge edit validation severity sources --- .../Validation/DataGridValidationTests.cs | 63 +++++++++- .../DataGrid.Editing.cs | 118 ++++++++---------- 2 files changed, 115 insertions(+), 66 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs index eb490798..6fe41119 100644 --- a/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs +++ b/src/Avalonia.Controls.DataGrid.UnitTests/Validation/DataGridValidationTests.cs @@ -833,6 +833,60 @@ public void INotifyDataErrorInfo_clear_preserves_identical_custom_binding_warnin } } + [AvaloniaFact] + public void INotifyDataErrorInfo_error_overrides_custom_binding_warning_during_edit() + { + var item = new SameMessageValidationItem(DataGridValidationSeverity.Error); + var root = new Window + { + Width = 600, + Height = 300 + }; + root.SetThemeStyles(DataGridTheme.Simple); + var grid = new DataGrid + { + ItemsSource = new ObservableCollection { item }, + AutoGenerateColumns = false + }; + var column = new SameMessageCustomValidationColumn(); + grid.ColumnsInternal.Add(column); + root.Content = grid; + root.Show(); + grid.ApplyTemplate(); + grid.UpdateLayout(); + + try + { + int slot = grid.SlotFromRowIndex(0); + Assert.True(grid.UpdateSelectionAndCurrency( + column.Index, + slot, + DataGridSelectionAction.SelectCurrent, + scrollIntoView: false)); + Assert.True(grid.BeginEdit()); + grid.UpdateLayout(); + + DataGridCell cell = FindCell(grid, item, column.Index); + DataGridRow row = FindRow(grid, item); + Assert.False(grid.CommitEdit()); + grid.UpdateLayout(); + + Assert.Equal(DataGridValidationSeverity.Error, cell.ValidationSeverity); + Assert.False(cell.IsValid); + Assert.Equal(DataGridValidationSeverity.Error, row.ValidationSeverity); + Assert.False(row.IsValid); + Assert.False(grid.IsValid); + Assert.Equal( + 2, + DataValidationErrors.GetErrors(cell).Count( + error => ErrorContainsMessage(error, SameMessageValidationItem.ErrorMessage))); + } + finally + { + root.Close(); + } + } + [AvaloniaFact] public void INotifyDataErrorInfo_change_refreshes_non_editing_cell_on_editing_row() { @@ -2296,9 +2350,16 @@ private sealed class SameMessageValidationItem : INotifyPropertyChanged, INotify { public const string ErrorMessage = "Value is rejected."; + private readonly DataGridValidationSeverity _modelSeverity; private string _value = "Initial"; private bool _hasModelError = true; + public SameMessageValidationItem( + DataGridValidationSeverity modelSeverity = DataGridValidationSeverity.Warning) + { + _modelSeverity = modelSeverity; + } + public event PropertyChangedEventHandler? PropertyChanged; public event EventHandler? ErrorsChanged; @@ -2330,7 +2391,7 @@ public IEnumerable GetErrors(string? propertyName) { return new[] { - new DataGridValidationResult(ErrorMessage, DataGridValidationSeverity.Warning) + new DataGridValidationResult(ErrorMessage, _modelSeverity) }; } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs index f0fcff33..29722718 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.Editing.cs @@ -503,16 +503,46 @@ private bool EndCellEdit(DataGridEditAction editAction, bool exitEditingMode, bo // If we're committing, explicitly update the source but watch out for any validation errors if (editAction == DataGridEditAction.Commit) { - void SetValidationStatus(ICellEditBinding binding) + DataGridValidationSeverity SetValidationStatus(ICellEditBinding binding) { - var severity = ValidationUtil.GetValidationSeverity(binding.ValidationErrors); + var bindingErrors = binding.ValidationErrors.ToList(); + var notifyDataErrorInfoErrors = editingCell is null + ? new List() + : GetNotifyDataErrorInfoValidationExceptions( + editingRow?.DataContext, + editingCell.OwningColumn); + var displayedErrors = new List(); + if (_editingCellPreservedValidationErrors is not null) + { + displayedErrors.AddRange(_editingCellPreservedValidationErrors); + } + + if (notifyDataErrorInfoErrors.Count > 0 && !binding.HasSourceWriteError) + { + RemoveMatchingValidationErrors(bindingErrors, notifyDataErrorInfoErrors); + } + if (bindingErrors.Count > 0) + { + displayedErrors.Add(new AggregateException(bindingErrors)); + } + + if (notifyDataErrorInfoErrors.Count > 0) + { + object ownedError = notifyDataErrorInfoErrors.Count == 1 + ? notifyDataErrorInfoErrors[0] + : new AggregateException(notifyDataErrorInfoErrors); + displayedErrors.Add(ownedError); + _notifyDataErrorInfoCellErrors[editingCell] = new object[] { ownedError }; + } + else if (editingCell is not null) + { + _notifyDataErrorInfoCellErrors.Remove(editingCell); + } + + var severity = GetDisplayedValidationSeverity(displayedErrors); if (severity == DataGridValidationSeverity.None) { ResetValidationStatus(editingCell); - if (editingElement != null) - { - DataValidationErrors.ClearErrors(editingElement); - } } else { @@ -525,78 +555,36 @@ void SetValidationStatus(ICellEditBinding binding) } UpdateGridValidationState(); - if (editingElement != null) - { - DataValidationErrors.ClearErrors(editingElement); - } - if (editingCell != null) { - var bindingErrors = binding.ValidationErrors.ToList(); - var notifyDataErrorInfoErrors = GetNotifyDataErrorInfoValidationExceptions( - editingRow?.DataContext, - editingCell.OwningColumn); - if (notifyDataErrorInfoErrors.Count > 0) - { - var displayedErrors = new List(); - if (_editingCellPreservedValidationErrors is not null) - { - displayedErrors.AddRange(_editingCellPreservedValidationErrors); - } - - if (!binding.HasSourceWriteError) - { - RemoveMatchingValidationErrors(bindingErrors, notifyDataErrorInfoErrors); - } - if (bindingErrors.Count > 0) - { - displayedErrors.Add(new AggregateException(bindingErrors)); - } - - object ownedError = notifyDataErrorInfoErrors.Count == 1 - ? notifyDataErrorInfoErrors[0] - : new AggregateException(notifyDataErrorInfoErrors); - displayedErrors.Add(ownedError); - DataValidationErrors.SetErrors(editingCell, displayedErrors); - _notifyDataErrorInfoCellErrors[editingCell] = - new object[] { ownedError }; - } - else - { - _notifyDataErrorInfoCellErrors.Remove(editingCell); - DataValidationErrors.SetError( - editingCell, - new AggregateException(bindingErrors)); - } + DataValidationErrors.SetErrors(editingCell, displayedErrors); } } + + if (editingElement != null) + { + DataValidationErrors.ClearErrors(editingElement); + } + + return severity; } var editBinding = CurrentColumn?.CellEditBinding; if (editBinding != null) { editBinding.CommitEdit(); - var severity = ValidationUtil.GetValidationSeverity(editBinding.ValidationErrors); - if (severity != DataGridValidationSeverity.None) + var severity = SetValidationStatus(editBinding); + if (severity == DataGridValidationSeverity.Error) { - SetValidationStatus(editBinding); - - if (severity == DataGridValidationSeverity.Error) - { - _validationSubscription?.Dispose(); - _validationSubscription = editBinding.ValidationChanged.Subscribe(v => SetValidationStatus(editBinding)); - - ScrollSlotIntoView(CurrentColumnIndex, CurrentSlot, forCurrentCellChange: false, forceHorizontalScroll: true); - return false; - } - _validationSubscription?.Dispose(); - _validationSubscription = null; - } - else - { - ResetValidationStatus(editingCell); + _validationSubscription = editBinding.ValidationChanged.Subscribe(v => SetValidationStatus(editBinding)); + + ScrollSlotIntoView(CurrentColumnIndex, CurrentSlot, forCurrentCellChange: false, forceHorizontalScroll: true); + return false; } + + _validationSubscription?.Dispose(); + _validationSubscription = null; } else {