Browse Source

Improve AvaloniaObject.GetValue performance (#15342)

* Improve AvaloniaProperty.GetValue performance

* Remove unneeded MethodImpl on Optional.GetValueOrDefault

* Use ReferenceEqualityComparer instead of TypeEqualityComparer

---------

Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>
pull/15485/head
Julien Lebosquain 2 years ago
committed by GitHub
parent
commit
edceb96f34
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 56
      src/Avalonia.Base/AvaloniaObject.cs
  2. 92
      src/Avalonia.Base/AvaloniaProperty.cs
  3. 10
      src/Avalonia.Base/AvaloniaPropertyMetadata.cs
  4. 22
      src/Avalonia.Base/Compatibility/ReferenceEqualityComparer.cs
  5. 2
      src/Avalonia.Base/Data/BindingOperations.cs
  6. 4
      src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs
  7. 3
      src/Avalonia.Base/Data/Optional.cs
  8. 10
      src/Avalonia.Base/DirectProperty.cs
  9. 37
      src/Avalonia.Base/DirectPropertyBase.cs
  10. 5
      src/Avalonia.Base/DirectPropertyMetadata`1.cs
  11. 7
      src/Avalonia.Base/IDirectPropertyAccessor.cs
  12. 9
      src/Avalonia.Base/IStyledPropertyAccessor.cs
  13. 6
      src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
  14. 2
      src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs
  15. 2
      src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs
  16. 12
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  17. 3
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  18. 4
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs
  19. 2
      src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs
  20. 2
      src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs
  21. 63
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  22. 74
      src/Avalonia.Base/StyledProperty.cs
  23. 12
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  24. 208
      src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs
  25. 2
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
  26. 2
      src/Markup/Avalonia.Markup/Data/Binding.cs
  27. 2
      src/Markup/Avalonia.Markup/Data/BindingBase.cs
  28. 2
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  29. 2
      src/Shared/StringCompatibilityExtensions.cs
  30. 9
      tests/Avalonia.Base.UnitTests/Utilities/AvaloniaPropertyDictionaryTests.cs
  31. 2
      tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs

56
src/Avalonia.Base/AvaloniaObject.cs

@ -9,8 +9,8 @@ using Avalonia.Data.Core;
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia
{
@ -126,7 +126,7 @@ namespace Avalonia
/// <param name="property">The property.</param>
public void ClearValue(AvaloniaProperty property)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
_values.ClearValue(property);
}
@ -137,7 +137,7 @@ namespace Avalonia
/// <param name="property">The property.</param>
public void ClearValue<T>(AvaloniaProperty<T> property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
switch (property)
@ -159,7 +159,7 @@ namespace Avalonia
/// <param name="property">The property.</param>
public void ClearValue<T>(StyledProperty<T> property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
_values.ClearValue(property);
@ -171,11 +171,11 @@ namespace Avalonia
/// <param name="property">The property.</param>
public void ClearValue<T>(DirectPropertyBase<T> property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
p.InvokeSetter(this, p.GetUnsetValue(GetType()));
p.InvokeSetter(this, p.GetUnsetValue(this));
}
/// <summary>
@ -216,7 +216,7 @@ namespace Avalonia
/// <returns>The value.</returns>
public object? GetValue(AvaloniaProperty property)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
if (property.IsDirect)
return property.RouteGetValue(this);
@ -232,7 +232,7 @@ namespace Avalonia
/// <returns>The value.</returns>
public T GetValue<T>(StyledProperty<T> property)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
return _values.GetValue(property);
}
@ -245,7 +245,7 @@ namespace Avalonia
/// <returns>The value.</returns>
public T GetValue<T>(DirectPropertyBase<T> property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
var registered = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
@ -262,7 +262,7 @@ namespace Avalonia
/// </remarks>
public Optional<T> GetBaseValue<T>(StyledProperty<T> property)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
return _values.GetBaseValue(property);
}
@ -274,7 +274,7 @@ namespace Avalonia
/// <returns>True if the property is animating, otherwise false.</returns>
public bool IsAnimating(AvaloniaProperty property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
@ -292,7 +292,7 @@ namespace Avalonia
/// </remarks>
public bool IsSet(AvaloniaProperty property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
@ -310,7 +310,7 @@ namespace Avalonia
object? value,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
return property.RouteSetValue(this, value, priority);
}
@ -330,7 +330,7 @@ namespace Avalonia
T value,
BindingPriority priority = BindingPriority.LocalValue)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
ValidatePriority(priority);
@ -357,7 +357,7 @@ namespace Avalonia
/// <param name="value">The value.</param>
public void SetValue<T>(DirectPropertyBase<T> property, T value)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
@ -401,7 +401,7 @@ namespace Avalonia
/// </remarks>
public void SetCurrentValue<T>(StyledProperty<T> property, T value)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
LogPropertySet(property, value, BindingPriority.LocalValue);
@ -458,8 +458,8 @@ namespace Avalonia
IObservable<object?> source,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source));
ThrowHelper.ThrowIfNull(property, nameof(property));
ThrowHelper.ThrowIfNull(source, nameof(source));
VerifyAccess();
ValidatePriority(priority);
@ -495,8 +495,8 @@ namespace Avalonia
IObservable<T> source,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source));
ThrowHelper.ThrowIfNull(property, nameof(property));
ThrowHelper.ThrowIfNull(source, nameof(source));
VerifyAccess();
ValidatePriority(priority);
@ -518,8 +518,8 @@ namespace Avalonia
IObservable<BindingValue<T>> source,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source));
ThrowHelper.ThrowIfNull(property, nameof(property));
ThrowHelper.ThrowIfNull(source, nameof(source));
VerifyAccess();
ValidatePriority(priority);
@ -539,7 +539,7 @@ namespace Avalonia
DirectPropertyBase<T> property,
IObservable<object?> source)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
@ -574,7 +574,7 @@ namespace Avalonia
DirectPropertyBase<T> property,
IObservable<T> source)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
@ -600,7 +600,7 @@ namespace Avalonia
DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
@ -796,7 +796,7 @@ namespace Avalonia
{
if (value is UnsetValueType)
{
property.InvokeSetter(this, property.GetUnsetValue(GetType()));
property.InvokeSetter(this, property.GetUnsetValue(this));
}
else if (!(value is DoNothingType))
{
@ -815,7 +815,7 @@ namespace Avalonia
{
case BindingValueType.UnsetValue:
case BindingValueType.BindingError:
var fallback = value.HasValue ? value : value.WithValue(property.GetUnsetValue(GetType()));
var fallback = value.HasValue ? value : value.WithValue(property.GetUnsetValue(this));
property.InvokeSetter(this, fallback);
break;
case BindingValueType.DataValidationError:
@ -828,7 +828,7 @@ namespace Avalonia
break;
}
var metadata = property.GetMetadata(GetType());
var metadata = property.GetMetadata(this);
if (metadata.EnableDataValidation == true)
{

92
src/Avalonia.Base/AvaloniaProperty.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.PropertyStore;
@ -28,10 +29,11 @@ namespace Avalonia
/// <summary>
/// Provides a fast path when the property has no metadata overrides.
/// </summary>
private KeyValuePair<Type, AvaloniaPropertyMetadata>? _singleMetadata;
private Type? _singleHostType;
private AvaloniaPropertyMetadata? _singleMetadata;
private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadata;
private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadataCache = new Dictionary<Type, AvaloniaPropertyMetadata>();
private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadata = new(ReferenceEqualityComparer.Instance);
private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadataCache = new(ReferenceEqualityComparer.Instance);
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaProperty"/> class.
@ -60,8 +62,6 @@ namespace Avalonia
throw new ArgumentException("'name' may not contain periods.");
}
_metadata = new Dictionary<Type, AvaloniaPropertyMetadata>();
Name = name;
PropertyType = valueType;
OwnerType = ownerType;
@ -71,7 +71,8 @@ namespace Avalonia
metadata.Freeze();
_metadata.Add(hostType, metadata);
_defaultMetadata = metadata.GenerateTypeSafeMetadata();
_singleMetadata = new(hostType, metadata);
_singleHostType = hostType;
_singleMetadata = metadata;
}
/// <summary>
@ -85,8 +86,6 @@ namespace Avalonia
Type ownerType,
AvaloniaPropertyMetadata? metadata)
{
_metadata = new Dictionary<Type, AvaloniaPropertyMetadata>();
Name = source?.Name ?? throw new ArgumentNullException(nameof(source));
PropertyType = source.PropertyType;
OwnerType = ownerType ?? throw new ArgumentNullException(nameof(ownerType));
@ -474,11 +473,39 @@ namespace Avalonia
/// Gets the <see cref="AvaloniaPropertyMetadata"/> which applies to this property when it is used with the specified type.
/// </summary>
/// <typeparam name="T">The type for which to retrieve metadata.</typeparam>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public AvaloniaPropertyMetadata GetMetadata<T>() where T : AvaloniaObject => GetMetadata(typeof(T));
/// <inheritdoc cref="GetMetadata{T}"/>
/// <param name="type">The type for which to retrieve metadata.</param>
public AvaloniaPropertyMetadata GetMetadata(Type type) => GetMetadataWithOverrides(type);
/// <remarks>
/// For performance, prefer the <see cref="GetMetadata(Avalonia.AvaloniaObject)"/> overload when possible.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public AvaloniaPropertyMetadata GetMetadata(Type type)
{
if (_singleMetadata == _defaultMetadata)
{
return _defaultMetadata;
}
return GetMetadataFromCache(type);
}
/// <summary>
/// Gets the <see cref="AvaloniaPropertyMetadata"/> which applies to this property when it is used with the specified object.
/// </summary>
/// <param name="owner">The object for which to retrieve metadata.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public AvaloniaPropertyMetadata GetMetadata(AvaloniaObject owner)
{
if (_singleMetadata == _defaultMetadata)
{
return _defaultMetadata;
}
return GetMetadataFromCache(owner);
}
/// <summary>
/// Checks whether the <paramref name="value"/> is valid for the property.
@ -593,42 +620,65 @@ namespace Avalonia
_metadataCache.Clear();
_singleMetadata = null;
_singleHostType = null;
}
private protected abstract IObservable<AvaloniaPropertyChangedEventArgs> GetChanged();
private AvaloniaPropertyMetadata GetMetadataWithOverrides(Type type)
[MethodImpl(MethodImplOptions.NoInlining)]
private AvaloniaPropertyMetadata GetMetadataFromCache(AvaloniaObject obj)
{
// Don't cache if we have _singleMetadata: IsInstanceOfType is faster than a dictionary lookup.
if (_singleMetadata is not null)
{
return _singleHostType!.IsInstanceOfType(obj) ? _singleMetadata : _defaultMetadata;
}
var type = obj.GetType();
if (!_metadataCache.TryGetValue(type, out var result))
{
_metadataCache[type] = result = GetMetadataUncached(type);
}
return result;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private AvaloniaPropertyMetadata GetMetadataFromCache(Type type)
{
if (type is null)
if (!_metadataCache.TryGetValue(type, out var result))
{
throw new ArgumentNullException(nameof(type));
_metadataCache[type] = result = GetMetadataUncached(type);
}
if (_metadataCache.TryGetValue(type, out var result))
return result;
}
private AvaloniaPropertyMetadata GetMetadataUncached(Type type)
{
if (_singleMetadata == _defaultMetadata)
{
return result;
return _defaultMetadata;
}
if (_singleMetadata is { } singleMetadata)
if (_singleMetadata is not null)
{
return _metadataCache[type] = singleMetadata.Key.IsAssignableFrom(type) ? singleMetadata.Value : _defaultMetadata;
return _singleHostType!.IsAssignableFrom(type) ? _singleMetadata : _defaultMetadata;
}
var currentType = type;
while (currentType != null)
while (currentType is not null)
{
if (_metadata.TryGetValue(currentType, out result))
if (_metadata.TryGetValue(currentType, out var result))
{
_metadataCache[type] = result;
return result;
}
currentType = currentType.BaseType;
}
return _metadataCache[type] = _defaultMetadata;
return _defaultMetadata;
}
bool IPropertyInfo.CanGet => true;

10
src/Avalonia.Base/AvaloniaPropertyMetadata.cs

@ -8,7 +8,6 @@ namespace Avalonia
/// </summary>
public abstract class AvaloniaPropertyMetadata
{
private bool _isReadOnly;
private BindingMode _defaultBindingMode;
/// <summary>
@ -47,6 +46,11 @@ namespace Avalonia
/// </remarks>
public bool? EnableDataValidation { get; private set; }
/// <summary>
/// Gets whether this instance is read-only and can't be modified.
/// </summary>
public bool IsReadOnly { get; private set; }
/// <summary>
/// Merges the metadata with the base metadata.
/// </summary>
@ -56,7 +60,7 @@ namespace Avalonia
AvaloniaPropertyMetadata baseMetadata,
AvaloniaProperty property)
{
if (_isReadOnly)
if (IsReadOnly)
{
throw new InvalidOperationException("The metadata is read-only.");
}
@ -74,7 +78,7 @@ namespace Avalonia
/// No further modifications are allowed after this call.
/// </summary>
public void Freeze()
=> _isReadOnly = true;
=> IsReadOnly = true;
/// <summary>
/// Gets a copy of this object configured for use with any owner type.

22
src/Avalonia.Base/Compatibility/ReferenceEqualityComparer.cs

@ -0,0 +1,22 @@
#if NETSTANDARD2_0
using System.Runtime.CompilerServices;
namespace System.Collections.Generic;
internal sealed class ReferenceEqualityComparer : IEqualityComparer<object?>, IEqualityComparer
{
public static ReferenceEqualityComparer Instance { get; } = new();
private ReferenceEqualityComparer()
{
}
public new bool Equals(object? x, object? y)
=> ReferenceEquals(x, y);
public int GetHashCode(object? obj)
=> RuntimeHelpers.GetHashCode(obj);
}
#endif

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

@ -34,7 +34,7 @@ namespace Avalonia.Data
if (mode == BindingMode.Default)
{
mode = property.GetMetadata(target.GetType()).DefaultBindingMode;
mode = property.GetMetadata(target).DefaultBindingMode;
}
switch (mode)

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

@ -532,9 +532,9 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
if (TargetProperty is not null && _target?.TryGetTarget(out var target) == true)
{
if (TargetProperty.IsDirect)
_defaultValue = ((IDirectPropertyAccessor)TargetProperty).GetUnsetValue(target.GetType());
_defaultValue = ((IDirectPropertyAccessor)TargetProperty).GetUnsetValue(target);
else
_defaultValue = ((IStyledPropertyAccessor)TargetProperty).GetDefaultValue(target.GetType());
_defaultValue = ((IStyledPropertyAccessor)TargetProperty).GetDefaultValue(target);
_isDefaultValueInitialized = true;
return _defaultValue;

3
src/Avalonia.Base/Data/Optional.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Avalonia.Data
{
@ -67,7 +68,7 @@ namespace Avalonia.Data
/// Gets the value if present, otherwise the default value.
/// </summary>
/// <returns>The value.</returns>
public T? GetValueOrDefault() => HasValue ? _value : default;
public T? GetValueOrDefault() => _value;
/// <summary>
/// Gets the value if present, otherwise a default value.

10
src/Avalonia.Base/DirectProperty.cs

@ -97,7 +97,7 @@ namespace Avalonia
metadata.Freeze();
var result = new DirectProperty<TNewOwner, TValue>(
(DirectPropertyBase<TValue>)this,
this,
getter,
setter,
metadata);
@ -144,9 +144,9 @@ namespace Avalonia
}
object? IDirectPropertyAccessor.GetUnsetValue(Type type)
{
var metadata = GetMetadata(type);
return metadata.UnsetValue;
}
=> GetMetadata(type).UnsetValue;
object? IDirectPropertyAccessor.GetUnsetValue(AvaloniaObject owner)
=> GetMetadata(owner).UnsetValue;
}
}

37
src/Avalonia.Base/DirectPropertyBase.cs

@ -1,4 +1,5 @@
using System;
using System.Runtime.CompilerServices;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.PropertyStore;
@ -70,21 +71,37 @@ namespace Avalonia
/// <param name="type">The type.</param>
/// <returns>The unset value.</returns>
public TValue GetUnsetValue(Type type)
{
type = type ?? throw new ArgumentNullException(nameof(type));
return GetMetadata(type).UnsetValue;
}
=> GetMetadata(type).UnsetValue;
/// <summary>
/// Gets the property metadata for the specified type.
/// Gets the unset value for the property on the specified object.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>
/// The property metadata.
/// </returns>
/// <param name="owner">The object.</param>
/// <returns>The unset value.</returns>
public TValue GetUnsetValue(AvaloniaObject owner)
=> GetMetadata(owner).UnsetValue;
/// <inheritdoc cref="AvaloniaProperty.GetMetadata(System.Type)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new DirectPropertyMetadata<TValue> GetMetadata(Type type)
=> CastMetadata(base.GetMetadata(type));
/// <inheritdoc cref="AvaloniaProperty.GetMetadata(Avalonia.AvaloniaObject)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new DirectPropertyMetadata<TValue> GetMetadata(AvaloniaObject owner)
=> CastMetadata(base.GetMetadata(owner));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static DirectPropertyMetadata<TValue> CastMetadata(AvaloniaPropertyMetadata metadata)
{
return (DirectPropertyMetadata<TValue>)base.GetMetadata(type);
#if DEBUG
return (DirectPropertyMetadata<TValue>)metadata;
#else
// Avoid casts in release mode for performance.
// We control every path:
// it shouldn't be possible a metadata type other than a DirectPropertyMetadata<T> stored for a DirectPropertyBase<T>.
return Unsafe.As<DirectPropertyMetadata<TValue>>(metadata);
#endif
}
/// <summary>

5
src/Avalonia.Base/DirectPropertyMetadata`1.cs

@ -49,6 +49,11 @@ namespace Avalonia
/// <inheritdoc />
public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata()
{
if (IsReadOnly)
{
return this;
}
var copy = new DirectPropertyMetadata<TValue>(UnsetValue, DefaultBindingMode, EnableDataValidation);
copy.Freeze();
return copy;

7
src/Avalonia.Base/IDirectPropertyAccessor.cs

@ -1,5 +1,4 @@
using System;
using Avalonia.Metadata;
namespace Avalonia
{
@ -38,5 +37,11 @@ namespace Avalonia
/// </summary>
/// <param name="type">The type.</param>
object? GetUnsetValue(Type type);
/// <summary>
/// Gets the unset value of the property for the specified object.
/// </summary>
/// <param name="owner">The object.</param>
object? GetUnsetValue(AvaloniaObject owner);
}
}

9
src/Avalonia.Base/IStyledPropertyAccessor.cs

@ -16,6 +16,15 @@ namespace Avalonia
/// </returns>
object? GetDefaultValue(Type type);
/// <summary>
/// Gets the default value for the property for the specified object.
/// </summary>
/// <param name="owner">The object.</param>
/// <returns>
/// The default value.
/// </returns>
object? GetDefaultValue(AvaloniaObject owner);
/// <summary>
/// Validates the specified property value.
/// </summary>

6
src/Avalonia.Base/PropertyStore/BindingEntryBase.cs

@ -45,7 +45,7 @@ namespace Avalonia.PropertyStore
Frame = frame;
Property = property;
Source = source;
if (property.GetMetadata(target.GetType()).EnableDataValidation == true)
if (property.GetMetadata(target).EnableDataValidation == true)
_uncommon = new() { _hasDataValidation = true };
}
@ -112,7 +112,7 @@ namespace Avalonia.PropertyStore
protected abstract BindingValue<TValue> ConvertAndValidate(TSource value);
protected abstract BindingValue<TValue> ConvertAndValidate(BindingValue<TSource> value);
protected abstract TValue GetDefaultValue(Type ownerType);
protected abstract TValue GetDefaultValue(AvaloniaObject owner);
protected virtual void Start(bool produceValue)
{
@ -186,7 +186,7 @@ namespace Avalonia.PropertyStore
if (_uncommon?._isDefaultValueInitialized != true)
{
_uncommon ??= new();
_uncommon._defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType());
_uncommon._defaultValue = GetDefaultValue(Frame.Owner!.Owner);
_uncommon._isDefaultValueInitialized = true;
}

2
src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs

@ -15,7 +15,7 @@ namespace Avalonia.PropertyStore
public DirectBindingObserver(ValueStore owner, DirectPropertyBase<T> property)
{
_owner = owner;
_hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false;
_hasDataValidation = property.GetMetadata(owner.Owner).EnableDataValidation ?? false;
Property = property;
}

2
src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs

@ -16,7 +16,7 @@ namespace Avalonia.PropertyStore
public DirectUntypedBindingObserver(ValueStore owner, DirectPropertyBase<T> property)
{
_owner = owner;
_hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false;
_hasDataValidation = property.GetMetadata(owner.Owner).EnableDataValidation ?? false;
Property = property;
}

12
src/Avalonia.Base/PropertyStore/EffectiveValue.cs

@ -11,6 +11,11 @@ namespace Avalonia.PropertyStore
/// </remarks>
internal abstract class EffectiveValue
{
/// <summary>
/// Gets the property targeted by this value.
/// </summary>
public AvaloniaProperty Property { get; protected init; }
/// <summary>
/// Gets the current effective value as a boxed value.
/// </summary>
@ -53,6 +58,13 @@ namespace Avalonia.PropertyStore
/// </summary>
public bool IsCoercedDefaultValue { get; set; }
/// <summary>
/// Initializes a new instance of <see cref="EffectiveValue"/>.
/// </summary>
/// <param name="property">The property targeted by this value.</param>
protected EffectiveValue(AvaloniaProperty property)
=> Property = property;
/// <summary>
/// Begins a reevaluation pass on the effective value.
/// </summary>

3
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@ -23,10 +23,11 @@ namespace Avalonia.PropertyStore
AvaloniaObject owner,
StyledProperty<T> property,
EffectiveValue<T>? inherited)
: base(property)
{
Priority = BindingPriority.Unset;
BasePriority = BindingPriority.Unset;
_metadata = property.GetMetadata(owner.GetType());
_metadata = property.GetMetadata(owner);
var value = inherited is null ? _metadata.DefaultValue : inherited.Value;

4
src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs

@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore
{
_owner = owner;
Property = property;
_hasDataValidation = property.GetMetadata(owner.Owner.GetType()).EnableDataValidation ?? false;
_hasDataValidation = property.GetMetadata(owner.Owner).EnableDataValidation ?? false;
}
public StyledProperty<T> Property { get;}
@ -121,7 +121,7 @@ namespace Avalonia.PropertyStore
{
if (!_isDefaultValueInitialized)
{
_defaultValue = Property.GetDefaultValue(_owner.Owner.GetType());
_defaultValue = Property.GetDefaultValue(_owner.Owner);
_isDefaultValueInitialized = true;
}

2
src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs

@ -33,6 +33,6 @@ namespace Avalonia.PropertyStore
throw new NotSupportedException();
}
protected override TTarget GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType);
protected override TTarget GetDefaultValue(AvaloniaObject owner) => Property.GetDefaultValue(owner);
}
}

2
src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs

@ -51,6 +51,6 @@ namespace Avalonia.PropertyStore
return value;
}
protected override T GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType);
protected override T GetDefaultValue(AvaloniaObject owner) => Property.GetDefaultValue(owner);
}
}

63
src/Avalonia.Base/PropertyStore/ValueStore.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Diagnostics;
@ -283,11 +284,25 @@ namespace Avalonia.PropertyStore
public T GetValue<T>(StyledProperty<T> property)
{
// <!> Performance critical method
if (_effectiveValues.TryGetValue(property, out var v))
return ((EffectiveValue<T>)v).Value;
return CastEffectiveValue<T>(v).Value;
if (property.Inherits && TryGetInheritedValue(property, out v))
return ((EffectiveValue<T>)v).Value;
return property.GetDefaultValue(Owner.GetType());
return CastEffectiveValue<T>(v).Value;
return property.GetDefaultValue(Owner);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static EffectiveValue<T> CastEffectiveValue<T>(EffectiveValue value)
{
#if DEBUG
return (EffectiveValue<T>)value;
#else
// Avoid casts in release mode for performance since GetValue is a very hot path.
// We control every path:
// it shouldn't be possible to have something else than an EffectiveValue<T> stored for a T property.
return Unsafe.As<EffectiveValue<T>>(value);
#endif
}
public bool IsAnimating(AvaloniaProperty property)
@ -309,7 +324,7 @@ namespace Avalonia.PropertyStore
public void CoerceDefaultValue<T>(StyledProperty<T> property)
{
var metadata = property.GetMetadata(Owner.GetType());
var metadata = property.GetMetadata(Owner);
if (metadata.CoerceValue is null)
return;
@ -388,9 +403,9 @@ namespace Avalonia.PropertyStore
for (var i = 0; i < count; ++i)
{
f._effectiveValues.GetKeyValue(i, out var key, out var value);
if (key.Inherits)
values.TryAdd(key, new(value));
var value = f._effectiveValues.GetValue(i);
if (value.Property.Inherits)
values.TryAdd(value.Property, new(value));
}
f = f.InheritanceAncestor;
@ -405,19 +420,20 @@ namespace Avalonia.PropertyStore
for (var i = 0; i < count; ++i)
{
f._effectiveValues.GetKeyValue(i, out var key, out var value);
var value = f._effectiveValues.GetValue(i);
var property = value.Property;
if (!key.Inherits)
if (!property.Inherits)
continue;
if (values.TryGetValue(key, out var existing))
if (values.TryGetValue(property, out var existing))
{
if (existing.NewValue is null)
values[key] = existing.WithNewValue(value);
values[property] = existing.WithNewValue(value);
}
else
{
values.Add(key, new(null, value));
values.Add(property, new(null, value));
}
}
@ -431,12 +447,15 @@ namespace Avalonia.PropertyStore
var count = values.Count;
for (var i = 0; i < count; ++i)
{
values.GetKeyValue(i, out var key, out var v);
var v = values.GetValue(i);
var oldValue = v.OldValue;
var newValue = v.NewValue;
if (oldValue != newValue)
InheritedValueChanged(key, oldValue, newValue);
{
var property = v.OldValue?.Property ?? v.NewValue!.Property;
InheritedValueChanged(property, oldValue, newValue);
}
}
}
@ -1077,14 +1096,15 @@ namespace Avalonia.PropertyStore
// Remove all effective values that are still unset.
for (var i = _effectiveValues.Count - 1; i >= 0; --i)
{
_effectiveValues.GetKeyValue(i, out var key, out var e);
var e = _effectiveValues.GetValue(i);
var property = e.Property;
e.EndReevaluation(this, key);
e.EndReevaluation(this, property);
if (e.CanRemove())
{
RemoveEffectiveValue(key, i);
e.DisposeAndRaiseUnset(this, key);
RemoveEffectiveValue(property, i);
e.DisposeAndRaiseUnset(this, property);
if (i > _effectiveValues.Count)
break;
@ -1134,10 +1154,7 @@ namespace Avalonia.PropertyStore
AvaloniaProperty property,
[NotNullWhen(true)] out EffectiveValue? value)
{
if (_effectiveValues.TryGetValue(property, out value))
return true;
value = null;
return false;
return _effectiveValues.TryGetValue(property, out value);
}
private EffectiveValue? GetEffectiveValue(AvaloniaProperty property)
@ -1149,7 +1166,7 @@ namespace Avalonia.PropertyStore
private object? GetDefaultValue(AvaloniaProperty property)
{
return ((IStyledPropertyAccessor)property).GetDefaultValue(Owner.GetType());
return ((IStyledPropertyAccessor)property).GetDefaultValue(Owner);
}
private void DisposeExistingLocalValueBinding(AvaloniaProperty property)

74
src/Avalonia.Base/StyledProperty.cs

@ -1,7 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.PropertyStore;
using Avalonia.Utilities;
@ -12,6 +12,10 @@ namespace Avalonia
/// </summary>
public class StyledProperty<TValue> : AvaloniaProperty<TValue>, IStyledPropertyAccessor
{
// For performance, cache the default value if there's only one (mostly for AvaloniaObject.GetValue()),
// avoiding a GetMetadata() call which might need to iterate through the control hierarchy.
private Optional<TValue> _singleDefaultValue;
/// <summary>
/// Initializes a new instance of the <see cref="StyledProperty{T}"/> class.
/// </summary>
@ -41,8 +45,11 @@ namespace Avalonia
if (validate?.Invoke(metadata.DefaultValue) == false)
{
throw new ArgumentException(
$"'{metadata.DefaultValue}' is not a valid default value for '{name}'.");
$"'{metadata.DefaultValue}' is not a valid default value for '{name}'.",
nameof(metadata));
}
_singleDefaultValue = metadata.DefaultValue;
}
/// <summary>
@ -68,7 +75,7 @@ namespace Avalonia
public TValue CoerceValue(AvaloniaObject instance, TValue baseValue)
{
var metadata = GetMetadata(instance.GetType());
var metadata = GetMetadata(instance);
if (metadata.CoerceValue != null)
{
@ -83,22 +90,51 @@ namespace Avalonia
/// </summary>
/// <param name="type">The type.</param>
/// <returns>The default value.</returns>
/// <remarks>
/// For performance, prefer the <see cref="GetDefaultValue(Avalonia.AvaloniaObject)"/> overload when possible.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue GetDefaultValue(Type type)
{
return GetMetadata(type).DefaultValue;
return _singleDefaultValue.HasValue ?
_singleDefaultValue.GetValueOrDefault()! :
GetMetadata(type).DefaultValue;
}
/// <summary>
/// Gets the property metadata for the specified type.
/// Gets the default value for the property on the specified object.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>
/// The property metadata.
/// </returns>
/// <param name="owner">The object.</param>
/// <returns>The default value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue GetDefaultValue(AvaloniaObject owner)
{
return _singleDefaultValue.HasValue ?
_singleDefaultValue.GetValueOrDefault()! :
GetMetadata(owner).DefaultValue;
}
/// <inheritdoc cref="AvaloniaProperty.GetMetadata(System.Type)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new StyledPropertyMetadata<TValue> GetMetadata(Type type)
=> CastMetadata(base.GetMetadata(type));
/// <inheritdoc cref="AvaloniaProperty.GetMetadata(Avalonia.AvaloniaObject)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new StyledPropertyMetadata<TValue> GetMetadata(AvaloniaObject owner)
=> CastMetadata(base.GetMetadata(owner));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static StyledPropertyMetadata<TValue> CastMetadata(AvaloniaPropertyMetadata metadata)
{
_ = type ?? throw new ArgumentNullException(nameof(type));
return (StyledPropertyMetadata<TValue>)base.GetMetadata(type);
#if DEBUG
return (StyledPropertyMetadata<TValue>)metadata;
#else
// Avoid casts in release mode for performance (GetMetadata is a hot path).
// We control every path:
// it shouldn't be possible a metadata type other than a StyledPropertyMetadata<T> stored for a StyledProperty<T>.
return Unsafe.As<StyledPropertyMetadata<TValue>>(metadata);
#endif
}
/// <summary>
@ -145,6 +181,11 @@ namespace Avalonia
}
base.OverrideMetadata(type, metadata);
if (_singleDefaultValue != metadata.DefaultValue)
{
_singleDefaultValue = default;
}
}
/// <summary>
@ -156,8 +197,9 @@ namespace Avalonia
return Name;
}
/// <inheritdoc/>
object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultValue(type);
object? IStyledPropertyAccessor.GetDefaultValue(AvaloniaObject owner) => GetDefaultValue(owner);
bool IStyledPropertyAccessor.ValidateValue(object? value)
{
@ -253,11 +295,5 @@ namespace Avalonia
throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})");
}
}
private object? GetDefaultBoxedValue(Type type)
{
_ = type ?? throw new ArgumentNullException(nameof(type));
return GetMetadata(type).DefaultValue;
}
}
}

12
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@ -1,4 +1,5 @@
using System;
using System.Runtime.CompilerServices;
using Avalonia.Data;
namespace Avalonia
@ -31,7 +32,11 @@ namespace Avalonia
/// <summary>
/// Gets the default value for the property.
/// </summary>
public TValue DefaultValue => _defaultValue.GetValueOrDefault()!;
public TValue DefaultValue
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _defaultValue.GetValueOrDefault()!;
}
/// <summary>
/// Gets the value coercion callback, if any.
@ -62,6 +67,11 @@ namespace Avalonia
/// <inheritdoc />
public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata()
{
if (IsReadOnly && CoerceValue is null)
{
return this;
}
var copy = new StyledPropertyMetadata<TValue>(DefaultValue, DefaultBindingMode, null, EnableDataValidation ?? false);
copy.Freeze();
return copy;

208
src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Avalonia.Utilities
{
@ -52,7 +54,7 @@ namespace Avalonia.Utilities
/// </summary>
/// <param name="property">The key to get or set.</param>
/// <returns>
/// The value associated with the specified key. If the key is not found, a get operation
/// The value associated with the specified key. If the key is not found, a get operation
/// throws a <see cref="KeyNotFoundException"/>, and a set operation creates a
/// new element for the specified key.
/// </returns>
@ -63,16 +65,18 @@ namespace Avalonia.Utilities
{
get
{
if (!TryGetEntry(property.Id, out var index))
var index = FindEntry(property.Id);
if (index < 0)
ThrowNotFound();
return _entries[index].Value;
return UnsafeGetEntryRef(index).Value;
}
set
{
if (TryGetEntry(property.Id, out var index))
_entries[index] = new Entry(property, value);
var index = FindEntry(property.Id);
if (index >= 0)
UnsafeGetEntryRef(index) = new Entry(property, value);
else
InsertEntry(new Entry(property, value), index);
InsertEntry(new Entry(property, value), ~index);
}
}
@ -88,7 +92,7 @@ namespace Avalonia.Utilities
{
if (index >= _entryCount)
ThrowOutOfRange();
return _entries![index].Value;
return UnsafeGetEntryRef(index).Value;
}
}
@ -99,11 +103,12 @@ namespace Avalonia.Utilities
/// <param name="value">The value of the element to add.</param>
public void Add(AvaloniaProperty property, TValue value)
{
if (TryGetEntry(property.Id, out var index))
var index = FindEntry(property.Id);
if (index >= 0)
ThrowDuplicate();
InsertEntry(new Entry(property, value), index);
InsertEntry(new Entry(property, value), ~index);
}
/// <summary>
/// Removes all keys and values from the collection.
/// </summary>
@ -124,27 +129,19 @@ namespace Avalonia.Utilities
/// Determines whether the collection contains the specified key.
/// </summary>
/// <param name="property">The key.</param>
public bool ContainsKey(AvaloniaProperty property) => TryGetEntry(property.Id, out _);
public bool ContainsKey(AvaloniaProperty property) => FindEntry(property.Id) >= 0;
/// <summary>
/// Gets the key and value at the specified index.
/// Gets value at the specified index.
/// </summary>
/// <param name="index">
/// The index of the entry, between 0 and <see cref="Count"/> - 1.
/// </param>
/// <param name="key">
/// When this method returns, contains the key at the specified index.
/// </param>
/// <param name="value">
/// When this method returns, contains the value at the specified index.
/// </param>
public void GetKeyValue(int index, out AvaloniaProperty key, out TValue value)
/// <param name="index">The index of the entry, between 0 and <see cref="Count"/> - 1.</param>
/// <returns>The value at the specified index.</returns>
public TValue GetValue(int index)
{
if (index >= _entryCount)
ThrowOutOfRange();
ref var entry = ref _entries![index];
key = entry.Property;
value = entry.Value;
ref var entry = ref UnsafeGetEntryRef(index);
return entry.Value;
}
/// <summary>
@ -157,7 +154,8 @@ namespace Avalonia.Utilities
/// </returns>
public bool Remove(AvaloniaProperty property)
{
if (TryGetEntry(property.Id, out var index))
var index = FindEntry(property.Id);
if (index >= 0)
{
RemoveAt(index);
return true;
@ -178,9 +176,10 @@ namespace Avalonia.Utilities
/// </returns>
public bool Remove(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{
if (TryGetEntry(property.Id, out var index))
var index = FindEntry(property.Id);
if (index >= 0)
{
value = _entries[index].Value;
value = UnsafeGetEntryRef(index).Value;
RemoveAt(index);
return true;
}
@ -196,11 +195,11 @@ namespace Avalonia.Utilities
public void RemoveAt(int index)
{
if (_entries is null)
throw new IndexOutOfRangeException();
ThrowOutOfRange();
Array.Copy(_entries, index + 1, _entries, index, _entryCount - index - 1);
_entryCount--;
_entries[_entryCount] = default;
UnsafeGetEntryRef(_entryCount) = default;
}
/// <summary>
@ -211,9 +210,10 @@ namespace Avalonia.Utilities
/// <returns></returns>
public bool TryAdd(AvaloniaProperty property, TValue value)
{
if (TryGetEntry(property.Id, out var index))
var index = FindEntry(property.Id);
if (index >= 0)
return false;
InsertEntry(new Entry(property, value), index);
InsertEntry(new Entry(property, value), ~index);
return true;
}
@ -228,73 +228,91 @@ namespace Avalonia.Utilities
/// <returns></returns>
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{
if (TryGetEntry(property.Id, out var index))
// <!> Very performance critical code: FindEntry has been manually inlined here.
// This gives a ~20% speedup in micro-benchmarks.
var lo = 0;
var hi = _entryCount - 1;
if (hi >= 0)
{
value = _entries[index].Value;
return true;
var propertyId = property.Id;
ref var entry0 = ref UnsafeGetEntryRef(0);
do
{
// hi and lo are never negative: there's no overflow using unsigned math
var i = (int)(((uint)hi + (uint)lo) >> 1);
#if NET6_0_OR_GREATER
// nuint cast to force zero extend instead of sign extend
ref var entry = ref Unsafe.Add(ref entry0, (nuint)i);
#else
ref var entry = ref Unsafe.Add(ref entry0, i);
#endif
var entryId = entry.Id;
if (entryId == propertyId)
{
value = entry.Value;
return true;
}
if (entryId < propertyId)
{
lo = i + 1;
}
else
{
hi = i - 1;
}
} while (lo <= hi);
}
value = default;
return false;
}
[MemberNotNullWhen(true, nameof(_entries))]
private bool TryGetEntry(int propertyId, out int index)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int FindEntry(int propertyId)
{
int checkIndex;
int iLo = 0;
int iHi = _entryCount;
var lo = 0;
var hi = _entryCount - 1;
if (iHi <= 0)
if (hi >= 0)
{
index = 0;
return false;
}
ref var entry0 = ref UnsafeGetEntryRef(0);
// Do a binary search to find the value
while (iHi - iLo > 3)
{
int iPv = (iHi + iLo) / 2;
checkIndex = _entries![iPv].Property.Id;
if (propertyId == checkIndex)
do
{
index = iPv;
return true;
}
if (propertyId <= checkIndex)
{
iHi = iPv;
}
else
{
iLo = iPv + 1;
}
// hi and lo are never negative: there's no overflow using unsigned math
var i = (int)(((uint)hi + (uint)lo) >> 1);
#if NET6_0_OR_GREATER
// nuint cast to force zero extend instead of sign extend
ref var entry = ref Unsafe.Add(ref entry0, (nuint)i);
#else
ref var entry = ref Unsafe.Add(ref entry0, i);
#endif
var entryId = entry.Id;
if (entryId == propertyId)
{
return i;
}
if (entryId < propertyId)
{
lo = i + 1;
}
else
{
hi = i - 1;
}
} while (lo <= hi);
}
// Now we only have three values to search; switch to a linear search
do
{
checkIndex = _entries![iLo].Property.Id;
if (checkIndex == propertyId)
{
index = iLo;
return true;
}
if (checkIndex > propertyId)
{
// we've gone past the targetIndex - return not found
break;
}
iLo++;
} while (iLo < iHi);
index = iLo;
return false;
return ~lo;
}
[MemberNotNull(nameof(_entries))]
@ -327,18 +345,30 @@ namespace Avalonia.Utilities
entryIndex + 1,
_entryCount - entryIndex);
_entries[entryIndex] = entry;
UnsafeGetEntryRef(entryIndex) = entry;
}
}
else
{
_entries ??= new Entry[DefaultInitialCapacity];
_entries[0] = entry;
UnsafeGetEntryRef(0) = entry;
}
_entryCount++;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ref Entry UnsafeGetEntryRef(int index)
{
#if NET6_0_OR_GREATER && !DEBUG
// This type is performance critical: in release mode, skip any bound check the JIT compiler couldn't elide.
// The index parameter should always be correct when calling this method: no unchecked user input should get here.
return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_entries!), (uint)index);
#else
return ref _entries![index];
#endif
}
[DoesNotReturn]
private static void ThrowOutOfRange() => throw new IndexOutOfRangeException();
@ -351,12 +381,12 @@ namespace Avalonia.Utilities
private readonly struct Entry
{
public readonly AvaloniaProperty Property;
public readonly int Id;
public readonly TValue Value;
public Entry(AvaloniaProperty property, TValue value)
{
Property = property;
Id = property.Id;
Value = value;
}
}

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

@ -63,7 +63,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
AvaloniaObject target,
object? anchor)
{
var enableDataValidation = targetProperty.GetMetadata(target.GetType()).EnableDataValidation ?? false;
var enableDataValidation = targetProperty.GetMetadata(target).EnableDataValidation ?? false;
return InstanceCore(target, targetProperty, anchor, enableDataValidation);
}

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

@ -77,7 +77,7 @@ namespace Avalonia.Data
AvaloniaObject target,
object? anchor)
{
var enableDataValidation = targetProperty.GetMetadata(target.GetType()).EnableDataValidation ?? false;
var enableDataValidation = targetProperty.GetMetadata(target).EnableDataValidation ?? false;
return InstanceCore(targetProperty, target, anchor, enableDataValidation);
}

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

@ -109,7 +109,7 @@ namespace Avalonia.Data
if (mode == BindingMode.Default)
{
if (targetProperty?.GetMetadata(target.GetType()) is { } metadata)
if (targetProperty?.GetMetadata(target) is { } metadata)
mode = metadata.DefaultBindingMode;
else
mode = BindingMode.OneWay;

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

@ -75,7 +75,7 @@ namespace Avalonia.Data
{
var input = InstanceCore(target, targetProperty);
var mode = Mode == BindingMode.Default ?
targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode;
targetProperty?.GetMetadata(target).DefaultBindingMode : Mode;
switch (mode)
{

2
src/Shared/StringCompatibilityExtensions.cs

@ -7,7 +7,7 @@ internal static class StringCompatibilityExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Contains(this string str, char search) =>
str.Contains(search.ToString());
str.IndexOf(search) >= 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool EndsWith(this string str, char search) =>

9
tests/Avalonia.Base.UnitTests/Utilities/AvaloniaPropertyDictionaryTests.cs

@ -161,7 +161,7 @@ namespace Avalonia.Base.UnitTests.Utilities
[Theory]
[MemberData(nameof(Counts))]
public void GetKeyValue_Finds_Value(int count)
public void GetValue_Finds_Value(int count)
{
if (count == 0)
return;
@ -169,20 +169,19 @@ namespace Avalonia.Base.UnitTests.Utilities
var target = CreateTarget(count);
var index = count / 2;
target.GetKeyValue(index, out var property, out var value);
var value = target.GetValue(index);
Assert.NotNull(property);
Assert.NotNull(value);
}
[Theory]
[MemberData(nameof(Counts))]
public void GetKeyValue_Throws_If_Index_Out_Of_Range(int count)
public void GetValue_Throws_If_Index_Out_Of_Range(int count)
{
var target = CreateTarget(count);
var index = count;
Assert.Throws<IndexOutOfRangeException>(() => target.GetKeyValue(index, out var _, out var _));
Assert.Throws<IndexOutOfRangeException>(() => target.GetValue(index));
}
[Theory]

2
tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs

@ -14,7 +14,7 @@ namespace Avalonia.Benchmarks
for (int j = 0; j < innerCount; ++j)
{
var child = new Button();
var child = new Button() { Width = 100, Height = 50 };
parent.Children.Add(child);

Loading…
Cancel
Save