Repository: AvaloniaUI/Avalonia

Issue:
DataValidationErrors not working correctly with Bindings (in AutoCompleteBox control)
### Describe the bug

When using Bindings to synchronize `DataValidationErrors.Errors` for one control with the `DataValidationErrors.Errors` property of another control, the secondary control's `DataValidationErrors.HasErrors` property does not get updated correctly. 

### To Reproduce

This is most easily observed in the AutoComlpeteBox control. The control uses a child TextBox control, and in the theme file, the TextBox uses this line to synchronize its Errors property: `DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"`

When the parent AutoCompleteBox's Errors property gets updated, the child TextBox does have its Errors synchronized, however its HasErrors property remains false even though the property is true for the parent control. (You can easily see this with the F12 DevTools, looking at the `DataValidationErrors` properties for the child TextBox)

[AutoCompleteBoxTest.zip](https://github.com/user-attachments/files/19825398/AutoCompleteBoxTest.zip)

NOTE: I have also included the AutoCompleteBoxTest project in my fork of Avalonia here: [https://github.com/drone1400/Avalonia/tree/temp/drone-samples-autocompletebox-test](https://github.com/drone1400/Avalonia/tree/temp/drone-samples-autocompletebox-test)

I have attached a simple application with both an AutoCompleteBox and second TextBox control with both of their Text properties bound to the same string property in the view model. When the string property change triggers a Validation update, the stand-alone TextBox control displays the validation error message correctly, while the AutoCompleteBox does not.

NOTE: The AutoCompleteBox does display the validation error message upon first initialization, but not subsequent updates.



### Expected behavior

The inner TextBox control of the AutoCompleteBox should have its `DataValidationErrors.HasErrors` property updated when its `DataValidationErrors.Errors` property is updated using Bindings.

### Avalonia version

11.2.7

### OS

Windows

### Additional context

I actually looked into why this was happening and found a fix for it before submitting this issue. I will be submitting a PR as well for it, but here is an explanation.

The issue is located in the Avalonia.Controls project, in the DataValidationErrors.cs file.

The root of the issue seems to be that the DataValidationErrors uses the `private static bool s_overridingErrors` field to temporarily inhibit the `ErrorsChanged` handler after updating the `Errors` property internally in `OnErrorsOrConverterChanged`

While this is usually fine, when using bindings, the 2nd control's `ErrorsChanged` handler gets called before `s_overridingErrors` has a chance to be cleared and thus no longer gets executed.

In the case of the AutoCompleteBox, after changing its Errors property, upon entering the handler `private static void ErrorsChanged(AvaloniaPropertyChangedEventArgs e)`, the `e.Sender` and `s_overridingErrors` properties end up having the following values:

|   | e.Sender        | s_overridingErrors |
|---|-----------------|--------------------|
| 1 | AutoCompleteBox | false              |
| 2 | AutoCompleteBox | true               |
| 3 | PART_TextBox    | true               |
| 4 | AutoCompleteBox | false              |
| 5 | AutoCompleteBox |  true             |
| 6 | PART_TextBox    | true               |

When the handler gets executed for the child TextBox, the `s_overridingErrors` property is true each time and nothing gets executed.

As I mentioned above, I will be submitting a PR with my fix for this issue momentarily.


Hints:

Patch:
diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs
index 243032e725c..f11c6210304 100644
--- a/src/Avalonia.Controls/DataValidationErrors.cs
+++ b/src/Avalonia.Controls/DataValidationErrors.cs
@@ -18,7 +18,6 @@ namespace Avalonia.Controls
     [PseudoClasses(":error")]
     public class DataValidationErrors : ContentControl
     {
-        private static bool s_overridingErrors;
         
         /// <summary>
         /// Defines the DataValidationErrors.Errors attached property.
@@ -49,6 +48,12 @@ public class DataValidationErrors : ContentControl
         /// </summary>
         private static readonly AttachedProperty<IEnumerable<object>?> OriginalErrorsProperty =
             AvaloniaProperty.RegisterAttached<DataValidationErrors, Control, IEnumerable<object>?>("OriginalErrors");
+        
+        /// <summary>
+        /// Prevents executing ErrorsChanged after they are updated internally from OnErrorsOrConverterChanged
+        /// </summary>
+        private static readonly AttachedProperty<bool> OverridingErrorsInternallyProperty =
+            AvaloniaProperty.RegisterAttached<DataValidationErrors, Control, bool>("OverridingErrorsInternally", defaultValue: false);
 
         private Control? _owner;
 
@@ -96,9 +101,10 @@ public IDataTemplate ErrorTemplate
 
         private static void ErrorsChanged(AvaloniaPropertyChangedEventArgs e)
         {
-            if (s_overridingErrors) return;
-
             var control = (Control)e.Sender;
+            
+            if (control.GetValue(OverridingErrorsInternallyProperty)) return;
+            
             var errors = (IEnumerable<object>?)e.NewValue;
 
             // Update original errors
@@ -140,14 +146,14 @@ private static void OnErrorsOrConverterChanged(Control control)
                     .Where(e => e is not null))?
                 .ToArray();
 
-            s_overridingErrors = true;
+            control.SetCurrentValue(OverridingErrorsInternallyProperty, true);
             try
             {
                 control.SetCurrentValue(ErrorsProperty, newErrors!);
             }
             finally
             {
-                s_overridingErrors = false;
+                control.SetCurrentValue(OverridingErrorsInternallyProperty, false);
             }
 
             control.SetValue(HasErrorsProperty, newErrors?.Any() == true);


Test patch:
diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
index 67f55055ec8..d13a25f2105 100644
--- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
@@ -421,6 +421,28 @@ public void Text_Validation()
             });
         }
         
+        [Fact]
+        public void Text_Validation_TextBox_Errors_Binding()
+        {
+            RunTest((control, textbox) =>
+            {
+                // simulate the TemplateBinding that would be used within the AutoCompleteBox control theme for the inner PART_TextBox
+                //      DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
+                textbox.Bind(DataValidationErrors.ErrorsProperty, control.GetBindingObservable(DataValidationErrors.ErrorsProperty));
+                
+                var exception = new InvalidCastException("failed validation");
+                var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
+                control.Bind(AutoCompleteBox.TextProperty, textObservable);
+                Dispatcher.UIThread.RunJobs();
+                
+                Assert.True(DataValidationErrors.GetHasErrors(control));
+                Assert.Equal([exception], DataValidationErrors.GetErrors(control));
+                
+                Assert.True(DataValidationErrors.GetHasErrors(textbox));
+                Assert.Equal([exception], DataValidationErrors.GetErrors(textbox));
+            });
+        }
+        
         [Fact]
         public void SelectedItem_Validation()
         {