diff --git a/src/Controls/src/Core/BindableObject.cs b/src/Controls/src/Core/BindableObject.cs index 88502e8472a1..f60147f81991 100644 --- a/src/Controls/src/Core/BindableObject.cs +++ b/src/Controls/src/Core/BindableObject.cs @@ -6,7 +6,6 @@ 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; @@ -39,8 +38,8 @@ public BindableObject() } internal ushort _triggerCount = 0; - internal Dictionary _triggerSpecificity = new(); - readonly Dictionary _properties = new(4); + internal Dictionary _triggerSpecificity = new Dictionary(); + readonly Dictionary _properties = new Dictionary(4); bool _applying; WeakReference _inheritedContext; @@ -173,19 +172,66 @@ 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) { - var properties = _properties; + Dictionary properties = _properties; var resultArray = new (bool IsSet, T Value)[propArray.Length]; for (int i = 0; i < propArray.Length; i++) { - ref var result = ref resultArray[i]; - if (properties.TryGetValue(propArray[i].InternalId, out var context)) + if (properties.TryGetValue(propArray[i], out var context)) { var pair = context.Values.GetSpecificityAndValue(); - result.IsSet = pair.Key != SetterSpecificity.DefaultValue; - result.Value = (T)pair.Value; + resultArray[i].IsSet = pair.Key != SetterSpecificity.DefaultValue; + resultArray[i].Value = (T)pair.Value; + } + else + { + resultArray[i].IsSet = false; + resultArray[i].Value = default(T); } } @@ -716,7 +762,7 @@ static void BindingContextPropertyChanged(BindableObject bindable, object oldval } [MethodImpl(MethodImplOptions.AggressiveInlining)] - BindablePropertyContext CreateContext(BindableProperty property) + BindablePropertyContext CreateAndAddContext(BindableProperty property) { var defaultValueCreator = property.DefaultValueCreator; var context = new BindablePropertyContext { Property = property }; @@ -725,31 +771,15 @@ BindablePropertyContext CreateContext(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.InternalId, out var result) ? result : null; + internal BindablePropertyContext GetContext(BindableProperty property) => _properties.TryGetValue(property, out var result) ? result : null; [MethodImpl(MethodImplOptions.AggressiveInlining)] - 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; - } + BindablePropertyContext GetOrCreateContext(BindableProperty property) => GetContext(property) ?? CreateAndAddContext(property); 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 371446d3bb1b..31e0fc7bbf31 100644 --- a/src/Controls/src/Core/BindableProperty.cs +++ b/src/Controls/src/Core/BindableProperty.cs @@ -6,7 +6,6 @@ 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; @@ -182,9 +181,6 @@ public sealed class BindableProperty /// A sentinel object used to indicate that a BindableProperty value has not been set. 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) @@ -195,8 +191,6 @@ 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/Controls/tests/Core.UnitTests/BindableObjectUnitTests.cs b/src/Controls/tests/Core.UnitTests/BindableObjectUnitTests.cs index f1f9316cf5e7..a9ee0fa89ab3 100644 --- a/src/Controls/tests/Core.UnitTests/BindableObjectUnitTests.cs +++ b/src/Controls/tests/Core.UnitTests/BindableObjectUnitTests.cs @@ -1564,6 +1564,100 @@ public void GetValues() Assert.Equal(5, values[2]); } + [Fact] + public void GetValuesReturnsSetStateAndValue() + { + var prop = BindableProperty.Create("Foo", typeof(int), typeof(MockBindable), 0); + var prop1 = BindableProperty.Create("Foo1", typeof(int), typeof(MockBindable), 1); + var prop2 = BindableProperty.Create("Foo2", typeof(int), typeof(MockBindable), 2); + var bindable = new MockBindable(); + + bindable.SetValue(prop, 3); + bindable.SetValue(prop2, 5); + + var values = bindable.GetValues(new[] { prop, prop1, prop2 }); + + Assert.Equal(3, values.Length); + Assert.True(values[0].IsSet); + Assert.Equal(3, values[0].Value); + Assert.False(values[1].IsSet); + Assert.Equal(0, values[1].Value); + Assert.True(values[2].IsSet); + Assert.Equal(5, values[2].Value); + } + + [Fact] + public void LocalValueEnumeratorReturnsLocallySetValues() + { + var prop = BindableProperty.Create("Foo", typeof(int), typeof(MockBindable), 0); + var prop1 = BindableProperty.Create("Foo1", typeof(int), typeof(MockBindable), 1); + var prop2 = BindableProperty.Create("Foo2", typeof(int), typeof(MockBindable), 2); + var bindable = new MockBindable(); + + bindable.SetValue(prop, 3); + bindable.SetValue(prop2, 5); + + var sawFirst = false; + var sawSecond = false; + + using var enumerator = bindable.GetLocalValueEnumerator(); + while (enumerator.MoveNext()) + { + var current = enumerator.Current; + + if (current.Property == prop) + { + sawFirst = true; + Assert.Equal(3, current.Value); + } + else if (current.Property == prop2) + { + sawSecond = true; + Assert.Equal(5, current.Value); + } + } + + Assert.True(sawFirst); + Assert.True(sawSecond); + } + + [Fact] + public void DefaultValueCreatorCachesValueWhenReentrantPropertyAddsResizeStore() + { + var reentrantProperties = new BindableProperty[8]; + for (var i = 0; i < reentrantProperties.Length; i++) + { + reentrantProperties[i] = BindableProperty.Create($"Reentrant{i}", typeof(int), typeof(MockBindable), 0); + } + + var defaultValueCreatorInvocations = 0; + var propertyWithCreator = BindableProperty.Create( + "ReentrantDefault", + typeof(int), + typeof(MockBindable), + 0, + defaultValueCreator: b => + { + defaultValueCreatorInvocations++; + for (var i = 0; i < reentrantProperties.Length; i++) + { + b.SetValue(reentrantProperties[i], i + 1); + } + + return 42; + }); + + var bindable = new MockBindable(); + + var first = (int)bindable.GetValue(propertyWithCreator); + var second = (int)bindable.GetValue(propertyWithCreator); + + Assert.Equal(42, first); + Assert.Equal(42, second); + Assert.Equal(1, defaultValueCreatorInvocations); + Assert.True(bindable.IsSet(propertyWithCreator)); + } + class BindingContextConverter : IValueConverter { diff --git a/src/Core/tests/Benchmarks/Benchmarks/BindableObjectBenchmarker.cs b/src/Core/tests/Benchmarks/Benchmarks/BindableObjectBenchmarker.cs deleted file mode 100644 index f768a4944c57..000000000000 --- a/src/Core/tests/Benchmarks/Benchmarks/BindableObjectBenchmarker.cs +++ /dev/null @@ -1,38 +0,0 @@ -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