diff --git a/src/Directory.Build.props b/src/Directory.Build.props index cc0bba6d99..5aca179d24 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -25,8 +25,8 @@ $(NoWarn);CS1591;CS1573 true true - netstandard2.1;net6.0;net472 - netstandard2.1;net6.0 + net8.0;net6.0;netstandard2.1;net472 + net8.0;net6.0;netstandard2.1 true false diff --git a/src/Framework/Framework/Binding/ActiveDotvvmProperty.cs b/src/Framework/Framework/Binding/ActiveDotvvmProperty.cs index 831b85d787..7d87b9982e 100644 --- a/src/Framework/Framework/Binding/ActiveDotvvmProperty.cs +++ b/src/Framework/Framework/Binding/ActiveDotvvmProperty.cs @@ -13,6 +13,10 @@ namespace DotVVM.Framework.Binding /// An abstract DotvvmProperty which contains code to be executed when the assigned control is being rendered. public abstract class ActiveDotvvmProperty : DotvvmProperty { + internal ActiveDotvvmProperty(string name, Type declaringType, bool isValueInherited) : base(name, declaringType, isValueInherited) + { + } + public abstract void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context, DotvvmControl control); diff --git a/src/Framework/Framework/Binding/CompileTimeOnlyDotvvmProperty.cs b/src/Framework/Framework/Binding/CompileTimeOnlyDotvvmProperty.cs index b982ee6569..971ca845d8 100644 --- a/src/Framework/Framework/Binding/CompileTimeOnlyDotvvmProperty.cs +++ b/src/Framework/Framework/Binding/CompileTimeOnlyDotvvmProperty.cs @@ -11,7 +11,7 @@ namespace DotVVM.Framework.Binding /// public class CompileTimeOnlyDotvvmProperty : DotvvmProperty { - public CompileTimeOnlyDotvvmProperty() + private CompileTimeOnlyDotvvmProperty(string name, Type declaringType) : base(name, declaringType, isValueInherited: false) { } @@ -37,7 +37,7 @@ public override bool IsSet(DotvvmBindableObject control, bool inherit = true) /// public static CompileTimeOnlyDotvvmProperty Register(string propertyName) { - var property = new CompileTimeOnlyDotvvmProperty(); + var property = new CompileTimeOnlyDotvvmProperty(propertyName, typeof(TDeclaringType)); return (CompileTimeOnlyDotvvmProperty)Register(propertyName, property: property); } } diff --git a/src/Framework/Framework/Binding/DelegateActionProperty.cs b/src/Framework/Framework/Binding/DelegateActionProperty.cs index 81c1e23c86..3bb96daef1 100644 --- a/src/Framework/Framework/Binding/DelegateActionProperty.cs +++ b/src/Framework/Framework/Binding/DelegateActionProperty.cs @@ -13,9 +13,9 @@ namespace DotVVM.Framework.Binding /// DotvvmProperty which calls the function passed in the Register method, when the assigned control is being rendered. public sealed class DelegateActionProperty: ActiveDotvvmProperty { - private Action func; + private readonly Action func; - public DelegateActionProperty(Action func) + public DelegateActionProperty(Action func, string name, Type declaringType, bool isValueInherited) : base(name, declaringType, isValueInherited) { this.func = func; } @@ -27,7 +27,8 @@ public override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestCon public static DelegateActionProperty Register(string name, Action func, [AllowNull] TValue defaultValue = default(TValue)) { - return (DelegateActionProperty)DotvvmProperty.Register(name, defaultValue, false, new DelegateActionProperty(func)); + var property = new DelegateActionProperty(func, name, typeof(TDeclaringType), isValueInherited: false); + return (DelegateActionProperty)DotvvmProperty.Register(name, defaultValue, false, property); } } diff --git a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs index 41e03091c4..f18f3d3de4 100644 --- a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs +++ b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs @@ -105,8 +105,10 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyG valueParameter ) ); - } + + static readonly ConstructorInfo DotvvmPropertyIdConstructor = typeof(DotvvmPropertyId).GetConstructor(new [] { typeof(uint) }).NotNull(); + public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyAccessors(Type type, DotvvmProperty property) { if (property is DotvvmPropertyAlias propertyAlias) @@ -123,18 +125,26 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyA var valueParameter = Expression.Parameter(type, "value"); var unwrappedType = type.UnwrapNullableType(); + var defaultObj = TypeConversion.BoxToObject(Constant(property.DefaultValue)); + // try to access the readonly static field, as .NET can optimize that better than whatever Linq.Expression Constant compiles to + var propertyExpr = + property.AttributeProvider is FieldInfo field && field.IsStatic && field.IsInitOnly && field.GetValue(null) == property + ? Field(null, field) + : (Expression)Constant(property); + var propertyIdExpr = New(DotvvmPropertyIdConstructor, Constant(property.Id.Id, typeof(uint))); + var boxedValueParameter = TypeConversion.BoxToObject(valueParameter); var setValueRaw = canUseDirectAccess - ? Call(typeof(Helpers), nameof(Helpers.SetValueDirect), Type.EmptyTypes, currentControlParameter, Constant(property), boxedValueParameter) - : Call(currentControlParameter, nameof(DotvvmBindableObject.SetValueRaw), Type.EmptyTypes, Constant(property), boxedValueParameter); + ? Call(typeof(Helpers), nameof(Helpers.SetValueDirect), Type.EmptyTypes, currentControlParameter, propertyIdExpr, defaultObj, boxedValueParameter) + : Call(currentControlParameter, nameof(DotvvmBindableObject.SetValueRaw), Type.EmptyTypes, propertyExpr, boxedValueParameter); if (typeof(IBinding).IsAssignableFrom(type)) { var getValueRaw = canUseDirectAccess - ? Call(typeof(Helpers), nameof(Helpers.GetValueRawDirect), Type.EmptyTypes, currentControlParameter, Constant(property)) - : Call(currentControlParameter, nameof(DotvvmBindableObject.GetValueRaw), Type.EmptyTypes, Constant(property), Constant(property.IsValueInherited)); + ? Call(typeof(Helpers), nameof(Helpers.GetValueRawDirect), Type.EmptyTypes, currentControlParameter, propertyIdExpr, defaultObj) + : Call(currentControlParameter, nameof(DotvvmBindableObject.GetValueRaw), Type.EmptyTypes, propertyExpr, Constant(property.IsValueInherited)); return ( Lambda( Convert(getValueRaw, type), @@ -173,11 +183,17 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyA Expression.Call( getValueOrBindingMethod, currentControlParameter, - Constant(property)), + canUseDirectAccess ? propertyIdExpr : propertyExpr, + defaultObj), currentControlParameter ), Expression.Lambda( - Expression.Call(setValueOrBindingMethod, currentControlParameter, Expression.Constant(property), valueParameter), + Expression.Call( + setValueOrBindingMethod, + currentControlParameter, + canUseDirectAccess ? propertyIdExpr : propertyExpr, + defaultObj, + valueParameter), currentControlParameter, valueParameter ) ); @@ -191,18 +207,18 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyA Expression getValue; if (canUseDirectAccess && unwrappedType.IsValueType) { - getValue = Call(typeof(Helpers), nameof(Helpers.GetStructValueDirect), new Type[] { unwrappedType }, currentControlParameter, Constant(property)); - if (type.IsNullable()) + getValue = Call(typeof(Helpers), nameof(Helpers.GetStructValueDirect), new Type[] { unwrappedType }, currentControlParameter, propertyIdExpr, Constant(property.DefaultValue, type.MakeNullableType())); + if (!type.IsNullable()) getValue = Expression.Property(getValue, "Value"); } else { - getValue = Call(currentControlParameter, getValueMethod, Constant(property), Constant(property.IsValueInherited)); + getValue = Call(currentControlParameter, getValueMethod, propertyExpr, Constant(property.IsValueInherited)); } return ( Expression.Lambda( Expression.Convert( - Expression.Call(currentControlParameter, getValueMethod, Expression.Constant(property), Expression.Constant(false)), + Expression.Call(currentControlParameter, getValueMethod, propertyExpr, Expression.Constant(false)), type ), currentControlParameter diff --git a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs index 12b4253588..4f771e0f3e 100644 --- a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs +++ b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs @@ -12,60 +12,60 @@ public partial class DotvvmCapabilityProperty { internal static class Helpers { - public static ValueOrBinding? GetOptionalValueOrBinding(DotvvmBindableObject c, DotvvmProperty p) + public static ValueOrBinding? GetOptionalValueOrBinding(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue) { if (c.properties.TryGet(p, out var x)) return ValueOrBinding.FromBoxedValue(x); else return null; } - public static ValueOrBinding GetValueOrBinding(DotvvmBindableObject c, DotvvmProperty p) + public static ValueOrBinding GetValueOrBinding(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue) { if (!c.properties.TryGet(p, out var x)) - x = p.DefaultValue; + x = defaultValue; return ValueOrBinding.FromBoxedValue(x); } - public static ValueOrBinding? GetOptionalValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p) + public static ValueOrBinding? GetOptionalValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p, object? defaultValue) { if (c.IsPropertySet(p)) return ValueOrBinding.FromBoxedValue(c.GetValue(p)); else return null; } - public static ValueOrBinding GetValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p) + public static ValueOrBinding GetValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p, object? defaultValue) { return ValueOrBinding.FromBoxedValue(c.GetValue(p)); } - public static void SetOptionalValueOrBinding(DotvvmBindableObject c, DotvvmProperty p, ValueOrBinding? val) + public static void SetOptionalValueOrBinding(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue, ValueOrBinding? val) { if (val.HasValue) { - SetValueOrBinding(c, p, val.GetValueOrDefault()); + SetValueOrBinding(c, p, defaultValue, val.GetValueOrDefault()); } else { c.properties.Remove(p); } } - public static void SetValueOrBinding(DotvvmBindableObject c, DotvvmProperty p, ValueOrBinding val) + public static void SetValueOrBinding(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue, ValueOrBinding val) { var boxedVal = val.UnwrapToObject(); - SetValueDirect(c, p, boxedVal); + SetValueDirect(c, p, defaultValue, boxedVal); } - public static void SetOptionalValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p, ValueOrBinding? val) + public static void SetOptionalValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p, object? defaultValue, ValueOrBinding? val) { if (val.HasValue) { - SetValueOrBindingSlow(c, p, val.GetValueOrDefault()); + SetValueOrBindingSlow(c, p, defaultValue, val.GetValueOrDefault()); } else { - c.SetValue(p, p.DefaultValue); // set to default value, just in case this property is backed in a different place than c.properties[p] + c.SetValue(p, defaultValue); // set to default value, just in case this property is backed in a different place than c.properties[p] c.properties.Remove(p); } } - public static void SetValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p, ValueOrBinding val) + public static void SetValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p, object? defaultValue, ValueOrBinding val) { var boxedVal = val.UnwrapToObject(); - if (Object.Equals(boxedVal, p.DefaultValue) && c.IsPropertySet(p)) + if (Object.Equals(boxedVal, defaultValue) && c.IsPropertySet(p)) { // setting to default value and the property is not set -> do nothing } @@ -75,15 +75,15 @@ public static void SetValueOrBindingSlow(DotvvmBindableObject c, DotvvmProper } } - public static object? GetValueRawDirect(DotvvmBindableObject c, DotvvmProperty p) + public static object? GetValueRawDirect(DotvvmBindableObject c, DotvvmPropertyId p, object defaultValue) { if (c.properties.TryGet(p, out var x)) { return x; } - else return p.DefaultValue; + else return defaultValue; } - public static T? GetStructValueDirect(DotvvmBindableObject c, DotvvmProperty p) + public static T? GetStructValueDirect(DotvvmBindableObject c, DotvvmPropertyId p, T? defaultValue) where T: struct { if (c.properties.TryGet(p, out var x)) @@ -94,11 +94,11 @@ public static void SetValueOrBindingSlow(DotvvmBindableObject c, DotvvmProper return xValue; return (T?)c.EvalPropertyValue(p, x); } - else return (T?)p.DefaultValue; + else return defaultValue; } - public static void SetValueDirect(DotvvmBindableObject c, DotvvmProperty p, object? value) + public static void SetValueDirect(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue, object? value) { - if (Object.Equals(p.DefaultValue, value) && !c.properties.Contains(p)) + if (Object.Equals(defaultValue, value) && !c.properties.Contains(p)) { // setting to default value and the property is not set -> do nothing } diff --git a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.cs b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.cs index aa22586320..dedcac62dc 100644 --- a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.cs +++ b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.cs @@ -45,13 +45,9 @@ private DotvvmCapabilityProperty( Type declaringType, ICustomAttributeProvider? attributeProvider, DotvvmCapabilityProperty? declaringCapability - ): base() + ): base(name ?? prefix + type.Name, declaringType, isValueInherited: false) { - name ??= prefix + type.Name; - - this.Name = name; this.PropertyType = type; - this.DeclaringType = declaringType; this.Prefix = prefix; this.AddUsedInCapability(declaringCapability); @@ -63,14 +59,15 @@ private DotvvmCapabilityProperty( AssertPropertyNotDefined(this, postContent: false); - var dotnetFieldName = ToPascalCase(name.Replace("-", "_").Replace(":", "_")); + var dotnetFieldName = ToPascalCase(Name.Replace("-", "_").Replace(":", "_")); attributeProvider ??= declaringType.GetProperty(dotnetFieldName) ?? declaringType.GetField(dotnetFieldName) ?? (ICustomAttributeProvider?)declaringType.GetField(dotnetFieldName + "Property") ?? - throw new Exception($"Capability backing field could not be found and capabilityAttributeProvider argument was not provided. Property: {declaringType.Name}.{name}. Please declare a field or property named {dotnetFieldName}."); + throw new Exception($"Capability backing field could not be found and capabilityAttributeProvider argument was not provided. Property: {declaringType.Name}.{Name}. Please declare a field or property named {dotnetFieldName}."); DotvvmProperty.InitializeProperty(this, attributeProvider); + this.MarkupOptions._mappingMode ??= MappingMode.Exclude; } public override object GetValue(DotvvmBindableObject control, bool inherit = true) => Getter(control); @@ -200,15 +197,8 @@ static DotvvmCapabilityProperty RegisterCapability(DotvvmCapabilityProperty prop { var declaringType = property.DeclaringType.NotNull(); var capabilityType = property.PropertyType.NotNull(); - var name = property.Name.NotNull(); AssertPropertyNotDefined(property); - var attributes = new CustomAttributesProvider( - new MarkupOptionsAttribute - { - MappingMode = MappingMode.Exclude - } - ); - DotvvmProperty.Register(name, capabilityType, declaringType, DBNull.Value, false, property, attributes); + DotvvmProperty.Register(property); if (!capabilityRegistry.TryAdd((declaringType, capabilityType, property.Prefix), property)) throw new($"unhandled naming conflict when registering capability {capabilityType}."); capabilityListRegistry.AddOrUpdate( diff --git a/src/Framework/Framework/Binding/DotvvmProperty.cs b/src/Framework/Framework/Binding/DotvvmProperty.cs index 8eb7261768..897549b88f 100644 --- a/src/Framework/Framework/Binding/DotvvmProperty.cs +++ b/src/Framework/Framework/Binding/DotvvmProperty.cs @@ -25,11 +25,13 @@ namespace DotVVM.Framework.Binding [DebuggerDisplay("{FullName}")] public class DotvvmProperty : IPropertyDescriptor { + public DotvvmPropertyId Id { get; } /// /// Gets or sets the name of the property. /// - public string Name { get; protected set; } + public string Name { get; } + [JsonIgnore] ITypeDescriptor IControlAttributeDescriptor.DeclaringType => new ResolvedTypeDescriptor(DeclaringType); @@ -50,7 +52,7 @@ public class DotvvmProperty : IPropertyDescriptor /// /// Gets the type of the class where the property is registered. /// - public Type DeclaringType { get; protected set; } + public Type DeclaringType { get; } /// /// Gets whether the value can be inherited from the parent controls. @@ -61,17 +63,17 @@ public class DotvvmProperty : IPropertyDescriptor /// Gets or sets the Reflection property information. /// [JsonIgnore] - public PropertyInfo? PropertyInfo { get; private set; } + public PropertyInfo? PropertyInfo { get; protected set; } /// /// Provider of custom attributes for this property. /// - internal ICustomAttributeProvider AttributeProvider { get; set; } + internal ICustomAttributeProvider AttributeProvider { get; private protected set; } /// /// Gets or sets the markup options. /// - public MarkupOptionsAttribute MarkupOptions { get; set; } + public MarkupOptionsAttribute MarkupOptions { get; protected set; } /// /// Determines if property type inherits from IBinding @@ -109,6 +111,8 @@ public string FullName IPropertyDescriptor? IControlAttributeDescriptor.OwningCapability => OwningCapability; IEnumerable IControlAttributeDescriptor.UsedInCapabilities => UsedInCapabilities; + private bool initialized = false; + internal void AddUsedInCapability(DotvvmCapabilityProperty? p) { @@ -119,12 +123,24 @@ internal void AddUsedInCapability(DotvvmCapabilityProperty? p) } } - /// - /// Prevents a default instance of the class from being created. - /// #pragma warning disable CS8618 // DotvvmProperty is usually initialized by InitializeProperty - internal DotvvmProperty() + internal DotvvmProperty(string name, Type declaringType, bool isValueInherited) + { + if (name is null) throw new ArgumentNullException(nameof(name)); + if (declaringType is null) throw new ArgumentNullException(nameof(declaringType)); + this.Name = name; + this.DeclaringType = declaringType; + this.IsValueInherited = isValueInherited; + this.Id = DotvvmPropertyIdAssignment.RegisterProperty(this); + } + internal DotvvmProperty(DotvvmPropertyId id, string name, Type declaringType) { + if (name is null) throw new ArgumentNullException(nameof(name)); + if (declaringType is null) throw new ArgumentNullException(nameof(declaringType)); + if (id.Id == 0) throw new ArgumentException("DotvvmProperty must have an ID", nameof(id)); + this.Name = name; + this.DeclaringType = declaringType; + this.Id = id; } internal DotvvmProperty( #pragma warning restore CS8618 @@ -133,7 +149,8 @@ internal DotvvmProperty( Type declaringType, object? defaultValue, bool isValueInherited, - ICustomAttributeProvider attributeProvider + ICustomAttributeProvider attributeProvider, + DotvvmPropertyId id = default ) { this.Name = name ?? throw new ArgumentNullException(nameof(name)); @@ -142,6 +159,9 @@ ICustomAttributeProvider attributeProvider this.DefaultValue = defaultValue; this.IsValueInherited = isValueInherited; this.AttributeProvider = attributeProvider ?? throw new ArgumentNullException(nameof(attributeProvider)); + if (id.Id == 0) + id = DotvvmPropertyIdAssignment.RegisterProperty(this); + this.Id = id; InitializeProperty(this); } @@ -158,6 +178,23 @@ public T[] GetAttributes() return attrA.Concat(attrB).ToArray(); } + public T? GetAttribute() where T: Attribute + { + var t = typeof(T); + var provider = AttributeProvider; + if (provider.IsDefined(t, true)) + { + return (T)provider.GetCustomAttributes(t, true).Single(); + } + var property = PropertyInfo; + if (property is {} && !object.ReferenceEquals(property, provider)) + { + return (T?)property.GetCustomAttribute(t, true); + } + + return null; + } + public bool IsOwnedByCapability(Type capability) => (this is DotvvmCapabilityProperty && this.PropertyType == capability) || OwningCapability?.IsOwnedByCapability(capability) == true; @@ -254,14 +291,32 @@ public virtual void SetValue(DotvvmBindableObject control, object? value) public static DotvvmProperty Register(string propertyName, Type propertyType, Type declaringType, object? defaultValue, bool isValueInherited, DotvvmProperty? property, ICustomAttributeProvider attributeProvider, bool throwOnDuplicateRegistration = true) { - if (property == null) property = new DotvvmProperty(); + if (propertyName is null) throw new ArgumentNullException(nameof(propertyName)); + if (propertyType is null) throw new ArgumentNullException(nameof(propertyType)); + if (declaringType is null) throw new ArgumentNullException(nameof(declaringType)); + if (attributeProvider is null) throw new ArgumentNullException(nameof(attributeProvider)); - property.Name = propertyName ?? throw new ArgumentNullException(nameof(propertyName)); - property.IsValueInherited = isValueInherited; - property.DeclaringType = declaringType ?? throw new ArgumentNullException(nameof(declaringType)); - property.PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); - property.DefaultValue = defaultValue; - property.AttributeProvider = attributeProvider ?? throw new ArgumentNullException(nameof(attributeProvider)); + if (property == null) + { + property = new DotvvmProperty(propertyName, propertyType, declaringType, defaultValue, isValueInherited, attributeProvider); + } + else + { + if (!property.initialized) + { + property.PropertyType = propertyType; + property.DefaultValue = defaultValue; + property.IsValueInherited = isValueInherited; + property.AttributeProvider = attributeProvider; + InitializeProperty(property, attributeProvider); + } + if (property.Name != propertyName) throw new ArgumentException("The property name does not match the existing property.", nameof(propertyName)); + if (property.IsValueInherited != isValueInherited) throw new ArgumentException("The IsValueInherited does not match the existing property.", nameof(isValueInherited)); + if (property.DeclaringType != declaringType) throw new ArgumentException("The declaring type does not match the existing property.", nameof(declaringType)); + if (property.PropertyType != propertyType) throw new ArgumentException("The property type does not match the existing property.", nameof(propertyType)); + if (property.DefaultValue != defaultValue) throw new ArgumentException("The default value does not match the existing property.", nameof(defaultValue)); + if (property.AttributeProvider != attributeProvider) throw new ArgumentException("The attribute provider does not match the existing property.", nameof(attributeProvider)); + } return Register(property, throwOnDuplicateRegistration); } @@ -284,7 +339,11 @@ public override string Message { get { internal static DotvvmProperty Register(DotvvmProperty property, bool throwOnDuplicateRegistration = true) { - InitializeProperty(property); + if (property.Id.Id == 0) + throw new Exception("DotvvmProperty must have an ID"); + + if (!property.initialized) + throw new Exception("DotvvmProperty must be initialized before registration."); var key = (property.DeclaringType, property.Name); if (!registeredProperties.TryAdd(key, property)) @@ -369,8 +428,8 @@ public static DotvvmPropertyAlias RegisterAlias( aliasName, declaringType, aliasAttribute.AliasedPropertyName, - aliasAttribute.AliasedPropertyDeclaringType ?? declaringType); - propertyAlias.AttributeProvider = attributeProvider; + aliasAttribute.AliasedPropertyDeclaringType ?? declaringType, + attributeProvider); propertyAlias.ObsoleteAttribute = attributeProvider.GetCustomAttribute(); var key = (propertyAlias.DeclaringType, propertyAlias.Name); @@ -392,6 +451,8 @@ public static DotvvmPropertyAlias RegisterAlias( public static void InitializeProperty(DotvvmProperty property, ICustomAttributeProvider? attributeProvider = null) { + if (property.initialized) + throw new Exception("DotvvmProperty should not be initialized twice."); if (string.IsNullOrWhiteSpace(property.Name)) throw new Exception("DotvvmProperty must not have empty name."); if (property.DeclaringType is null || property.PropertyType is null) @@ -405,24 +466,26 @@ public static void InitializeProperty(DotvvmProperty property, ICustomAttributeP property.PropertyInfo ?? throw new ArgumentNullException(nameof(attributeProvider)); property.MarkupOptions ??= - property.GetAttributes().SingleOrDefault() + property.GetAttribute() ?? new MarkupOptionsAttribute(); if (string.IsNullOrEmpty(property.MarkupOptions.Name)) property.MarkupOptions.Name = property.Name; property.DataContextChangeAttributes ??= - property.GetAttributes().ToArray(); + property.GetAttributes(); property.DataContextManipulationAttribute ??= - property.GetAttributes().SingleOrDefault(); - if (property.DataContextManipulationAttribute != null && property.DataContextChangeAttributes.Any()) + property.GetAttribute(); + if (property.DataContextManipulationAttribute != null && property.DataContextChangeAttributes.Length != 0) throw new ArgumentException($"{nameof(DataContextChangeAttributes)} and {nameof(DataContextManipulationAttribute)} cannot be set both at property '{property.FullName}'."); property.IsBindingProperty = typeof(IBinding).IsAssignableFrom(property.PropertyType); - property.ObsoleteAttribute = property.AttributeProvider.GetCustomAttribute(); + property.ObsoleteAttribute = property.GetAttribute(); if (property.IsBindingProperty) { property.MarkupOptions.AllowHardCodedValue = false; } + + property.initialized = true; } public static void CheckAllPropertiesAreRegistered(Type controlType) diff --git a/src/Framework/Framework/Binding/DotvvmPropertyAlias.cs b/src/Framework/Framework/Binding/DotvvmPropertyAlias.cs index dca78455b9..1adb9deeee 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyAlias.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyAlias.cs @@ -12,10 +12,9 @@ public DotvvmPropertyAlias( string aliasName, Type declaringType, string aliasedPropertyName, - Type aliasedPropertyDeclaringType) + Type aliasedPropertyDeclaringType, + System.Reflection.ICustomAttributeProvider attributeProvider): base(aliasName, declaringType, isValueInherited: false) { - Name = aliasName; - DeclaringType = declaringType; AliasedPropertyName = aliasedPropertyName; AliasedPropertyDeclaringType = aliasedPropertyDeclaringType; MarkupOptions = new MarkupOptionsAttribute(); diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs new file mode 100644 index 0000000000..9cf5aad070 --- /dev/null +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs @@ -0,0 +1,465 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Controls.Infrastructure; + +namespace DotVVM.Framework.Binding +{ + public readonly struct DotvvmPropertyId: IEquatable, IEquatable, IComparable + { + public readonly uint Id; + public DotvvmPropertyId(uint id) + { + Id = id; + } + + public DotvvmPropertyId(ushort typeOrGroupId, ushort memberId) + { + Id = ((uint)typeOrGroupId << 16) | memberId; + } + + [MemberNotNullWhen(true, nameof(PropertyGroupInstance), nameof(GroupMemberName))] + public bool IsPropertyGroup => (int)Id < 0; + public ushort TypeId => (ushort)(Id >> 16); + public ushort GroupId => (ushort)((Id >> 16) ^ 0x80_00); + public ushort MemberId => (ushort)(Id & 0xFFFF); + + public bool IsZero => Id == 0; + + public DotvvmProperty PropertyInstance => DotvvmPropertyIdAssignment.GetProperty(Id) ?? throw new Exception($"Property with ID {Id} not registered."); + public DotvvmPropertyGroup? PropertyGroupInstance => !IsPropertyGroup ? null : DotvvmPropertyIdAssignment.GetPropertyGroup(GroupId); + public string? GroupMemberName => !IsPropertyGroup ? null : DotvvmPropertyIdAssignment.GetGroupMemberName(MemberId); + + public bool IsInPropertyGroup(ushort id) => (this.Id >> 16) == ((uint)id | 0x80_00u); + + public static DotvvmPropertyId CreatePropertyGroupId(ushort groupId, ushort memberId) => new DotvvmPropertyId((ushort)(groupId | 0x80_00), memberId); + + public static implicit operator DotvvmPropertyId(uint id) => new DotvvmPropertyId(id); + + public bool Equals(DotvvmPropertyId other) => Id == other.Id; + public bool Equals(uint other) => Id == other; + public override bool Equals(object? obj) => obj is DotvvmPropertyId id && Equals(id); + public override int GetHashCode() => (int)Id; + + public static bool operator ==(DotvvmPropertyId left, DotvvmPropertyId right) => left.Equals(right); + public static bool operator !=(DotvvmPropertyId left, DotvvmPropertyId right) => !left.Equals(right); + + public override string ToString() => $"PropId={Id}"; + public int CompareTo(DotvvmPropertyId other) => Id.CompareTo(other.Id); + } + + static class DotvvmPropertyIdAssignment + { + const int DEFAULT_PROPERTY_COUNT = 16; + static readonly ConcurrentDictionary typeIds; + private static readonly object controlTypeRegisterLock = new object(); + private static int controlCounter = 256; // first 256 types are reserved for DotVVM controls + private static ControlTypeInfo[] controls = new ControlTypeInfo[1024]; + private static readonly object groupRegisterLock = new object(); + private static int groupCounter = 256; // first 256 types are reserved for DotVVM controls + private static DotvvmPropertyGroup?[] propertyGroups = new DotvvmPropertyGroup[1024]; + private static ulong[] propertyGroupActiveBitmap = new ulong[1024 / 64]; + static readonly ConcurrentDictionary propertyGroupMemberIds = new(concurrencyLevel: 1, capacity: 256) { + ["id"] = GroupMembers.id, + ["class"] = GroupMembers.@class, + ["style"] = GroupMembers.style, + ["name"] = GroupMembers.name, + ["data-bind"] = GroupMembers.databind, + }; + private static readonly object groupMemberRegisterLock = new object(); + static string?[] propertyGroupMemberNames = new string[1024]; + + static DotvvmPropertyIdAssignment() + { + foreach (var n in propertyGroupMemberIds) + { + propertyGroupMemberNames[n.Value] = n.Key; + } + + typeIds = new() { + [typeof(DotvvmBindableObject)] = TypeIds.DotvvmBindableObject, + [typeof(DotvvmControl)] = TypeIds.DotvvmControl, + [typeof(HtmlGenericControl)] = TypeIds.HtmlGenericControl, + [typeof(RawLiteral)] = TypeIds.RawLiteral, + [typeof(Literal)] = TypeIds.Literal, + [typeof(ButtonBase)] = TypeIds.ButtonBase, + [typeof(Button)] = TypeIds.Button, + [typeof(LinkButton)] = TypeIds.LinkButton, + [typeof(TextBox)] = TypeIds.TextBox, + [typeof(RouteLink)] = TypeIds.RouteLink, + [typeof(CheckableControlBase)] = TypeIds.CheckableControlBase, + [typeof(CheckBox)] = TypeIds.CheckBox, + [typeof(Validator)] = TypeIds.Validator, + [typeof(Validation)] = TypeIds.Validation, + [typeof(ValidationSummary)] = TypeIds.ValidationSummary, + }; + } + +#region Optimized metadata accessors + public static bool IsInherited(DotvvmPropertyId propertyId) + { + if (propertyId.IsPropertyGroup) + return false; + return BitmapRead(controls[propertyId.TypeId].inheritedBitmap, propertyId.MemberId); + } + + public static bool UsesStandardAccessors(DotvvmPropertyId propertyId) + { + if (propertyId.IsPropertyGroup) + { + // property groups can't override GetValue, otherwise VirtualPropertyGroupDictionary wouldn't work either + return true; + } + else + { + var bitmap = controls[propertyId.TypeId].standardBitmap; + var index = propertyId.MemberId; + return BitmapRead(bitmap, index); + } + } + + public static bool IsActive(DotvvmPropertyId propertyId) + { + Debug.Assert(DotvvmPropertyIdAssignment.GetProperty(propertyId) != null, $"Property {propertyId} not registered."); + ulong[] bitmap; + uint index; + if (propertyId.IsPropertyGroup) + { + bitmap = propertyGroupActiveBitmap; + index = propertyId.GroupId; + } + else + { + bitmap = controls[propertyId.TypeId].activeBitmap; + index = propertyId.MemberId; + } + return BitmapRead(bitmap, index); + } + + public static DotvvmProperty? GetProperty(DotvvmPropertyId id) + { + if (id.IsPropertyGroup) + { + var groupIx = id.GroupId; + if (groupIx >= propertyGroups.Length) + return null; + var group = propertyGroups[groupIx]; + if (group is null) + return null; + + return group.GetDotvvmProperty(id.MemberId); + } + else + { + var typeId = id.TypeId; + if (typeId >= controls.Length) + return null; + var typeProps = controls[typeId].properties; + if (typeProps is null) + return null; + return typeProps[id.MemberId]; + } + } + + public static Compilation.IControlAttributeDescriptor? GetPropertyOrPropertyGroup(DotvvmPropertyId id) + { + if (id.IsPropertyGroup) + { + var groupIx = id.GroupId; + if (groupIx >= propertyGroups.Length) + return null; + return propertyGroups[groupIx]; + } + else + { + var typeId = id.TypeId; + if (typeId >= controls.Length) + return null; + var typeProps = controls[typeId].properties; + if (typeProps is null) + return null; + return typeProps[id.MemberId]; + } + } + + public static object? GetValueRaw(DotvvmBindableObject obj, DotvvmPropertyId id, bool inherit = true) + { + if (id.IsPropertyGroup) + { + // property groups can't override GetValue + if (obj.properties.TryGet(id, out var value)) + return value; + + return propertyGroups[id.GroupId]!.DefaultValue; + } + else + { + // TODO: maybe try if using the std/inherit bitmaps would be faster + var property = controls[id.TypeId].properties[id.MemberId]; + return property!.GetValue(obj, inherit); + } + } + + public static MarkupOptionsAttribute GetMarkupOptions(DotvvmPropertyId id) + { + if (id.IsPropertyGroup) + { + var groupIx = id.GroupId; + return propertyGroups[groupIx]!.MarkupOptions; + } + else + { + var typeId = id.TypeId; + var typeProps = controls[typeId].properties; + return typeProps[id.MemberId]!.MarkupOptions; + } + } + + /// Property or property group has type assignable to IBinding and bindings should not be evaluated in GetValue + public static bool IsBindingProperty(DotvvmPropertyId id) + { + if (id.IsPropertyGroup) + { + var groupIx = id.GroupId; + return propertyGroups[groupIx]!.IsBindingProperty; + } + else + { + var typeId = id.TypeId; + var typeProps = controls[typeId].properties; + return typeProps[id.MemberId]!.IsBindingProperty; + } + } + + public static DotvvmPropertyGroup? GetPropertyGroup(ushort id) + { + if (id >= propertyGroups.Length) + return null; + return propertyGroups[id]; + } +#endregion + +#region Registration + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort RegisterType(Type type) + { + if (typeIds.TryGetValue(type, out var existingId) && controls[existingId].locker is {}) + return existingId; + + return unlikely(type); + + static ushort unlikely(Type type) + { + var types = MemoryMarshal.CreateReadOnlySpan(ref type, 1); + Span ids = stackalloc ushort[1]; + RegisterTypes(types, ids); + return ids[0]; + } + } + public static void RegisterTypes(ReadOnlySpan types, Span ids) + { + if (types.Length == 0) + return; + + lock (controlTypeRegisterLock) + { + if (controlCounter + types.Length >= controls.Length) + { + VolatileResize(ref controls, 1 << (BitOperations.Log2((uint)(controlCounter + types.Length)) + 1)); + } + for (int i = 0; i < types.Length; i++) + { + var type = types[i]; + if (!typeIds.TryGetValue(type, out var id)) + { + id = (ushort)controlCounter++; + } + if (controls[id].locker is null) + { + controls[id].locker = new object(); + controls[id].controlType = type; + controls[id].properties = new DotvvmProperty[DEFAULT_PROPERTY_COUNT]; + controls[id].inheritedBitmap = new ulong[(DEFAULT_PROPERTY_COUNT - 1) / 64 + 1]; + controls[id].standardBitmap = new ulong[(DEFAULT_PROPERTY_COUNT - 1) / 64 + 1]; + controls[id].activeBitmap = new ulong[(DEFAULT_PROPERTY_COUNT - 1) / 64 + 1]; + typeIds[type] = id; + } + ids[i] = id; + } + } + } + public static DotvvmPropertyId RegisterProperty(DotvvmProperty property) + { + if (property.GetType() == typeof(GroupedDotvvmProperty)) + throw new ArgumentException("RegisterProperty cannot be called with GroupedDotvvmProperty!"); + + var typeId = RegisterType(property.DeclaringType); + ref ControlTypeInfo control = ref controls[typeId]; + lock (control.locker) + { + var id = ++control.counter; + if (id > ushort.MaxValue) + throw new Exception("Too many properties registered for a single control type."); + if (id >= control.properties.Length) + { + VolatileResize(ref control.properties, control.properties.Length * 2); + VolatileResize(ref control.inheritedBitmap, control.inheritedBitmap.Length * 2); + VolatileResize(ref control.standardBitmap, control.standardBitmap.Length * 2); + VolatileResize(ref control.activeBitmap, control.activeBitmap.Length * 2); + } + + if (property.IsValueInherited) + BitmapSet(control.inheritedBitmap, (uint)id); + if (property.GetType() == typeof(DotvvmProperty)) + BitmapSet(control.standardBitmap, (uint)id); + if (property is ActiveDotvvmProperty) + BitmapSet(control.activeBitmap, (uint)id); + + control.properties[id] = property; + return new DotvvmPropertyId(typeId, (ushort)id); + } + } + + public static ushort RegisterPropertyGroup(DotvvmPropertyGroup group) + { + lock (groupRegisterLock) + { + var id = (ushort)groupCounter++; + if (id == 0) + throw new Exception("Too many property groups registered already."); + + if (id >= propertyGroups.Length) + { + VolatileResize(ref propertyGroups, propertyGroups.Length * 2); + VolatileResize(ref propertyGroupActiveBitmap, propertyGroupActiveBitmap.Length * 2); + } + + propertyGroups[id] = group; + if (group is ActiveDotvvmPropertyGroup) + BitmapSet(propertyGroupActiveBitmap, id); + return id; + } + } + + /// Thread-safe to read from the array while we are resizing + private static void VolatileResize(ref T[] array, int newSize) + { + var local = array; + Array.Resize(ref local, newSize); + Thread.MemoryBarrier(); // prevent reordering of the array assignment and array contents copy on weakly-ordered platforms + array = local; + } + +#endregion Registration + +#region Group members + private static ushort PredefinedPropertyGroupMemberId(ReadOnlySpan name) + { + switch (name) + { + case "class": return GroupMembers.@class; + case "id": return GroupMembers.id; + case "style": return GroupMembers.style; + case "name": return GroupMembers.name; + case "data-bind": return GroupMembers.databind; + default: return 0; + } + } + + public static ushort GetGroupMemberId(string name, bool registerIfNotFound) + { + var id = PredefinedPropertyGroupMemberId(name); + if (id != 0) + return id; + if (propertyGroupMemberIds.TryGetValue(name, out id)) + return id; + if (!registerIfNotFound) + return 0; + return RegisterGroupMember(name); + } + + private static ushort RegisterGroupMember(string name) + { + lock (groupMemberRegisterLock) + { + if (propertyGroupMemberIds.TryGetValue(name, out var id)) + return id; + id = (ushort)(propertyGroupMemberIds.Count + 1); + if (id == 0) + throw new Exception("Too many property group members registered already."); + if (id >= propertyGroupMemberNames.Length) + VolatileResize(ref propertyGroupMemberNames, propertyGroupMemberNames.Length * 2); + propertyGroupMemberNames[id] = name; + propertyGroupMemberIds[name] = id; + return id; + } + } + + internal static string? GetGroupMemberName(ushort id) + { + if (id < propertyGroupMemberNames.Length) + return propertyGroupMemberNames[id]; + return null; + } +#endregion Group members + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool BitmapRead(ulong[] bitmap, uint index) + { + return (bitmap[index / 64] & (1ul << (int)(index % 64))) != 0; + } + + static void BitmapSet(ulong[] bitmap, uint index) + { + bitmap[index / 64] |= 1ul << (int)(index % 64); + } + + private struct ControlTypeInfo + { + public object locker; + public DotvvmProperty?[] properties; + public Type controlType; + public ulong[] inheritedBitmap; + public ulong[] standardBitmap; + public ulong[] activeBitmap; + public int counter; + } + + public static class GroupMembers + { + public const ushort id = 1; + public const ushort @class = 2; + public const ushort style = 3; + public const ushort name = 4; + public const ushort databind = 5; + } + + public static class TypeIds + { + public const ushort DotvvmBindableObject = 1; + public const ushort DotvvmControl = 2; + public const ushort HtmlGenericControl = 3; + public const ushort RawLiteral = 4; + public const ushort Literal = 5; + public const ushort ButtonBase = 6; + public const ushort Button = 7; + public const ushort LinkButton = 8; + public const ushort TextBox = 9; + public const ushort RouteLink = 10; + public const ushort CheckableControlBase = 11; + public const ushort CheckBox = 12; + public const ushort Validator = 13; + public const ushort Validation = 14; + public const ushort ValidationSummary = 15; + // public const short Internal = 4; + } + + } +} diff --git a/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs b/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs index 08ede07bb6..f41257d340 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs @@ -16,7 +16,7 @@ public sealed class DotvvmPropertyWithFallback : DotvvmProperty /// public DotvvmProperty FallbackProperty { get; private set; } - public DotvvmPropertyWithFallback(DotvvmProperty fallbackProperty) + public DotvvmPropertyWithFallback(DotvvmProperty fallbackProperty, string name, Type declaringType, bool isValueInherited): base(name, declaringType, isValueInherited) { this.FallbackProperty = fallbackProperty; } @@ -61,7 +61,7 @@ public static DotvvmProperty Register(Expression< /// Indicates whether the value can be inherited from the parent controls. public static DotvvmPropertyWithFallback Register(string propertyName, DotvvmProperty fallbackProperty, bool isValueInherited = false) { - var property = new DotvvmPropertyWithFallback(fallbackProperty); + var property = new DotvvmPropertyWithFallback(fallbackProperty, propertyName, typeof(TDeclaringType), isValueInherited: isValueInherited); Register(propertyName, isValueInherited: isValueInherited, property: property); property.DefaultValue = fallbackProperty.DefaultValue; return property; diff --git a/src/Framework/Framework/Binding/GroupedDotvvmProperty.cs b/src/Framework/Framework/Binding/GroupedDotvvmProperty.cs index 4f5f91c0d1..8556b39e87 100644 --- a/src/Framework/Framework/Binding/GroupedDotvvmProperty.cs +++ b/src/Framework/Framework/Binding/GroupedDotvvmProperty.cs @@ -16,28 +16,26 @@ public sealed class GroupedDotvvmProperty : DotvvmProperty, IGroupedPropertyDesc IPropertyGroupDescriptor IGroupedPropertyDescriptor.PropertyGroup => PropertyGroup; - public GroupedDotvvmProperty(string groupMemberName, DotvvmPropertyGroup propertyGroup) + private GroupedDotvvmProperty(string memberName, ushort memberId, DotvvmPropertyGroup group) + : base(DotvvmPropertyId.CreatePropertyGroupId(group.Id, memberId), group.Name + ":" + memberName, group.DeclaringType) { - this.GroupMemberName = groupMemberName; - this.PropertyGroup = propertyGroup; + this.GroupMemberName = memberName; + this.PropertyGroup = group; } - public static GroupedDotvvmProperty Create(DotvvmPropertyGroup group, string name) + public static GroupedDotvvmProperty Create(DotvvmPropertyGroup group, string name, ushort id) { - var propname = group.Name + ":" + name; - var prop = new GroupedDotvvmProperty(name, group) { + var prop = new GroupedDotvvmProperty(name, id, group) { PropertyType = group.PropertyType, - DeclaringType = group.DeclaringType, DefaultValue = group.DefaultValue, IsValueInherited = false, - Name = propname, ObsoleteAttribute = group.ObsoleteAttribute, OwningCapability = group.OwningCapability, UsedInCapabilities = group.UsedInCapabilities }; - DotvvmProperty.InitializeProperty(prop, group.AttributeProvider); + DotvvmProperty.InitializeProperty(prop, group.AttributeProvider); // TODO: maybe inline and specialize to just copy the group attributes return prop; } } diff --git a/src/Framework/Framework/Binding/ValueOrBinding.cs b/src/Framework/Framework/Binding/ValueOrBinding.cs index 7c6a073fe9..b3f4e254ff 100644 --- a/src/Framework/Framework/Binding/ValueOrBinding.cs +++ b/src/Framework/Framework/Binding/ValueOrBinding.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Binding.Properties; @@ -50,6 +51,7 @@ public ValueOrBinding(IStaticValueBinding binding) } /// Creates new ValueOrBinding which contains the specified value. Note that there is an implicit conversion for this, so calling the constructor explicitly may be unnecessary. + [DebuggerStepThrough] public ValueOrBinding(T value) { this.value = value; diff --git a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs index 8be168dd0e..f967e80f9b 100644 --- a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs +++ b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs @@ -24,17 +24,24 @@ public VirtualPropertyGroupDictionary(DotvvmBindableObject control, DotvvmProper this.group = group; } + DotvvmPropertyId GetMemberId(string key, bool createNew = false) + { + var memberId = DotvvmPropertyIdAssignment.GetGroupMemberId(key, registerIfNotFound: createNew); + return DotvvmPropertyId.CreatePropertyGroupId(group.Id, memberId); + } + + string GetMemberName(DotvvmPropertyId key) + { + return DotvvmPropertyIdAssignment.GetGroupMemberName((ushort)(key.Id & 0xFF_FF))!; + } + public IEnumerable Keys { get { - foreach (var (p, _) in control.properties) + foreach (var (p, _) in control.properties.PropertyGroup(group.Id)) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return pg.GroupMemberName; - } + yield return GetMemberName(p); } } } @@ -44,13 +51,9 @@ public IEnumerable Values { get { - foreach (var (p, _) in control.properties) + foreach (var (p, value) in control.properties.PropertyGroup(group.Id)) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return (TValue)control.GetValue(p)!; - } + yield return (TValue)control.EvalPropertyValue(group, value)!; } } } @@ -59,48 +62,17 @@ public IEnumerable Properties { get { - foreach (var (p, _) in control.properties) + foreach (var (p, _) in control.properties.PropertyGroup(group.Id)) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return pg; - } + var prop = group.GetDotvvmProperty(p.MemberId); + yield return prop; } } } - public int Count - { - get - { - // we don't want to use Linq Enumerable.Count() as it would allocate - // the enumerator. foreach gets the struct enumerator so it does not allocate anything - var count = 0; - foreach (var (p, _) in control.properties) - { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - count++; - } - } - return count; - } - } + public int Count => control.properties.CountPropertyGroup(group.Id); - public bool Any() - { - foreach (var (p, _) in control.properties) - { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - return true; - } - } - return false; - } + public bool Any() => control.properties.ContainsPropertyGroup(group.Id); public bool IsReadOnly => false; @@ -113,65 +85,79 @@ public TValue this[string key] { get { - var p = group.GetDotvvmProperty(key); + var p = GetMemberId(key); if (control.properties.TryGet(p, out var value)) - return (TValue)control.EvalPropertyValue(p, value)!; + return (TValue)control.EvalPropertyValue(group, value)!; else - return (TValue)p.DefaultValue!; + return (TValue)group.DefaultValue!; } set { - control.properties.Set(group.GetDotvvmProperty(key), value); + control.properties.Set(GetMemberId(key), value); } } /// Gets the value binding set to a specified property. Returns null if the property is not a binding, throws if the binding some kind of command. - public IValueBinding? GetValueBinding(string key) => control.GetValueBinding(group.GetDotvvmProperty(key)); + public IValueBinding? GetValueBinding(string key) + { + var binding = GetBinding(key); + if (binding != null && binding is not IStaticValueBinding) // throw exception on incompatible binding types + { + throw new BindingHelper.BindingNotSupportedException(binding) { RelatedControl = control }; + } + return binding as IValueBinding; + + } /// Gets the binding set to a specified property. Returns null if the property is not set or if the value is not a binding. - public IBinding? GetBinding(string key) => control.GetBinding(group.GetDotvvmProperty(key)); + public IBinding? GetBinding(string key) => GetValueRaw(key) as IBinding; /// Gets the value or a binding object for a specified property. public object? GetValueRaw(string key) { - var p = group.GetDotvvmProperty(key); - if (control.properties.TryGet(p, out var value)) + if (control.properties.TryGet(GetMemberId(key), out var value)) return value; else - return p.DefaultValue!; + return group.DefaultValue!; } /// Adds value or overwrites the property identified by . public void Set(string key, ValueOrBinding value) { - control.properties.Set(group.GetDotvvmProperty(key), value.UnwrapToObject()); + control.properties.Set(GetMemberId(key, createNew: true), value.UnwrapToObject()); } /// Adds value or overwrites the property identified by with the value. public void Set(string key, TValue value) => - control.properties.Set(group.GetDotvvmProperty(key), value); + control.properties.Set(GetMemberId(key, createNew: true), value); /// Adds binding or overwrites the property identified by with the binding. public void SetBinding(string key, IBinding binding) => - control.properties.Set(group.GetDotvvmProperty(key), binding); + control.properties.Set(GetMemberId(key, createNew: true), binding); public bool ContainsKey(string key) { - return control.Properties.ContainsKey(group.GetDotvvmProperty(key)); + return control.properties.Contains(GetMemberId(key)); } - private void AddOnConflict(GroupedDotvvmProperty property, object? value) + private void AddOnConflict(DotvvmPropertyId id, string key, object? value) { var merger = this.group.ValueMerger; if (merger is null) - throw new ArgumentException($"Cannot Add({property.Name}, {value}) since the value is already set and merging is not enabled on this property group."); - var mergedValue = merger.MergePlainValues(property, control.properties.GetOrThrow(property), value); - control.properties.Set(property, mergedValue); + throw new ArgumentException($"Cannot Add({key}, {value}) since the value is already set and merging is not enabled on this property group."); + var mergedValue = merger.MergePlainValues(id, control.properties.GetOrThrow(id), value); + control.properties.Set(id, mergedValue); } + internal void AddInternal(ushort key, object? val) + { + var prop = DotvvmPropertyId.CreatePropertyGroupId(group.Id, key); + if (!control.properties.TryAdd(prop, val)) + AddOnConflict(prop, prop.GroupMemberName.NotNull(), val); + } /// Adds the property identified by . If the property is already set, it tries appending the value using the group's public void Add(string key, ValueOrBinding value) { - var prop = group.GetDotvvmProperty(key); - object? val = value.UnwrapToObject(); + var prop = GetMemberId(key, createNew: true); + object? val = value.UnwrapToObject(); // TODO VOB boxing if (!control.properties.TryAdd(prop, val)) - AddOnConflict(prop, val); + AddOnConflict(prop, key, val); } /// Adds the property identified by . If the property is already set, it tries appending the value using the group's @@ -206,13 +192,14 @@ public static IDictionary CreateValueDictionary(DotvvmBindableOb var result = new Dictionary(); foreach (var (p, valueRaw) in control.properties) { - if (p is GroupedDotvvmProperty pg && pg.PropertyGroup == group) + if (p.IsInPropertyGroup(group.Id)) { - var valueObj = control.EvalPropertyValue(p, valueRaw); + var name = DotvvmPropertyIdAssignment.GetGroupMemberName((ushort)(p.Id & 0xFF_FF))!; + var valueObj = control.EvalPropertyValue(group, valueRaw); if (valueObj is TValue value) - result.Add(pg.GroupMemberName, value); + result.Add(name, value); else if (valueObj is null) - result.Add(pg.GroupMemberName, default!); + result.Add(name, default!); } } return result; @@ -223,16 +210,17 @@ public static IDictionary> CreatePropertyDictiona var result = new Dictionary>(); foreach (var (p, valRaw) in control.properties) { - if (p is GroupedDotvvmProperty pg && pg.PropertyGroup == group) + if (p.IsInPropertyGroup(group.Id)) { - result.Add(pg.GroupMemberName, ValueOrBinding.FromBoxedValue(valRaw)); + var name = DotvvmPropertyIdAssignment.GetGroupMemberName((ushort)(p.Id & 0xFF_FF))!; + result.Add(name, ValueOrBinding.FromBoxedValue(valRaw)); } } return result; } public bool Remove(string key) { - return control.Properties.Remove(group.GetDotvvmProperty(key)); + return control.properties.Remove(GetMemberId(key)); } /// Tries getting value of property identified by . If the property contains a binding, it will be automatically evaluated. @@ -240,10 +228,11 @@ public bool Remove(string key) public bool TryGetValue(string key, [MaybeNullWhen(false)] out TValue value) #pragma warning restore CS8767 { - var prop = group.GetDotvvmProperty(key); - if (control.properties.TryGet(prop, out var valueRaw)) + var memberId = DotvvmPropertyIdAssignment.GetGroupMemberId(key, registerIfNotFound: false); + var p = DotvvmPropertyId.CreatePropertyGroupId(group.Id, memberId); + if (control.properties.TryGet(p, out var valueRaw)) { - value = (TValue)control.EvalPropertyValue(prop, valueRaw)!; + value = (TValue)control.EvalPropertyValue(group, valueRaw)!; return true; } else @@ -262,31 +251,26 @@ public void Add(KeyValuePair item) public void Clear() { // we want to avoid allocating the list if there is only one property - DotvvmProperty? toRemove = null; - List? toRemoveRest = null; + DotvvmPropertyId toRemove = default; + List? toRemoveRest = null; - foreach (var (p, _) in control.properties) + foreach (var (p, _) in control.properties.PropertyGroup(group.Id)) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) + if (toRemove.Id == 0) + toRemove = p; + else { - if (toRemove is null) - toRemove = p; - else - { - if (toRemoveRest is null) - toRemoveRest = new List(); - toRemoveRest.Add(p); - } + toRemoveRest ??= new List(); + toRemoveRest.Add(p); } } - if (toRemove is {}) - control.Properties.Remove(toRemove); + if (toRemove.Id != 0) + control.properties.Remove(toRemove); if (toRemoveRest is {}) foreach (var p in toRemoveRest) - control.Properties.Remove(p); + control.properties.Remove(p); } public bool Contains(KeyValuePair item) @@ -321,31 +305,73 @@ public bool Remove(KeyValuePair item) /// Enumerates all keys and values. If a property contains a binding, it will be automatically evaluated. public IEnumerator> GetEnumerator() { - foreach (var (p, value) in control.properties) + foreach (var (p, value) in control.properties.PropertyGroup(group.Id)) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return new KeyValuePair(pg.GroupMemberName, (TValue)control.EvalPropertyValue(p, value)!); - } + var name = GetMemberName(p); + yield return new KeyValuePair(name, (TValue)control.EvalPropertyValue(group, value)!); } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// Enumerates all keys and values, without evaluating the bindings. - public IEnumerable> RawValues + public RawValuesCollection RawValues => new RawValuesCollection(this); + + public readonly struct RawValuesCollection: IEnumerable>, IReadOnlyDictionary { - get + readonly VirtualPropertyGroupDictionary self; + + internal RawValuesCollection(VirtualPropertyGroupDictionary self) { - foreach (var (p, value) in control.properties) + this.self = self; + } + + public object? this[string key] => self.GetValueRaw(key); + public bool TryGetValue(string key, [MaybeNullWhen(false)] out object? value) => + self.control.properties.TryGet(self.GetMemberId(key), out value); + + public IEnumerable Keys => self.Keys; + + public IEnumerable Values + { + get { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return new KeyValuePair(pg.GroupMemberName, value!); - } + foreach (var (_, value) in self.control.properties.PropertyGroup(self.group.Id)) + yield return value; } } + + public int Count => self.Count; + + public bool ContainsKey(string key) => self.ContainsKey(key); + + public RawValuesEnumerator GetEnumerator() => new RawValuesEnumerator(self.control.properties.EnumeratePropertyGroup(self.group.Id)); + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + public struct RawValuesEnumerator : IEnumerator> + { + private DotvvmControlPropertyIdGroupEnumerator inner; + + public KeyValuePair Current + { + get + { + var (p, value) = inner.Current; + var mem = DotvvmPropertyIdAssignment.GetGroupMemberName((ushort)(p.Id & 0xFF_FF))!; + return new KeyValuePair(mem, value); + } + } + + object IEnumerator.Current => Current; + + public RawValuesEnumerator(DotvvmControlPropertyIdGroupEnumerator dotvvmControlPropertyIdEnumerator) + { + this.inner = dotvvmControlPropertyIdEnumerator; + } + + public bool MoveNext() => inner.MoveNext(); + public void Reset() => inner.Reset(); + public void Dispose() => inner.Dispose(); } } } diff --git a/src/Framework/Framework/Compilation/AttributeValueMergerBase.cs b/src/Framework/Framework/Compilation/AttributeValueMergerBase.cs index a4248d2287..05d175ed2c 100644 --- a/src/Framework/Framework/Compilation/AttributeValueMergerBase.cs +++ b/src/Framework/Framework/Compilation/AttributeValueMergerBase.cs @@ -20,7 +20,7 @@ namespace DotVVM.Framework.Compilation /// /// Merges provided values based on implemented static 'MergeValues' or 'MergeExpression' method: /// - /// implement public static object MergeValues([DotvvmProperty], ValueA, ValueB) and this will decide which will should be used + /// implement public static object MergeValues([DotvvmPropertyId], ValueA, ValueB) and this will decide which will should be used /// or implement public static Expression MergeExpressions(DotvvmProperty, Expression a, Expression b) /// public abstract class AttributeValueMergerBase : IAttributeValueMerger @@ -60,7 +60,10 @@ public abstract class AttributeValueMergerBase : IAttributeValueMerger if (bindingA.BindingType != bindingB.BindingType) { error = $"Cannot merge values of different binding types"; return null; } } - var resultExpression = TryOptimizeMethodCall(TryFindMethod(GetType(), MergeExpressionsMethodName, Expression.Constant(property), Expression.Constant(valA), Expression.Constant(valB))) as Expression; + var resultExpression = TryOptimizeMethodCall( + TryFindMethod(GetType(), MergeExpressionsMethodName, Expression.Constant(property), Expression.Constant(valA), Expression.Constant(valB)) ?? + TryFindMethod(GetType(), MergeExpressionsMethodName, Expression.Constant(property.Id), Expression.Constant(valA), Expression.Constant(valB)) + ) as Expression; // Try to find MergeValues method if MergeExpression does not exists, or try to eval it to constant if expression is not constant if (resultExpression == null || valA.NodeType == ExpressionType.Constant && valB.NodeType == ExpressionType.Constant && resultExpression.NodeType != ExpressionType.Constant) @@ -121,6 +124,7 @@ protected virtual ResolvedPropertySetter EmitConstant(object? value, DotvvmPrope protected virtual MethodCallExpression? TryFindMergeMethod(DotvvmProperty property, Expression a, Expression b) { return + TryFindMethod(GetType(), MergeValuesMethodName, Expression.Constant(property.Id), a, b) ?? TryFindMethod(GetType(), MergeValuesMethodName, Expression.Constant(property), a, b) ?? TryFindMethod(GetType(), MergeValuesMethodName, a, b); } @@ -143,7 +147,7 @@ protected virtual ResolvedPropertySetter EmitConstant(object? value, DotvvmPrope return methodCall; else return null; } - public virtual object? MergePlainValues(DotvvmProperty prop, object? a, object? b) + public virtual object? MergePlainValues(DotvvmPropertyId prop, object? a, object? b) { return ((dynamic)this).MergeValues(prop, a, b); } diff --git a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs index 7405493110..dd0f043f14 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs @@ -1,11 +1,16 @@ using System; +using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using DotVVM.Framework.Binding; +using DotVVM.Framework.Compilation.Binding; using DotVVM.Framework.Compilation.ControlTree.Resolved; using DotVVM.Framework.Configuration; using DotVVM.Framework.Controls; @@ -24,10 +29,10 @@ public class DefaultControlResolver : ControlResolverBase private readonly CompiledAssemblyCache compiledAssemblyCache; private readonly Dictionary? controlNameMappings; - private static object locker = new object(); - private static bool isInitialized = false; - private static object dotvvmLocker = new object(); - private static bool isDotvvmInitialized = false; + private static readonly object locker = new object(); + private static volatile bool isInitialized = false; + private static readonly object dotvvmLocker = new object(); + private static volatile bool isDotvvmInitialized = false; public DefaultControlResolver(DotvvmConfiguration configuration, IControlBuilderFactory controlBuilderFactory, CompiledAssemblyCache compiledAssemblyCache) : base(configuration.Markup) @@ -65,6 +70,7 @@ internal static Task InvokeStaticConstructorsOnDotvvmControls() lock(dotvvmLocker) { if (isDotvvmInitialized) return; InvokeStaticConstructorsOnAllControls(typeof(DotvvmControl).Assembly); + isDotvvmInitialized = true; } }); } @@ -74,29 +80,149 @@ internal static Task InvokeStaticConstructorsOnDotvvmControls() /// private void InvokeStaticConstructorsOnAllControls() { - var dotvvmAssembly = typeof(DotvvmControl).Assembly.GetName().Name!; var dotvvmInitTask = InvokeStaticConstructorsOnDotvvmControls(); - if (configuration.ExperimentalFeatures.ExplicitAssemblyLoading.Enabled) - { + var dotvvmAssembly = typeof(DotvvmControl).Assembly.GetName().Name!; + + var assemblies = configuration.ExperimentalFeatures.ExplicitAssemblyLoading.Enabled ? // use only explicitly specified assemblies from configuration - // and do not call GetTypeInfo to prevent unnecessary dependent assemblies from loading - var assemblies = compiledAssemblyCache.GetAllAssemblies() - .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)) - .Distinct(); - - Parallel.ForEach(assemblies, a => { - InvokeStaticConstructorsOnAllControls(a); - }); + compiledAssemblyCache.GetReferencedAssemblies() : + compiledAssemblyCache.GetAllAssemblies(); + InvokeStaticConstructorsOnAllControls(OrderAndFilterAssemblies(assemblies, dotvvmAssembly)); + dotvvmInitTask.Wait(); + } + + /// Filters out assemblies which don't reference DotVVM.Framework, and topologically orders them according to their references, then alphabetically to resolve ties + private List OrderAndFilterAssemblies(IEnumerable assemblies, string rootAssembly) + { + var assemblyList = new List(); + var renumbering = new Dictionary(); + var references = new List(); + + var namelessAssemblies = new List(); // place them at the end + foreach (var a in assemblies) + { + var name = a.GetName(); + var r = a.GetReferencedAssemblies(); + if (ReferencesAssembly(r, rootAssembly)) + { + if (name.Name is null) + namelessAssemblies.Add(a); + else if (renumbering.TryAdd(name.Name, assemblyList.Count)) + { + assemblyList.Add(a); + references.Add(r); + } + } } - else + + // Kahn's algorithm - https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm + // with additional sorting step to resolve ties + var inDegree = new int[assemblyList.Count]; + var forwardReferences = new List?[assemblyList.Count]; + var roots = new List(); + for (int i = 0; i < references.Count; i++) { - var assemblies = GetAllRelevantAssemblies(dotvvmAssembly); - Parallel.ForEach(assemblies, a => { - InvokeStaticConstructorsOnAllControls(a); - }); + var inCount = 0; + foreach (var r in references[i]) + if (renumbering.TryGetValue(r.Name!, out var idx)) + { + inCount++; + forwardReferences[idx] ??= new List(); + forwardReferences[idx]!.Add(i); + } + inDegree[i] = inCount; + if (inCount == 0) + roots.Add(i); } - dotvvmInitTask.Wait(); + + var sorted = new List(capacity: assemblyList.Count + namelessAssemblies.Count); + var newRoots = new List(); + var comparer = makeComparator(assemblyList); + while (roots.Count > 0) + { + if (roots.Count > 1) + roots.Sort(comparer); + + foreach (var item in roots) + { + sorted.Add(assemblyList[item]); + if (forwardReferences[item] is null) + continue; + foreach (var r in forwardReferences[item]!) + { + inDegree[r]--; + if (inDegree[r] == 0) + newRoots.Add(r); + } + } + roots.Clear(); + (roots, newRoots) = (newRoots, roots); + } + + // no need to throw in production, we only want the topological ordering for consistent property IDs + Debug.Assert(sorted.Count == assemblyList.Count, "Loop in assembly references detected"); + + sorted.AddRange(namelessAssemblies); + return sorted; + + static Comparison makeComparator(List assemblyList) => (a, b) => string.Compare(assemblyList[a].GetName().Name, assemblyList[b].GetName().Name, StringComparison.Ordinal); + } + static bool ReferencesAssembly(AssemblyName[] references, string root) + { + foreach (var r in references) + if (r.Name == root) + return true; + return false; + } + + private void InvokeStaticConstructorsOnAllControls(List assemblies) + { + // try to assign property IDs consistently across runs, as the order of properties depends on this which may be observable to the user + // we first assigns IDs to all controls is each assembly, then we run the static constructors in parallel + + // this means we sequence the control registration, while parallelizing assembly loading and property registration + // in practice, control registration is trivial, the main performance hit might arrise from a single assembly taking longer to load + + // Assembly1 loading ................|register controls|register properties + // Assembly2 loading. waiting |registercontrols|... + // Assembly3 loading. waiting |registercontrols|... + + var paralelismLimiter = new SemaphoreSlim(Environment.ProcessorCount); + + var controlIdsAssigned = Enumerable.Range(0, assemblies.Count).Select(_ => new TaskCompletionSource()).ToArray(); + + var tasks = Enumerable.Range(0, assemblies.Count).Select(i => Task.Run(async () => { + await paralelismLimiter.WaitAsync(); + try { + var controls = new List(); + foreach (var type in assemblies[i].GetLoadableTypes()) + { + if (type.IsClass && !type.ContainsGenericParameters && type.IsDefined(typeof(ContainsDotvvmPropertiesAttribute), true)) + { + controls.Add(type); + } + } + // wait for the previous assembly to finish loading and assigning control IDs + if (i > 0) + await controlIdsAssigned[i - 1].Task; + var controlIds = new ushort[controls.Count]; + DotvvmPropertyIdAssignment.RegisterTypes(CollectionsMarshal.AsSpan(controls), controlIds); + + // let the next assembly run + controlIdsAssigned[i].SetResult(); + + foreach (var type in controls) + { + InitType(type); + } + } + finally { + paralelismLimiter.Release(); + } + + })).ToArray(); + Task.WaitAll(tasks); } private static void InvokeStaticConstructorsOnAllControls(Assembly assembly) @@ -106,7 +232,6 @@ private static void InvokeStaticConstructorsOnAllControls(Assembly assembly) if (!c.IsClass || c.ContainsGenericParameters) continue; - InitType(c); } } @@ -162,14 +287,13 @@ private static void RegisterCapabilitiesFromInterfaces(Type type) } } - private IEnumerable GetAllRelevantAssemblies(string dotvvmAssembly) + private Assembly[] GetAllRelevantAssemblies(string dotvvmAssembly) { #if DotNetCore - var assemblies = compiledAssemblyCache.GetAllAssemblies() - .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)); + var assemblies = compiledAssemblyCache.GetAllAssemblies(); #else var loadedAssemblies = compiledAssemblyCache.GetAllAssemblies() - .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)); + .Where(a => ReferencesAssembly(a.GetReferencedAssemblies(), dotvvmAssembly)); var visitedAssemblies = new HashSet(); @@ -186,8 +310,9 @@ private IEnumerable GetAllRelevantAssemblies(string dotvvmAssembly) throw new Exception($"Unable to load assembly '{an.FullName}' referenced by '{a.FullName}'.", ex); } })) - .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)) - .Distinct(); + .Where(a => ReferencesAssembly(a.GetReferencedAssemblies(), dotvvmAssembly)) + .Distinct() + .ToArray(); #endif return assemblies; } diff --git a/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs b/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs index 2fa03d4137..d86bf91649 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs @@ -9,6 +9,7 @@ using DotVVM.Framework.Utils; using System.Runtime.CompilerServices; using System.Collections.Immutable; +using DotVVM.Framework.Binding.Expressions; namespace DotVVM.Framework.Compilation.ControlTree { @@ -38,8 +39,10 @@ public class DotvvmPropertyGroup : IPropertyGroupDescriptor public Type PropertyType { get; } ITypeDescriptor IControlAttributeDescriptor.PropertyType => new ResolvedTypeDescriptor(PropertyType); public IAttributeValueMerger? ValueMerger { get; } + public bool IsBindingProperty { get; } + internal ushort Id { get; } - private ConcurrentDictionary generatedProperties = new(); + private readonly ConcurrentDictionary> generatedProperties = new(); /// The capability which declared this property. When the property is declared by an capability, it can only be used by this capability. public DotvvmCapabilityProperty? OwningCapability { get; } @@ -61,7 +64,9 @@ internal DotvvmPropertyGroup(PrefixArray prefixes, Type valueType, Type declarin { ValueMerger = (IAttributeValueMerger?)Activator.CreateInstance(MarkupOptions.AttributeValueMerger); } + this.IsBindingProperty = typeof(IBinding).IsAssignableFrom(valueType); this.OwningCapability = owningCapability; + this.Id = DotvvmPropertyIdAssignment.RegisterPropertyGroup(this); } private static (MarkupOptionsAttribute, DataContextChangeAttribute[], DataContextStackManipulationAttribute?, ObsoleteAttribute?) @@ -89,7 +94,32 @@ internal void AddUsedInCapability(DotvvmCapabilityProperty? p) IPropertyDescriptor IPropertyGroupDescriptor.GetDotvvmProperty(string name) => GetDotvvmProperty(name); public GroupedDotvvmProperty GetDotvvmProperty(string name) { - return generatedProperties.GetOrAdd(name, n => GroupedDotvvmProperty.Create(this, name)); + var id = DotvvmPropertyIdAssignment.GetGroupMemberId(name, registerIfNotFound: true); + return GetDotvvmProperty(id); + } + + public GroupedDotvvmProperty GetDotvvmProperty(ushort nameId) + { + while (true) + { + if (generatedProperties.TryGetValue(nameId, out var resultRef)) + { + if (resultRef.TryGetTarget(out var result)) + return result; + else + generatedProperties.TryUpdate(nameId, new(CreateMemberProperty(nameId)), resultRef); + } + else + { + generatedProperties.TryAdd(nameId, new(CreateMemberProperty(nameId))); + } + } + } + + private GroupedDotvvmProperty CreateMemberProperty(ushort nameId) + { + var name = DotvvmPropertyIdAssignment.GetGroupMemberName(nameId).NotNull(); + return GroupedDotvvmProperty.Create(this, name, nameId); } private static ConcurrentDictionary<(Type, string), DotvvmPropertyGroup> descriptorDictionary = new(); diff --git a/src/Framework/Framework/Compilation/HtmlAttributeValueMerger.cs b/src/Framework/Framework/Compilation/HtmlAttributeValueMerger.cs index df2546f49b..6a2a2182f5 100644 --- a/src/Framework/Framework/Compilation/HtmlAttributeValueMerger.cs +++ b/src/Framework/Framework/Compilation/HtmlAttributeValueMerger.cs @@ -22,10 +22,12 @@ public static Expression MergeExpressions(GroupedDotvvmProperty property, Expres // return Expression.Call(typeof(string).GetMethod("Concat", new[] { typeof(string), typeof(string), typeof(string) }), a, Expression.Constant(separator), b); } - public static string? MergeValues(GroupedDotvvmProperty property, string? a, string? b) + public static string? MergeValues(DotvvmPropertyId property, string? a, string? b) { + if (!property.IsPropertyGroup) throw new ArgumentException("HtmlAttributeValueMerger only supports property group", nameof(property)); + var attributeName = property.GroupMemberName; // for perf reasons only do this compile time - we'll deduplicate the attribute if it's a CSS class - if (property.GroupMemberName == "class" && a is string && b is string) + if (attributeName == "class" && a is string && b is string) { var classesA = a.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); var classesB = b.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries) @@ -33,18 +35,19 @@ public static Expression MergeExpressions(GroupedDotvvmProperty property, Expres b = string.Join(" ", classesB); } - return HtmlWriter.JoinAttributeValues(property.GroupMemberName, a, b); + return HtmlWriter.JoinAttributeValues(attributeName, a, b); } - public override object? MergePlainValues(DotvvmProperty prop, object? a, object? b) + public override object? MergePlainValues(DotvvmPropertyId prop, object? a, object? b) { - var gProp = (GroupedDotvvmProperty)prop; + if (!prop.IsPropertyGroup) throw new ArgumentException("HtmlAttributeValueMerger only supports property group", nameof(prop)); + var attributeName = prop.GroupMemberName; if (a is null) return b; if (b is null) return a; if (a is string aString && b is string bString) - return HtmlWriter.JoinAttributeValues(gProp.GroupMemberName, aString, bString); + return HtmlWriter.JoinAttributeValues(attributeName, aString, bString); // append to list. Order does not matter in html attributes if (a is HtmlGenericControl.AttributeList alist) diff --git a/src/Framework/Framework/Compilation/IAttributeValueMerger.cs b/src/Framework/Framework/Compilation/IAttributeValueMerger.cs index 16608691f8..ea764b40df 100644 --- a/src/Framework/Framework/Compilation/IAttributeValueMerger.cs +++ b/src/Framework/Framework/Compilation/IAttributeValueMerger.cs @@ -12,6 +12,6 @@ namespace DotVVM.Framework.Compilation public interface IAttributeValueMerger { ResolvedPropertySetter? MergeResolvedValues(ResolvedPropertySetter a, ResolvedPropertySetter b, out string? error); - object? MergePlainValues(DotvvmProperty prop, object? a, object? b); + object? MergePlainValues(DotvvmPropertyId prop, object? a, object? b); } } diff --git a/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs b/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs index 24ac760cf6..354d7e6c26 100644 --- a/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs +++ b/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs @@ -52,7 +52,7 @@ public static ResolvedControl FromRuntimeControl( { var templateControl = (DotvvmMarkupControl)Activator.CreateInstance(descriptor.ControlType)!; markupControl.SetProperties(templateControl); - foreach (var p in templateControl.properties) + foreach (var p in templateControl.Properties) { var propertyDC = GetPropertyDataContext(obj, p.Key, dataContext); control.SetProperty( @@ -81,7 +81,7 @@ public static ResolvedControl FromRuntimeControl( rc.ConstructorParameters = new object[] { htmlControl.TagName! }; } - foreach (var p in obj.properties) + foreach (var p in obj.Properties) { var propertyDC = GetPropertyDataContext(obj, p.Key, dataContext); rc.SetProperty( diff --git a/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs b/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs index d9dd2a0476..e1fee4bacc 100644 --- a/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs +++ b/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -175,30 +176,110 @@ public void CommitDotvvmProperties(string name) controlProperties.Remove(name); if (properties.Count == 0) return; - properties.Sort((a, b) => string.Compare(a.prop.FullName, b.prop.FullName, StringComparison.Ordinal)); + properties.Sort((a, b) => a.prop.Id.CompareTo(b.prop.Id)); - var (hashSeed, keys, values) = PropertyImmutableHashtable.CreateTableWithValues(properties.Select(p => p.prop).ToArray(), properties.Select(p => p.value).ToArray()); + if (!TryEmitPerfectHashAssignment(GetParameterOrVariable(name), properties)) + { + EmitDictionaryAssignment(GetParameterOrVariable(name), properties); + } + } - Expression valueExpr; - if (TryCreateArrayOfConstants(values, out var invertedValues)) + /// Set DotVVM properties as array of keys and array of values + private bool TryEmitPerfectHashAssignment(ParameterExpression control, List<(DotvvmProperty prop, Expression value)> properties) + { + return false; + if (properties.Count > 50) { - valueExpr = EmitValue(invertedValues); + return false; } - else + + try { - valueExpr = EmitCreateArray( - typeof(object), - values.Select(v => v ?? EmitValue(null)) - ); + var (flags, keys, values) = PropertyImmutableHashtable.CreateTableWithValues(properties.Select(p => p.prop.Id).ToArray(), properties.Select(p => p.value).ToArray()); + + Expression valueExpr; + if (TryCreateArrayOfConstants(values, out var invertedValues)) + { + valueExpr = EmitValue(invertedValues); + } + else + { + valueExpr = EmitCreateArray( + typeof(object), + values.Select(v => v ?? EmitValue(null)) + ); + flags |= 1u << 31; // owns values flag + } + + var keyExpr = EmitValue(keys); + + // PropertyImmutableHashtable.SetValuesToDotvvmControl(control, keys, values, hashSeed) + var magicSetValueCall = Expression.Call(typeof(PropertyImmutableHashtable), nameof(PropertyImmutableHashtable.SetValuesToDotvvmControl), emptyTypeArguments, Expression.Convert(control, typeof(DotvvmBindableObject)), keyExpr, valueExpr, EmitValue(flags)); + + EmitStatement(magicSetValueCall); } + catch (PropertyImmutableHashtable.CannotMakeHashtableException) + { + return false; + } + return true; + } - var keyExpr = EmitValue(keys); - // control.MagicSetValue(keys, values, hashSeed) - var controlParameter = GetParameterOrVariable(name); + /// Set DotVVM properties as a Dictionary, potentially shared one across different instantiations + private void EmitDictionaryAssignment(ParameterExpression control, List<(DotvvmProperty prop, Expression value)> properties) + { + if (properties.Count == 0) + { + return; + } + var constants = new Dictionary(capacity: properties.Count); + var variables = new List>(); + + foreach (var (prop, value) in properties) + { + if (value is ConstantExpression constant) + { + Debug.Assert(constant.Value is not DotvvmBindableObject and not IEnumerable, "Internal compiler bug: We cannot allow sharing of DotvvmBindableObject instances in the constants dictionary."); + constants.Add(prop.Id, constant.Value); + } + else + { + variables.Add(new (prop.Id, value)); + } + } + + Expression dict; + + if (variables.Count == 0) + { + dict = EmitValue(constants); + } + else + { + throw new Exception("kokoooot"); + var variable = Expression.Parameter(typeof(Dictionary), "props_" + control.Name); + + // var dict = new Dictionary(constants); + // dict.Add(variables[0].Key, variables[0].Value); + // dict.Add(variables[1].Key, variables[1].Value); + // ... + var copyConstructor = typeof(Dictionary).GetConstructor([ typeof(IDictionary) ]).NotNull(); + var propIdConstructor = typeof(DotvvmPropertyId).GetConstructor([ typeof(uint) ]).NotNull(); + dict = Expression.Block(new [] { variable }, + Expression.Assign(variable, + Expression.New(copyConstructor, EmitValue(constants))), + Expression.Block(variables.Select(kv => + Expression.Call(variable, "Add", emptyTypeArguments, Expression.New(propIdConstructor, EmitValue(kv.Key.Id)), kv.Value))), + variable); + } - var magicSetValueCall = Expression.Call(controlParameter, nameof(DotvvmBindableObject.MagicSetValue), emptyTypeArguments, keyExpr, valueExpr, EmitValue(hashSeed)); + // PropertyImmutableHashtable.SetValuesToDotvvmControl(control, dict) + var magicSetValueCall = Expression.Call(typeof(PropertyImmutableHashtable), nameof(PropertyImmutableHashtable.SetValuesToDotvvmControl), emptyTypeArguments, + /*control:*/ Expression.Convert(control, typeof(DotvvmBindableObject)), + /*dict:*/ dict, + /*owns:*/ EmitValue(variables.Count > 0)); EmitStatement(magicSetValueCall); } diff --git a/src/Framework/Framework/Controls/CompositeControl.cs b/src/Framework/Framework/Controls/CompositeControl.cs index cf8b774555..c7803b4efc 100644 --- a/src/Framework/Framework/Controls/CompositeControl.cs +++ b/src/Framework/Framework/Controls/CompositeControl.cs @@ -154,10 +154,10 @@ private void CopyPostbackHandlersAndAdd(IEnumerable childControls } var commands = new List<(string, ICommandBinding)>(); - foreach (var (property, value) in this.Properties) + foreach (var (property, value) in this.properties) { if (value is ICommandBinding command) - commands.Add((property.Name, command)); + commands.Add((property.PropertyInstance.Name, command)); } foreach (var child in childControls) @@ -180,10 +180,10 @@ protected internal T CopyPostBackHandlersRecursive(T target) return target; var commands = new List<(string, ICommandBinding)>(); - foreach (var (property, value) in this.Properties) + foreach (var (property, value) in this.properties) { if (value is ICommandBinding command) - commands.Add((property.Name, command)); + commands.Add((property.PropertyInstance.Name, command)); } CopyPostBackHandlersRecursive(handlers, commands, target); @@ -193,7 +193,7 @@ protected internal T CopyPostBackHandlersRecursive(T target) private static void CopyPostBackHandlersRecursive(PostBackHandlerCollection handlers, List<(string, ICommandBinding)> commands, DotvvmBindableObject target) { PostBackHandlerCollection? childHandlers = null; - foreach (var (property, value) in target.Properties) + foreach (var (property, value) in target.properties) { if (value is ICommandBinding command) { @@ -201,7 +201,7 @@ private static void CopyPostBackHandlersRecursive(PostBackHandlerCollection hand { if (object.ReferenceEquals(command, matchedCommand)) { - CopyMatchingPostBackHandlers(handlers, oldName, property.Name, ref childHandlers); + CopyMatchingPostBackHandlers(handlers, oldName, property.PropertyInstance.Name, ref childHandlers); break; } } diff --git a/src/Framework/Framework/Controls/DotvvmBindableObject.cs b/src/Framework/Framework/Controls/DotvvmBindableObject.cs index cb45b1d0a6..1d224671cd 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObject.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObject.cs @@ -5,6 +5,7 @@ using System.Linq; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Utils; @@ -124,6 +125,56 @@ public T GetValue(DotvvmProperty property, bool inherit = true) return value; } + /// If the object is IBinding and the property is not of type IBinding, it is evaluated. + internal object? EvalPropertyValue(DotvvmPropertyGroup property, object? value) + { + if (property.IsBindingProperty) return value; + if (value is IBinding) + { + // handle binding + if (value is IStaticValueBinding binding) + { + value = binding.Evaluate(this); + } + else if (value is ICommandBinding command) + { + value = command.GetCommandDelegate(this); + } + else + { + throw new NotSupportedException($"Cannot evaluate binding {value} of type {value.GetType().Name}."); + } + } + return value; + } + + /// If the object is IBinding and the property is not of type IBinding, it is evaluated. + internal object? EvalPropertyValue(DotvvmPropertyId property, object? value) + { + if (DotvvmPropertyIdAssignment.IsBindingProperty(property)) return value; + if (value is IBinding) + { + DotvvmBindableObject control = this; + // DataContext is always bound to it's parent, setting it right here is a bit faster + if (property == DataContextProperty.Id) + control = control.Parent ?? throw new DotvvmControlException(this, "Cannot set DataContext binding on the root control"); + // handle binding + if (value is IStaticValueBinding binding) + { + value = binding.Evaluate(control); + } + else if (value is ICommandBinding command) + { + value = command.GetCommandDelegate(control); + } + else + { + throw new NotSupportedException($"Cannot evaluate binding {value} of type {value.GetType().Name}."); + } + } + return value; + } + /// /// Gets the value of a specified property. If the property contains a binding, it is evaluted. /// @@ -138,10 +189,9 @@ public T GetValue(DotvvmProperty property, bool inherit = true) return property.GetValue(this, inherit); } - /// For internal use, public because it's used from our generated code. If want to use it, create the arguments using - public void MagicSetValue(DotvvmProperty[] keys, object[] values, int hashSeed) + public virtual object? GetValueRaw(DotvvmPropertyId property, bool inherit = true) { - this.properties.AssignBulk(keys, values, hashSeed); + return DotvvmPropertyIdAssignment.GetValueRaw(this, property, inherit); } /// Sets the value of a specified property. diff --git a/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs b/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs index 580b3f6f7c..9b7768b93c 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs @@ -223,9 +223,9 @@ public static TControl AddAttributes(this TControl control, Vi public static TControl AddCssClass(this TControl control, ValueOrBinding className) where TControl : IControlWithHtmlAttributes { - var classNameObj = className.UnwrapToObject(); - if (classNameObj is null or "") return control; - return AddAttribute(control, "class", classNameObj); + if (className.ValueOrDefault is null or "") return control; + control.Attributes.AddInternal(DotvvmPropertyIdAssignment.GroupMembers.@class, className.UnwrapToObject()); + return control; } /// Appends a css class to this control. Note that it is currently not supported if multiple bindings would have to be joined together. Returns for fluent API usage. @@ -234,7 +234,8 @@ public static TControl AddCssClass(this TControl control, string? clas { if (className is null or "") return control; - return AddAttribute(control, "class", className); + control.Attributes.AddInternal(DotvvmPropertyIdAssignment.GroupMembers.@class, className); + return control; } /// Appends a list of css classes to this control. Returns for fluent API usage. diff --git a/src/Framework/Framework/Controls/DotvvmControl.cs b/src/Framework/Framework/Controls/DotvvmControl.cs index b03c192a39..1201afcfbd 100644 --- a/src/Framework/Framework/Controls/DotvvmControl.cs +++ b/src/Framework/Framework/Controls/DotvvmControl.cs @@ -234,16 +234,19 @@ protected struct RenderState } [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static bool TouchProperty(DotvvmProperty property, object? val, ref RenderState r) + protected static bool TouchProperty(DotvvmPropertyId property, object? val, ref RenderState r) { - if (property == DotvvmControl.IncludeInPageProperty) + if (property == DotvvmControl.IncludeInPageProperty.Id) r.IncludeInPage = val; - else if (property == DotvvmControl.DataContextProperty) + else if (property == DotvvmControl.DataContextProperty.Id) r.DataContext = val as IValueBinding; - else if (property is ActiveDotvvmProperty) - r.HasActives = true; - else if (property is GroupedDotvvmProperty groupedProperty && groupedProperty.PropertyGroup is ActiveDotvvmPropertyGroup) - r.HasActiveGroups = true; + else if (DotvvmPropertyIdAssignment.IsActive(property)) + { + if (property.IsPropertyGroup) + r.HasActiveGroups = true; + else + r.HasActives = true; + } else return false; return true; } @@ -266,20 +269,20 @@ protected bool RenderBeforeControl(in RenderState r, IHtmlWriter writer, IDotvvm if (r.HasActives) foreach (var item in properties) { - if (item.Key is ActiveDotvvmProperty activeProp) + if (!item.Key.IsPropertyGroup && DotvvmPropertyIdAssignment.IsActive(item.Key)) { - activeProp.AddAttributesToRender(writer, context, this); + ((ActiveDotvvmProperty)item.Key.PropertyInstance).AddAttributesToRender(writer, context, this); } } if (r.HasActiveGroups) { var groups = properties - .Where(p => p.Key is GroupedDotvvmProperty gp && gp.PropertyGroup is ActiveDotvvmPropertyGroup) - .GroupBy(p => ((GroupedDotvvmProperty)p.Key).PropertyGroup); + .Where(p => p.Key.PropertyGroupInstance is ActiveDotvvmPropertyGroup) + .GroupBy(p => (ActiveDotvvmPropertyGroup)p.Key.PropertyGroupInstance!); foreach (var item in groups) { - ((ActiveDotvvmPropertyGroup)item.Key).AddAttributesToRender(writer, context, this, item.Select(i => i.Key)); + item.Key.AddAttributesToRender(writer, context, this, item.Select(i => i.Key.PropertyInstance)); } } diff --git a/src/Framework/Framework/Controls/DotvvmControlProperties.cs b/src/Framework/Framework/Controls/DotvvmControlProperties.cs index 468cb79e57..e73858d11d 100644 --- a/src/Framework/Framework/Controls/DotvvmControlProperties.cs +++ b/src/Framework/Framework/Controls/DotvvmControlProperties.cs @@ -3,25 +3,24 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using DotVVM.Framework.Binding; +using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Utils; namespace DotVVM.Framework.Controls { [StructLayout(LayoutKind.Explicit)] - internal struct DotvvmControlProperties : IEnumerable> + internal struct DotvvmControlProperties : IEnumerable> { // There are 3 possible states of this structure: // 1. keys == values == null --> it is empty - // 2. keys == null & values is Dictionary --> it falls back to traditional mutable property dictionary - // 3. keys is DotvvmProperty[] & values is object[] --> read-only perfect 2-slot hashing + // 2. keys == null & values is Dictionary --> it falls back to traditional mutable property dictionary + // 3. keys is DotvvmPropertyId[] & values is object[] --> read-only perfect 2-slot hashing [FieldOffset(0)] - private object? keys; - - [FieldOffset(0)] - private DotvvmProperty?[] keysAsArray; + private DotvvmPropertyId[]? keys; [FieldOffset(8)] private object? values; @@ -30,50 +29,82 @@ internal struct DotvvmControlProperties : IEnumerable valuesAsDictionary; + private Dictionary valuesAsDictionary; + /// + /// flags >> 31: 1bit - ownsValues + /// flags >> 30: 1bit - ownsKeys + /// flags >> 0: 30bits - hashSeed + /// [FieldOffset(16)] - private int hashSeed; + private uint flags; + private uint hashSeed + { + readonly get => flags & 0x3F_FF_FF_FF; + set => flags = (flags & ~0x3F_FF_FF_FFu) | value; + } + private bool ownsKeys + { + readonly get => (flags >> 30) != 0; + set => flags = (flags & ~(1u << 30)) | ((uint)BoolToInt(value) << 30); + } + private bool ownsValues + { + readonly get => (flags >> 31) != 0; + set => flags = (flags & ~(1u << 31)) | ((uint)BoolToInt(value) << 31); + } - public void AssignBulk(DotvvmProperty?[] keys, object?[] values, int hashSeed) + public void AssignBulk(DotvvmPropertyId[] keys, object?[] values, uint flags) { - // The explicit layout is quite likely to mess with array covariance, just make sure we don't encounter that + // The explicit layout is quite likely to mess up with array covariance, just make sure we don't encounter that Debug.Assert(values.GetType() == typeof(object[])); - Debug.Assert(keys.GetType() == typeof(DotvvmProperty[])); + Debug.Assert(keys.GetType() == typeof(DotvvmPropertyId[])); Debug.Assert(keys.Length == values.Length); - if (this.values == null || this.keys == keys) + if (this.values == null || Object.ReferenceEquals(this.keys, keys)) { this.valuesAsArray = values; - this.keysAsArray = keys; - this.hashSeed = hashSeed; + this.keys = keys; + this.flags = flags; } else { // we can just to check if all current properties are in the proposed set // if they are not we will have to copy it - if (this.keys == null) // TODO: is this heuristic actually useful? + for (int i = 0; i < keys.Length; i++) { - var ok = true; - foreach (var x in (Dictionary)this.values) - { - var e = PropertyImmutableHashtable.FindSlot(keys, hashSeed, x.Key); - if (e < 0 || !Object.Equals(values[e], x.Value)) - ok = false; - } - if (ok) + if (keys[i].Id != 0) + this.Set(keys[i]!, values[i]); + } + } + } + + public void AssignBulk(Dictionary values, bool owns) + { + if (this.values == null || object.ReferenceEquals(this.values, values)) + { + this.keys = null; + this.valuesAsDictionary = values; + this.flags = (uint)BoolToInt(owns) << 31; + } + else + { + if (owns) + { + foreach (var (k, v) in this) { - this.values = values; - this.keys = keys; - this.hashSeed = hashSeed; - return; + values.TryAdd(k, v); } + this.values = values; + this.keys = null; + this.flags = 1u << 31; } - - for (int i = 0; i < keys.Length; i++) + else { - if (keys[i] != null) - this.Set(keys[i]!, values[i]); + foreach (var (k, v) in values) + { + this.Set(k, v); + } } } } @@ -84,181 +115,268 @@ public void ClearEverything() keys = null; } - public bool Contains(DotvvmProperty p) + public readonly bool Contains(DotvvmProperty p) => Contains(p.Id); + public readonly bool Contains(DotvvmPropertyId p) { - if (values == null) { return false; } - - if (keys == null) + if (keys is {}) + { + Debug.Assert(values is object[]); + Debug.Assert(keys is DotvvmPropertyId[]); + return PropertyImmutableHashtable.ContainsKey(this.keys, this.hashSeed, p); + } + else if (values is null) { return false; } + else { - Debug.Assert(values is Dictionary); + Debug.Assert(values is Dictionary); return valuesAsDictionary.ContainsKey(p); } + } + + public readonly bool ContainsPropertyGroup(DotvvmPropertyGroup group) => ContainsPropertyGroup(group.Id); + public readonly bool ContainsPropertyGroup(ushort groupId) + { - Debug.Assert(values is object[]); - Debug.Assert(keys is DotvvmProperty[]); - return PropertyImmutableHashtable.ContainsKey(this.keysAsArray, this.hashSeed, p); + if (keys is {}) + { + return PropertyImmutableHashtable.ContainsPropertyGroup(this.keys, groupId); + } + else if (values is null) return false; + else + { + Debug.Assert(values is Dictionary); + foreach (var key in valuesAsDictionary.Keys) + { + if (key.IsInPropertyGroup(groupId)) + return true; + } + return false; + } + } + + public readonly int CountPropertyGroup(DotvvmPropertyGroup group) => CountPropertyGroup(group.Id); + public readonly int CountPropertyGroup(ushort groupId) + { + if (keys is {}) + { + return PropertyImmutableHashtable.CountPropertyGroup(this.keys, groupId); + } + else if (values is null) return 0; + else + { + Debug.Assert(values is Dictionary); + int count = 0; + foreach (var key in valuesAsDictionary.Keys) + { + if (key.IsInPropertyGroup(groupId)) + count++; + } + return count; + } } - public bool TryGet(DotvvmProperty p, out object? value) + public readonly bool TryGet(DotvvmProperty p, out object? value) => TryGet(p.Id, out value); + public readonly bool TryGet(DotvvmPropertyId p, out object? value) { - value = null; - if (values == null) { return false; } + if (keys != null) + { + Debug.Assert(values is object[]); + Debug.Assert(keys is DotvvmPropertyId[]); + var index = PropertyImmutableHashtable.FindSlot(this.keys, this.hashSeed, p); + if (index >= 0) + { + value = this.valuesAsArray[index]; + return true; + } + else + { + value = null; + return false; + } + } - if (keys == null) + else if (values == null) { value = null; return false; } + + else { - Debug.Assert(values is Dictionary); + Debug.Assert(values is Dictionary); return valuesAsDictionary.TryGetValue(p, out value); } - - Debug.Assert(values is object[]); - Debug.Assert(keys is DotvvmProperty[]); - var index = PropertyImmutableHashtable.FindSlot(this.keysAsArray, this.hashSeed, p); - if (index != -1) - value = this.valuesAsArray[index & (this.keysAsArray.Length - 1)]; - return index != -1; } - public object? GetOrThrow(DotvvmProperty p) + public readonly object? GetOrThrow(DotvvmProperty p) => GetOrThrow(p.Id); + public readonly object? GetOrThrow(DotvvmPropertyId p) { if (this.TryGet(p, out var x)) return x; throw new KeyNotFoundException(); } - public void Set(DotvvmProperty p, object? value) + public void Set(DotvvmProperty p, object? value) => Set(p.Id, value); + public void Set(DotvvmPropertyId p, object? value) { - if (values == null) + if (p.MemberId == 0) + throw new ArgumentException("Invalid (unitialized) property id cannot be set into the DotvvmControlProperties dictionary.", nameof(p)); + + if (keys != null) + { + Debug.Assert(values is object[]); + Debug.Assert(keys is DotvvmPropertyId[]); + var slot = PropertyImmutableHashtable.FindSlotOrFree(keys, hashSeed, p, out var exists); + if (slot >= 0) + { + if (!exists) + { + OwnKeys(); + OwnValues(); + keys[slot] = p; + valuesAsArray[slot] = value; + } + else if (Object.ReferenceEquals(valuesAsArray[slot], value)) + { + // no-op, we would be changing it to the same value + } + else + { + this.OwnValues(); + valuesAsArray[slot] = value; + } + } + else + { + SwitchToDictionary(); + Debug.Assert(values is Dictionary); + valuesAsDictionary[p] = value; + keys = null; + } + } + else if (values == null) { Debug.Assert(keys == null); - var d = new Dictionary(); - d[p] = value; - this.values = d; + this.flags = 0b11u << 30; + this.keys = new DotvvmPropertyId[PropertyImmutableHashtable.AdhocTableSize]; + this.keys[0] = p; + this.valuesAsArray = new object?[PropertyImmutableHashtable.AdhocTableSize]; + this.valuesAsArray[0] = value; } - else if (keys == null) + else { - Debug.Assert(values is Dictionary); + Debug.Assert(values is Dictionary); valuesAsDictionary[p] = value; } - else + } + + /// Tries to set value into the dictionary without overwriting anything. + public bool TryAdd(DotvvmProperty p, object? value) => TryAdd(p.Id, value); + + /// Tries to set value into the dictionary without overwriting anything. + public bool TryAdd(DotvvmPropertyId p, object? value) + { + if (keys != null) { Debug.Assert(this.values is object[]); - Debug.Assert(this.keys is DotvvmProperty[]); - var keys = this.keysAsArray; - var values = this.valuesAsArray; - var slot = PropertyImmutableHashtable.FindSlot(keys, this.hashSeed, p); - if (slot >= 0 && Object.Equals(values[slot], value)) + Debug.Assert(keys is DotvvmPropertyId[]); + var slot = PropertyImmutableHashtable.FindSlotOrFree(keys, this.hashSeed, p, out var exists); + if (slot >= 0) { - // no-op, we would be changing it to the same value + if (exists) + { + // value already exists + return Object.ReferenceEquals(valuesAsArray[slot], value); + } + else + { + OwnKeys(); + OwnValues(); + // set the value + keys[slot] = p; + valuesAsArray[slot] = value; + return true; + } } else { - var d = new Dictionary(); - for (int i = 0; i < keys.Length; i++) - { - if (keys[i] != null) - d[keys[i]!] = values[i]; - } - d[p] = value; - this.valuesAsDictionary = d; - this.keys = null; + // no free slots, move to standard Dictionary + SwitchToDictionary(); + this.valuesAsDictionary.Add(p, value); + return true; } } - } - - /// Tries to set value into the dictionary without overwriting anything. - public bool TryAdd(DotvvmProperty p, object? value) - { if (values == null) { + // empty dict -> initialize 8-slot array Debug.Assert(keys == null); - var d = new Dictionary(); - d[p] = value; - this.values = d; + this.flags = 0b11u << 30; + this.keys = new DotvvmPropertyId[PropertyImmutableHashtable.AdhocTableSize]; + this.keys[0] = p; + this.valuesAsArray = new object?[PropertyImmutableHashtable.AdhocTableSize]; + this.valuesAsArray[0] = value; return true; } - else if (keys == null) + else { - Debug.Assert(values is Dictionary); + // System.Dictionary backend + Debug.Assert(values is Dictionary); #if CSharp8Polyfill if (valuesAsDictionary.TryGetValue(p, out var existingValue)) - return Object.Equals(existingValue, value); + return Object.ReferenceEquals(existingValue, value); else { valuesAsDictionary.Add(p, value); return true; } #else - if (valuesAsDictionary.TryAdd(p, value)) - return true; - else - return Object.Equals(valuesAsDictionary[p], value); + return valuesAsDictionary.TryAdd(p, value) || Object.ReferenceEquals(valuesAsDictionary[p], value); #endif } - else - { - Debug.Assert(this.values is object[]); - Debug.Assert(this.keys is DotvvmProperty[]); - var keys = this.keysAsArray; - var values = this.valuesAsArray; - var slot = PropertyImmutableHashtable.FindSlot(keys, this.hashSeed, p); - if (slot >= 0) - { - // value already exists - return Object.Equals(values[slot], value); - } - else - { - var d = new Dictionary(); - for (int i = 0; i < keys.Length; i++) - { - if (keys[i] != null) - d[keys[i]!] = values[i]; - } - d[p] = value; - this.valuesAsDictionary = d; - this.keys = null; - return true; - } - } } - public DotvvmControlPropertiesEnumerator GetEnumerator() + public readonly DotvvmControlPropertyIdEnumerator GetEnumerator() { + if (keys != null) return new DotvvmControlPropertyIdEnumerator(keys, valuesAsArray); + if (values == null) return EmptyEnumerator; - if (keys == null) return new DotvvmControlPropertiesEnumerator(valuesAsDictionary.GetEnumerator()); - return new DotvvmControlPropertiesEnumerator(this.keysAsArray, this.valuesAsArray); + + Debug.Assert(values is Dictionary); + return new DotvvmControlPropertyIdEnumerator(valuesAsDictionary.GetEnumerator()); } - IEnumerator> IEnumerable>.GetEnumerator() => + readonly IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - private static DotvvmControlPropertiesEnumerator EmptyEnumerator = new DotvvmControlPropertiesEnumerator(new DotvvmProperty[0], new object[0]); + public readonly PropertyGroupEnumerable PropertyGroup(ushort groupId) => new(in this, groupId); - public bool Remove(DotvvmProperty key) - { - if (!Contains(key)) return false; - if (this.keys == null && valuesAsDictionary != null) - { - return valuesAsDictionary.Remove(key); - } + readonly public DotvvmControlPropertyIdGroupEnumerator EnumeratePropertyGroup(ushort id) => + this.keys is {} keys ? new(keys, valuesAsArray, id) : + this.values is {} ? new(valuesAsDictionary.GetEnumerator(), id) : + default; - // move from read-only struct to mutable struct - { - var keysTmp = this.keysAsArray; - var valuesTmp = this.valuesAsArray; - var d = new Dictionary(); + private static readonly DotvvmControlPropertyIdEnumerator EmptyEnumerator = new DotvvmControlPropertyIdEnumerator(Array.Empty(), Array.Empty()); - for (int i = 0; i < keysTmp.Length; i++) + public bool Remove(DotvvmProperty key) => Remove(key.Id); + public bool Remove(DotvvmPropertyId key) + { + if (this.keys != null) + { + var slot = PropertyImmutableHashtable.FindSlot(this.keys, this.hashSeed, key); + if (slot < 0) + return false; + this.OwnKeys(); + this.keys[slot] = default; + if (this.ownsValues) { - if (keysTmp[i] != null && keysTmp[i] != key) - d[keysTmp[i]!] = valuesTmp[i]; + this.valuesAsArray[slot] = default; } - this.valuesAsDictionary = d; - this.keys = null; return true; } + if (this.values == null) + return false; + else + { + Debug.Assert(values is Dictionary); + return valuesAsDictionary.Remove(key); + } } private static object? CloneValue(object? value) @@ -268,21 +386,18 @@ public bool Remove(DotvvmProperty key) return null; } - public int Count() + public readonly int Count() { if (this.values == null) return 0; if (this.keys == null) return this.valuesAsDictionary.Count; - int count = 0; - for (int i = 0; i < this.keysAsArray.Length; i++) - { - // get rid of a branch which would be almost always misspredicted - var x = this.keysAsArray[i] is not null; - count += Unsafe.As(ref x); - } - return count; + + return PropertyImmutableHashtable.Count(this.keys); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte BoolToInt(bool x) => Unsafe.As(ref x); + internal void CloneInto(ref DotvvmControlProperties newDict) { if (this.values == null) @@ -296,48 +411,240 @@ internal void CloneInto(ref DotvvmControlProperties newDict) if (dictionary.Count > 30) { newDict = this; - newDict.valuesAsDictionary = new Dictionary(dictionary); + newDict.keys = null; + newDict.valuesAsDictionary = new Dictionary(dictionary); foreach (var (key, value) in dictionary) if (CloneValue(value) is {} newValue) newDict.valuesAsDictionary[key] = newValue; return; } // move to immutable version if it's reasonably small. It will be probably cloned multiple times again - var properties = new DotvvmProperty[dictionary.Count]; - var values = new object?[properties.Length]; - int j = 0; - foreach (var x in this.valuesAsDictionary) - { - (properties[j], values[j]) = x; - j++; - } - Array.Sort(properties, values, PropertyImmutableHashtable.DotvvmPropertyComparer.Instance); - (this.hashSeed, this.keysAsArray, this.valuesAsArray) = PropertyImmutableHashtable.CreateTableWithValues(properties, values); + SwitchToPerfectHashing(); } newDict = this; + newDict.ownsKeys = false; + newDict.ownsValues = false; for (int i = 0; i < newDict.valuesAsArray.Length; i++) { if (CloneValue(newDict.valuesAsArray[i]) is {} newValue) { // clone the array if we didn't do that already if (newDict.values == this.values) + { newDict.values = this.valuesAsArray.Clone(); + newDict.ownsValues = true; + } newDict.valuesAsArray[i] = newValue; } } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void OwnKeys() + { + if (this.ownsKeys) return; + CloneKeys(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void OwnValues() + { + if (this.ownsValues) return; + CloneValues(); + } + void CloneKeys() + { + var oldKeys = this.keys; + var newKeys = new DotvvmPropertyId[oldKeys!.Length]; + MemoryExtensions.CopyTo(oldKeys, newKeys.AsSpan()); + this.keys = newKeys; + this.ownsKeys = true; + } + void CloneValues() + { + if (keys is {}) + { + var oldValues = this.valuesAsArray; + var newValues = new object?[oldValues.Length]; + MemoryExtensions.CopyTo(oldValues, newValues.AsSpan()); + this.valuesAsArray = newValues; + this.ownsValues = true; + } + else if (values is null) + return; + else + { + this.valuesAsDictionary = new Dictionary(this.valuesAsDictionary); + this.flags = 1u << 31; + } + } + + /// Converts the internal representation to System.Collections.Generic.Dictionary + void SwitchToDictionary() + { + if (this.keys is {}) + { + var keysTmp = this.keys; + var valuesTmp = this.valuesAsArray; + var d = new Dictionary(capacity: keysTmp.Length); + + for (int i = 0; i < keysTmp.Length; i++) + { + if (keysTmp[i].Id != 0) + d[keysTmp[i]] = valuesTmp[i]; + } + this.valuesAsDictionary = d; + this.keys = null; + this.flags = 1u << 31; + } + else if (this.values is null) + { + // already in the dictionary + return; + } + else + { + Debug.Assert(this.values is null); + // empty state + this.valuesAsDictionary = new Dictionary(); + this.flags = 1u << 31; + } + } + + /// Converts the internal representation to the DotVVM small dictionary implementation + void SwitchToPerfectHashing() + { + if (this.keys is {}) + { + // already in the perfect hashing + return; + } + else if (this.values is {}) + { + var properties = new DotvvmPropertyId[valuesAsDictionary.Count]; + var values = new object?[properties.Length]; + int j = 0; + foreach (var x in this.valuesAsDictionary) + { + (properties[j], values[j]) = x; + j++; + } + Array.Sort(properties, values); + (this.hashSeed, this.keys, this.valuesAsArray) = PropertyImmutableHashtable.CreateTableWithValues(properties, values); + this.ownsKeys = false; + this.ownsValues = true; + } + else + { + } + } + + public readonly struct PropertyGroupEnumerable: IEnumerable> + { + private readonly DotvvmControlProperties properties; + private readonly ushort groupId; + public PropertyGroupEnumerable(in DotvvmControlProperties properties, ushort groupId) + { + this.properties = properties; + this.groupId = groupId; + } + + public IEnumerator> GetEnumerator() => properties.EnumeratePropertyGroup(groupId); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } - public struct DotvvmControlPropertiesEnumerator : IEnumerator> + public struct DotvvmControlPropertyIdGroupEnumerator : IEnumerator> + { + private DotvvmPropertyId[]? keys; + private object?[]? values; + private int index; + private ushort groupId; + private ushort bitmap; // TODO!! + private Dictionary.Enumerator dictEnumerator; + + internal DotvvmControlPropertyIdGroupEnumerator(DotvvmPropertyId[] keys, object?[] values, ushort groupId) + { + this.keys = keys; + this.values = values; + this.index = -1; + this.groupId = groupId; + this.bitmap = 0; + dictEnumerator = default; + } + + internal DotvvmControlPropertyIdGroupEnumerator(Dictionary.Enumerator e, ushort groupId) + { + this.keys = null; + this.values = null; + this.index = 0; + this.groupId = groupId; + this.dictEnumerator = e; + } + + public KeyValuePair Current => this.keys is {} keys ? new(keys[index]!, values![index]) : dictEnumerator.Current; + + object IEnumerator.Current => this.Current; + + public void Dispose() { } + + public bool MoveNext() + { + var keys = this.keys; + if (keys is {}) + { + var index = (uint)(this.index + 1); + var bitmap = this.bitmap; + while (index < keys.Length) + { + if (index % 16 == 0) + { + bitmap = PropertyImmutableHashtable.FindGroupInNext16Slots(keys, index, groupId); + } + var localIndex = BitOperations.TrailingZeroCount(bitmap); + if (localIndex < 16) + { + this.index = (int)index + localIndex; + this.bitmap = (ushort)(bitmap >> (localIndex + 1)); + return true; + } + index += 16; + } + this.index = keys.Length; + return false; + } + else + { + // `default(T)` - empty collection + if (groupId == 0) + return false; + + while (dictEnumerator.MoveNext()) + { + if (dictEnumerator.Current.Key.IsInPropertyGroup(groupId)) + return true; + } + return false; + } + } + + public void Reset() + { + if (keys == null) + ((IEnumerator)dictEnumerator).Reset(); + else index = -1; + } + } + + public struct DotvvmControlPropertyIdEnumerator : IEnumerator> { - private DotvvmProperty?[]? keys; + private DotvvmPropertyId[]? keys; private object?[]? values; private int index; - private Dictionary.Enumerator dictEnumerator; + private Dictionary.Enumerator dictEnumerator; - internal DotvvmControlPropertiesEnumerator(DotvvmProperty?[] keys, object?[] values) + internal DotvvmControlPropertyIdEnumerator(DotvvmPropertyId[] keys, object?[] values) { this.keys = keys; this.values = values; @@ -345,7 +652,7 @@ internal DotvvmControlPropertiesEnumerator(DotvvmProperty?[] keys, object?[] val dictEnumerator = default; } - internal DotvvmControlPropertiesEnumerator(in Dictionary.Enumerator e) + internal DotvvmControlPropertyIdEnumerator(Dictionary.Enumerator e) { this.keys = null; this.values = null; @@ -353,7 +660,7 @@ internal DotvvmControlPropertiesEnumerator(in Dictionary Current => keys == null ? dictEnumerator.Current : new KeyValuePair(keys[index]!, values![index]); + public KeyValuePair Current => this.keys is {} keys ? new(keys[index]!, values![index]) : dictEnumerator.Current; object IEnumerator.Current => this.Current; @@ -363,22 +670,50 @@ public void Dispose() public bool MoveNext() { - if (keys == null) + var keys = this.keys; + if (keys is null) return dictEnumerator.MoveNext(); - while (++index < keys.Length && keys[index] == null) { } + var index = this.index; + while (++index < keys.Length && keys[index].Id == 0) { } + this.index = index; return index < keys.Length; } public void Reset() { - if (keys == null) + if (keys is null) ((IEnumerator)dictEnumerator).Reset(); else index = -1; } } + public struct DotvvmControlPropertiesEnumerator : IEnumerator> + { + DotvvmControlPropertyIdEnumerator idEnumerator; + public DotvvmControlPropertiesEnumerator(DotvvmControlPropertyIdEnumerator idEnumerator) + { + this.idEnumerator = idEnumerator; + } + + public KeyValuePair Current + { + get + { + var x = idEnumerator.Current; + return new KeyValuePair(x.Key.PropertyInstance, x.Value); + } + } + + object IEnumerator.Current => this.Current; + + public void Dispose() => idEnumerator.Dispose(); + public bool MoveNext() => idEnumerator.MoveNext(); + public void Reset() => idEnumerator.Reset(); + } + public readonly struct DotvvmPropertyDictionary : IDictionary { + private readonly DotvvmBindableObject control; public DotvvmPropertyDictionary(DotvvmBindableObject control) @@ -418,13 +753,13 @@ public void Clear() public void CopyTo(KeyValuePair[] array, int arrayIndex) { - foreach (var x in control.properties) + foreach (var x in this) { array[arrayIndex++] = x; } } - public DotvvmControlPropertiesEnumerator GetEnumerator() => control.properties.GetEnumerator(); + public DotvvmControlPropertiesEnumerator GetEnumerator() => new(control.properties.GetEnumerator()); public bool Remove(DotvvmProperty key) { diff --git a/src/Framework/Framework/Controls/DotvvmControlPropertyIdGroupEnumerator.cs b/src/Framework/Framework/Controls/DotvvmControlPropertyIdGroupEnumerator.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Framework/Framework/Controls/HtmlGenericControl.cs b/src/Framework/Framework/Controls/HtmlGenericControl.cs index b441ff5c63..cb8230eb47 100644 --- a/src/Framework/Framework/Controls/HtmlGenericControl.cs +++ b/src/Framework/Framework/Controls/HtmlGenericControl.cs @@ -181,25 +181,25 @@ public bool RenderOnServer(HtmlGenericControl @this) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected bool TouchProperty(DotvvmProperty prop, object? value, ref RenderState r) + protected bool TouchProperty(DotvvmPropertyId prop, object? value, ref RenderState r) { - if (prop == VisibleProperty) + if (prop == VisibleProperty.Id) r.Visible = value; - else if (prop == ClientIDProperty) + else if (prop == ClientIDProperty.Id) r.ClientId = value; - else if (prop == IDProperty && value != null) + else if (prop == IDProperty.Id && value != null) r.HasId = true; - else if (prop == InnerTextProperty) + else if (prop == InnerTextProperty.Id) r.InnerText = value; - else if (prop == PostBack.UpdateProperty) + else if (prop == PostBack.UpdateProperty.Id) r.HasPostbackUpdate = (bool)this.EvalPropertyValue(prop, value)!; - else if (prop is GroupedDotvvmProperty gp) + else if (prop.IsPropertyGroup) { - if (gp.PropertyGroup == CssClassesGroupDescriptor) + if (prop.IsInPropertyGroup(CssClassesGroupDescriptor.Id)) r.HasClass = true; - else if (gp.PropertyGroup == CssStylesGroupDescriptor) + else if (prop.IsInPropertyGroup(CssStylesGroupDescriptor.Id)) r.HasStyle = true; - else if (gp.PropertyGroup == AttributesGroupDescriptor) + else if (prop.IsInPropertyGroup(AttributesGroupDescriptor.Id)) r.HasAttributes = true; else return false; } @@ -409,14 +409,10 @@ private void AddHtmlAttributesToRender(ref RenderState r, IHtmlWriter writer) { KnockoutBindingGroup? attributeBindingGroup = null; - if (r.HasAttributes) foreach (var (prop, valueRaw) in this.properties) + if (r.HasAttributes) foreach (var (attributeName, valueRaw) in this.Attributes.RawValues) { - if (prop is not GroupedDotvvmProperty gprop || gprop.PropertyGroup != AttributesGroupDescriptor) - continue; - - var attributeName = gprop.GroupMemberName; var knockoutExpression = valueRaw switch { - AttributeList list => list.GetKnockoutBindingExpression(this, HtmlWriter.GetSeparatorForAttribute(gprop.GroupMemberName)), + AttributeList list => list.GetKnockoutBindingExpression(this, HtmlWriter.GetSeparatorForAttribute(attributeName)), IValueBinding binding => binding.GetKnockoutBindingExpression(this), _ => null }; diff --git a/src/Framework/Framework/Controls/Literal.cs b/src/Framework/Framework/Controls/Literal.cs index a8f310e450..f5db466025 100644 --- a/src/Framework/Framework/Controls/Literal.cs +++ b/src/Framework/Framework/Controls/Literal.cs @@ -119,13 +119,13 @@ bool isFormattedType(Type? type) => } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TouchProperty(DotvvmProperty prop, object? value, ref RenderState r) + private bool TouchProperty(DotvvmPropertyId prop, object? value, ref RenderState r) { - if (prop == TextProperty) + if (prop == TextProperty.Id) r.Text = value; - else if (prop == RenderSpanElementProperty) + else if (prop == RenderSpanElementProperty.Id) r.RenderSpanElement = (bool)EvalPropertyValue(RenderSpanElementProperty, value)!; - else if (prop == FormatStringProperty) + else if (prop == FormatStringProperty.Id) r.HasFormattingStuff = true; else if (base.TouchProperty(prop, value, ref r.HtmlState)) { } else if (DotvvmControl.TouchProperty(prop, value, ref r.BaseState)) { } diff --git a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs b/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs index 41215deb0d..e46e2d08d1 100644 --- a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs +++ b/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs @@ -3,59 +3,365 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using DotVVM.Framework.Binding; +using DotVVM.Framework.Hosting.ErrorPages; +using RecordExceptions; + +#if NET6_0_OR_GREATER +using System.Runtime.Intrinsics; +#endif namespace DotVVM.Framework.Controls { internal static class PropertyImmutableHashtable { - static int HashCombine(int a, int b) => a + b; + /// Up to this size, we don't bother with hashing as all keys can just be compared and searched with a single AVX instruction. + public const int AdhocTableSize = 8; + public const int ArrayMultipleSize = 8; + + static int HashCombine(int a, int b) => HashCode.Combine(a, b); - public static bool ContainsKey(DotvvmProperty?[] keys, int hashSeed, DotvvmProperty p) + public static bool ContainsKey(DotvvmPropertyId[] keys, uint flags, DotvvmPropertyId p) { - var len = keys.Length; - if (len == 4) +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + Debug.Assert(Vector256.Count == AdhocTableSize); + var v = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetArrayDataReference((Array)keys)); + if (Vector256.EqualsAny(v, Vector256.Create(p.Id))) + { + return true; + } + } +#else + if (false) { } +#endif + else + { + if (keys[7].Id == p.Id || keys[6].Id == p.Id || keys[5].Id == p.Id || keys[4].Id == p.Id || keys[3].Id == p.Id || keys[2].Id == p.Id || keys[1].Id == p.Id || keys[0] == p) + { + return true; + } + } + + if (keys.Length == AdhocTableSize) { - return keys[0] == p | keys[1] == p | keys[2] == p | keys[3] == p; + return false; } - var lengthMap = len - 1; // trims the hash to be in bounds of the array - var hash = HashCombine(p.GetHashCode(), hashSeed) & lengthMap; + var hashSeed = flags & 0x3FFF_FFFF; + var lengthMap = keys.Length - 1; // trims the hash to be in bounds of the array + var hash = HashCombine(p.GetHashCode(), (int)hashSeed) & lengthMap; var i1 = hash & -2; // hash with last bit == 0 (-2 is something like ff...fe because two's complement) var i2 = hash | 1; // hash with last bit == 1 - return keys[i1] == p | keys[i2] == p; + return keys[i1].Id == p.Id | keys[i2].Id == p.Id; } - public static int FindSlot(DotvvmProperty?[] keys, int hashSeed, DotvvmProperty p) + public static int FindSlot(DotvvmPropertyId[] keys, uint flags, DotvvmPropertyId p) { - var len = keys.Length; - if (len == 4) +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) { - for (int i = 0; i < 4; i++) + Debug.Assert(Vector256.Count == AdhocTableSize); + var v = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetArrayDataReference((Array)keys)); + var eq = Vector256.Equals(v, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + if (eq != 0) { - if (keys[i] == p) return i; + return BitOperations.TrailingZeroCount(eq); } + } +#else + if (false) { } +#endif + else + { + if (keys[7].Id == p.Id) return 7; + if (keys[6].Id == p.Id) return 6; + if (keys[5].Id == p.Id) return 5; + if (keys[4].Id == p.Id) return 4; + if (keys[3].Id == p.Id) return 3; + if (keys[2].Id == p.Id) return 2; + if (keys[1].Id == p.Id) return 1; + if (keys[0].Id == p.Id) return 0; + } + + if (keys.Length == AdhocTableSize) + { return -1; } - var lengthMap = len - 1; // trims the hash to be in bounds of the array - var hash = HashCombine(p.GetHashCode(), hashSeed) & lengthMap; + var lengthMap = keys.Length - 1; // trims the hash to be in bounds of the array + var hashSeed = flags & 0x3FFF_FFFF; + var hash = HashCombine(p.GetHashCode(), (int)hashSeed) & lengthMap; var i1 = hash & -2; // hash with last bit == 0 (-2 is something like ff...fe because two's complement) var i2 = hash | 1; // hash with last bit == 1 - if (keys[i1] == p) return i1; - if (keys[i2] == p) return i2; + if (keys[i1].Id == p.Id) return i1; + if (keys[i2].Id == p.Id) return i2; return -1; } - static ConcurrentDictionary tableCache = new ConcurrentDictionary(new EqCmp()); + public static int FindSlotOrFree(DotvvmPropertyId[] keys, uint flags, DotvvmPropertyId p, out bool exists) + { + int free = -1; + exists = true; + +#if NET8_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + var v = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetArrayDataReference((Array)keys)); + var eq = Vector256.Equals(v, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + if (eq != 0) + { + return BitOperations.TrailingZeroCount(eq); + } + var eq0 = Vector256.Equals(v, Vector256.Create(0u)).ExtractMostSignificantBits(); + if (eq0 != 0) + { + free = BitOperations.TrailingZeroCount(eq0); + } + } +#else + if (false) { } +#endif + else + { + if (keys[7].Id == p.Id) return 7; + if (keys[6].Id == p.Id) return 6; + if (keys[5].Id == p.Id) return 5; + if (keys[4].Id == p.Id) return 4; + if (keys[3].Id == p.Id) return 3; + if (keys[2].Id == p.Id) return 2; + if (keys[1].Id == p.Id) return 1; + if (keys[0].Id == p.Id) return 0; + if (keys[7].Id == 0) free = 7; + else if (keys[6].Id == 0) free = 6; + else if (keys[5].Id == 0) free = 5; + else if (keys[4].Id == 0) free = 4; + else if (keys[3].Id == 0) free = 3; + else if (keys[2].Id == 0) free = 2; + else if (keys[1].Id == 0) free = 1; + else if (keys[0].Id == 0) free = 0; + } + + if (keys.Length == 8) + { + exists = false; + return free; + } + + var lengthMap = keys.Length - 1; // trims the hash to be in bounds of the array + var hashSeed = flags & 0x3FFF_FFFF; + var hash = HashCombine(p.GetHashCode(), (int)hashSeed) & lengthMap; - class EqCmp : IEqualityComparer + var i1 = hash & -2; // hash with last bit == 0 (-2 is something like ff...fe because two's complement) + var i2 = hash | 1; // hash with last bit == 1 + + if (keys[i1].Id == p.Id) return i1; + if (keys[i2].Id == p.Id) return i2; + exists = false; + if (keys[i1].Id == 0) return i1; + if (keys[i2].Id == 0) return i2; + return free; + } + + + public static int FindFreeAdhocSlot(DotvvmPropertyId[] keys) + { + Debug.Assert(keys.Length >= AdhocTableSize); +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + Debug.Assert(Vector256.Count == AdhocTableSize); + var v = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetArrayDataReference((Array)keys)); + var eq = Vector256.Equals(v, Vector256.Create(0u)).ExtractMostSignificantBits(); + if (eq != 0) + { + return BitOperations.TrailingZeroCount(eq); + } + } +#else + if (false) { } +#endif + else + { + if (keys[7].Id == 0) return 7; + if (keys[6].Id == 0) return 6; + if (keys[5].Id == 0) return 5; + if (keys[4].Id == 0) return 4; + if (keys[3].Id == 0) return 3; + if (keys[2].Id == 0) return 2; + if (keys[1].Id == 0) return 1; + if (keys[0].Id == 0) return 0; + } + return -1; + } + + public static ushort FindGroupInNext16Slots(DotvvmPropertyId[] keys, uint startIndex, ushort groupId) { - public bool Equals(DotvvmProperty[]? x, DotvvmProperty[]? y) + Debug.Assert(keys.Length % ArrayMultipleSize == 0); + Debug.Assert(keys.Length >= AdhocTableSize); + + ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; + ushort bitmap = 0; + ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + var v1 = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keysRef, (int)startIndex))); + bitmap = (ushort)Vector256.Equals(v1 >> 16, Vector256.Create((uint)idPrefix)).ExtractMostSignificantBits(); + if (keys.Length > startIndex + 8) + { + var v2 = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keysRef, (int)startIndex + 8))); + bitmap |= (ushort)(Vector256.Equals(v2 >> 16, Vector256.Create((uint)idPrefix)).ExtractMostSignificantBits() << 8); + } + } +#else + if (false) { } +#endif + else + { + bitmap |= (ushort)((keys[startIndex + 7].TypeId == idPrefix ? 1 : 0) << 7); + bitmap |= (ushort)((keys[startIndex + 6].TypeId == idPrefix ? 1 : 0) << 6); + bitmap |= (ushort)((keys[startIndex + 5].TypeId == idPrefix ? 1 : 0) << 5); + bitmap |= (ushort)((keys[startIndex + 4].TypeId == idPrefix ? 1 : 0) << 4); + bitmap |= (ushort)((keys[startIndex + 3].TypeId == idPrefix ? 1 : 0) << 3); + bitmap |= (ushort)((keys[startIndex + 2].TypeId == idPrefix ? 1 : 0) << 2); + bitmap |= (ushort)((keys[startIndex + 1].TypeId == idPrefix ? 1 : 0) << 1); + bitmap |= (ushort)((keys[startIndex + 0].TypeId == idPrefix ? 1 : 0) << 0); + if (keys.Length > startIndex + 8) + { + bitmap |= (ushort)((keys[startIndex + 15].TypeId == idPrefix ? 1 : 0) << 15); + bitmap |= (ushort)((keys[startIndex + 14].TypeId == idPrefix ? 1 : 0) << 14); + bitmap |= (ushort)((keys[startIndex + 13].TypeId == idPrefix ? 1 : 0) << 13); + bitmap |= (ushort)((keys[startIndex + 12].TypeId == idPrefix ? 1 : 0) << 12); + bitmap |= (ushort)((keys[startIndex + 11].TypeId == idPrefix ? 1 : 0) << 11); + bitmap |= (ushort)((keys[startIndex + 10].TypeId == idPrefix ? 1 : 0) << 10); + bitmap |= (ushort)((keys[startIndex + 9].TypeId == idPrefix ? 1 : 0) << 9); + bitmap |= (ushort)((keys[startIndex + 8].TypeId == idPrefix ? 1 : 0) << 8); + } + } + return bitmap; + } + + public static bool ContainsPropertyGroup(DotvvmPropertyId[] keys, ushort groupId) + { + Debug.Assert(keys.Length % ArrayMultipleSize == 0 && Vector256.Count == ArrayMultipleSize); + Debug.Assert(keys.Length >= AdhocTableSize); + + ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; + ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + Debug.Assert(keys.Length % 8 == 0); + +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + for (int i = 0; i < keys.Length; i += 8) + { + var v = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keysRef, i))); + if (Vector256.EqualsAny(v >> 16, Vector256.Create((uint)idPrefix))) + { + return true; + } + } + return false; + } +#else + if (false) { } +#endif + else + { + for (int i = 0; i < keys.Length; i++) + { + if (keys[i].TypeId == idPrefix) + { + return true; + } + } + return false; + } + } + + public static int Count(DotvvmPropertyId[] keys) + { + Debug.Assert(keys.Length % ArrayMultipleSize == 0 && Vector256.Count == ArrayMultipleSize); + Debug.Assert(keys.Length >= AdhocTableSize); + + ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + Debug.Assert(keys.Length % 8 == 0); + +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + int zeroCount = 0; + for (int i = 0; i < keys.Length; i += Vector256.Count) + { + var v = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keysRef, i))); + var isZero = Vector256.Equals(v, Vector256.Create(0u)).ExtractMostSignificantBits(); + zeroCount += BitOperations.PopCount(isZero); + } + return keys.Length - zeroCount; + } +#else + if (false) { } +#endif + else + { + int count = 0; + for (int i = 0; i < keys.Length; i++) + { + count += BoolToInt(keys[i].Id == 0); + } + return count; + } + + } + + public static int CountPropertyGroup(DotvvmPropertyId[] keys, ushort groupId) + { + ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; + ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + Debug.Assert(keys.Length % ArrayMultipleSize == 0); + + int count = 0; + +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + for (int i = 0; i < keys.Length; i += Vector256.Count) + { + var v = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keysRef, i))); + count += BitOperations.PopCount(Vector256.Equals(v >> 16, Vector256.Create((uint)idPrefix)).ExtractMostSignificantBits()); + } + } +#else + if (false) { } +#endif + else + { + for (int i = 0; i < keys.Length; i++) + { + count += BoolToInt(keys[i].TypeId == idPrefix); + } + } + return count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte BoolToInt(bool x) => Unsafe.As(ref x); + + static ConcurrentDictionary tableCache = new(new EqCmp()); + + class EqCmp : IEqualityComparer + { + public bool Equals(DotvvmPropertyId[]? x, DotvvmPropertyId[]? y) { if (object.ReferenceEquals(x, y)) return true; if (x == null || y == null) return false; @@ -66,7 +372,7 @@ public bool Equals(DotvvmProperty[]? x, DotvvmProperty[]? y) return true; } - public int GetHashCode(DotvvmProperty[] obj) + public int GetHashCode(DotvvmPropertyId[] obj) { var h = obj.Length; foreach (var i in obj) @@ -76,25 +382,44 @@ public int GetHashCode(DotvvmProperty[] obj) } // Some primes. Numbers not divisible by 2 should help shuffle the table in a different way every time. - public static int[] hashSeeds = new [] {0, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541}; + public static uint[] hashSeeds = [0, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541]; - public static (int hashSeed, DotvvmProperty?[] keys) BuildTable(DotvvmProperty[] a) + private static bool IsOrderedWithoutDuplicatesAndZero(DotvvmPropertyId[] keys) { - Debug.Assert(a.OrderBy(x => x.FullName, StringComparer.Ordinal).SequenceEqual(a)); + uint last = 0; + foreach (var k in keys) + { + if (k.Id <= last) return false; + if (k.MemberId == 0) return false; + last = k.Id; + } + return true; + } + + public static (uint hashSeed, DotvvmPropertyId[] keys) BuildTable(DotvvmPropertyId[] keys) + { + if (!IsOrderedWithoutDuplicatesAndZero(keys)) + { + throw new ArgumentException("Keys must be ordered, without duplicates and without zero.", nameof(keys)); + } // make sure that all tables have the same keys so that they don't take much RAM (and remain in cache and make things go faster) - return tableCache.GetOrAdd(a, keys => { - if (keys.Length < 4) + return tableCache.GetOrAdd(keys, static keys => { + if (keys.Length <= 8) { // just pad them to make things regular - var result = new DotvvmProperty[4]; + var result = new DotvvmPropertyId[8]; Array.Copy(keys, result, keys.Length); return (0, result); } else { // first try closest size of power two - var size = 1 << (int)Math.Log(keys.Length + 1, 2); + var size = 1 << (int)Math.Ceiling(Math.Log(keys.Length, 2)); + + // all vector optimizations assume length at least 8 + size = Math.Max(size, AdhocTableSize); + Debug.Assert(size % ArrayMultipleSize == 0); while(true) { @@ -111,40 +436,40 @@ public static (int hashSeed, DotvvmProperty?[] keys) BuildTable(DotvvmProperty[] size *= 2; - if (size <= 4) throw new InvalidOperationException("Could not build hash table"); + if (size <= 4) throw new CannotMakeHashtableException(); } } }); } - static bool TestTableCorrectness(DotvvmProperty[] keys, int hashSeed, DotvvmProperty?[] table) + static bool TestTableCorrectness(DotvvmPropertyId[] keys, uint hashSeed, DotvvmPropertyId[] table) { - return keys.All(k => FindSlot(table, hashSeed, k) >= 0); + return keys.All(k => FindSlot(table, hashSeed, k) >= 0) && keys.Select(k => FindSlot(table, hashSeed, k)).Distinct().Count() == keys.Length; } /// Builds the core of the property hash table. Returns null if the table cannot be built due to collisions. - static DotvvmProperty?[]? TryBuildTable(DotvvmProperty[] a, int size, int hashSeed) + static DotvvmPropertyId[]? TryBuildTable(DotvvmPropertyId[] a, int size, uint hashSeed) { - var t = new DotvvmProperty?[size]; - var lengthMap = (size) - 1; // trims the hash to be in bounds of the array + var t = new DotvvmPropertyId[size]; + var lengthMap = size - 1; // trims the hash to be in bounds of the array foreach (var k in a) { - var hash = HashCombine(k.GetHashCode(), hashSeed) & lengthMap; + var hash = HashCombine(k.GetHashCode(), (int)hashSeed) & lengthMap; var i1 = hash & -2; // hash with last bit == 0 (-2 is something like ff...fe because two's complement) var i2 = hash | 1; // hash with last bit == 1 - if (t[i1] == null) + if (t[i1].IsZero) t[i1] = k; - else if (t[i2] == null) + else if (t[i2].IsZero) t[i2] = k; else return null; // if neither of these slots work, we can't build the table } return t; } - public static (int hashSeed, DotvvmProperty?[] keys, T[] valueTable) CreateTableWithValues(DotvvmProperty[] properties, T[] values) + public static (uint hashSeed, DotvvmPropertyId[] keys, T[] valueTable) CreateTableWithValues(DotvvmPropertyId[] properties, T[] values) { var (hashSeed, keys) = BuildTable(properties); var valueTable = new T[keys.Length]; @@ -155,18 +480,43 @@ public static (int hashSeed, DotvvmProperty?[] keys, T[] valueTable) CreateTable return (hashSeed, keys, valueTable); } - public static Action CreateBulkSetter(DotvvmProperty[] properties, object[] values) + public static Action CreateBulkSetter(DotvvmProperty[] properties, object?[] values) { - var (hashSeed, keys, valueTable) = CreateTableWithValues(properties, values); - return (obj) => obj.properties.AssignBulk(keys, valueTable, hashSeed); + var ids = properties.Select(p => p.Id).ToArray(); + Array.Sort(ids); + return CreateBulkSetter(ids, values); + } + public static Action CreateBulkSetter(DotvvmPropertyId[] properties, object?[] values) + { + if (properties.Length > 30) + { + var dict = new Dictionary(capacity: properties.Length); + for (int i = 0; i < properties.Length; i++) + { + dict[properties[i]] = values[i]; + } + return (obj) => obj.properties.AssignBulk(dict, false); + } + else + { + var (hashSeed, keys, valueTable) = CreateTableWithValues(properties, values); + return (obj) => obj.properties.AssignBulk(keys, valueTable, hashSeed); + } } - public class DotvvmPropertyComparer : IComparer + public static void SetValuesToDotvvmControl(DotvvmBindableObject obj, DotvvmPropertyId[] properties, object?[] values, uint flags) { - public int Compare(DotvvmProperty? a, DotvvmProperty? b) => - string.Compare(a?.FullName, b?.FullName, StringComparison.Ordinal); + obj.properties.AssignBulk(properties, values, flags); + } - public static readonly DotvvmPropertyComparer Instance = new(); + public static void SetValuesToDotvvmControl(DotvvmBindableObject obj, Dictionary values, bool owns) + { + obj.properties.AssignBulk(values, owns); + } + + public record CannotMakeHashtableException: RecordException + { + public override string Message => "Cannot make hashtable"; } } } diff --git a/src/Framework/Framework/Controls/RouteLink.cs b/src/Framework/Framework/Controls/RouteLink.cs index f4044a96c6..7beabaf2ac 100644 --- a/src/Framework/Framework/Controls/RouteLink.cs +++ b/src/Framework/Framework/Controls/RouteLink.cs @@ -139,6 +139,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest if (GetValue(EnabledProperty) == false) { writer.AddAttribute("disabled", "disabled"); + // this.CssClasses["a"] = true; if (enabledBinding is null) WriteEnabledBinding(writer, false); diff --git a/src/Tests/Runtime/DotvvmPropertyTests.cs b/src/Tests/Runtime/DotvvmPropertyTests.cs index fef60275b3..3f26b2a6c5 100644 --- a/src/Tests/Runtime/DotvvmPropertyTests.cs +++ b/src/Tests/Runtime/DotvvmPropertyTests.cs @@ -356,5 +356,20 @@ public void DotvvmProperty_CheckCorrectValueInDataBinding() } } } + + [TestMethod] + public void DotvvmProperty_ManyItemsSetter() + { + var properties = Enumerable.Range(0, 1000).Select(i => HtmlGenericControl.AttributesGroupDescriptor.GetDotvvmProperty("data-" + i.ToString())).ToArray(); + + var setter = PropertyImmutableHashtable.CreateBulkSetter(properties, Enumerable.Range(0, 1000).Select(i => (object?)i).ToArray()); + + var control1 = new HtmlGenericControl("div"); + setter(control1); + var control2 = new HtmlGenericControl("div"); + setter(control2); + + Assert.AreEqual(1000, control1.Properties.Count); + } } } diff --git a/src/Tests/Runtime/PropertyGroupTests.cs b/src/Tests/Runtime/PropertyGroupTests.cs new file mode 100644 index 0000000000..8e68d8d231 --- /dev/null +++ b/src/Tests/Runtime/PropertyGroupTests.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using DotVVM.Framework.Controls; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotVVM.Framework.Tests.Runtime +{ + [TestClass] + public class PropertyGroupTests + { + [TestMethod] + public void PGroup_Enumerate() + { + var el = new HtmlGenericControl("div"); + el.Attributes.Add("a", "1"); + el.Attributes.Add("b", "2"); + el.Attributes.Add("c", "3"); + + var expected = new[] { "1", "2", "3" }; + XAssert.Equal(expected, el.Attributes.Select(p => p.Value).OrderBy(a => a)); + XAssert.Equal(expected, el.Attributes.RawValues.Select(p => p.Value).OrderBy(a => a)); + XAssert.Equal(expected, el.Attributes.Values.OrderBy(a => a)); + XAssert.Equal(expected, el.Attributes.Properties.Select(p => el.GetValue(p)).OrderBy(a => a)); + XAssert.Equal(expected, el.Attributes.Keys.Select(k => el.Attributes[k]).OrderBy(a => a)); + XAssert.Equal(["a", "b", "c"], el.Attributes.Keys.OrderBy(a => a)); + } + + [TestMethod] + public void PGroup_AddMergeValues() + { + var el = new HtmlGenericControl("div"); + el.Attributes.Add("a", "1"); + el.Attributes.Add("a", "2"); + + XAssert.Equal("1;2", el.Attributes["a"]); + + el.Attributes.Add("class", "c1"); + el.Attributes.Add("class", "c2"); + + XAssert.Equal("c1 c2", el.Attributes["class"]); + + el.Attributes.Add("data-bind", "a: 1"); + el.Attributes.Add("data-bind", "b: 2"); + + XAssert.Equal("a: 1,b: 2", el.Attributes["data-bind"]); + } + } +}