Browse Source

Move compiled bindings to Avalonia.Base. (#20439)

* Reminder for future me.

* Move compiled bindings to Avalonia base.

* Update suppressions.

* Update API suppressions

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/20480/head
Steven Kirk 3 weeks ago
committed by GitHub
parent
commit
1086d9b5b0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 64
      api/Avalonia.nupkg.xml
  2. 164
      src/Avalonia.Base/Data/CompiledBinding.cs
  3. 29
      src/Avalonia.Base/Data/CompiledBindingPath.cs
  4. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
  5. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  6. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
  7. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  8. 183
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
  9. 2
      src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs
  10. 3
      tests/Avalonia.Base.UnitTests/Data/Core/CompiledBindingPathFromExpressionBuilder.cs
  11. 24
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

64
api/Avalonia.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
@ -7,12 +7,36 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPathBuilder</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Media.IGlyphTypeface</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPathBuilder</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.FontManager.TryGetGlyphTypeface(Avalonia.Media.Typeface,Avalonia.Media.IGlyphTypeface@)</Target>
@ -289,6 +313,24 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension.#ctor(Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension.get_Path</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension.ProvideValue(System.IServiceProvider)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.GlyphMetrics.get_Height</Target>
@ -655,6 +697,24 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension.#ctor(Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension.get_Path</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension.ProvideValue(System.IServiceProvider)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Markup.Xaml.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.GlyphMetrics.get_Height</Target>
@ -1309,4 +1369,4 @@
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll</Right>
</Suppression>
</Suppressions>
</Suppressions>

164
src/Avalonia.Base/Data/CompiledBinding.cs

@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Data.Core;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.Parsers;
namespace Avalonia.Data;
/// <summary>
/// A binding which does not use reflection to access members.
/// </summary>
public class CompiledBinding : BindingBase
{
/// <summary>
/// Initializes a new instance of the <see cref="CompiledBinding"/> class.
/// </summary>
public CompiledBinding() { }
/// <summary>
/// Initializes a new instance of the <see cref="CompiledBinding"/> class.
/// </summary>
/// <param name="path">The binding path.</param>
public CompiledBinding(CompiledBindingPath path) => Path = path;
/// <summary>
/// Gets or sets the amount of time, in milliseconds, to wait before updating the binding
/// source after the value on the target changes.
/// </summary>
/// <remarks>
/// There is no delay when the source is updated via <see cref="UpdateSourceTrigger.LostFocus"/>
/// or <see cref="BindingExpressionBase.UpdateSource"/>. Nor is there a delay when
/// <see cref="BindingMode.OneWayToSource"/> is active and a new source object is provided.
/// </remarks>
public int Delay { get; set; }
/// <summary>
/// Gets or sets the <see cref="IValueConverter"/> to use.
/// </summary>
public IValueConverter? Converter { get; set; }
/// <summary>
/// Gets or sets the culture in which to evaluate the converter.
/// </summary>
/// <value>The default value is null.</value>
/// <remarks>
/// If this property is not set then <see cref="CultureInfo.CurrentCulture"/> will be used.
/// </remarks>
[TypeConverter(typeof(CultureInfoIetfLanguageTagConverter))]
public CultureInfo? ConverterCulture { get; set; }
/// <summary>
/// Gets or sets a parameter to pass to <see cref="Converter"/>.
/// </summary>
public object? ConverterParameter { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding is unable to produce a value.
/// </summary>
public object? FallbackValue { get; set; } = AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
public BindingMode Mode { get; set; }
/// <summary>
/// Gets or sets the binding path.
/// </summary>
public CompiledBindingPath? Path { get; set; }
/// <summary>
/// Gets or sets the binding priority.
/// </summary>
public BindingPriority Priority { get; set; }
/// <summary>
/// Gets or sets the source for the binding.
/// </summary>
public object? Source { get; set; } = AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets or sets the string format.
/// </summary>
public string? StringFormat { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding result is null.
/// </summary>
public object? TargetNullValue { get; set; } = AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets or sets a value that determines the timing of binding source updates for
/// <see cref="BindingMode.TwoWay"/> and <see cref="BindingMode.OneWayToSource"/> bindings.
/// </summary>
public UpdateSourceTrigger UpdateSourceTrigger { get; set; }
internal WeakReference? DefaultAnchor { get; set; }
internal WeakReference<INameScope?>? NameScope { get; set; }
internal override BindingExpressionBase CreateInstance(
AvaloniaObject target,
AvaloniaProperty? targetProperty,
object? anchor)
{
var enableDataValidation = targetProperty?.GetMetadata(target).EnableDataValidation ?? false;
var nodes = new List<ExpressionNode>();
var isRooted = false;
Path?.BuildExpression(nodes, out isRooted);
// If the binding isn't rooted (i.e. doesn't have a Source or start with $parent, $self,
// #elementName etc.) then we need to add a data context source node.
if (Source == AvaloniaProperty.UnsetValue && !isRooted)
nodes.Insert(0, ExpressionNodeFactory.CreateDataContext(targetProperty));
// If the first node is an ISourceNode then allow it to select the source; otherwise
// use the binding source if specified, falling back to the target.
var source = nodes?.Count > 0 && nodes[0] is SourceNode sn
? sn.SelectSource(Source, target, anchor ?? DefaultAnchor?.Target)
: Source != AvaloniaProperty.UnsetValue ? Source : target;
var (mode, trigger) = ResolveDefaultsFromMetadata(target, targetProperty);
return new BindingExpression(
source,
nodes,
FallbackValue,
delay: TimeSpan.FromMilliseconds(Delay),
converter: Converter,
converterCulture: ConverterCulture,
converterParameter: ConverterParameter,
enableDataValidation: enableDataValidation,
mode: mode,
priority: Priority,
stringFormat: StringFormat,
targetNullValue: TargetNullValue,
targetProperty: targetProperty,
targetTypeConverter: TargetTypeConverter.GetDefaultConverter(),
updateSourceTrigger: trigger);
}
private (BindingMode, UpdateSourceTrigger) ResolveDefaultsFromMetadata(
AvaloniaObject target,
AvaloniaProperty? targetProperty)
{
var mode = Mode;
var trigger = UpdateSourceTrigger == UpdateSourceTrigger.Default ?
UpdateSourceTrigger.PropertyChanged : UpdateSourceTrigger;
if (mode == BindingMode.Default)
{
if (targetProperty?.GetMetadata(target) is { } metadata)
mode = metadata.DefaultBindingMode;
else
mode = BindingMode.OneWay;
}
return (mode, trigger);
}
}

29
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs → src/Avalonia.Base/Data/CompiledBindingPath.cs

@ -4,10 +4,10 @@ using System.Reflection;
using Avalonia.Controls;
using Avalonia.Data.Core;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.Parsers;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Parsers;
namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
namespace Avalonia.Data
{
public class CompiledBindingPath
{
@ -96,21 +96,17 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
/// <inheritdoc />
public override string ToString()
=> string.Concat((IEnumerable<ICompiledBindingPathElement>) _elements);
=> string.Concat((IEnumerable<ICompiledBindingPathElement>)_elements);
}
public class CompiledBindingPathBuilder
{
private readonly int _apiVersion;
private readonly List<ICompiledBindingPathElement> _elements = new();
public CompiledBindingPathBuilder()
{
}
// TODO12: Remove this constructor. apiVersion is only needed for compatibility with
// versions of Avalonia which used $self.Property() for building TemplatedParent bindings.
public CompiledBindingPathBuilder(int apiVersion) => _apiVersion = apiVersion;
public CompiledBindingPathBuilder Not()
{
@ -120,22 +116,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
public CompiledBindingPathBuilder Property(IPropertyInfo info, Func<WeakReference<object?>, IPropertyInfo, IPropertyAccessor> accessorFactory)
{
// Older versions of Avalonia used $self.Property() for building TemplatedParent bindings.
// Try to detect this and upgrade to using a TemplatedParentPathElement so that logging works
// correctly.
if (_apiVersion == 0 &&
info.Name == "TemplatedParent" &&
_elements.Count >= 1 &&
_elements[_elements.Count - 1] is SelfPathElement)
{
_elements.Add(new TemplatedParentPathElement());
}
else
{
return Property(info, accessorFactory, acceptsNull: false);
}
return this;
return Property(info, accessorFactory, acceptsNull: false);
}
public CompiledBindingPathBuilder Property(
@ -285,7 +266,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
public MethodInfo Method { get; }
public Type DelegateType { get; }
public bool AcceptsNull { get; }
}

6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs

@ -142,7 +142,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
IXamlType? itemsCollectionType = null;
if (context.GetAvaloniaTypes().BindingBase.IsAssignableFrom(parentItemsValue.Type.GetClrType()))
{
if (parentItemsValue.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)
if (parentItemsValue.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBinding)
&& parentItemsValue is XamlMarkupExtensionNode ext && ext.Value is XamlAstConstructableObjectNode parentItemsBinding)
{
var parentItemsDataContext = context.ParentNodes().SkipWhile(n => n != parentObject).OfType<AvaloniaXamlIlDataContextTypeMetadataNode>().FirstOrDefault();
@ -176,7 +176,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
private static AvaloniaXamlIlDataContextTypeMetadataNode ParseDataContext(AstTransformationContext context, XamlAstConstructableObjectNode on, XamlAstConstructableObjectNode obj)
{
var bindingType = context.GetAvaloniaTypes().BindingBase;
if (!bindingType.IsAssignableFrom(obj.Type.GetClrType()) && !obj.Type.GetClrType().Equals(context.GetAvaloniaTypes().ReflectionBindingExtension))
if (!bindingType.IsAssignableFrom(obj.Type.GetClrType()) &&
!(obj.Type.GetClrType().Equals(context.GetAvaloniaTypes().ReflectionBindingExtension) ||
obj.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)))
{
return new AvaloniaXamlIlDataContextTypeMetadataNode(on, obj.Type.GetClrType());
}

6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -51,6 +51,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType ClrPropertyInfo { get; }
public IXamlType IPropertyAccessor { get; }
public IXamlType PropertyInfoAccessorFactory { get; }
public IXamlType CompiledBinding { get; }
public IXamlType CompiledBindingPathBuilder { get; }
public IXamlType CompiledBindingPath { get; }
public IXamlType CompiledBindingExtension { get; }
@ -242,8 +243,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
ClrPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.ClrPropertyInfo");
IPropertyAccessor = cfg.TypeSystem.GetType("Avalonia.Data.Core.Plugins.IPropertyAccessor");
PropertyInfoAccessorFactory = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.PropertyInfoAccessorFactory");
CompiledBindingPathBuilder = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPathBuilder");
CompiledBindingPath = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath");
CompiledBinding = cfg.TypeSystem.GetType("Avalonia.Data.CompiledBinding");
CompiledBindingPathBuilder = cfg.TypeSystem.GetType("Avalonia.Data.CompiledBindingPathBuilder");
CompiledBindingPath = cfg.TypeSystem.GetType("Avalonia.Data.CompiledBindingPath");
CompiledBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension");
ResolveByNameExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ResolveByNameExtension");
DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate");

6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs

@ -1013,11 +1013,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
var intType = context.Configuration.TypeSystem.GetType("System.Int32");
var types = context.GetAvaloniaTypes();
// We're calling the CompiledBindingPathBuilder(int apiVersion) with an apiVersion
// of 1 to indicate that we don't want TemplatedParent compatibility hacks enabled.
codeGen
.Ldc_I4(1)
.Newobj(types.CompiledBindingPathBuilder.GetConstructor(new() { intType }));
codeGen.Newobj(types.CompiledBindingPathBuilder.GetConstructor());
foreach (var transform in _transformElements)
{

1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -20,7 +20,6 @@
<Compile Include="Extensions.cs" />
<Compile Include="MarkupExtension.cs" />
<Compile Include="MarkupExtensions\CompiledBindingExtension.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\CompiledBindingPath.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\PropertyInfoAccessorFactory.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\TaskStreamPlugin.cs" />
<Compile Include="MarkupExtensions\DynamicResourceExtension.cs" />

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

@ -7,15 +7,13 @@ using Avalonia.Data.Converters;
using Avalonia.Data.Core;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.Parsers;
using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
namespace Avalonia.Markup.Xaml.MarkupExtensions
{
public sealed class CompiledBindingExtension : BindingBase
public sealed class CompiledBindingExtension : CompiledBinding
{
public CompiledBindingExtension()
{
Path = new CompiledBindingPath();
}
public CompiledBindingExtension(CompiledBindingPath path)
@ -23,9 +21,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Path = path;
}
public CompiledBindingExtension ProvideValue(IServiceProvider provider)
public CompiledBinding ProvideValue(IServiceProvider? provider)
{
return new CompiledBindingExtension
return new CompiledBinding
{
Path = Path,
Delay = Delay,
@ -38,184 +36,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Priority = Priority,
StringFormat = StringFormat,
Source = Source,
DefaultAnchor = new WeakReference(provider.GetDefaultAnchor()),
DefaultAnchor = new WeakReference(provider?.GetDefaultAnchor()),
UpdateSourceTrigger = UpdateSourceTrigger,
};
}
/// <summary>
/// Gets or sets the amount of time, in milliseconds, to wait before updating the binding
/// source after the value on the target changes.
/// </summary>
/// <remarks>
/// There is no delay when the source is updated via <see cref="UpdateSourceTrigger.LostFocus"/>
/// or <see cref="BindingExpressionBase.UpdateSource"/>. Nor is there a delay when
/// <see cref="BindingMode.OneWayToSource"/> is active and a new source object is provided.
/// </remarks>
public int Delay { get; set; }
/// <summary>
/// Gets or sets the <see cref="IValueConverter"/> to use.
/// </summary>
public IValueConverter? Converter { get; set; }
/// <summary>
/// Gets or sets the culture in which to evaluate the converter.
/// </summary>
/// <value>The default value is null.</value>
/// <remarks>
/// If this property is not set then <see cref="CultureInfo.CurrentCulture"/> will be used.
/// </remarks>
[TypeConverter(typeof(CultureInfoIetfLanguageTagConverter))]
public CultureInfo? ConverterCulture { get; set; }
/// <summary>
/// Gets or sets a parameter to pass to <see cref="Converter"/>.
/// </summary>
public object? ConverterParameter { get; set; }
public Type? DataType { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding is unable to produce a value.
/// </summary>
public object? FallbackValue { get; set; } = AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
public BindingMode Mode { get; set; }
[ConstructorArgument("path")]
public CompiledBindingPath Path { get; set; }
/// <summary>
/// Gets or sets the binding priority.
/// </summary>
public BindingPriority Priority { get; set; }
/// <summary>
/// Gets or sets the source for the binding.
/// </summary>
public object? Source { get; set; } = AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets or sets the string format.
/// </summary>
public string? StringFormat { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding result is null.
/// </summary>
public object? TargetNullValue { get; set; } = AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets or sets a value that determines the timing of binding source updates for
/// <see cref="BindingMode.TwoWay"/> and <see cref="BindingMode.OneWayToSource"/> bindings.
/// </summary>
public UpdateSourceTrigger UpdateSourceTrigger { get; set; }
internal WeakReference? DefaultAnchor { get; set; }
internal override BindingExpressionBase CreateInstance(
AvaloniaObject target,
AvaloniaProperty? targetProperty,
object? anchor)
{
var enableDataValidation = targetProperty?.GetMetadata(target).EnableDataValidation ?? false;
return InstanceCore(target, targetProperty, anchor, enableDataValidation);
}
/// <summary>
/// Hack for TreeDataTemplate to create a binding expression for an item.
/// </summary>
/// <param name="source">The item.</param>
/// <remarks>
/// Ideally we'd do this in a more generic way but didn't have time to refactor
/// ITreeDataTemplate in time for 11.0. We should revisit this in 12.0.
/// </remarks>
// TODO12: Refactor
internal BindingExpression CreateObservableForTreeDataTemplate(object source)
{
if (Source != AvaloniaProperty.UnsetValue)
throw new NotSupportedException("Source bindings are not supported in this context.");
var nodes = new List<ExpressionNode>();
Path.BuildExpression(nodes, out var isRooted);
if (isRooted)
throw new NotSupportedException("Rooted binding paths are not supported in this context.");
return new BindingExpression(
source,
nodes,
FallbackValue,
delay: TimeSpan.FromMilliseconds(Delay),
converter: Converter,
converterParameter: ConverterParameter,
targetNullValue: TargetNullValue);
}
private BindingExpression InstanceCore(
AvaloniaObject target,
AvaloniaProperty? targetProperty,
object? anchor,
bool enableDataValidation)
{
var nodes = new List<ExpressionNode>();
// Build the expression nodes from the binding path.
Path.BuildExpression(nodes, out var isRooted);
// If the binding isn't rooted (i.e. doesn't have a Source or start with $parent, $self,
// #elementName etc.) then we need to add a data context source node.
if (Source == AvaloniaProperty.UnsetValue && !isRooted)
nodes.Insert(0, ExpressionNodeFactory.CreateDataContext(targetProperty));
// If the first node is an ISourceNode then allow it to select the source; otherwise
// use the binding source if specified, falling back to the target.
var source = nodes.Count > 0 && nodes[0] is SourceNode sn
? sn.SelectSource(Source, target, anchor ?? DefaultAnchor?.Target)
: Source != AvaloniaProperty.UnsetValue ? Source : target;
var (mode, trigger) = ResolveDefaultsFromMetadata(target, targetProperty);
return new BindingExpression(
source,
nodes,
FallbackValue,
delay: TimeSpan.FromMilliseconds(Delay),
converter: Converter,
converterCulture: ConverterCulture,
converterParameter: ConverterParameter,
enableDataValidation: enableDataValidation,
mode: mode,
priority: Priority,
stringFormat: StringFormat,
targetNullValue: TargetNullValue,
targetProperty: targetProperty,
targetTypeConverter: TargetTypeConverter.GetDefaultConverter(),
updateSourceTrigger: trigger);
}
private (BindingMode, UpdateSourceTrigger) ResolveDefaultsFromMetadata(
AvaloniaObject target,
AvaloniaProperty? targetProperty)
{
var mode = Mode;
var trigger = UpdateSourceTrigger == UpdateSourceTrigger.Default ?
UpdateSourceTrigger.PropertyChanged : UpdateSourceTrigger;
if (mode == BindingMode.Default)
{
if (targetProperty?.GetMetadata(target) is { } metadata)
mode = metadata.DefaultBindingMode;
else
mode = BindingMode.OneWay;
}
return (mode, trigger);
}
}
}

2
src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs

@ -33,7 +33,7 @@ namespace Avalonia.Markup.Xaml
Type Resolve (string qualifiedTypeName);
}
// TODO12: Move to Avalonia.Base
[AttributeUsage(AttributeTargets.Property)]
public sealed class ConstructorArgumentAttribute : Attribute
{

3
tests/Avalonia.Base.UnitTests/Data/Core/CompiledBindingPathFromExpressionBuilder.cs

@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;

24
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@ -639,8 +639,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
target.ApplyTemplate();
// Assert DataGridLikeColumn.Binding data type.
var compiledPath = ((CompiledBindingExtension)column.Binding!).Path;
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath.Elements));
var compiledPath = ((CompiledBinding)column.Binding!).Path;
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath!.Elements));
Assert.Equal(typeof(string), node.Property.PropertyType);
Assert.Equal(nameof(TestData.StringProperty), node.Property.Name);
@ -682,8 +682,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
target.ApplyTemplate();
// Assert DataGridLikeColumn.Binding data type.
var compiledPath = ((CompiledBindingExtension)column.Binding!).Path;
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath.Elements));
var compiledPath = ((CompiledBinding)column.Binding!).Path;
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath!.Elements));
Assert.Equal(typeof(int), node.Property.PropertyType);
// Assert DataGridLikeColumn.Template data type by evaluating the template.
@ -727,8 +727,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
target.ApplyTemplate();
// Assert DataGridLikeColumn.Binding data type.
var compiledPath = ((CompiledBindingExtension)column.Binding!).Path;
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath.Elements));
var compiledPath = ((CompiledBinding)column.Binding!).Path;
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath!.Elements));
Assert.Equal(typeof(int), node.Property.PropertyType);
// Assert DataGridLikeColumn.Template data type by evaluating the template.
@ -2060,9 +2060,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
x:DataType='local:TestDataContext'
X='{CompiledBinding StringProperty}' />";
var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var compiledPath = ((CompiledBindingExtension)control.X!).Path;
var compiledPath = ((CompiledBinding)control.X!).Path;
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath.Elements));
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath!.Elements));
Assert.Equal(typeof(string), node.Property.PropertyType);
}
}
@ -2078,9 +2078,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
X='{CompiledBinding StringProperty, DataType=local:TestDataContext}' />";
var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var compiledPath = ((CompiledBindingExtension)control.X!).Path;
var compiledPath = ((CompiledBinding)control.X!).Path;
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath.Elements));
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath!.Elements));
Assert.Equal(typeof(string), node.Property.PropertyType);
}
}
@ -2097,9 +2097,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
X='{CompiledBinding StringProperty, DataType=local:TestDataContext}' />";
var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(new RuntimeXamlLoaderDocument(xaml),
new RuntimeXamlLoaderConfiguration { UseCompiledBindingsByDefault = true });
var compiledPath = ((CompiledBindingExtension)control.X!).Path;
var compiledPath = ((CompiledBinding)control.X!).Path;
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath.Elements));
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath!.Elements));
Assert.Equal(typeof(string), node.Property.PropertyType);
}
}

Loading…
Cancel
Save