Skip to content

Commit

Permalink
Fix bindings without property path (#16729)
Browse files Browse the repository at this point in the history
* Added failing binding tests

* Fix bindings without property path

---------

Co-authored-by: Benedikt Stebner <[email protected]>
  • Loading branch information
MrJul and Gillibald authored Sep 14, 2024
1 parent 751b273 commit 20f77a3
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 5 deletions.
5 changes: 5 additions & 0 deletions src/Avalonia.Base/Data/Core/BindingExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ internal void OnNodeValueChanged(int nodeIndex, object? value, Exception? dataVa

if (nodeIndex == _nodes.Count - 1)
{
// If the binding source is a data context without any path and is currently null, treat it as an invalid
// value. This allows bindings to DataContext and DataContext.Property to share the same behavior.
if (value is null && _nodes[nodeIndex] is DataContextNodeBase)
value = AvaloniaProperty.UnsetValue;

// The leaf node has changed. If the binding mode is not OneWayToSource, publish the
// value to the target.
if (_mode != BindingMode.OneWayToSource)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ public void TargetNullValue_Should_Be_Used_When_Source_String_Is_Null()
GC.KeepAlive(data);
}

[Fact]
public void TargetNullValue_Should_Not_Be_Used_When_Source_Is_Data_Context_And_Null()
{
var target = CreateTarget<string?, string?>(
o => o,
targetNullValue: "bar");

Assert.Equal(null, target.String);
}

[Fact]
public void Can_Use_UpdateTarget_To_Update_From_Non_INPC_Data()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,31 @@ public void OneTime_Binding_Waits_For_DataContext_With_Matching_Property_Type()
Assert.Equal(0.5, target.Double);
}

[Fact]
public void OneTime_Binding_Waits_For_DataContext_Without_Property_Path()
{
var target = CreateTarget<string?, string?>(
x => x,
mode: BindingMode.OneTime);

target.DataContext = "foo";

Assert.Equal("foo", target.String);
}

[Fact]
public void OneTime_Binding_Waits_For_DataContext_Without_Property_Path_With_StringFormat()
{
var target = CreateTarget<string?, string?>(
x => x,
mode: BindingMode.OneTime,
stringFormat: "bar: {0}");

target.DataContext = "foo";

Assert.Equal("bar: foo", target.String);
}

[Fact]
public void OneWayToSource_Binding_Updates_Source_When_Target_Changes()
{
Expand Down
15 changes: 13 additions & 2 deletions tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ private protected override (TargetClass, BindingExpression) CreateTargetCore<TIn
RelativeSource? relativeSource,
Optional<TIn> source,
object? targetNullValue,
string? stringFormat,
UpdateSourceTrigger updateSourceTrigger)
{
var target = new TargetClass { DataContext = dataContext };
Expand Down Expand Up @@ -66,6 +67,7 @@ private protected override (TargetClass, BindingExpression) CreateTargetCore<TIn
mode: mode,
targetNullValue: targetNullValue,
targetTypeConverter: TargetTypeConverter.GetReflectionConverter(),
stringFormat: stringFormat,
updateSourceTrigger: updateSourceTrigger);

target.GetValueStore().AddBinding(targetProperty, bindingExpression);
Expand All @@ -87,6 +89,7 @@ private protected override (TargetClass, BindingExpression) CreateTargetCore<TIn
RelativeSource? relativeSource,
Optional<TIn> source,
object? targetNullValue,
string? stringFormat,
UpdateSourceTrigger updateSourceTrigger)
{
var target = new TargetClass { DataContext = dataContext };
Expand All @@ -112,6 +115,7 @@ private protected override (TargetClass, BindingExpression) CreateTargetCore<TIn
mode: mode,
targetNullValue: targetNullValue,
targetTypeConverter: TargetTypeConverter.GetReflectionConverter(),
stringFormat: stringFormat,
updateSourceTrigger: updateSourceTrigger);
target.GetValueStore().AddBinding(targetProperty, bindingExpression);
return (target, bindingExpression);
Expand All @@ -129,7 +133,8 @@ protected TargetClass CreateTarget<TIn, TOut>(
BindingMode mode = BindingMode.OneWay,
RelativeSource? relativeSource = null,
Optional<TIn> source = default,
object? targetNullValue = null)
object? targetNullValue = null,
string? stringFormat = null)
where TIn : class?
{
var (target, _) = CreateTargetAndExpression(
Expand All @@ -143,7 +148,8 @@ protected TargetClass CreateTarget<TIn, TOut>(
mode,
relativeSource,
source,
targetNullValue);
targetNullValue,
stringFormat);
return target;
}

Expand All @@ -158,6 +164,7 @@ protected TargetClass CreateTargetWithSource<TIn, TOut>(
BindingMode mode = BindingMode.OneWay,
RelativeSource? relativeSource = null,
object? targetNullValue = null,
string? stringFormat = null,
UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.PropertyChanged)
where TIn : class?
{
Expand All @@ -173,6 +180,7 @@ protected TargetClass CreateTargetWithSource<TIn, TOut>(
relativeSource,
source,
targetNullValue,
stringFormat,
updateSourceTrigger);
return target;
}
Expand All @@ -189,6 +197,7 @@ private protected (TargetClass, BindingExpression) CreateTargetAndExpression<TIn
RelativeSource? relativeSource = null,
Optional<TIn> source = default,
object? targetNullValue = null,
string? stringFormat = null,
UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.PropertyChanged)
where TIn : class?
{
Expand All @@ -213,6 +222,7 @@ private protected (TargetClass, BindingExpression) CreateTargetAndExpression<TIn
relativeSource,
source,
targetNullValue,
stringFormat,
updateSourceTrigger);
}

Expand All @@ -228,6 +238,7 @@ private protected abstract (TargetClass, BindingExpression) CreateTargetCore<TIn
RelativeSource? relativeSource,
Optional<TIn> source,
object? targetNullValue,
string? stringFormat,
UpdateSourceTrigger updateSourceTrigger)
where TIn : class?;

Expand Down
6 changes: 3 additions & 3 deletions tests/Avalonia.Markup.UnitTests/Data/BindingTests_Logging.cs
Original file line number Diff line number Diff line change
Expand Up @@ -305,16 +305,16 @@ public void Should_Log_Invalid_FallbackValue(bool rooted)
[InlineData(false)]
public void Should_Log_Invalid_TargetNullValue(bool rooted)
{
var target = new Decorator { };
var binding = new Binding() { TargetNullValue = "foo" };
var target = new Decorator { DataContext = new { Bar = (string?) null } };
var binding = new Binding("Bar") { TargetNullValue = "foo" };

if (rooted)
new TestRoot(target);

// An invalid target null value is invalid whether the control is rooted or not.
using (AssertLog(
target,
"",
binding.Path,
"Could not convert TargetNullValue 'foo' to 'System.Double'.",
level: LogEventLevel.Error,
property: Visual.OpacityProperty))
Expand Down

0 comments on commit 20f77a3

Please sign in to comment.