diff --git a/src/Controls/src/Core/BindableObject.cs b/src/Controls/src/Core/BindableObject.cs index f205dbe7e611..5fd53e0680ca 100644 --- a/src/Controls/src/Core/BindableObject.cs +++ b/src/Controls/src/Core/BindableObject.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using Microsoft.Maui.Controls.Internals; using Microsoft.Maui.Dispatching; @@ -38,8 +39,8 @@ public BindableObject() } internal ushort _triggerCount = 0; - internal Dictionary _triggerSpecificity = new Dictionary(); - readonly Dictionary _properties = new Dictionary(4); + internal Dictionary _triggerSpecificity = new(); + readonly Dictionary _properties = new(4); bool _applying; WeakReference _inheritedContext; @@ -172,66 +173,19 @@ public object GetValue(BindableProperty property) return context == null ? property.DefaultValue : context.Values.GetValue(); } - internal LocalValueEnumerator GetLocalValueEnumerator() => new LocalValueEnumerator(this); - - internal sealed class LocalValueEnumerator : IEnumerator - { - Dictionary.Enumerator _propertiesEnumerator; - internal LocalValueEnumerator(BindableObject bindableObject) => _propertiesEnumerator = bindableObject._properties.GetEnumerator(); - - object IEnumerator.Current => Current; - public LocalValueEntry Current { get; private set; } - - public bool MoveNext() - { - if (_propertiesEnumerator.MoveNext()) - { - Current = new LocalValueEntry(_propertiesEnumerator.Current.Key, _propertiesEnumerator.Current.Value.Values.GetValue(), _propertiesEnumerator.Current.Value.Attributes); - return true; - } - return false; - } - - public void Dispose() => _propertiesEnumerator.Dispose(); - - void IEnumerator.Reset() - { - ((IEnumerator)_propertiesEnumerator).Reset(); - Current = null; - } - } - - internal sealed class LocalValueEntry - { - internal LocalValueEntry(BindableProperty property, object value, BindableContextAttributes attributes) - { - Property = property; - Value = value; - Attributes = attributes; - } - - public BindableProperty Property { get; } - public object Value { get; } - public BindableContextAttributes Attributes { get; } - } - internal (bool IsSet, T Value)[] GetValues(BindableProperty[] propArray) { - Dictionary properties = _properties; + var properties = _properties; var resultArray = new (bool IsSet, T Value)[propArray.Length]; for (int i = 0; i < propArray.Length; i++) { - if (properties.TryGetValue(propArray[i], out var context)) + ref var result = ref resultArray[i]; + if (properties.TryGetValue(propArray[i].InternalId, out var context)) { var pair = context.Values.GetSpecificityAndValue(); - resultArray[i].IsSet = pair.Key != SetterSpecificity.DefaultValue; - resultArray[i].Value = (T)pair.Value; - } - else - { - resultArray[i].IsSet = false; - resultArray[i].Value = default(T); + result.IsSet = pair.Key != SetterSpecificity.DefaultValue; + result.Value = (T)pair.Value; } } @@ -736,7 +690,7 @@ static void BindingContextPropertyChanged(BindableObject bindable, object oldval } [MethodImpl(MethodImplOptions.AggressiveInlining)] - BindablePropertyContext CreateAndAddContext(BindableProperty property) + BindablePropertyContext CreateContext(BindableProperty property) { var defaultValueCreator = property.DefaultValueCreator; var context = new BindablePropertyContext { Property = property }; @@ -745,15 +699,31 @@ BindablePropertyContext CreateAndAddContext(BindableProperty property) if (defaultValueCreator != null) context.Attributes = BindableContextAttributes.IsDefaultValueCreated; - _properties.Add(property, context); return context; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal BindablePropertyContext GetContext(BindableProperty property) => _properties.TryGetValue(property, out var result) ? result : null; + internal BindablePropertyContext GetContext(BindableProperty property) => _properties.TryGetValue(property.InternalId, out var result) ? result : null; [MethodImpl(MethodImplOptions.AggressiveInlining)] - BindablePropertyContext GetOrCreateContext(BindableProperty property) => GetContext(property) ?? CreateAndAddContext(property); + BindablePropertyContext GetOrCreateContext(BindableProperty property) + { +#if NETSTANDARD + var context = GetContext(property); + if (context is null) + { + context = CreateContext(property); + _properties.Add(property.InternalId, context); + } +#else + ref var context = ref CollectionsMarshal.GetValueRefOrAddDefault(_properties, property.InternalId, out var exists); + if (!exists) + { + context = CreateContext(property); + } +#endif + return context; + } void RemoveBinding(BindableProperty property, BindablePropertyContext context, SetterSpecificity specificity) { diff --git a/src/Controls/src/Core/BindableProperty.cs b/src/Controls/src/Core/BindableProperty.cs index ed771a2c94c8..43e25867c81a 100644 --- a/src/Controls/src/Core/BindableProperty.cs +++ b/src/Controls/src/Core/BindableProperty.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; +using System.Threading; using Microsoft.Maui.Controls.Xaml; using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics.Converters; @@ -181,6 +182,9 @@ public sealed class BindableProperty /// public static readonly object UnsetValue = new object(); + private static int _nextInternalId = int.MinValue; + internal readonly int InternalId; + BindableProperty(string propertyName, [DynamicallyAccessedMembers(ReturnTypeMembers)] Type returnType, [DynamicallyAccessedMembers(DeclaringTypeMembers)] Type declaringType, object defaultValue, BindingMode defaultBindingMode = BindingMode.OneWay, ValidateValueDelegate validateValue = null, BindingPropertyChangedDelegate propertyChanged = null, BindingPropertyChangingDelegate propertyChanging = null, CoerceValueDelegate coerceValue = null, BindablePropertyBindingChanging bindingChanging = null, bool isReadOnly = false, CreateDefaultValueDelegate defaultValueCreator = null) @@ -191,6 +195,8 @@ public sealed class BindableProperty throw new ArgumentNullException(nameof(returnType)); if (declaringType is null) throw new ArgumentNullException(nameof(declaringType)); + + InternalId = Interlocked.Increment(ref _nextInternalId); // don't use Enum.IsDefined as its redonkulously expensive for what it does if (defaultBindingMode != BindingMode.Default && defaultBindingMode != BindingMode.OneWay && defaultBindingMode != BindingMode.OneWayToSource && defaultBindingMode != BindingMode.TwoWay && defaultBindingMode != BindingMode.OneTime) diff --git a/src/Core/tests/Benchmarks/Benchmarks/BindableObjectBenchmarker.cs b/src/Core/tests/Benchmarks/Benchmarks/BindableObjectBenchmarker.cs new file mode 100644 index 000000000000..f768a4944c57 --- /dev/null +++ b/src/Core/tests/Benchmarks/Benchmarks/BindableObjectBenchmarker.cs @@ -0,0 +1,38 @@ +using System.Linq; +using BenchmarkDotNet.Attributes; +using Microsoft.Maui.Controls; + +namespace Microsoft.Maui.Benchmarks +{ + [MemoryDiagnoser] + public class BindableObjectBenchmarker + { + BindableProperty[] _properties; + + [Params(1, 3, 8, 15, 30, 50)] + public int PropertiesToSet { get; set; } + + [GlobalSetup] + public void Setup() + { + _properties = Enumerable.Range(0, PropertiesToSet) + .Select(i => BindableProperty.Create($"Property{i}", typeof(int), typeof(BindableObject), -1)) + .ToArray(); + } + + private class Bindable : BindableObject {} + + [Benchmark] + public void SetsAndReadsProperties() + { + var bindable = new Bindable(); + + var count = _properties.Length; + for (int i = 0; i < count; i++) + { + bindable.SetValue(_properties[i], i); + _ = bindable.GetValue(_properties[i]); + } + } + } +} \ No newline at end of file