Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Made OneTime bindings update on DataContext changes #17683

Merged
merged 3 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
maxkatz6 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading