Browse Source

Made OneTime bindings update on DataContext changes (#17683)

* 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
release/11.2.3
Julien Lebosquain 1 year ago
committed by Max Katz
parent
commit
d9349581a1
  1. 21
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  2. 16
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.GetValue.cs
  3. 54
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs
  4. 15
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

21
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -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.
@ -83,6 +84,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
_mode = mode;
_nodes = nodes ?? s_emptyExpressionNodes;
_targetTypeConverter = targetTypeConverter;
_shouldUpdateOneTimeBindingTarget = _mode == BindingMode.OneTime;
if (converter is not null ||
converterCulture is not null ||
@ -231,10 +233,14 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
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.
@ -245,10 +251,6 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
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)
{
@ -260,6 +262,9 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
}
else
{
if (_mode == BindingMode.OneTime && _nodes[nodeIndex] is DataContextNodeBase)
_shouldUpdateOneTimeBindingTarget = true;
_nodes[nodeIndex + 1].SetSource(value, dataValidationError);
}
}

16
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.GetValue.cs

@ -95,13 +95,13 @@ public abstract partial class BindingExpressionTests
}
[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]
@ -151,4 +151,16 @@ public abstract partial class BindingExpressionTests
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);
}
}

54
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs

@ -1,6 +1,5 @@
using Avalonia.Data;
using Xunit;
using Xunit.Sdk;
#nullable enable
@ -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);

15
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@ -151,21 +151,6 @@ namespace Avalonia.Markup.UnitTests.Data
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()
{

Loading…
Cancel
Save