Selaa lähdekoodia

Fixes spurious DataGrid data validation error (#15716)

* Added failing tests for #15081.

* Provide target property in BindingExpression ctor.

Usually it is not necessary to provide the target property when creating a `BindingExpression` because the property will be assigned when the binding expression is attached to the target in `BindingExpressionBase.Attach`.

This is however one case where `Attach` is not called: when the obsolete `binding.Initiate` method is called and then an observable is read from the `InstancedBinding` without the binding actually being attached to the target object. In this case, prior to the binding refactor in #13970 the value produced by the observable was still converted to the target type. After #13970, because the target property (and hence the target type) is not yet set, the conversion is to the target type is no longer done.

`DataGrid` uses this obsolete method when editing cells, causing #15081. Ideally we'd fix that in `DataGrid` but I'm not happy making this change so close to 11.1, so instead fix this use-case to behave as before.

Fixes #15081
Steven Kirk 1 vuosi sitten
vanhempi
sitoutus
8fe6e08020

+ 3 - 1
src/Avalonia.Base/Data/Core/BindingExpression.cs

@@ -48,6 +48,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
     /// <param name="mode">The binding mode.</param>
     /// <param name="priority">The binding priority.</param>
     /// <param name="stringFormat">The format string to use.</param>
+    /// <param name="targetProperty">The target property being bound to.</param>
     /// <param name="targetNullValue">The null target value.</param>
     /// <param name="targetTypeConverter">
     /// A final type converter to be run on the produced value.
@@ -65,9 +66,10 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
         BindingPriority priority = BindingPriority.LocalValue,
         string? stringFormat = null,
         object? targetNullValue = null,
+        AvaloniaProperty? targetProperty = null,
         TargetTypeConverter? targetTypeConverter = null,
         UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.PropertyChanged)
-            : base(priority, enableDataValidation)
+            : base(priority, targetProperty, enableDataValidation)
     {
         if (mode == BindingMode.Default)
             throw new ArgumentException("Binding mode cannot be Default.", nameof(mode));

+ 7 - 1
src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs

@@ -39,12 +39,16 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
     /// <param name="defaultPriority">
     /// The default binding priority for the expression.
     /// </param>
+    /// <param name="targetProperty">The target property being bound to.</param>
     /// <param name="isDataValidationEnabled">Whether data validation is enabled.</param>
     public UntypedBindingExpressionBase(
         BindingPriority defaultPriority,
+        AvaloniaProperty? targetProperty = null,
         bool isDataValidationEnabled = false)
     {
         Priority = defaultPriority;
+        TargetProperty = targetProperty;
+        TargetType = targetProperty?.PropertyType ?? typeof(object);
         _isDataValidationEnabled = isDataValidationEnabled;
     }
 
@@ -86,7 +90,7 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
     /// Gets the target type of the binding expression; that is, the type that values produced by
     /// the expression should be converted to.
     /// </summary>
-    public Type TargetType { get; private set; } = typeof(object);
+    public Type TargetType { get; private set; }
 
     AvaloniaProperty IValueEntry.Property => TargetProperty ?? throw new Exception();
 
@@ -262,6 +266,8 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
     {
         if (_sink is not null)
             throw new InvalidOperationException("BindingExpression was already attached.");
+        if (TargetProperty is not null && TargetProperty != targetProperty)
+            throw new InvalidOperationException("BindingExpression was already attached to a different property.");
 
         _sink = sink;
         _frame = frame;

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs

@@ -133,6 +133,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
                 priority: Priority,
                 stringFormat: StringFormat,
                 targetNullValue: TargetNullValue,
+                targetProperty: targetProperty,
                 targetTypeConverter: TargetTypeConverter.GetDefaultConverter(),
                 updateSourceTrigger: trigger);
         }

+ 1 - 0
src/Markup/Avalonia.Markup/Data/Binding.cs

@@ -175,6 +175,7 @@ namespace Avalonia.Data
                 mode: mode,
                 priority: Priority,
                 stringFormat: StringFormat,
+                targetProperty: targetProperty,
                 targetNullValue: TargetNullValue,
                 targetTypeConverter: TargetTypeConverter.GetReflectionConverter(),
                 updateSourceTrigger: trigger);

+ 51 - 0
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Obsolete.cs

@@ -0,0 +1,51 @@
+using System.Reactive.Linq;
+using Avalonia.Data;
+using Avalonia.Markup.Xaml.MarkupExtensions;
+using Xunit;
+
+#nullable enable
+#pragma warning disable CS0618 // Type or member is obsolete
+
+namespace Avalonia.Base.UnitTests.Data.Core;
+
+public abstract partial class BindingExpressionTests
+{
+    public partial class Reflection
+    {
+        [Fact]
+        public void Obsolete_Initiate_Method_Produces_Observable_With_Correct_Target_Type()
+        {
+            // Issue #15081
+            var viewModel = new ViewModel { DoubleValue = 42.5 };
+            var target = new TargetClass { DataContext = viewModel };
+            var binding = new Binding(nameof(viewModel.DoubleValue));
+            var instanced = binding.Initiate(target, TargetClass.StringProperty);
+
+            Assert.NotNull(instanced);
+
+            var value = instanced.Observable.First();
+
+            Assert.Equal("42.5", value);
+        }
+    }
+
+    public partial class Compiled
+    {
+        [Fact]
+        public void Obsolete_Initiate_Method_Produces_Observable_With_Correct_Target_Type()
+        {
+            // Issue #15081
+            var viewModel = new ViewModel { DoubleValue = 42.5 };
+            var target = new TargetClass { DataContext = viewModel };
+            var path = CompiledBindingPathFromExpressionBuilder.Build<ViewModel, double>(x => x.DoubleValue, true);
+            var binding = new CompiledBindingExtension(path);
+            var instanced = binding.Initiate(target, TargetClass.StringProperty);
+
+            Assert.NotNull(instanced);
+
+            var value = instanced.Observable.First();
+
+            Assert.Equal("42.5", value);
+        }
+    }
+}

+ 2 - 2
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Base.UnitTests.Data.Core;
 [InvariantCulture]
 public abstract partial class BindingExpressionTests
 {
-    public class Reflection : BindingExpressionTests
+    public partial class Reflection : BindingExpressionTests
     {
         private protected override (TargetClass, BindingExpression) CreateTargetCore<TIn, TOut>(
             Expression<Func<TIn, TOut>> expression,
@@ -73,7 +73,7 @@ public abstract partial class BindingExpressionTests
         }
     }
 
-    public class Compiled : BindingExpressionTests
+    public partial class Compiled : BindingExpressionTests
     {
         private protected override (TargetClass, BindingExpression) CreateTargetCore<TIn, TOut>(
             Expression<Func<TIn, TOut>> expression,