Skip to content

Commit

Permalink
Made OneTime bindings update on DataContext changes (#17683)
Browse files Browse the repository at this point in the history
* Add failing tests for OneTime and null data context bindings

* Made OneTime bindings update on DataContext changes

Also allows null as a valid value for bindings without path

* Remove now obsolete test
  • Loading branch information
MrJul authored Dec 15, 2024
1 parent be8edab commit 9d03a01
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 31 deletions.
21 changes: 13 additions & 8 deletions src/Avalonia.Base/Data/Core/BindingExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
private readonly List<ExpressionNode> _nodes;
private readonly TargetTypeConverter? _targetTypeConverter;
private readonly UncommonFields? _uncommon;
private bool _shouldUpdateOneTimeBindingTarget;

/// <summary>
/// Initializes a new instance of the <see cref="BindingExpression"/> class.
Expand Down Expand Up @@ -83,6 +84,7 @@ public BindingExpression(
_mode = mode;
_nodes = nodes ?? s_emptyExpressionNodes;
_targetTypeConverter = targetTypeConverter;
_shouldUpdateOneTimeBindingTarget = _mode == BindingMode.OneTime;

if (converter is not null ||
converterCulture is not null ||
Expand Down Expand Up @@ -231,10 +233,14 @@ 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;
if (_mode == BindingMode.OneTime)
{
// In OneTime mode, only changing the data context updates the binding.
if (!_shouldUpdateOneTimeBindingTarget && _nodes[nodeIndex] is not DataContextNodeBase)
return;

_shouldUpdateOneTimeBindingTarget = false;
}

// The leaf node has changed. If the binding mode is not OneWayToSource, publish the
// value to the target.
Expand All @@ -245,10 +251,6 @@ internal void OnNodeValueChanged(int nodeIndex, object? value, Exception? dataVa
null;
ConvertAndPublishValue(value, error);
}

// If the binding mode is OneTime, then stop the binding if a valid value was published.
if (_mode == BindingMode.OneTime && GetValue() != AvaloniaProperty.UnsetValue)
Stop();
}
else if (_mode == BindingMode.OneWayToSource && nodeIndex == _nodes.Count - 2 && value is not null)
{
Expand All @@ -260,6 +262,9 @@ internal void OnNodeValueChanged(int nodeIndex, object? value, Exception? dataVa
}
else
{
if (_mode == BindingMode.OneTime && _nodes[nodeIndex] is DataContextNodeBase)
_shouldUpdateOneTimeBindingTarget = true;

_nodes[nodeIndex + 1].SetSource(value, dataValidationError);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,13 @@ public void TargetNullValue_Should_Be_Used_When_Source_String_Is_Null()
}

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

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

[Fact]
Expand Down Expand Up @@ -151,4 +151,16 @@ public void Should_Not_Pass_UnsetValue_To_Converter_Until_First_Value_Produced()

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

[Fact]
public void Should_Use_Converter_For_Null_DataContext_Without_Path()
{
var converter = new PrefixConverter();
var target = CreateTarget<string?, string?>(
o => o,
converter: converter,
converterParameter: "foo");

Assert.Equal("foo", target.String);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Avalonia.Data;
using Xunit;
using Xunit.Sdk;

#nullable enable

Expand All @@ -9,21 +8,64 @@ namespace Avalonia.Base.UnitTests.Data.Core;
public partial class BindingExpressionTests
{
[Fact]
public void OneTime_Binding_Sets_Target_Only_Once()
public void OneTime_Binding_Sets_Target_Only_Once_If_Data_Context_Does_Not_Change()
{
var data = new ViewModel { StringValue = "foo" };
var target = CreateTargetWithSource(data, x => x.StringValue, mode: BindingMode.OneTime);
var data = new ViewModel { Next = new ViewModel { StringValue = "foo" } };
var target = CreateTarget<ViewModel, string?>(x => x.Next!.StringValue, mode: BindingMode.OneTime);
target.DataContext = data;

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

data.StringValue = "bar";
data.Next!.StringValue = "bar";
Assert.Equal("foo", target.String);

data.Next = new ViewModel { StringValue = "baz" };
Assert.Equal("foo", target.String);
}

[Fact]
public void OneTime_Binding_With_Simple_Path_Sets_Target_When_Data_Context_Changes()
{
var data1 = new ViewModel { StringValue = "foo" };
var target = CreateTarget<ViewModel, string?>(x => x.StringValue, mode: BindingMode.OneTime);
target.DataContext = data1;

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

var data2 = new ViewModel { StringValue = "bar" };
target.DataContext = data2;
Assert.Equal("bar", target.String);
}

[Fact]
public void OneTime_Binding_With_Complex_Path_Sets_Target_When_Data_Context_Changes()
{
var data1 = new ViewModel { Next = new ViewModel { StringValue = "foo" } };
var target = CreateTarget<ViewModel, string?>(x => x.Next!.StringValue, mode: BindingMode.OneTime);
target.DataContext = data1;

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

var data2 = new ViewModel { Next = new ViewModel { StringValue = "bar" } };
target.DataContext = data2;
Assert.Equal("bar", target.String);
}

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

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

target.DataContext = "bar";
Assert.Equal("bar", target.String);
}

[Fact]
public void OneTime_Binding_Waits_For_DataContext()
{
var data = new ViewModel { StringValue = "foo" };
var target = CreateTarget<ViewModel, string?>(
x => x.StringValue,
mode: BindingMode.OneTime);
Expand Down
15 changes: 0 additions & 15 deletions tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,21 +151,6 @@ public void OneTime_Binding_Should_Be_Set_Up()
Assert.Equal("bar", source.Foo);
}

[Fact]
public void OneTime_Binding_Releases_Subscription_If_DataContext_Set_Later()
{
var target = new TextBlock();
var source = new Source { Foo = "foo" };

target.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneTime));
target.DataContext = source;

// Forces WeakEvent compact
Dispatcher.UIThread.RunJobs();

Assert.Equal(0, source.SubscriberCount);
}

[Fact]
public void OneWayToSource_Binding_Should_Be_Set_Up()
{
Expand Down

0 comments on commit 9d03a01

Please sign in to comment.