Browse Source

Finish exposing a BindingExpression API.

refactor/bindingexpressions-in-valuestore
Steven Kirk 3 years ago
parent
commit
08c8497d3d
  1. 23
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  2. 6
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  3. 25
      src/Avalonia.Base/Data/Core/BindingExpressionBase.cs
  4. 2
      src/Avalonia.Base/Data/Core/IBinding2.cs
  5. 1
      src/Avalonia.Base/Data/Core/IndexerBindingExpression.cs
  6. 57
      src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs
  7. 4
      src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs
  8. 2
      src/Avalonia.Base/Data/IndexerBinding.cs
  9. 10
      src/Avalonia.Base/Data/TemplateBinding.cs
  10. 1
      src/Avalonia.Base/PropertyStore/ValueFrame.cs
  11. 30
      src/Avalonia.Base/Styling/Setter.cs
  12. 5
      src/Markup/Avalonia.Markup.Xaml/Data/DynamicResourceExpression.cs
  13. 2
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
  14. 12
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs
  15. 2
      src/Markup/Avalonia.Markup/Data/Binding.cs
  16. 4
      src/Markup/Avalonia.Markup/Data/BindingBase.cs
  17. 4
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  18. 4
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  19. 99
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Mode.cs
  20. 9
      tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs

23
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -237,7 +237,7 @@ namespace Avalonia
/// can be used to provide this context.
/// </param>
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
[Obsolete]
[Obsolete("Use AvaloniaObject.Bind(AvaloniaProperty, IBinding")]
public static IDisposable Bind(
this AvaloniaObject target,
AvaloniaProperty property,
@ -248,20 +248,7 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property));
binding = binding ?? throw new ArgumentNullException(nameof(binding));
var result = binding.Initiate(
target,
property,
anchor,
property.GetMetadata(target.GetType()).EnableDataValidation ?? false);
if (result != null)
{
return BindingOperations.Apply(target, property, result, anchor);
}
else
{
return Disposable.Empty;
}
return target.Bind(property, binding);
}
/// <summary>
@ -384,13 +371,13 @@ namespace Avalonia
object? anchor = null,
bool enableDataValidation = false)
{
var expression = new UntypedObservableBindingExpression(_source);
var expression = new UntypedObservableBindingExpression(_source, BindingPriority.LocalValue);
return new InstancedBinding(expression, BindingMode.OneWay, BindingPriority.LocalValue);
}
IBindingExpression IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property)
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property)
{
return new UntypedObservableBindingExpression(_source);
return new UntypedObservableBindingExpression(_source, BindingPriority.LocalValue);
}
}

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

@ -43,6 +43,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
/// Whether data validation should be enabled for the binding.
/// </param>
/// <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="targetNullValue">The null target value.</param>
/// <param name="targetTypeConverter">
@ -57,10 +58,11 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
object? converterParameter = null,
bool enableDataValidation = false,
BindingMode mode = BindingMode.OneWay,
BindingPriority priority = BindingPriority.LocalValue,
string? stringFormat = null,
object? targetNullValue = null,
TargetTypeConverter? targetTypeConverter = null)
: base(enableDataValidation)
: base(priority, enableDataValidation)
{
if (mode == BindingMode.Default)
throw new ArgumentException("Binding mode cannot be Default.", nameof(mode));
@ -154,6 +156,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
bool enableDataValidation = false,
Optional<object?> fallbackValue = default,
BindingMode mode = BindingMode.OneWay,
BindingPriority priority = BindingPriority.LocalValue,
object? targetNullValue = null,
bool allowReflection = true)
where TIn : class?
@ -170,6 +173,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
converterParameter: converterParameter,
enableDataValidation: enableDataValidation,
mode: mode,
priority: priority,
targetNullValue: targetNullValue,
targetTypeConverter: allowReflection ?
TargetTypeConverter.GetReflectionConverter() :

25
src/Avalonia.Base/Data/Core/BindingExpressionBase.cs

@ -1,10 +1,31 @@
using Avalonia.Styling;
using Avalonia.PropertyStore;
using Avalonia.Styling;
namespace Avalonia.Data.Core;
public abstract class BindingExpressionBase : ISetterInstance
public abstract class BindingExpressionBase : IBindingExpression, ISetterInstance
{
private protected BindingExpressionBase()
{
}
internal BindingMode Mode { get; private protected set; }
public virtual void Dispose()
{
}
/// <summary>
/// When overridden in a derived class, attaches the binding expression to a value store but
/// does not start it.
/// </summary>
/// <param name="valueStore">The value store to attach to.</param>
/// <param name="target">The target object.</param>
/// <param name="targetProperty">The target property.</param>
/// <param name="priority">The priority of the binding.</param>
internal abstract void Attach(
ValueStore valueStore,
AvaloniaObject target,
AvaloniaProperty targetProperty,
BindingPriority priority);
}

2
src/Avalonia.Base/Data/Core/IBinding2.cs

@ -2,5 +2,5 @@
internal interface IBinding2 : IBinding
{
IBindingExpression Instance(AvaloniaObject target, AvaloniaProperty targetProperty);
BindingExpressionBase Instance(AvaloniaObject target, AvaloniaProperty targetProperty);
}

1
src/Avalonia.Base/Data/Core/IndexerBindingExpression.cs

@ -16,6 +16,7 @@ internal class IndexerBindingExpression : UntypedBindingExpressionBase
AvaloniaObject target,
AvaloniaProperty? targetProperty,
BindingMode mode)
: base(BindingPriority.LocalValue)
{
_source = source;
_sourceProperty = sourceProperty;

57
src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs

@ -16,7 +16,6 @@ namespace Avalonia.Data.Core;
/// </summary>
[PrivateApi]
public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
IBindingExpression,
IDisposable,
IDescription,
IValueEntry
@ -36,9 +35,15 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
/// <summary>
/// Initializes a new instance of the <see cref="UntypedBindingExpressionBase"/> class.
/// </summary>
/// <param name="defaultPriority">
/// The default binding priority for the expression.
/// </param>
/// <param name="isDataValidationEnabled">Whether data validation is enabled.</param>
public UntypedBindingExpressionBase(bool isDataValidationEnabled = false)
public UntypedBindingExpressionBase(
BindingPriority defaultPriority,
bool isDataValidationEnabled = false)
{
Priority = defaultPriority;
_isDataValidationEnabled = isDataValidationEnabled;
}
@ -65,6 +70,10 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
/// <summary>
/// Gets the priority of the binding expression.
/// </summary>
/// <remarks>
/// Before being attached to a value store, this property describes the default priority of the
/// binding expression; this may change when the expression is attached to a value store.
/// </remarks>
public BindingPriority Priority { get; private set; }
/// <summary>
@ -92,7 +101,7 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
/// <summary>
/// Terminates the binding.
/// </summary>
public virtual void Dispose()
public override void Dispose()
{
if (_sink is null)
return;
@ -144,7 +153,7 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
/// <summary>
/// Starts the binding expression following a call to
/// <see cref="Attach(IBindingExpressionSink, AvaloniaObject, AvaloniaProperty, BindingPriority)"/>.
/// <see cref="AttachCore(IBindingExpressionSink, AvaloniaObject, AvaloniaProperty, BindingPriority)"/>.
/// </summary>
public void Start() => Start(produceValue: true);
@ -177,28 +186,13 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
void IValueEntry.Unsubscribe() => Stop();
/// <summary>
/// Attaches the binding expression to a subscriber with the specified subscriber but does not
/// start it.
/// </summary>
/// <param name="sink">The subscriber.</param>
/// <param name="target">The target object.</param>
/// <param name="targetProperty">The target property.</param>
/// <param name="priority">The priority of the binding.</param>
internal void Attach(
IBindingExpressionSink sink,
internal override void Attach(
ValueStore valueStore,
AvaloniaObject target,
AvaloniaProperty targetProperty,
BindingPriority priority)
{
if (_sink is not null)
throw new InvalidOperationException("BindingExpression was already attached.");
_sink = sink;
_target = new(target);
TargetProperty = targetProperty;
TargetType = targetProperty.PropertyType;
Priority = priority;
AttachCore(valueStore, target, targetProperty, priority);
}
/// <summary>
@ -215,7 +209,7 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
AvaloniaProperty targetProperty,
BindingPriority priority)
{
Attach(subscriber, target, targetProperty, priority);
AttachCore(subscriber, target, targetProperty, priority);
Start(produceValue: true);
}
@ -285,6 +279,23 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
/// </returns>
internal virtual bool WriteValueToSource(object? value) => false;
private void AttachCore(
IBindingExpressionSink sink,
AvaloniaObject target,
AvaloniaProperty targetProperty,
BindingPriority priority)
{
if (_sink is not null)
throw new InvalidOperationException("BindingExpression was already attached.");
_sink = sink;
_target = new(target);
TargetProperty = targetProperty;
TargetType = targetProperty.PropertyType;
Priority = priority;
}
/// <summary>
/// Converts a value using a value converter, logging a warning if necessary.
/// </summary>

4
src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs

@ -8,7 +8,9 @@ internal class UntypedObservableBindingExpression : UntypedBindingExpressionBase
private IDisposable? _subscription;
public UntypedObservableBindingExpression(
IObservable<object?> observable)
IObservable<object?> observable,
BindingPriority priority)
: base(priority)
{
_observable = observable;
}

2
src/Avalonia.Base/Data/IndexerBinding.cs

@ -31,7 +31,7 @@ namespace Avalonia.Data
return new InstancedBinding(expression, Mode, BindingPriority.LocalValue);
}
IBindingExpression IBinding2.Instance(AvaloniaObject target, AvaloniaProperty targetProperty)
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty targetProperty)
{
return new IndexerBindingExpression(Source, Property, target, targetProperty, Mode);
}

10
src/Avalonia.Base/Data/TemplateBinding.cs

@ -21,10 +21,12 @@ namespace Avalonia.Data
private bool _isSetterValue;
public TemplateBinding()
: base(BindingPriority.Template)
{
}
public TemplateBinding(AvaloniaProperty property)
: base(BindingPriority.Template)
{
Property = property;
}
@ -52,7 +54,11 @@ namespace Avalonia.Data
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
public BindingMode Mode { get; set; }
public new BindingMode Mode
{
get => base.Mode;
set => base.Mode = value;
}
/// <summary>
/// Gets or sets the name of the source property on the templated parent.
@ -73,7 +79,7 @@ namespace Avalonia.Data
return new(target, InstanceCore(), Mode, BindingPriority.Template);
}
IBindingExpression IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property)
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property)
{
return InstanceCore();
}

1
src/Avalonia.Base/PropertyStore/ValueFrame.cs

@ -2,6 +2,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Utilities;
namespace Avalonia.PropertyStore

30
src/Avalonia.Base/Styling/Setter.cs

@ -77,8 +77,10 @@ namespace Avalonia.Styling
throw new InvalidOperationException(
$"Cannot set direct property '{Property}' in '{instance.Source}' because the style has an activator.");
if (Value is IBinding binding)
if (Value is IBinding2 binding)
return SetBinding((StyleInstance)instance, ao, binding);
else if (Value is IBinding)
throw new AvaloniaInternalException("TODO: Make all IBindings implement IBinding2.");
else if (Value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(Property.PropertyType))
return new PropertySetterTemplateInstance(Property, template);
else if (!Property.IsValidValue(Value))
@ -103,31 +105,13 @@ namespace Avalonia.Styling
return Property ?? throw new InvalidOperationException("Setter.Property must be set.");
}
private ISetterInstance SetBinding(StyleInstance instance, AvaloniaObject target, IBinding binding)
private ISetterInstance SetBinding(StyleInstance instance, AvaloniaObject target, IBinding2 binding)
{
if (!Property!.IsDirect)
{
var hasDataValidation = Property.GetMetadata(target.GetType()).EnableDataValidation ?? false;
var i = binding.Initiate(target, Property, enableDataValidation: hasDataValidation)!;
var mode = i.Mode;
if (mode == BindingMode.Default)
{
mode = Property!.GetMetadata(target.GetType()).DefaultBindingMode;
}
if (mode == BindingMode.OneWay || mode == BindingMode.TwoWay)
{
if (i.Expression is { } expression)
{
expression.Attach(target.GetValueStore(), target, Property, instance.Priority);
return expression;
}
else
return new PropertySetterBindingInstance(target, instance, Property, mode, i.Source);
}
throw new NotSupportedException();
var expression = binding.Instance(target, Property);
expression.Attach(target.GetValueStore(), target, Property, instance.Priority);
return expression;
}
else
{

5
src/Markup/Avalonia.Markup.Xaml/Data/DynamicResourceExpression.cs

@ -1,6 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Logging;
using Avalonia.Markup.Xaml.Converters;
@ -22,7 +23,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public DynamicResourceExpression(
object resourceKey,
object? anchor,
ThemeVariant? themeVariant)
ThemeVariant? themeVariant,
BindingPriority priority)
: base(priority)
{
_resourceKey = resourceKey;
_anchor = anchor;

2
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs

@ -57,7 +57,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
return new InstancedBinding(target, expression, Mode, Priority);
}
private protected override IBindingExpression Instance(AvaloniaProperty targetProperty, AvaloniaObject target)
private protected override BindingExpressionBase Instance(AvaloniaProperty targetProperty, AvaloniaObject target)
{
var enableDataValidation = targetProperty.GetMetadata(target.GetType()).EnableDataValidation ?? false;
return InstanceCore(target, targetProperty, enableDataValidation);

12
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs

@ -1,11 +1,12 @@
using System;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Styling;
namespace Avalonia.Markup.Xaml.MarkupExtensions
{
public class DynamicResourceExtension : IBinding
public class DynamicResourceExtension : IBinding2
{
private object? _anchor;
private BindingPriority _priority;
@ -49,8 +50,15 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
{
if (ResourceKey is null)
return null;
var expression = new DynamicResourceExpression(ResourceKey, _anchor, _themeVariant);
var expression = new DynamicResourceExpression(ResourceKey, _anchor, _themeVariant, _priority);
return new InstancedBinding(target, expression, BindingMode.OneWay, _priority);
}
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty targetProperty)
{
if (ResourceKey is null)
throw new InvalidOperationException("DynamicResource must have a ResourceKey.");
return new DynamicResourceExpression(ResourceKey, _anchor, _themeVariant, _priority);
}
}
}

2
src/Markup/Avalonia.Markup/Data/Binding.cs

@ -71,7 +71,7 @@ namespace Avalonia.Data
return new InstancedBinding(target, expression, Mode, Priority);
}
private protected override IBindingExpression Instance(AvaloniaProperty targetProperty, AvaloniaObject target)
private protected override BindingExpressionBase Instance(AvaloniaProperty targetProperty, AvaloniaObject target)
{
var enableDataValidation = targetProperty.GetMetadata(target.GetType()).EnableDataValidation ?? false;
return InstanceCore(targetProperty, target, enableDataValidation);

4
src/Markup/Avalonia.Markup/Data/BindingBase.cs

@ -88,7 +88,7 @@ namespace Avalonia.Data
object? anchor = null,
bool enableDataValidation = false);
private protected abstract IBindingExpression Instance(AvaloniaProperty targetProperty, AvaloniaObject target);
private protected abstract BindingExpressionBase Instance(AvaloniaProperty targetProperty, AvaloniaObject target);
private protected BindingMode ResolveBindingMode(AvaloniaObject target, AvaloniaProperty? targetProperty)
{
@ -99,6 +99,6 @@ namespace Avalonia.Data
return targetProperty.GetMetadata(target.GetType()).DefaultBindingMode;
}
IBindingExpression IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property) => Instance(property, target);
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property) => Instance(property, target);
}
}

4
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@ -89,11 +89,11 @@ namespace Avalonia.Data
}
}
IBindingExpression IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property)
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property)
{
// TODO: Implement MultiBindingExpression instead of wrapping an observable.
var o = InstanceCore(target, property);
return new UntypedObservableBindingExpression(o);
return new UntypedObservableBindingExpression(o, BindingPriority.LocalValue);
}
private IObservable<object?> InstanceCore(AvaloniaObject target, AvaloniaProperty? targetProperty)

4
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -462,7 +462,7 @@ namespace Avalonia.Base.UnitTests
var target = new Class1();
var source = new Subject<object>();
target.Bind(Class1.QuxProperty, new TestOneTimeBinding(source));
target.Bind(Class1.QuxProperty, source);
source.OnNext(AvaloniaProperty.UnsetValue);
Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
@ -477,7 +477,7 @@ namespace Avalonia.Base.UnitTests
var target = new Class1();
var source = new Subject<object>();
target.Bind(Class1.QuxProperty, new TestOneTimeBinding(source));
target.Bind(Class1.QuxProperty, source);
source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error));
Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));

99
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Mode.cs

@ -0,0 +1,99 @@
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests.Data.Core
{
[InvariantCulture]
public class BindingExpressionTests_Mode
{
[Fact]
public void OneTime_Binding_Sets_Target_Only_Once()
{
var data = new ViewModel();
var binding = new Binding(nameof(data.Foo), BindingMode.OneTime);
var target = new Control { DataContext = data };
target.Bind(Control.TagProperty, binding);
Assert.Equal("foo", target.Tag);
data.Foo = "bar";
Assert.Equal("foo", target.Tag);
}
[Fact]
public void OneTime_Binding_Waits_For_DataContext()
{
var data = new ViewModel();
var binding = new Binding(nameof(data.Foo), BindingMode.OneTime);
var target = new Control();
target.Bind(Control.TagProperty, binding);
Assert.Null(target.Tag);
target.DataContext = data;
Assert.Equal("foo", target.Tag);
data.Foo = "bar";
Assert.Equal("foo", target.Tag);
}
[Fact]
public void OneTime_Binding_Waits_For_DataContext_With_Matching_Property_Name()
{
var data1 = new { Baz = "baz" };
var data2 = new ViewModel();
var binding = new Binding(nameof(data2.Foo), BindingMode.OneTime);
var target = new Control { DataContext = data1 };
target.Bind(Control.TagProperty, binding);
Assert.Null(target.Tag);
target.DataContext = data2;
Assert.Equal("foo", target.Tag);
data2.Foo = "bar";
Assert.Equal("foo", target.Tag);
}
private class ViewModel : NotifyingBase
{
private string _foo;
private double _bar;
public ViewModel(string foo = "foo", double bar = 0.5)
{
_foo = foo;
_bar = bar;
}
public string Foo
{
get => _foo;
set
{
if (_foo != value)
{
_foo = value;
RaisePropertyChanged();
}
}
}
public double Bar
{
get => _bar;
set
{
if (_bar != value)
{
_bar = value;
RaisePropertyChanged();
}
}
}
}
}
}

9
tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs

@ -6,7 +6,6 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Media;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
@ -31,9 +30,7 @@ namespace Avalonia.Base.UnitTests.Styling
{
var control = new TextBlock();
var subject = new BehaviorSubject<object>("foo");
var descriptor = InstancedBinding.OneWay(subject);
var binding = Mock.Of<IBinding>(x => x.Initiate(control, TextBlock.TagProperty, null, false) == descriptor);
var style = Mock.Of<IStyle>();
var binding = subject.ToBinding();
var setter = new Setter(TextBlock.TagProperty, binding);
Apply(setter, control);
@ -46,9 +43,7 @@ namespace Avalonia.Base.UnitTests.Styling
{
var control = new TextBlock();
var subject = new BehaviorSubject<object>(AvaloniaProperty.UnsetValue);
var descriptor = InstancedBinding.OneWay(subject);
var binding = Mock.Of<IBinding>(x => x.Initiate(control, TextBlock.TagProperty, null, false) == descriptor);
var style = Mock.Of<IStyle>();
var binding = subject.ToBinding();
var setter = new Setter(TextBlock.TagProperty, binding);
Apply(setter, control);

Loading…
Cancel
Save