Browse Source

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
pull/15722/head
Steven Kirk 2 years ago
committed by GitHub
parent
commit
8fe6e08020
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  2. 8
      src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs
  3. 1
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
  4. 1
      src/Markup/Avalonia.Markup/Data/Binding.cs
  5. 51
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Obsolete.cs
  6. 4
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

4
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));

8
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
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
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
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);
}
}
}

4
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,

Loading…
Cancel
Save