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/BindingHelper.cs b/src/Framework/Framework/Binding/BindingHelper.cs index fd2fb2264d..2cdbbca809 100644 --- a/src/Framework/Framework/Binding/BindingHelper.cs +++ b/src/Framework/Framework/Binding/BindingHelper.cs @@ -64,8 +64,8 @@ public static string FormatKnockoutScript(this ParametrizedCode code, DotvvmBind /// Gets Internal.PathFragmentProperty or DataContext.KnockoutExpression. Returns null if none of these is set. /// public static string? GetDataContextPathFragment(this DotvvmBindableObject currentControl) => - currentControl.properties.TryGet(Internal.PathFragmentProperty, out var pathFragment) && pathFragment is string pathFragmentStr ? pathFragmentStr : - currentControl.properties.TryGet(DotvvmBindableObject.DataContextProperty, out var dataContext) && dataContext is IValueBinding binding ? + currentControl.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_PathFragment, out var pathFragment) && pathFragment is string pathFragmentStr ? pathFragmentStr : + currentControl.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext, out var dataContext) && dataContext is IValueBinding binding ? binding.GetProperty() .Code.FormatKnockoutScript(currentControl, binding) : null; @@ -88,16 +88,16 @@ internal static (int stepsUp, DotvvmBindableObject target) FindDataContextTarget if (bindingContext == null || controlContext == null || controlContext.Equals(bindingContext)) return (0, control); var changes = 0; - foreach (var a in control.GetAllAncestors(includingThis: true)) + for (var ancestor = control; ancestor is {}; ancestor = ancestor.Parent) { - var ancestorContext = a.GetDataContextType(inherit: false); + var ancestorContext = ancestor.GetDataContextType(inherit: false); if (bindingContext.Equals(ancestorContext)) - return (changes, a); + return (changes, ancestor); // count only client-side data contexts (DataContext={resource:} is skipped in JS) - if (a.properties.TryGet(DotvvmBindableObject.DataContextProperty, out var ancestorRuntimeContext)) + if (ancestor.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext, out var ancestorRuntimeContext)) { - if (a.properties.TryGet(Internal.IsServerOnlyDataContextProperty, out var isServerOnly) && isServerOnly != null) + if (ancestor.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_IsServerOnlyDataContext) is {} isServerOnly) { if (isServerOnly is false) changes++; @@ -173,9 +173,8 @@ public static T ExecDelegate(this BindingDelegate func, DotvvmBindableObje // this has O(h^2) complexity because GetValue calls another GetDataContexts, // but this function is used rarely - for exceptions, manually created bindings, ... // Normal bindings have specialized code generated in BindingCompiler - if (c.IsPropertySet(DotvvmBindableObject.DataContextProperty, inherit: false)) + if (c.properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext)) { - Debug.Assert(c.properties.Contains(DotvvmBindableObject.DataContextProperty), "Control claims that DataContextProperty is set, but it's not present in the properties dictionary."); yield return c.GetValue(DotvvmBindableObject.DataContextProperty); count--; } 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 a0b7500aad..3c8fad160d 100644 --- a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs +++ b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs @@ -76,27 +76,30 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyG var valueParameter = Expression.Parameter(type, "value"); var ctor = typeof(VirtualPropertyGroupDictionary<>) .MakeGenericType(propType) - .GetConstructor(new [] { typeof(DotvvmBindableObject), typeof(DotvvmPropertyGroup) })!; + .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, [ typeof(DotvvmBindableObject), typeof(ushort), typeof(bool) ], null)!; var createMethod = typeof(VirtualPropertyGroupDictionary<>) .MakeGenericType(propType) .GetMethod( typeof(ValueOrBinding).IsAssignableFrom(elementType) ? nameof(VirtualPropertyGroupDictionary.CreatePropertyDictionary) : nameof(VirtualPropertyGroupDictionary.CreateValueDictionary), - BindingFlags.Public | BindingFlags.Static + BindingFlags.NonPublic | BindingFlags.Static, + binder: null, + [ typeof(DotvvmBindableObject), typeof(ushort) ], + modifiers: null )!; var enumerableType = typeof(IEnumerable<>).MakeGenericType(typeof(KeyValuePair<,>).MakeGenericType(typeof(string), elementType)); var copyFromMethod = typeof(VirtualPropertyGroupDictionary<>) .MakeGenericType(propType) - .GetMethod("CopyFrom", new [] { enumerableType, typeof(bool) })!; + .GetMethod("CopyFrom", [ enumerableType, typeof(bool) ])!; return ( Lambda( - Convert(Call(createMethod, currentControlParameter, Constant(pgroup)), type), + Convert(Call(createMethod, currentControlParameter, Constant(pgroup.Id)), type), currentControlParameter ), Lambda( Call( - New(ctor, currentControlParameter, Constant(pgroup)), + New(ctor, currentControlParameter, Constant(pgroup.Id), Constant(pgroup.IsBindingProperty)), copyFromMethod, Convert(valueParameter, enumerableType), Constant(true) // clear @@ -105,8 +108,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) @@ -114,27 +119,31 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyA // if the property does not override GetValue/SetValue, we'll use // control.properties dictionary directly to avoid virtual method calls - var canUseDirectAccess = - !property.IsValueInherited && ( - property.GetType() == typeof(DotvvmProperty) || - property.GetType().GetMethod(nameof(DotvvmProperty.GetValue), new [] { typeof(DotvvmBindableObject), typeof(bool) })!.DeclaringType == typeof(DotvvmProperty) && - property.GetType().GetMethod(nameof(DotvvmProperty.SetValue), new [] { typeof(DotvvmBindableObject), typeof(object) })!.DeclaringType == typeof(DotvvmProperty)); + var canUseDirectAccess = !property.IsValueInherited && DotvvmPropertyIdAssignment.TypeCanUseAnyDirectAccess(property.GetType()); 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 +182,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,13 +206,13 @@ 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)); + getValue = Call(typeof(Helpers), nameof(Helpers.GetStructValueDirect), [ 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)); getValue = Convert(getValue, type); } return ( diff --git a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs index a276c1b989..0375742a28 100644 --- a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs +++ b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs @@ -12,7 +12,7 @@ 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)) { @@ -25,13 +25,13 @@ internal static class Helpers } 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)) { @@ -45,42 +45,42 @@ public static ValueOrBinding GetValueOrBinding(DotvvmBindableObject c, Dot } 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 } @@ -90,15 +90,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 { // T being a struct allows us to invert the rather expensive `is IBinding` typecheck in EvalPropertyValue @@ -111,11 +111,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 c6846b28af..dfcb6b23fd 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 5bdfc5ef21..d394204fe0 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) { @@ -123,12 +127,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 @@ -137,7 +153,8 @@ internal DotvvmProperty( Type declaringType, object? defaultValue, bool isValueInherited, - ICustomAttributeProvider attributeProvider + ICustomAttributeProvider attributeProvider, + DotvvmPropertyId id = default ) { this.Name = name ?? throw new ArgumentNullException(nameof(name)); @@ -146,6 +163,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); } @@ -162,6 +182,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; @@ -172,9 +209,10 @@ public bool IsOwnedByCapability(DotvvmCapabilityProperty capability) => private object? GetInheritedValue(DotvvmBindableObject control) { + var id = this.Id; for (var p = control.Parent; p is not null; p = p.Parent) { - if (p.properties.TryGet(this, out var v)) + if (p.properties.TryGet(id, out var v)) return v; } return DefaultValue; @@ -185,7 +223,7 @@ public bool IsOwnedByCapability(DotvvmCapabilityProperty capability) => /// public virtual object? GetValue(DotvvmBindableObject control, bool inherit = true) { - if (control.properties.TryGet(this, out var value)) + if (control.properties.TryGet(Id, out var value)) { return value; } @@ -196,20 +234,29 @@ public bool IsOwnedByCapability(DotvvmCapabilityProperty capability) => return DefaultValue; } + private bool IsSetInherited(DotvvmBindableObject control) + { + for (var p = control.Parent; p is not null; p = p.Parent) + { + if (p.properties.Contains(Id)) + return true; + } + return false; + } /// /// Gets whether the value of the property is set /// public virtual bool IsSet(DotvvmBindableObject control, bool inherit = true) { - if (control.properties.Contains(this)) + if (control.properties.Contains(Id)) { return true; } - if (IsValueInherited && inherit && control.Parent != null) + if (IsValueInherited && inherit) { - return IsSet(control.Parent); + return IsSetInherited(control); } return false; @@ -221,7 +268,7 @@ public virtual bool IsSet(DotvvmBindableObject control, bool inherit = true) /// public virtual void SetValue(DotvvmBindableObject control, object? value) { - control.properties.Set(this, value); + control.properties.Set(Id, value); } /// @@ -258,14 +305,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); } @@ -288,7 +353,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)) @@ -373,8 +442,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); @@ -396,6 +465,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) @@ -409,25 +480,27 @@ 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.MarkupOptions.AllowResourceBinding = !typeof(IValueBinding).IsAssignableFrom(property.PropertyType); } + + 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/DotvvmPropertyId.cs b/src/Framework/Framework/Binding/DotvvmPropertyId.cs new file mode 100644 index 0000000000..f7c765899a --- /dev/null +++ b/src/Framework/Framework/Binding/DotvvmPropertyId.cs @@ -0,0 +1,148 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Controls; + +namespace DotVVM.Framework.Binding +{ + /// + /// Represents a unique ID, used as a key for . + /// + /// + /// The ID is a 32-bit unsigned integer, where: + /// - the most significant bit indicates whether the ID is of a property group (1) or a classic property (0) + /// - the next upper 15 bits are the (for classic properties) or (for property groups) + /// - the lower 16 bits are the , ID of the string key for property groups or the ID of the property for classic properties + /// - in case of classic properties, the LSB bit of the member ID indicates whether the property has GetValue/SetValue overrides and is not inherited (see ) + /// + public readonly struct DotvvmPropertyId: IEquatable, IEquatable, IComparable + { + /// Numeric representation of the property ID. + public readonly uint Id; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DotvvmPropertyId(uint id) + { + Id = id; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DotvvmPropertyId(ushort typeOrGroupId, ushort memberId) + { + Id = ((uint)typeOrGroupId << 16) | memberId; + } + + /// Returns true if the property is a other type of property. + [MemberNotNullWhen(true, nameof(PropertyGroupInstance), nameof(GroupMemberName))] + public bool IsPropertyGroup + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (int)Id < 0; + } + /// Returns the ID of the property declaring type + public ushort TypeId + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (ushort)(Id >> 16); + } + /// Returns the ID of the property group. + public ushort GroupId + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (ushort)((Id >> 16) ^ 0x8000); + } + /// Returns the ID of the property member, i.e. property-in-type id for classic properties, or the name ID for property groups. + public ushort MemberId + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (ushort)(Id & 0xFFFF); + } + + /// Returns true if the property does not have GetValue/SetValue overrides and is not inherited. That means it is sufficient to call properties.TryGet instead going through the DotvvmProperty.GetValue dynamic dispatch + public bool CanUseFastAccessors + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + // properties: we encode this information as the LSB bit of the member ID (i.e. odd/even numbers) + // property groups: always true, i.e. + const uint mask = (1u << 31) | (1u); + const uint targetValue = 1u; + return (Id & mask) != targetValue; + } + } + + /// Returns true if the ID is default. This ID is invalid for most purposes and can be used as a sentinel value. + public bool IsZero + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Id == 0; + } + + /// Returns the instance for this ID. Note that a new might need to be allocated. + public DotvvmProperty PropertyInstance => DotvvmPropertyIdAssignment.GetProperty(Id) ?? throw new Exception($"Property with ID {Id} not registered."); + + /// Returns the instance for this ID, or null if the ID is of a classic property. + + public DotvvmPropertyGroup? PropertyGroupInstance => !IsPropertyGroup ? null : DotvvmPropertyIdAssignment.GetPropertyGroup(GroupId); + + /// Returns the name (string dictionary key) of the property group member, or null if the ID is of a classic property. + public string? GroupMemberName => !IsPropertyGroup ? null : DotvvmPropertyIdAssignment.GetGroupMemberName(MemberId); + + /// Returns the type of the property. + public Type PropertyType => IsPropertyGroup ? PropertyGroupInstance.PropertyType : PropertyInstance.PropertyType; + + /// Returns the property declaring type. + public Type DeclaringType => IsPropertyGroup ? PropertyGroupInstance.DeclaringType : DotvvmPropertyIdAssignment.GetControlType(TypeId); + + /// Returns the property declaring type. + [MemberNotNullWhen(true, nameof(PropertyGroupInstance), nameof(GroupMemberName))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsInPropertyGroup(ushort id) => (this.Id >> 16) == ((uint)id | 0x80_00u); + + /// Constucts property ID from a property group name and a name ID. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DotvvmPropertyId CreatePropertyGroupId(ushort groupId, ushort memberId) => new DotvvmPropertyId((ushort)(groupId | 0x80_00), memberId); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator DotvvmPropertyId(uint id) => new DotvvmPropertyId(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator uint(DotvvmPropertyId id) => id.Id; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(DotvvmPropertyId other) => Id == other.Id; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(uint other) => Id == other; + + public override bool Equals(object? obj) => obj is DotvvmPropertyId id && Equals(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => (int)Id; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(DotvvmPropertyId left, DotvvmPropertyId right) => left.Equals(right); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(DotvvmPropertyId left, DotvvmPropertyId right) => !left.Equals(right); + + public override string ToString() + { + if (IsZero) + return "[0000_0000]"; + if (IsPropertyGroup) + { + var pg = PropertyGroupInstance; + return $"[{TypeId:x4}_{MemberId:x4}]{pg.DeclaringType.Name}.{pg.Name}:{GroupMemberName}"; + } + else + { + return $"[{TypeId:x4}_{MemberId:x4}]{PropertyInstance.FullName}"; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int CompareTo(DotvvmPropertyId other) => Id.CompareTo(other.Id); + } +} diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs new file mode 100644 index 0000000000..1c4f4ac173 --- /dev/null +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs @@ -0,0 +1,401 @@ +// Generated by scripts/generate-property-ids.mjs +using System; +using System.Collections.Immutable; + +namespace DotVVM.Framework.Binding; + +static partial class DotvvmPropertyIdAssignment +{ + public static class GroupMembers + { + public const ushort accept = 1; + public const ushort accesskey = 2; + public const ushort action = 3; + public const ushort align = 4; + public const ushort allow = 5; + public const ushort alt = 6; + public const ushort aria_checked = 7; + public const ushort aria_controls = 8; + public const ushort aria_describedby = 9; + public const ushort aria_expanded = 10; + public const ushort aria_hidden = 11; + public const ushort aria_label = 12; + public const ushort aria_selected = 13; + public const ushort @as = 14; + public const ushort async = 15; + public const ushort autocomplete = 16; + public const ushort autofocus = 17; + public const ushort border = 18; + public const ushort charset = 19; + public const ushort @checked = 20; + public const ushort @class = 21; + public const ushort cols = 22; + public const ushort colspan = 23; + public const ushort content = 24; + public const ushort contenteditable = 25; + public const ushort crossorigin = 26; + public const ushort data_bind = 27; + public const ushort data_dismiss = 28; + public const ushort data_dotvvm_id = 29; + public const ushort data_placement = 30; + public const ushort data_target = 31; + public const ushort data_toggle = 32; + public const ushort data_ui = 33; + public const ushort data_uitest_name = 34; + public const ushort dir = 35; + public const ushort disabled = 36; + public const ushort download = 37; + public const ushort draggable = 38; + public const ushort enctype = 39; + public const ushort @for = 40; + public const ushort form = 41; + public const ushort formaction = 42; + public const ushort formmethod = 43; + public const ushort formnovalidate = 44; + public const ushort formtarget = 45; + public const ushort height = 46; + public const ushort hidden = 47; + public const ushort href = 48; + public const ushort hreflang = 49; + public const ushort http_equiv = 50; + public const ushort id = 51; + public const ushort integrity = 52; + public const ushort itemprop = 53; + public const ushort lang = 54; + public const ushort list = 55; + public const ushort loading = 56; + public const ushort max = 57; + public const ushort maxlength = 58; + public const ushort media = 59; + public const ushort method = 60; + public const ushort min = 61; + public const ushort minlength = 62; + public const ushort multiple = 63; + public const ushort name = 64; + public const ushort novalidate = 65; + public const ushort pattern = 66; + public const ushort ping = 67; + public const ushort placeholder = 68; + public const ushort preload = 69; + public const ushort @readonly = 70; + public const ushort referrerpolicy = 71; + public const ushort rel = 72; + public const ushort required = 73; + public const ushort role = 74; + public const ushort rows = 75; + public const ushort sandbox = 76; + public const ushort scope = 77; + public const ushort selected = 78; + public const ushort size = 79; + public const ushort slot = 80; + public const ushort span = 81; + public const ushort spellcheck = 82; + public const ushort src = 83; + public const ushort step = 84; + public const ushort style = 85; + public const ushort tabindex = 86; + public const ushort target = 87; + public const ushort title = 88; + public const ushort translate = 89; + public const ushort type = 90; + public const ushort value = 91; + public const ushort width = 92; + public const ushort wrap = 93; + public const ushort background_color = 94; + public const ushort bottom = 95; + public const ushort color = 96; + public const ushort display = 97; + public const ushort font_size = 98; + public const ushort left = 99; + public const ushort line_height = 100; + public const ushort margin_bottom = 101; + public const ushort margin_right = 102; + public const ushort margin_top = 103; + public const ushort margin = 104; + public const ushort max_height = 105; + public const ushort max_width = 106; + public const ushort min_height = 107; + public const ushort min_width = 108; + public const ushort opacity = 109; + public const ushort padding_bottom = 110; + public const ushort padding_left = 111; + public const ushort padding_right = 112; + public const ushort padding_top = 113; + public const ushort padding = 114; + public const ushort position = 115; + public const ushort right = 116; + public const ushort top = 117; + public const ushort visibility = 118; + public const ushort z_index = 119; + public const ushort Id = 120; + public const ushort Name = 121; + public const ushort GroupId = 122; + public const ushort FileName = 123; + public const ushort UserId = 124; + public const ushort Slug = 125; + public const ushort slug = 126; + public const ushort Lang = 127; + + public static readonly ImmutableArray<(string Name, ushort ID)> List = ImmutableArray.Create( + ("accept", accept), + ("accesskey", accesskey), + ("action", action), + ("align", align), + ("allow", allow), + ("alt", alt), + ("aria-checked", aria_checked), + ("aria-controls", aria_controls), + ("aria-describedby", aria_describedby), + ("aria-expanded", aria_expanded), + ("aria-hidden", aria_hidden), + ("aria-label", aria_label), + ("aria-selected", aria_selected), + ("as", @as), + ("async", async), + ("autocomplete", autocomplete), + ("autofocus", autofocus), + ("border", border), + ("charset", charset), + ("checked", @checked), + ("class", @class), + ("cols", cols), + ("colspan", colspan), + ("content", content), + ("contenteditable", contenteditable), + ("crossorigin", crossorigin), + ("data-bind", data_bind), + ("data-dismiss", data_dismiss), + ("data-dotvvm-id", data_dotvvm_id), + ("data-placement", data_placement), + ("data-target", data_target), + ("data-toggle", data_toggle), + ("data-ui", data_ui), + ("data-uitest-name", data_uitest_name), + ("dir", dir), + ("disabled", disabled), + ("download", download), + ("draggable", draggable), + ("enctype", enctype), + ("for", @for), + ("form", form), + ("formaction", formaction), + ("formmethod", formmethod), + ("formnovalidate", formnovalidate), + ("formtarget", formtarget), + ("height", height), + ("hidden", hidden), + ("href", href), + ("hreflang", hreflang), + ("http-equiv", http_equiv), + ("id", id), + ("integrity", integrity), + ("itemprop", itemprop), + ("lang", lang), + ("list", list), + ("loading", loading), + ("max", max), + ("maxlength", maxlength), + ("media", media), + ("method", method), + ("min", min), + ("minlength", minlength), + ("multiple", multiple), + ("name", name), + ("novalidate", novalidate), + ("pattern", pattern), + ("ping", ping), + ("placeholder", placeholder), + ("preload", preload), + ("readonly", @readonly), + ("referrerpolicy", referrerpolicy), + ("rel", rel), + ("required", required), + ("role", role), + ("rows", rows), + ("sandbox", sandbox), + ("scope", scope), + ("selected", selected), + ("size", size), + ("slot", slot), + ("span", span), + ("spellcheck", spellcheck), + ("src", src), + ("step", step), + ("style", style), + ("tabindex", tabindex), + ("target", target), + ("title", title), + ("translate", translate), + ("type", type), + ("value", value), + ("width", width), + ("wrap", wrap), + ("background-color", background_color), + ("bottom", bottom), + ("color", color), + ("display", display), + ("font-size", font_size), + ("left", left), + ("line-height", line_height), + ("margin-bottom", margin_bottom), + ("margin-right", margin_right), + ("margin-top", margin_top), + ("margin", margin), + ("max-height", max_height), + ("max-width", max_width), + ("min-height", min_height), + ("min-width", min_width), + ("opacity", opacity), + ("padding-bottom", padding_bottom), + ("padding-left", padding_left), + ("padding-right", padding_right), + ("padding-top", padding_top), + ("padding", padding), + ("position", position), + ("right", right), + ("top", top), + ("visibility", visibility), + ("z-index", z_index), + ("Id", Id), + ("Name", Name), + ("GroupId", GroupId), + ("FileName", FileName), + ("UserId", UserId), + ("Slug", Slug), + ("slug", slug), + ("Lang", Lang) + ); + + public static ushort TryGetId(ReadOnlySpan attr) => + attr switch { + "accept" => accept, + "accesskey" => accesskey, + "action" => action, + "align" => align, + "allow" => allow, + "alt" => alt, + "aria-checked" => aria_checked, + "aria-controls" => aria_controls, + "aria-describedby" => aria_describedby, + "aria-expanded" => aria_expanded, + "aria-hidden" => aria_hidden, + "aria-label" => aria_label, + "aria-selected" => aria_selected, + "as" => @as, + "async" => async, + "autocomplete" => autocomplete, + "autofocus" => autofocus, + "border" => border, + "charset" => charset, + "checked" => @checked, + "class" => @class, + "cols" => cols, + "colspan" => colspan, + "content" => content, + "contenteditable" => contenteditable, + "crossorigin" => crossorigin, + "data-bind" => data_bind, + "data-dismiss" => data_dismiss, + "data-dotvvm-id" => data_dotvvm_id, + "data-placement" => data_placement, + "data-target" => data_target, + "data-toggle" => data_toggle, + "data-ui" => data_ui, + "data-uitest-name" => data_uitest_name, + "dir" => dir, + "disabled" => disabled, + "download" => download, + "draggable" => draggable, + "enctype" => enctype, + "for" => @for, + "form" => form, + "formaction" => formaction, + "formmethod" => formmethod, + "formnovalidate" => formnovalidate, + "formtarget" => formtarget, + "height" => height, + "hidden" => hidden, + "href" => href, + "hreflang" => hreflang, + "http-equiv" => http_equiv, + "id" => id, + "integrity" => integrity, + "itemprop" => itemprop, + "lang" => lang, + "list" => list, + "loading" => loading, + "max" => max, + "maxlength" => maxlength, + "media" => media, + "method" => method, + "min" => min, + "minlength" => minlength, + "multiple" => multiple, + "name" => name, + "novalidate" => novalidate, + "pattern" => pattern, + "ping" => ping, + "placeholder" => placeholder, + "preload" => preload, + "readonly" => @readonly, + "referrerpolicy" => referrerpolicy, + "rel" => rel, + "required" => required, + "role" => role, + "rows" => rows, + "sandbox" => sandbox, + "scope" => scope, + "selected" => selected, + "size" => size, + "slot" => slot, + "span" => span, + "spellcheck" => spellcheck, + "src" => src, + "step" => step, + "style" => style, + "tabindex" => tabindex, + "target" => target, + "title" => title, + "translate" => translate, + "type" => type, + "value" => value, + "width" => width, + "wrap" => wrap, + "background-color" => background_color, + "bottom" => bottom, + "color" => color, + "display" => display, + "font-size" => font_size, + "left" => left, + "line-height" => line_height, + "margin-bottom" => margin_bottom, + "margin-right" => margin_right, + "margin-top" => margin_top, + "margin" => margin, + "max-height" => max_height, + "max-width" => max_width, + "min-height" => min_height, + "min-width" => min_width, + "opacity" => opacity, + "padding-bottom" => padding_bottom, + "padding-left" => padding_left, + "padding-right" => padding_right, + "padding-top" => padding_top, + "padding" => padding, + "position" => position, + "right" => right, + "top" => top, + "visibility" => visibility, + "z-index" => z_index, + "Id" => Id, + "Name" => Name, + "GroupId" => GroupId, + "FileName" => FileName, + "UserId" => UserId, + "Slug" => Slug, + "slug" => slug, + "Lang" => Lang, + _ => 0, + }; + } +} diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyGroupIds.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyGroupIds.cs new file mode 100644 index 0000000000..d2f1f580e6 --- /dev/null +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyGroupIds.cs @@ -0,0 +1,31 @@ +// Generated by scripts/generate-property-ids.mjs +using DotVVM.Framework.Controls; + +namespace DotVVM.Framework.Binding; + +static partial class DotvvmPropertyIdAssignment +{ + public static class PropertyGroupIds + { + /// + public const ushort HtmlGenericControl_Attributes = 1; + + /// + public const ushort HtmlGenericControl_CssClasses = 2; + + /// + public const ushort HtmlGenericControl_CssStyles = 3; + + /// + public const ushort RouteLink_Params = 4; + + /// + public const ushort RouteLink_QueryParameters = 5; + + /// + public const ushort JsComponent_Props = 6; + + /// + public const ushort JsComponent_Templates = 7; + } +} diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs new file mode 100644 index 0000000000..2dde63c6f0 --- /dev/null +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs @@ -0,0 +1,590 @@ +// Generated by scripts/generate-property-ids.mjs +using DotVVM.Framework.Controls; +using DotVVM.Framework.Controls.Infrastructure; + +namespace DotVVM.Framework.Binding; + +static partial class DotvvmPropertyIdAssignment +{ + public static class PropertyIds + { + /// + public const uint DotvvmBindableObject_DataContext = TypeIds.DotvvmBindableObject << 16 | 1; + + /// + public const uint DotvvmControl_ID = TypeIds.DotvvmControl << 16 | 2; + + /// + public const uint DotvvmControl_ClientID = TypeIds.DotvvmControl << 16 | 4; + + /// + public const uint DotvvmControl_IncludeInPage = TypeIds.DotvvmControl << 16 | 6; + + /// + public const uint DotvvmControl_ClientIDMode = TypeIds.DotvvmControl << 16 | 1; + + /// + public const uint HtmlGenericControl_Visible = TypeIds.HtmlGenericControl << 16 | 2; + + /// + public const uint HtmlGenericControl_InnerText = TypeIds.HtmlGenericControl << 16 | 4; + + /// + public const uint HtmlGenericControl_HtmlCapability = TypeIds.HtmlGenericControl << 16 | 1; + + /// + public const uint Literal_Text = TypeIds.Literal << 16 | 2; + + /// + public const uint Literal_FormatString = TypeIds.Literal << 16 | 4; + + /// + public const uint Literal_RenderSpanElement = TypeIds.Literal << 16 | 6; + + /// + public const uint ButtonBase_Click = TypeIds.ButtonBase << 16 | 2; + + /// + public const uint ButtonBase_ClickArguments = TypeIds.ButtonBase << 16 | 4; + + /// + public const uint ButtonBase_Text = TypeIds.ButtonBase << 16 | 6; + + /// + public const uint ButtonBase_Enabled = TypeIds.ButtonBase << 16 | 1; + + /// + public const uint ButtonBase_TextOrContentCapability = TypeIds.ButtonBase << 16 | 3; + + /// + public const uint Button_ButtonTagName = TypeIds.Button << 16 | 2; + + /// + public const uint Button_IsSubmitButton = TypeIds.Button << 16 | 4; + + /// + public const uint TextBox_Text = TypeIds.TextBox << 16 | 2; + + /// + public const uint TextBox_Changed = TypeIds.TextBox << 16 | 4; + + /// + public const uint TextBox_Type = TypeIds.TextBox << 16 | 6; + + /// + public const uint TextBox_TextInput = TypeIds.TextBox << 16 | 8; + + /// + public const uint TextBox_FormatString = TypeIds.TextBox << 16 | 10; + + /// + public const uint TextBox_SelectAllOnFocus = TypeIds.TextBox << 16 | 12; + + /// + public const uint TextBox_Enabled = TypeIds.TextBox << 16 | 1; + + /// + public const uint TextBox_UpdateTextOnInput = TypeIds.TextBox << 16 | 3; + + /// + public const uint RouteLink_RouteName = TypeIds.RouteLink << 16 | 2; + + /// + public const uint RouteLink_Enabled = TypeIds.RouteLink << 16 | 4; + + /// + public const uint RouteLink_Text = TypeIds.RouteLink << 16 | 6; + + /// + public const uint RouteLink_UrlSuffix = TypeIds.RouteLink << 16 | 8; + + /// + public const uint RouteLink_Culture = TypeIds.RouteLink << 16 | 10; + + /// + public const uint CheckableControlBase_Text = TypeIds.CheckableControlBase << 16 | 2; + + /// + public const uint CheckableControlBase_CheckedValue = TypeIds.CheckableControlBase << 16 | 4; + + /// + public const uint CheckableControlBase_Changed = TypeIds.CheckableControlBase << 16 | 6; + + /// + public const uint CheckableControlBase_LabelCssClass = TypeIds.CheckableControlBase << 16 | 8; + + /// + public const uint CheckableControlBase_InputCssClass = TypeIds.CheckableControlBase << 16 | 10; + + /// + public const uint CheckableControlBase_ItemKeyBinding = TypeIds.CheckableControlBase << 16 | 12; + + /// + public const uint CheckBox_Checked = TypeIds.CheckBox << 16 | 2; + + /// + public const uint CheckBox_CheckedItems = TypeIds.CheckBox << 16 | 4; + + /// + public const uint CheckBox_DisableIndeterminate = TypeIds.CheckBox << 16 | 6; + + /// + public const uint RadioButton_Checked = TypeIds.RadioButton << 16 | 2; + + /// + public const uint RadioButton_CheckedItem = TypeIds.RadioButton << 16 | 4; + + /// + public const uint RadioButton_GroupName = TypeIds.RadioButton << 16 | 6; + + /// + public const uint Validator_HideWhenValid = TypeIds.Validator << 16 | 1; + + /// + public const uint Validator_InvalidCssClass = TypeIds.Validator << 16 | 3; + + /// + public const uint Validator_SetToolTipText = TypeIds.Validator << 16 | 5; + + /// + public const uint Validator_ShowErrorMessageText = TypeIds.Validator << 16 | 7; + + /// + public const uint Validation_Enabled = TypeIds.Validation << 16 | 1; + + /// + public const uint Validation_Target = TypeIds.Validation << 16 | 3; + + /// + public const uint ValidationSummary_IncludeErrorsFromChildren = TypeIds.ValidationSummary << 16 | 2; + + /// + public const uint ValidationSummary_HideWhenValid = TypeIds.ValidationSummary << 16 | 4; + + /// + public const uint ValidationSummary_IncludeErrorsFromTarget = TypeIds.ValidationSummary << 16 | 6; + + /// + public const uint ItemsControl_DataSource = TypeIds.ItemsControl << 16 | 2; + + /// + public const uint Repeater_EmptyDataTemplate = TypeIds.Repeater << 16 | 2; + + /// + public const uint Repeater_ItemTemplate = TypeIds.Repeater << 16 | 4; + + /// + public const uint Repeater_RenderWrapperTag = TypeIds.Repeater << 16 | 6; + + /// + public const uint Repeater_SeparatorTemplate = TypeIds.Repeater << 16 | 8; + + /// + public const uint Repeater_WrapperTagName = TypeIds.Repeater << 16 | 10; + + /// + public const uint Repeater_RenderAsNamedTemplate = TypeIds.Repeater << 16 | 12; + + /// + public const uint HierarchyRepeater_ItemChildrenBinding = TypeIds.HierarchyRepeater << 16 | 2; + + /// + public const uint HierarchyRepeater_ItemTemplate = TypeIds.HierarchyRepeater << 16 | 4; + + /// + public const uint HierarchyRepeater_EmptyDataTemplate = TypeIds.HierarchyRepeater << 16 | 6; + + /// + public const uint HierarchyRepeater_RenderWrapperTag = TypeIds.HierarchyRepeater << 16 | 8; + + /// + public const uint HierarchyRepeater_WrapperTagName = TypeIds.HierarchyRepeater << 16 | 10; + + /// + public const uint GridView_FilterPlacement = TypeIds.GridView << 16 | 2; + + /// + public const uint GridView_EmptyDataTemplate = TypeIds.GridView << 16 | 4; + + /// + public const uint GridView_Columns = TypeIds.GridView << 16 | 6; + + /// + public const uint GridView_RowDecorators = TypeIds.GridView << 16 | 8; + + /// + public const uint GridView_HeaderRowDecorators = TypeIds.GridView << 16 | 10; + + /// + public const uint GridView_EditRowDecorators = TypeIds.GridView << 16 | 12; + + /// + public const uint GridView_SortChanged = TypeIds.GridView << 16 | 14; + + /// + public const uint GridView_ShowHeaderWhenNoData = TypeIds.GridView << 16 | 16; + + /// + public const uint GridView_InlineEditing = TypeIds.GridView << 16 | 18; + + /// + public const uint GridView_LoadData = TypeIds.GridView << 16 | 20; + + /// + public const uint GridViewColumn_HeaderText = TypeIds.GridViewColumn << 16 | 2; + + /// + public const uint GridViewColumn_HeaderTemplate = TypeIds.GridViewColumn << 16 | 4; + + /// + public const uint GridViewColumn_FilterTemplate = TypeIds.GridViewColumn << 16 | 6; + + /// + public const uint GridViewColumn_SortExpression = TypeIds.GridViewColumn << 16 | 8; + + /// + public const uint GridViewColumn_SortAscendingHeaderCssClass = TypeIds.GridViewColumn << 16 | 10; + + /// + public const uint GridViewColumn_SortDescendingHeaderCssClass = TypeIds.GridViewColumn << 16 | 12; + + /// + public const uint GridViewColumn_AllowSorting = TypeIds.GridViewColumn << 16 | 14; + + /// + public const uint GridViewColumn_CssClass = TypeIds.GridViewColumn << 16 | 16; + + /// + public const uint GridViewColumn_IsEditable = TypeIds.GridViewColumn << 16 | 18; + + /// + public const uint GridViewColumn_HeaderCssClass = TypeIds.GridViewColumn << 16 | 20; + + /// + public const uint GridViewColumn_Width = TypeIds.GridViewColumn << 16 | 22; + + /// + public const uint GridViewColumn_Visible = TypeIds.GridViewColumn << 16 | 24; + + /// + public const uint GridViewColumn_CellDecorators = TypeIds.GridViewColumn << 16 | 26; + + /// + public const uint GridViewColumn_EditCellDecorators = TypeIds.GridViewColumn << 16 | 28; + + /// + public const uint GridViewColumn_EditTemplate = TypeIds.GridViewColumn << 16 | 30; + + /// + public const uint GridViewColumn_HeaderCellDecorators = TypeIds.GridViewColumn << 16 | 32; + + /// + public const uint GridViewTextColumn_FormatString = TypeIds.GridViewTextColumn << 16 | 2; + + /// + public const uint GridViewTextColumn_ChangedBinding = TypeIds.GridViewTextColumn << 16 | 4; + + /// + public const uint GridViewTextColumn_ValueBinding = TypeIds.GridViewTextColumn << 16 | 6; + + /// + public const uint GridViewTextColumn_ValidatorPlacement = TypeIds.GridViewTextColumn << 16 | 8; + + /// + public const uint GridViewCheckBoxColumn_ValueBinding = TypeIds.GridViewCheckBoxColumn << 16 | 2; + + /// + public const uint GridViewCheckBoxColumn_ValidatorPlacement = TypeIds.GridViewCheckBoxColumn << 16 | 4; + + /// + public const uint GridViewTemplateColumn_ContentTemplate = TypeIds.GridViewTemplateColumn << 16 | 2; + + /// + public const uint DataPager_DataSet = TypeIds.DataPager << 16 | 2; + + /// + public const uint DataPager_FirstPageTemplate = TypeIds.DataPager << 16 | 4; + + /// + public const uint DataPager_LastPageTemplate = TypeIds.DataPager << 16 | 6; + + /// + public const uint DataPager_PreviousPageTemplate = TypeIds.DataPager << 16 | 8; + + /// + public const uint DataPager_NextPageTemplate = TypeIds.DataPager << 16 | 10; + + /// + public const uint DataPager_RenderLinkForCurrentPage = TypeIds.DataPager << 16 | 12; + + /// + public const uint DataPager_HideWhenOnlyOnePage = TypeIds.DataPager << 16 | 14; + + /// + public const uint DataPager_LoadData = TypeIds.DataPager << 16 | 16; + + /// + public const uint AppendableDataPager_LoadTemplate = TypeIds.AppendableDataPager << 16 | 2; + + /// + public const uint AppendableDataPager_LoadingTemplate = TypeIds.AppendableDataPager << 16 | 4; + + /// + public const uint AppendableDataPager_EndTemplate = TypeIds.AppendableDataPager << 16 | 6; + + /// + public const uint AppendableDataPager_DataSet = TypeIds.AppendableDataPager << 16 | 8; + + /// + public const uint AppendableDataPager_LoadData = TypeIds.AppendableDataPager << 16 | 10; + + /// + public const uint SelectorBase_ItemTextBinding = TypeIds.SelectorBase << 16 | 2; + + /// + public const uint SelectorBase_ItemValueBinding = TypeIds.SelectorBase << 16 | 4; + + /// + public const uint SelectorBase_SelectionChanged = TypeIds.SelectorBase << 16 | 6; + + /// + public const uint SelectorBase_ItemTitleBinding = TypeIds.SelectorBase << 16 | 8; + + /// + public const uint Selector_SelectedValue = TypeIds.Selector << 16 | 2; + + /// + public const uint MultiSelector_SelectedValues = TypeIds.MultiSelector << 16 | 2; + + /// + public const uint ListBox_Size = TypeIds.ListBox << 16 | 2; + + /// + public const uint ComboBox_EmptyItemText = TypeIds.ComboBox << 16 | 2; + + /// + public const uint SelectorItem_Text = TypeIds.SelectorItem << 16 | 2; + + /// + public const uint SelectorItem_Value = TypeIds.SelectorItem << 16 | 4; + + /// + public const uint FileUpload_UploadedFiles = TypeIds.FileUpload << 16 | 2; + + /// + public const uint FileUpload_Capture = TypeIds.FileUpload << 16 | 4; + + /// + public const uint FileUpload_MaxFileSize = TypeIds.FileUpload << 16 | 6; + + /// + public const uint FileUpload_UploadCompleted = TypeIds.FileUpload << 16 | 8; + + /// + public const uint Timer_Command = TypeIds.Timer << 16 | 2; + + /// + public const uint Timer_Interval = TypeIds.Timer << 16 | 4; + + /// + public const uint Timer_Enabled = TypeIds.Timer << 16 | 6; + + /// + public const uint UpdateProgress_Delay = TypeIds.UpdateProgress << 16 | 2; + + /// + public const uint UpdateProgress_IncludedQueues = TypeIds.UpdateProgress << 16 | 4; + + /// + public const uint UpdateProgress_ExcludedQueues = TypeIds.UpdateProgress << 16 | 6; + + /// + public const uint Label_For = TypeIds.Label << 16 | 2; + + /// + public const uint EmptyData_WrapperTagName = TypeIds.EmptyData << 16 | 2; + + /// + public const uint EmptyData_RenderWrapperTag = TypeIds.EmptyData << 16 | 4; + + /// + public const uint Content_ContentPlaceHolderID = TypeIds.Content << 16 | 2; + + /// + public const uint TemplateHost_Template = TypeIds.TemplateHost << 16 | 2; + + /// + public const uint AddTemplateDecorator_AfterTemplate = TypeIds.AddTemplateDecorator << 16 | 2; + + /// + public const uint AddTemplateDecorator_BeforeTemplate = TypeIds.AddTemplateDecorator << 16 | 4; + + /// + public const uint SpaContentPlaceHolder_DefaultRouteName = TypeIds.SpaContentPlaceHolder << 16 | 2; + + /// + public const uint SpaContentPlaceHolder_PrefixRouteName = TypeIds.SpaContentPlaceHolder << 16 | 4; + + /// + public const uint SpaContentPlaceHolder_UseHistoryApi = TypeIds.SpaContentPlaceHolder << 16 | 6; + + /// + public const uint ModalDialog_Open = TypeIds.ModalDialog << 16 | 2; + + /// + public const uint ModalDialog_CloseOnBackdropClick = TypeIds.ModalDialog << 16 | 4; + + /// + public const uint ModalDialog_Close = TypeIds.ModalDialog << 16 | 6; + + /// + public const uint HtmlLiteral_Html = TypeIds.HtmlLiteral << 16 | 2; + + /// + public const uint RequiredResource_Name = TypeIds.RequiredResource << 16 | 2; + + /// + public const uint InlineScript_Dependencies = TypeIds.InlineScript << 16 | 2; + + /// + public const uint InlineScript_Script = TypeIds.InlineScript << 16 | 4; + + /// + public const uint RoleView_Roles = TypeIds.RoleView << 16 | 2; + + /// + public const uint RoleView_IsMemberTemplate = TypeIds.RoleView << 16 | 4; + + /// + public const uint RoleView_IsNotMemberTemplate = TypeIds.RoleView << 16 | 6; + + /// + public const uint RoleView_HideForAnonymousUsers = TypeIds.RoleView << 16 | 8; + + /// + public const uint ClaimView_Claim = TypeIds.ClaimView << 16 | 2; + + /// + public const uint ClaimView_Values = TypeIds.ClaimView << 16 | 4; + + /// + public const uint ClaimView_HasClaimTemplate = TypeIds.ClaimView << 16 | 6; + + /// + public const uint ClaimView_HideForAnonymousUsers = TypeIds.ClaimView << 16 | 8; + + /// + public const uint EnvironmentView_Environments = TypeIds.EnvironmentView << 16 | 2; + + /// + public const uint EnvironmentView_IsEnvironmentTemplate = TypeIds.EnvironmentView << 16 | 4; + + /// + public const uint EnvironmentView_IsNotEnvironmentTemplate = TypeIds.EnvironmentView << 16 | 6; + + /// + public const uint JsComponent_Global = TypeIds.JsComponent << 16 | 2; + + /// + public const uint JsComponent_Name = TypeIds.JsComponent << 16 | 4; + + /// + public const uint JsComponent_WrapperTagName = TypeIds.JsComponent << 16 | 6; + + /// + public const uint PostBackHandler_EventName = TypeIds.PostBackHandler << 16 | 2; + + /// + public const uint PostBackHandler_Enabled = TypeIds.PostBackHandler << 16 | 4; + + /// + public const uint SuppressPostBackHandler_Suppress = TypeIds.SuppressPostBackHandler << 16 | 2; + + /// + public const uint ConcurrencyQueueSetting_EventName = TypeIds.ConcurrencyQueueSetting << 16 | 2; + + /// + public const uint ConcurrencyQueueSetting_ConcurrencyQueue = TypeIds.ConcurrencyQueueSetting << 16 | 4; + + /// + public const uint NamedCommand_Name = TypeIds.NamedCommand << 16 | 2; + + /// + public const uint NamedCommand_Command = TypeIds.NamedCommand << 16 | 4; + + /// + public const uint PostBack_Update = TypeIds.PostBack << 16 | 2; + + /// + public const uint PostBack_Handlers = TypeIds.PostBack << 16 | 4; + + /// + public const uint PostBack_ConcurrencyQueueSettings = TypeIds.PostBack << 16 | 6; + + /// + public const uint PostBack_Concurrency = TypeIds.PostBack << 16 | 1; + + /// + public const uint PostBack_ConcurrencyQueue = TypeIds.PostBack << 16 | 3; + + /// + public const uint FormControls_Enabled = TypeIds.FormControls << 16 | 1; + + /// + public const uint UITests_GenerateStub = TypeIds.UITests << 16 | 1; + + /// + public const uint Internal_UniqueID = TypeIds.Internal << 16 | 2; + + /// + public const uint Internal_IsNamingContainer = TypeIds.Internal << 16 | 4; + + /// + public const uint Internal_IsControlBindingTarget = TypeIds.Internal << 16 | 6; + + /// + public const uint Internal_PathFragment = TypeIds.Internal << 16 | 8; + + /// + public const uint Internal_IsServerOnlyDataContext = TypeIds.Internal << 16 | 10; + + /// + public const uint Internal_MarkupLineNumber = TypeIds.Internal << 16 | 12; + + /// + public const uint Internal_ClientIDFragment = TypeIds.Internal << 16 | 14; + + /// + public const uint Internal_IsMasterPageCompositionFinished = TypeIds.Internal << 16 | 16; + + /// + public const uint Internal_CurrentIndexBinding = TypeIds.Internal << 16 | 18; + + /// + public const uint Internal_ReferencedViewModuleInfo = TypeIds.Internal << 16 | 20; + + /// + public const uint Internal_UsedPropertiesInfo = TypeIds.Internal << 16 | 22; + + /// + public const uint Internal_IsSpaPage = TypeIds.Internal << 16 | 1; + + /// + public const uint Internal_UseHistoryApiSpaNavigation = TypeIds.Internal << 16 | 3; + + /// + public const uint Internal_DataContextType = TypeIds.Internal << 16 | 5; + + /// + public const uint Internal_MarkupFileName = TypeIds.Internal << 16 | 7; + + /// + public const uint Internal_RequestContext = TypeIds.Internal << 16 | 9; + + /// + public const uint RenderSettings_Mode = TypeIds.RenderSettings << 16 | 1; + + /// + public const uint DotvvmView_Directives = TypeIds.DotvvmView << 16 | 1; + } +} diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs new file mode 100644 index 0000000000..248064e93d --- /dev/null +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs @@ -0,0 +1,139 @@ +// Generated by scripts/generate-property-ids.mjs +using System; +using System.Collections.Immutable; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Controls.Infrastructure; + +namespace DotVVM.Framework.Binding; + +static partial class DotvvmPropertyIdAssignment +{ + 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 RadioButton = 13; + public const ushort Validator = 14; + public const ushort Validation = 15; + public const ushort ValidationSummary = 16; + public const ushort ItemsControl = 17; + public const ushort Repeater = 18; + public const ushort HierarchyRepeater = 19; + public const ushort GridView = 20; + public const ushort GridViewColumn = 21; + public const ushort GridViewTextColumn = 22; + public const ushort GridViewCheckBoxColumn = 23; + public const ushort GridViewTemplateColumn = 24; + public const ushort DataPager = 25; + public const ushort AppendableDataPager = 26; + public const ushort SelectorBase = 27; + public const ushort Selector = 28; + public const ushort MultiSelector = 29; + public const ushort ListBox = 30; + public const ushort ComboBox = 31; + public const ushort SelectorItem = 32; + public const ushort FileUpload = 33; + public const ushort Timer = 34; + public const ushort UpdateProgress = 35; + public const ushort Label = 36; + public const ushort EmptyData = 37; + public const ushort Content = 38; + public const ushort TemplateHost = 39; + public const ushort AddTemplateDecorator = 40; + public const ushort SpaContentPlaceHolder = 41; + public const ushort ModalDialog = 42; + public const ushort HtmlLiteral = 43; + public const ushort RequiredResource = 44; + public const ushort InlineScript = 45; + public const ushort RoleView = 46; + public const ushort ClaimView = 47; + public const ushort EnvironmentView = 48; + public const ushort JsComponent = 49; + public const ushort PostBackHandler = 50; + public const ushort SuppressPostBackHandler = 51; + public const ushort ConcurrencyQueueSetting = 52; + public const ushort NamedCommand = 53; + public const ushort PostBack = 54; + public const ushort FormControls = 55; + public const ushort UITests = 56; + public const ushort Events = 57; + public const ushort Styles = 58; + public const ushort Internal = 59; + public const ushort RenderSettings = 60; + public const ushort DotvvmView = 61; + + public static readonly ImmutableArray<(Type type, ushort id)> List = ImmutableArray.Create( + (typeof(DotvvmBindableObject), DotvvmBindableObject), + (typeof(DotvvmControl), DotvvmControl), + (typeof(HtmlGenericControl), HtmlGenericControl), + (typeof(RawLiteral), RawLiteral), + (typeof(Literal), Literal), + (typeof(ButtonBase), ButtonBase), + (typeof(Button), Button), + (typeof(LinkButton), LinkButton), + (typeof(TextBox), TextBox), + (typeof(RouteLink), RouteLink), + (typeof(CheckableControlBase), CheckableControlBase), + (typeof(CheckBox), CheckBox), + (typeof(RadioButton), RadioButton), + (typeof(Validator), Validator), + (typeof(Validation), Validation), + (typeof(ValidationSummary), ValidationSummary), + (typeof(ItemsControl), ItemsControl), + (typeof(Repeater), Repeater), + (typeof(HierarchyRepeater), HierarchyRepeater), + (typeof(GridView), GridView), + (typeof(GridViewColumn), GridViewColumn), + (typeof(GridViewTextColumn), GridViewTextColumn), + (typeof(GridViewCheckBoxColumn), GridViewCheckBoxColumn), + (typeof(GridViewTemplateColumn), GridViewTemplateColumn), + (typeof(DataPager), DataPager), + (typeof(AppendableDataPager), AppendableDataPager), + (typeof(SelectorBase), SelectorBase), + (typeof(Selector), Selector), + (typeof(MultiSelector), MultiSelector), + (typeof(ListBox), ListBox), + (typeof(ComboBox), ComboBox), + (typeof(SelectorItem), SelectorItem), + (typeof(FileUpload), FileUpload), + (typeof(Timer), Timer), + (typeof(UpdateProgress), UpdateProgress), + (typeof(Label), Label), + (typeof(EmptyData), EmptyData), + (typeof(Content), Content), + (typeof(TemplateHost), TemplateHost), + (typeof(AddTemplateDecorator), AddTemplateDecorator), + (typeof(SpaContentPlaceHolder), SpaContentPlaceHolder), + (typeof(ModalDialog), ModalDialog), + (typeof(HtmlLiteral), HtmlLiteral), + (typeof(RequiredResource), RequiredResource), + (typeof(InlineScript), InlineScript), + (typeof(RoleView), RoleView), + (typeof(ClaimView), ClaimView), + (typeof(EnvironmentView), EnvironmentView), + (typeof(JsComponent), JsComponent), + (typeof(PostBackHandler), PostBackHandler), + (typeof(SuppressPostBackHandler), SuppressPostBackHandler), + (typeof(ConcurrencyQueueSetting), ConcurrencyQueueSetting), + (typeof(NamedCommand), NamedCommand), + (typeof(PostBack), PostBack), + (typeof(FormControls), FormControls), + (typeof(UITests), UITests), + (typeof(Events), Events), + (typeof(Styles), Styles), + (typeof(Internal), Internal), + (typeof(RenderSettings), RenderSettings), + (typeof(DotvvmView), DotvvmView) + ); + } +} diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs new file mode 100644 index 0000000000..b4bd6033cc --- /dev/null +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs @@ -0,0 +1,484 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Controls; +using FastExpressionCompiler; + +namespace DotVVM.Framework.Binding +{ + + internal static partial class DotvvmPropertyIdAssignment + { + /// Type and property group IDs bellow this are reserved for manual ID assignment + const int RESERVED_CONTROL_TYPES = 256; + /// Properties with ID bellow this are reserved for manual ID assignment (only makes sense for controls with manual type ID) + const int RESERVED_PROPERTY_COUNT = 32; + const int DEFAULT_PROPERTY_COUNT = 16; + static readonly ConcurrentDictionary typeIds = new(concurrencyLevel: 1, capacity: 256); + private static readonly object controlTypeRegisterLock = new object(); + private static int controlCounter = RESERVED_CONTROL_TYPES; // 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 = RESERVED_CONTROL_TYPES; // 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); + private static readonly object groupMemberRegisterLock = new object(); + static string?[] propertyGroupMemberNames = new string[1024]; + + static DotvvmPropertyIdAssignment() + { + foreach (var (type, id) in TypeIds.List) + { + typeIds[type] = id; + } + foreach (var (name, id) in GroupMembers.List) + { + propertyGroupMemberIds[name] = id; + propertyGroupMemberNames[id] = name; + } + } + +#region Optimized metadata accessors + /// Equivalent to + public static bool IsInherited(DotvvmPropertyId propertyId) + { + if (propertyId.CanUseFastAccessors) + return false; + + return BitmapRead(controls[propertyId.TypeId].inheritedBitmap, propertyId.MemberId); + } + + /// Returns if the DotvvmProperty uses standard GetValue/SetValue method and we can avoid the dynamic dispatch + /// + public static bool UsesStandardAccessors(DotvvmPropertyId propertyId) + { + if (propertyId.CanUseFastAccessors) + { + return true; + } + else + { + var bitmap = controls[propertyId.TypeId].standardBitmap; + var index = propertyId.MemberId; + return BitmapRead(bitmap, index); + } + } + + /// Returns if the given property is of the or type + public static bool IsActive(DotvvmPropertyId propertyId) + { + Debug.Assert(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); + } + + /// Returns the DotvvmProperty with a given ID, or returns null if no such property exists. New instance of might be created. + 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]; + } + } + + /// Returns the or with the given id + 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]; + } + } + + /// Returns the value of the property or property group. If the property is not set, returns the default value. + public static object? GetValueRaw(DotvvmBindableObject obj, DotvvmPropertyId id, bool inherit = true) + { + if (id.CanUseFastAccessors) + { + if (obj.properties.TryGet(id, out var value)) + return value; + + if (id.IsPropertyGroup) + return propertyGroups[id.GroupId]!.DefaultValue; + else + return controls[id.TypeId].properties[id.MemberId]!.DefaultValue; + } + else + { + var property = controls[id.TypeId].properties[id.MemberId]; + return property!.GetValue(obj, inherit); + } + } + + /// Returns the value of the property or property group. If the property is not set, returns the default value. + 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 + public static Type GetControlType(ushort id) + { + if (id == 0 || id >= controls.Length) + throw new ArgumentOutOfRangeException(nameof(id), id, "Control type ID is invalid."); + return controls[id].controlType; + } + [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); + + [MethodImpl(MethodImplOptions.NoInlining)] + static ushort unlikely(Type type) + { + Span ids = stackalloc ushort[1]; + RegisterTypes([type], 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) + { +#if NET6_0_OR_GREATER + var nextPow2 = 1 << (BitOperations.Log2((uint)(controlCounter + types.Length)) + 1); +#else + var nextPow2 = types.Length * 2; + while (nextPow2 < controlCounter + types.Length) + nextPow2 *= 2; +#endif + VolatileResize(ref controls, nextPow2); + } + 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]; + if (id < RESERVED_CONTROL_TYPES) + { + controls[id].counterStandard = RESERVED_PROPERTY_COUNT; + controls[id].counterNonStandard = RESERVED_PROPERTY_COUNT; + } + 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 typeCanUseDirectAccess = TypeCanUseAnyDirectAccess(property.GetType()); + var canUseDirectAccess = !property.IsValueInherited && typeCanUseDirectAccess; + + var typeId = RegisterType(property.DeclaringType); + ref ControlTypeInfo control = ref controls[typeId]; + lock (control.locker) // single control registrations are sequential anyway (most likely) + { + uint id; + if (typeId < RESERVED_CONTROL_TYPES && + typeof(PropertyIds).GetField(property.DeclaringType.Name + "_" + property.Name, BindingFlags.Static | BindingFlags.Public)?.GetValue(null) is {} predefinedId) + { + id = (uint)predefinedId; + if ((id & 0xffff) == 0) + throw new InvalidOperationException($"Predefined property ID of {property} cannot be 0."); + if (id >> 16 != typeId) + throw new InvalidOperationException($"Predefined property ID of {property} does not match the property declaring type ID."); + if ((id & 0xffff) > RESERVED_PROPERTY_COUNT) + throw new InvalidOperationException($"Predefined property ID of {property} is too high (there is only {RESERVED_PROPERTY_COUNT} reserved slots)."); + if (canUseDirectAccess != (id % 2 == 0)) + throw new InvalidOperationException($"Predefined property ID of {property} does not match the property canUseDirectAccess={canUseDirectAccess}. The ID must be {(canUseDirectAccess ? "even" : "odd")} number."); + id = id & 0xffff; + } + else if (canUseDirectAccess) + { + control.counterStandard += 1; + id = control.counterStandard * 2; + } + else + { + control.counterNonStandard += 1; + id = control.counterNonStandard * 2 + 1; + } + if (id > ushort.MaxValue) + ThrowTooManyException(property); + + // resize arrays (we hold a write lock, but others may be reading in parallel) + while (id >= control.properties.Length) + { + VolatileResize(ref control.properties, control.properties.Length * 2); + } + while (id / 64 >= control.inheritedBitmap.Length) + { + Debug.Assert(control.inheritedBitmap.Length == control.standardBitmap.Length); + Debug.Assert(control.inheritedBitmap.Length == control.activeBitmap.Length); + + 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 (typeCanUseDirectAccess) + BitmapSet(control.standardBitmap, (uint)id); + if (property is ActiveDotvvmProperty) + BitmapSet(control.activeBitmap, (uint)id); + + control.properties[id] = property; + return new DotvvmPropertyId(typeId, (ushort)id); + } + + static void ThrowTooManyException(DotvvmProperty property) => + throw new Exception($"Too many properties registered for a single control type ({property.DeclaringType.ToCode()}). Trying to register property '{property.Name}: {property.PropertyType.ToCode()}'"); + } + + private static readonly ConcurrentDictionary cacheTypeCanUseDirectAccess = new(concurrencyLevel: 1, capacity: 10); + + /// Does the property use the default GetValue/SetValue methods? + public static (bool getter, bool setter) TypeCanUseDirectAccess(Type propertyType) + { + if (propertyType == typeof(DotvvmProperty) || propertyType == typeof(GroupedDotvvmProperty)) + return (true, true); + + if (propertyType == typeof(DotvvmCapabilityProperty) || propertyType == typeof(DotvvmPropertyAlias) || propertyType == typeof(CompileTimeOnlyDotvvmProperty)) + return (false, false); + + if (propertyType.IsGenericType) + { + propertyType = propertyType.GetGenericTypeDefinition(); + if (propertyType == typeof(DelegateActionProperty<>)) + return (true, true); + } + + return cacheTypeCanUseDirectAccess.GetOrAdd(propertyType, static t => + { + var getter = t.GetMethod(nameof(DotvvmProperty.GetValue), new [] { typeof(DotvvmBindableObject), typeof(bool) })!.DeclaringType == typeof(DotvvmProperty); + var setter = t.GetMethod(nameof(DotvvmProperty.SetValue), new [] { typeof(DotvvmBindableObject), typeof(object) })!.DeclaringType == typeof(DotvvmProperty); + return (getter, setter); + }); + } + public static bool TypeCanUseAnyDirectAccess(Type propertyType) + { + var (getter, setter) = TypeCanUseDirectAccess(propertyType); + return getter && setter; + } + + public static ushort RegisterPropertyGroup(DotvvmPropertyGroup group) + { + lock (groupRegisterLock) + { + ushort id; + + // Check for predefined property group ID using reflection (similar to property registration) + var declaringTypeId = RegisterType(group.DeclaringType); + if (declaringTypeId < RESERVED_CONTROL_TYPES && + typeof(PropertyGroupIds).GetField(group.DeclaringType.Name + "_" + group.Name, BindingFlags.Static | BindingFlags.Public)?.GetValue(null) is {} predefinedId) + { + id = (ushort)predefinedId; + if (id == 0) + throw new InvalidOperationException($"Predefined property group ID of {group} cannot be 0."); + if (id > RESERVED_CONTROL_TYPES) + throw new InvalidOperationException($"Predefined property group ID of {group} is too high (there is only {RESERVED_CONTROL_TYPES} reserved slots)."); + } + else + { + 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 localRef = array; + var newArray = new T[newSize]; + localRef.AsSpan().CopyTo(newArray.AsSpan(0, localRef.Length)); + Volatile.Write(ref array, newArray); + } + +#endregion Registration + +#region Group members + public static ushort GetGroupMemberId(string name, bool registerIfNotFound) + { + var id = GroupMembers.TryGetId(name.AsSpan()); + 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 DotvvmProperty?[] properties; + /// Bitmap for + public ulong[] inheritedBitmap; + /// Bitmap for + public ulong[] standardBitmap; + /// Bitmap storing if property is + public ulong[] activeBitmap; + /// TODO split struct to part used during registration and part at runtime for lookups + public object locker; + public Type controlType; + public uint counterStandard; + public uint counterNonStandard; + } + } +} diff --git a/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs b/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs index 08ede07bb6..79263b228b 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; @@ -69,7 +69,7 @@ public static DotvvmPropertyWithFallback Register private bool TryGetValue(DotvvmBindableObject control, out object? value, bool inherit = true) { - if (control.properties.TryGet(this, out value)) + if (control.properties.TryGet(Id, out value)) { return true; } 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 bf06d6d635..58c0f87f43 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 System.Text.Json; using DotVVM.Framework.Binding.Expressions; @@ -16,10 +17,13 @@ public interface ValueOrBinding { IBinding? BindingOrDefault { get; } object? BoxedValue { get; } + + /// Returns the value or the binding from the ValueOrBinding container. Equivalent to calling vob.BindingOrDefault ?? vob.BoxedValue + object? UnwrapToObject(); } /// Represents either a binding or a constant value. In TypeScript this would be | . Note that `default()` is the same as `new (default(T))` - public struct ValueOrBinding : ValueOrBinding + public readonly struct ValueOrBinding : ValueOrBinding { private readonly IBinding? binding; [AllowNull] @@ -50,6 +54,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; @@ -91,6 +96,9 @@ public IBinding GetBinding() => public T GetValue() => HasValue ? value : throw new DotvvmControlException($"Value was expected but ValueOrBinding<{typeof(T).Name}> contains a binding: {binding}.") { RelatedBinding = binding }; + /// Returns the value or the binding from the ValueOrBinding container. Equivalent to calling vob.BindingOrDefault ?? vob.BoxedValue + public object? UnwrapToObject() => binding ?? BoxingUtils.BoxGeneric(value); + /// Returns a ValueOrBinding with new type T which is a base type of the old T2 public static ValueOrBinding DownCast(ValueOrBinding createFrom) where T2 : T => new ValueOrBinding(createFrom.binding, createFrom.value!); @@ -190,6 +198,5 @@ public TResult ProcessValueBinding(DotvvmBindableObject control, Func HasBinding ? binding.ToString() : value is null ? "null" : value.ToString(); - } } diff --git a/src/Framework/Framework/Binding/ValueOrBindingExtensions.cs b/src/Framework/Framework/Binding/ValueOrBindingExtensions.cs index a822e42cdf..0ada556d88 100644 --- a/src/Framework/Framework/Binding/ValueOrBindingExtensions.cs +++ b/src/Framework/Framework/Binding/ValueOrBindingExtensions.cs @@ -15,9 +15,6 @@ public static class ValueOrBindingExtensions { - /// Returns the value or the binding from the ValueOrBinding container. Equivalent to calling vob.BindingOrDefault ?? vob.BoxedValue - public static object? UnwrapToObject(this ValueOrBinding vob) => - vob.BindingOrDefault ?? vob.BoxedValue; /// If the obj is ValueOrBinding, returns the binding or the value from the container. Equivalent to obj is ValueOrBinding vob ? vob.UnwrapToObject() : obj public static object? UnwrapToObject(object? obj) => diff --git a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs index 8be168dd0e..213f5382ed 100644 --- a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs +++ b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs @@ -9,6 +9,12 @@ using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Utils; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Diagnostics; + +#if Vectorize +using System.Runtime.Intrinsics; +#endif namespace DotVVM.Framework.Binding { @@ -16,92 +22,92 @@ namespace DotVVM.Framework.Binding public readonly struct VirtualPropertyGroupDictionary : IDictionary, IReadOnlyDictionary { private readonly DotvvmBindableObject control; - private readonly DotvvmPropertyGroup group; + private readonly ushort groupId; + private readonly bool isBindingProperty; public VirtualPropertyGroupDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) { this.control = control; - this.group = group; + this.groupId = group.Id; + this.isBindingProperty = group.IsBindingProperty; } - public IEnumerable Keys + internal VirtualPropertyGroupDictionary(DotvvmBindableObject control, ushort groupId, bool isBindingProperty) { - get - { - foreach (var (p, _) in control.properties) - { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return pg.GroupMemberName; - } - } - } + this.control = control; + this.groupId = groupId; + this.isBindingProperty = isBindingProperty; } - /// Lists all values. If any of the properties contains a binding, it will be automatically evaluated. - public IEnumerable Values + public DotvvmBindableObject Control => control; + public ushort GroupId => groupId; + public DotvvmPropertyGroup Group => DotvvmPropertyIdAssignment.GetPropertyGroup(groupId).NotNull(); + + DotvvmPropertyId GetMemberId(string key, bool createNew = false) { - get + var memberId = DotvvmPropertyIdAssignment.GetGroupMemberId(key, registerIfNotFound: createNew); + return DotvvmPropertyId.CreatePropertyGroupId(groupId, memberId); + } + + TValue EvalPropertyValue(object? value) + { + if (typeof(TValue).IsValueType) + { // => cheap type check + if (value is TValue val) + return val; + return EvalPropertyValueCore(value); + } + else { - foreach (var (p, _) in control.properties) + if (!isBindingProperty && value is IBinding binding) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return (TValue)control.GetValue(p)!; - } + value = control.EvalBinding(binding, false); } + return (TValue)value!; } } + TValue EvalPropertyValueCore(object? value) => + (TValue)control.EvalPropertyValue(Group, value)!; - public IEnumerable Properties + public IEnumerable Keys { get { - foreach (var (p, _) in control.properties) + foreach (var (p, _) in control.properties.PropertyGroup(groupId)) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return pg; - } + yield return DotvvmPropertyIdAssignment.GetGroupMemberName(p.MemberId)!; } } } - public int Count + /// Lists all values. If any of the properties contains a binding, it will be automatically evaluated. + public IEnumerable Values { 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) + foreach (var (p, value) in control.properties.PropertyGroup(groupId)) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - count++; - } + yield return EvalPropertyValue(value); } - return count; } } - public bool Any() + public IEnumerable Properties { - foreach (var (p, _) in control.properties) + get { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) + var group = DotvvmPropertyIdAssignment.GetPropertyGroup(groupId).NotNull(); + foreach (var (p, _) in control.properties.PropertyGroup(groupId)) { - return true; + yield return group.GetDotvvmProperty(p.MemberId); } } - return false; } + public int Count => control.properties.CountPropertyGroup(groupId); + + public bool Any() => control.properties.ContainsPropertyGroup(groupId); + public bool IsReadOnly => false; ICollection IDictionary.Keys => Keys.ToList(); @@ -113,77 +119,94 @@ 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 EvalPropertyValue(value); else - return (TValue)p.DefaultValue!; - } - set - { - control.properties.Set(group.GetDotvvmProperty(key), value); + return (TValue)Group.DefaultValue!; } + set => control.properties.Set(GetMemberId(key, createNew: true), 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 DotvvmPropertyIdAssignment.GetPropertyGroup(groupId)!.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) + internal void SetInternal(ushort key, object? value) { - return control.Properties.ContainsKey(group.GetDotvvmProperty(key)); + Debug.Assert(DotvvmPropertyIdAssignment.GetGroupMemberName(key) is not null); + control.properties.Set(DotvvmPropertyId.CreatePropertyGroupId(groupId, key), value); } - private void AddOnConflict(GroupedDotvvmProperty property, object? value) + public bool ContainsKey(string key) => + control.properties.Contains(GetMemberId(key)); + + private void AddOnConflict(DotvvmPropertyId id, string key, object? value) { - var merger = this.group.ValueMerger; + 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(groupId, 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); + var prop = GetMemberId(key, createNew: true); object? val = value.UnwrapToObject(); 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 - public void Add(string key, TValue value) => - this.Add(key, new ValueOrBinding(value)); - - /// Adds the property identified by . If the property is already set, it tries appending the value using the group's - public void AddBinding(string key, IBinding? binding) + public void Add(string key, TValue value) { - Add(key, new ValueOrBinding(binding!)); + var prop = GetMemberId(key, createNew: true); + var val = BoxingUtils.BoxGeneric(value); + if (!control.properties.TryAdd(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 + public void AddBinding(string key, IBinding? binding) => Add(key, new ValueOrBinding(binding!)); + public void CopyFrom(IEnumerable> values, bool clear = false) { if (clear) this.Clear(); @@ -201,38 +224,86 @@ public void CopyFrom(IEnumerable>> v Set(item.Key, item.Value); } } - public static IDictionary CreateValueDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) + + public void CopyFrom(VirtualPropertyGroupDictionary values, bool clear = false) + where TValue2 : TValue + { + if (clear) this.Clear(); + foreach (var (oldId, value) in values.control.properties.PropertyGroup(values.groupId)) + { + var newId = DotvvmPropertyId.CreatePropertyGroupId(groupId, oldId.MemberId); + control.properties.Set(newId, value); + } + } + + public static IDictionary CreateValueDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) => + CreateValueDictionary(control, group.Id); + + internal static IDictionary CreateValueDictionary(DotvvmBindableObject control, ushort groupId) { - var result = new Dictionary(); + Dictionary result; +#if Vectorize + // don't bother counting without vector instructions + if (Vector128.IsHardwareAccelerated) + { + var count = control.properties.CountPropertyGroup(groupId); + result = new(count); + if (count == 0) + return result; + } + else + result = new(); +#else + result = new(); +#endif foreach (var (p, valueRaw) in control.properties) { - if (p is GroupedDotvvmProperty pg && pg.PropertyGroup == group) + if (p.IsInPropertyGroup(groupId)) { - var valueObj = control.EvalPropertyValue(p, valueRaw); + var name = DotvvmPropertyIdAssignment.GetGroupMemberName(p.MemberId)!; + var valueObj = control.EvalPropertyValue(DotvvmPropertyId.CreatePropertyGroupId(groupId, 1), 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; } - public static IDictionary> CreatePropertyDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) + public static IDictionary> CreatePropertyDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) => + CreatePropertyDictionary(control, group.Id); + + internal static IDictionary> CreatePropertyDictionary(DotvvmBindableObject control, ushort groupId) { - var result = new Dictionary>(); + Dictionary> result; +#if Vectorize + // don't bother counting without vector instructions + if (Vector128.IsHardwareAccelerated) + { + var count = control.properties.CountPropertyGroup(groupId); + result = new(count); + if (count == 0) + return result; + } + else + result = new(); +#else + result = new(); +#endif foreach (var (p, valRaw) in control.properties) { - if (p is GroupedDotvvmProperty pg && pg.PropertyGroup == group) + if (p.IsInPropertyGroup(groupId)) { - result.Add(pg.GroupMemberName, ValueOrBinding.FromBoxedValue(valRaw)); + var name = DotvvmPropertyIdAssignment.GetGroupMemberName(p.MemberId)!; + 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 +311,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(groupId, memberId); + if (control.properties.TryGet(p, out var valueRaw)) { - value = (TValue)control.EvalPropertyValue(prop, valueRaw)!; + value = EvalPropertyValue(valueRaw); return true; } else @@ -254,40 +326,9 @@ public bool TryGetValue(string key, [MaybeNullWhen(false)] out TValue value) } /// Adds the property-value pair to the dictionary. If the property is already set, it tries appending the value using the group's - public void Add(KeyValuePair item) - { - Add(item.Key, item.Value); - } - - public void Clear() - { - // we want to avoid allocating the list if there is only one property - DotvvmProperty? toRemove = null; - List? toRemoveRest = null; - - foreach (var (p, _) in control.properties) - { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - if (toRemove is null) - toRemove = p; - else - { - if (toRemoveRest is null) - toRemoveRest = new List(); - toRemoveRest.Add(p); - } - } - } + public void Add(KeyValuePair item) => Add(item.Key, item.Value); - if (toRemove is {}) - control.Properties.Remove(toRemove); - - if (toRemoveRest is {}) - foreach (var p in toRemoveRest) - control.Properties.Remove(p); - } + public void Clear() => control.properties.ClearPropertyGroup(groupId); public bool Contains(KeyValuePair item) { @@ -319,33 +360,101 @@ public bool Remove(KeyValuePair item) } /// Enumerates all keys and values. If a property contains a binding, it will be automatically evaluated. - public IEnumerator> GetEnumerator() + public Enumerator GetEnumerator() => + new Enumerator(control, Group, control.properties.EnumeratePropertyGroup(groupId)); + + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// Enumerates all keys and values, without evaluating the bindings. + public RawValuesCollection RawValues => new RawValuesCollection(this); + + public readonly struct RawValuesCollection: IEnumerable>, IReadOnlyDictionary { - foreach (var (p, value) in control.properties) + readonly VirtualPropertyGroupDictionary self; + + internal RawValuesCollection(VirtualPropertyGroupDictionary self) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) + 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 { - yield return new KeyValuePair(pg.GroupMemberName, (TValue)control.EvalPropertyValue(p, value)!); + foreach (var (_, value) in self.control.properties.PropertyGroup(self.groupId)) + 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.groupId)); + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public struct RawValuesEnumerator : IEnumerator> + { + private DotvvmControlPropertyIdGroupEnumerator inner; - /// Enumerates all keys and values, without evaluating the bindings. - public IEnumerable> RawValues + public KeyValuePair Current + { + get + { + var (p, value) = inner.Current; + var mem = DotvvmPropertyIdAssignment.GetGroupMemberName(p.MemberId)!; + 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(); + } + public struct Enumerator : IEnumerator> { - get + private readonly DotvvmPropertyGroup group; + private readonly DotvvmBindableObject control; + private DotvvmControlPropertyIdGroupEnumerator inner; + + public KeyValuePair Current { - foreach (var (p, value) in control.properties) + get { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return new KeyValuePair(pg.GroupMemberName, value!); - } + var (p, valueRaw) = inner.Current; + var mem = DotvvmPropertyIdAssignment.GetGroupMemberName(p.MemberId)!; + var value = (TValue)control.EvalPropertyValue(group, valueRaw)!; + return new KeyValuePair(mem, value); } } + + object IEnumerator.Current => Current; + + public Enumerator(DotvvmBindableObject control, DotvvmPropertyGroup group, DotvvmControlPropertyIdGroupEnumerator dotvvmControlPropertyIdEnumerator) + { + this.control = control; + this.group = group; + 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/Binding/TypeConversions.cs b/src/Framework/Framework/Compilation/Binding/TypeConversions.cs index 1f04527a48..aafda71140 100644 --- a/src/Framework/Framework/Compilation/Binding/TypeConversions.cs +++ b/src/Framework/Framework/Compilation/Binding/TypeConversions.cs @@ -48,6 +48,10 @@ public class TypeConversion public static Expression BoxToObject(Expression src) { var type = src.Type; + if (src is ConstantExpression { Value: bool boolean }) + return Expression.Field(null, typeof(BoxingUtils), boolean ? nameof(BoxingUtils.True) : nameof(BoxingUtils.False)); + if (src is ConstantExpression { Value: (int)0 }) + return Expression.Field(null, typeof(BoxingUtils), nameof(BoxingUtils.Zero)); if (type == typeof(bool) || type == typeof(bool?) || type == typeof(int) || type == typeof(int?)) return Expression.Call(typeof(BoxingUtils), "Box", Type.EmptyTypes, src); if (src is ConstantExpression { Value: var constant }) diff --git a/src/Framework/Framework/Compilation/BindingCompiler.cs b/src/Framework/Framework/Compilation/BindingCompiler.cs index 7c03ae06e3..19f0ab5cae 100644 --- a/src/Framework/Framework/Compilation/BindingCompiler.cs +++ b/src/Framework/Framework/Compilation/BindingCompiler.cs @@ -297,7 +297,7 @@ public static DotvvmBindableObject GetContextControl(int skip, DotvvmBindableObj { while (control != null) { - if (control.properties.Contains(DotvvmBindableObject.DataContextProperty)) + if (control.properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext)) { if (skip == 0) return control; @@ -315,7 +315,7 @@ public static (DotvvmBindableObject control, T context, bool isNull) GetContextA { while (control != null) { - if (control.properties.TryGet(DotvvmBindableObject.DataContextProperty, out var contextRaw)) + if (control.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext, out var contextRaw)) { if (skip == 0) { diff --git a/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs b/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs index 995d796447..1f9960df64 100644 --- a/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs +++ b/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs @@ -92,12 +92,18 @@ public class CurrentCollectionIndexExtensionParameter : BindingExtensionParamete } - internal static int GetIndex(DotvvmBindableObject c) => - (c.NotNull("control is null, is the binding executed in the right data context?") - .GetAllAncestors(true, false) - .OfType() - .FirstOrDefault() ?? throw new DotvvmControlException(c, "Could not find ancestor DataItemContainer that stores the current collection index.")) - .DataItemIndex ?? throw new DotvvmControlException(c, "Nearest DataItemContainer does have the collection index specified."); + internal static int GetIndex(DotvvmBindableObject c) + { + c.NotNull("control is null, is the binding executed in the right data context?"); + for (var ancestor = c; ancestor != null; ancestor = ancestor.Parent) + { + if (ancestor is DataItemContainer container) + { + return container.DataItemIndex ?? throw new DotvvmControlException(c, "Nearest DataItemContainer does have the collection index specified."); + } + } + throw new DotvvmControlException(c, "Could not find ancestor DataItemContainer that stores the current collection index."); + } public override Expression GetServerEquivalent(Expression controlParameter) { diff --git a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs index db88f85242..32626c8817 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,161 @@ internal static Task InvokeStaticConstructorsOnDotvvmControls() /// private void InvokeStaticConstructorsOnAllControls() { - var dotvvmAssembly = typeof(DotvvmControl).Assembly.GetName().Name!; var dotvvmInitTask = InvokeStaticConstructorsOnDotvvmControls(); - if (configuration.Runtime.ExplicitAssemblyLoading.Enabled) - { + var dotvvmAssembly = typeof(DotvvmControl).Assembly.GetName().Name!; + + var assemblies = configuration.Runtime.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); +#if DotNetCore + else if (renumbering.TryAdd(name.Name, assemblyList.Count)) + { +#else + else if (!renumbering.ContainsKey(name.Name)) + { + renumbering.Add(name.Name, assemblyList.Count); +#endif + 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( +#if NET6_0_OR_GREATER + CollectionsMarshal.AsSpan(controls), +#else + controls.ToArray().AsSpan(), +#endif + controlIds); + + // let the next assembly run + controlIdsAssigned[i].SetResult(true); + + foreach (var type in controls) + { + InitType(type); + } + } + finally { + paralelismLimiter.Release(); + } + + })).ToArray(); + Task.WaitAll(tasks); } private static void InvokeStaticConstructorsOnAllControls(Assembly assembly) @@ -106,7 +244,6 @@ private static void InvokeStaticConstructorsOnAllControls(Assembly assembly) if (!c.IsClass || c.ContainsGenericParameters) continue; - InitType(c); } } @@ -162,36 +299,6 @@ private static void RegisterCapabilitiesFromInterfaces(Type type) } } - private IEnumerable GetAllRelevantAssemblies(string dotvvmAssembly) - { -#if DotNetCore - var assemblies = compiledAssemblyCache.GetAllAssemblies() - .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)); -#else - var loadedAssemblies = compiledAssemblyCache.GetAllAssemblies() - .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)); - - var visitedAssemblies = new HashSet(); - - // ReflectionUtils.GetAllAssemblies() in netframework returns only assemblies which have already been loaded into - // the current AppDomain, to return all assemblies we traverse recursively all referenced Assemblies - var assemblies = loadedAssemblies - .SelectRecursively(a => a.GetReferencedAssemblies().Where(an => visitedAssemblies.Add(an.FullName)).Select(an => { - try - { - return Assembly.Load(an); - } - catch (Exception ex) - { - throw new Exception($"Unable to load assembly '{an.FullName}' referenced by '{a.FullName}'.", ex); - } - })) - .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)) - .Distinct(); -#endif - return assemblies; - } - /// /// After all DotvvmProperties have been registered, those marked with PropertyAliasAttribute can be resolved. /// diff --git a/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs b/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs index 96c84f10cb..ad71ace00d 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs @@ -10,6 +10,7 @@ using System.Runtime.CompilerServices; using System.Collections.Immutable; using System.Threading; +using DotVVM.Framework.Binding.Expressions; namespace DotVVM.Framework.Compilation.ControlTree { @@ -39,8 +40,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; } @@ -62,7 +65,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?) @@ -93,7 +98,27 @@ 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) + { + if (generatedProperties.TryGetValue(nameId, out var result)) + { + return result; + } + else + { + generatedProperties.TryAdd(nameId, CreateMemberProperty(nameId)); + return generatedProperties[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 e927a514d6..59e2d596bc 100644 --- a/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs +++ b/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs @@ -53,7 +53,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( @@ -82,7 +82,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..8e6af33439 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,15 +176,31 @@ 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); + } + + } + + /// Set DotVVM properties as array of keys and array of values + private bool TryEmitPerfectHashAssignment(ParameterExpression control, List<(DotvvmProperty prop, Expression value)> properties) + { + if (properties.Count > PropertyDictionaryImpl.MaxArrayTableSize) + { + return false; + } + var (_, keys, values) = PropertyDictionaryImpl.CreateTableWithValues(properties.Select(p => p.prop.Id).ToArray(), properties.Select(p => p.value).ToArray()); Expression valueExpr; + bool ownsValues; if (TryCreateArrayOfConstants(values, out var invertedValues)) { valueExpr = EmitValue(invertedValues); + ownsValues = false; } else { @@ -191,14 +208,71 @@ public void CommitDotvvmProperties(string name) typeof(object), values.Select(v => v ?? EmitValue(null)) ); + ownsValues = true; } var keyExpr = EmitValue(keys); - // control.MagicSetValue(keys, values, hashSeed) - var controlParameter = GetParameterOrVariable(name); + // PropertyImmutableHashtable.SetValuesToDotvvmControl(control, keys, values, hashSeed) + var magicSetValueCall = Expression.Call(typeof(PropertyDictionaryImpl), nameof(PropertyDictionaryImpl.SetValuesToDotvvmControl), emptyTypeArguments, Expression.Convert(control, typeof(DotvvmBindableObject)), keyExpr, valueExpr, EmitValue(false), EmitValue(ownsValues)); + + EmitStatement(magicSetValueCall); + return true; + } + + + /// 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 + { + 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(PropertyDictionaryImpl), nameof(PropertyDictionaryImpl.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 d0dd7252f9..a6ac5f473a 100644 --- a/src/Framework/Framework/Controls/CompositeControl.cs +++ b/src/Framework/Framework/Controls/CompositeControl.cs @@ -155,10 +155,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) @@ -181,10 +181,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); @@ -194,7 +194,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) { @@ -202,7 +202,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/DataItemContainer.cs b/src/Framework/Framework/Controls/DataItemContainer.cs index e56ba90f6e..2c61543ada 100644 --- a/src/Framework/Framework/Controls/DataItemContainer.cs +++ b/src/Framework/Framework/Controls/DataItemContainer.cs @@ -50,10 +50,14 @@ public int? DataItemIndex { if (this.index is int) return this.index; - var value = GetValue(Internal.UniqueIDProperty); + var value = properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID); return value is string id && int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index) ? index : (int?)null; } - set { this.index = value; SetValue(Internal.UniqueIDProperty, value?.ToString()); } + set + { + this.index = value; + properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID, value?.ToString()); + } } public bool RenderItemBinding { get; set; } = true; diff --git a/src/Framework/Framework/Controls/DotvvmBindableObject.cs b/src/Framework/Framework/Controls/DotvvmBindableObject.cs index b42b8e20db..e664f6bcd4 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObject.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObject.cs @@ -3,8 +3,11 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; +using DotVVM.Core.Storage; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Utils; @@ -33,7 +36,19 @@ public abstract class DotvvmBindableObject: IDotvvmObjectLike /// public virtual bool RenderOnServer { - get { return (RenderMode)GetValue(RenderSettings.ModeProperty)! == RenderMode.Server; } + get + { + for (var c = this; c != null; c = c.Parent) + { + if (c.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.RenderSettings_Mode, out var value)) + { + if (value is IBinding binding) + value = c.EvalBinding(binding, isDataContext: false); + return ((RenderMode)value!) == RenderMode.Server; + } + } + return false; + } } /// @@ -43,7 +58,7 @@ public virtual bool RenderOnServer public DotvvmBindableObject? Parent { get; set; } // WORKAROUND: Roslyn is unable to cache the delegate itself - private static Func _dotvvmProperty_ResolveProperties = DotvvmProperty.ResolveProperties; + private static readonly Func _dotvvmProperty_ResolveProperties = DotvvmProperty.ResolveProperties; /// /// Gets all properties declared on this class or on any of its base classes. @@ -71,17 +86,20 @@ public bool IsPropertySet(DotvvmProperty property, bool inherit = true) [MarkupOptions(AllowHardCodedValue = false, AllowResourceBinding = true)] public object? DataContext { - get { + get + { for (var c = this; c != null; c = c.Parent) { - if (c.properties.TryGet(DotvvmBindableObject.DataContextProperty, out var value)) + if (c.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext, out var value)) { - return c.EvalPropertyValue(DotvvmBindableObject.DataContextProperty, value); + return value is IBinding binding + ? c.EvalBinding(binding, isDataContext: true) + : value; } } return null; } - set { this.properties.Set(DataContextProperty, value); } + set => properties.Set(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext, value); } DotvvmBindableObject IDotvvmObjectLike.Self => this; @@ -97,30 +115,58 @@ public T GetValue(DotvvmProperty property, bool inherit = true) } /// If the object is IBinding and the property is not of type IBinding, it is evaluated. +#if NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] // let PGO specialize over value independently of call-site +#endif internal object? EvalPropertyValue(DotvvmProperty property, object? value) { - if (property.IsBindingProperty) return value; - if (value is IBinding) + if (!property.IsBindingProperty && value is IBinding binding) + return EvalBinding(binding, property == DataContextProperty); + return value; + } + + /// If the object is IBinding and the property is not of type IBinding, it is evaluated. +#if NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] // let PGO specialize over value independently of call-site +#endif + internal object? EvalPropertyValue(DotvvmPropertyGroup property, object? value) + { + if (!property.IsBindingProperty && value is IBinding binding) + return EvalBinding(binding, isDataContext: false); + return value; + } + + /// If the object is IBinding and the property is not of type IBinding, it is evaluated. +#if NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] // let PGO specialize over value independently of call-site +#endif + internal object? EvalPropertyValue(DotvvmPropertyId property, object? value) + { + if (value is IBinding binding && !DotvvmPropertyIdAssignment.IsBindingProperty(property)) + return EvalBinding(binding, property.Id == DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext); + return value; + } + + internal object? EvalBinding(IBinding binding, bool isDataContext) + { + DotvvmBindableObject control = this; + // DataContext is always bound to it's parent + if (isDataContext) + control = control.Parent ?? throw new DotvvmControlException(this, "Cannot set DataContext binding on the root control"); + // handle binding + if (binding is IStaticValueBinding resourceBinding) { - DotvvmBindableObject control = this; - // DataContext is always bound to it's parent, setting it right here is a bit faster - if (property == DataContextProperty) - 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 resourceBinding.Evaluate(control); } - return value; + else if (binding is ICommandBinding command) + { + return command.GetCommandDelegate(control); + } + else + { + throw new NotSupportedException($"Cannot evaluate binding {binding} of type {binding.GetType().Name}."); + } + } /// @@ -137,10 +183,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. @@ -156,9 +201,7 @@ public void SetValue(DotvvmProperty property, ValueOrBinding valueOrBindin public ValueOrBinding GetValueOrBinding(DotvvmProperty property, bool inherit = true) { var value = this.GetValueRaw(property, inherit); - if (value is IBinding binding) - return new ValueOrBinding(binding); - else return new ValueOrBinding((T)value!); + return ValueOrBinding.FromBoxedValue(value); } /// @@ -203,6 +246,17 @@ public void SetValueRaw(DotvvmProperty property, object? value) property.SetValue(this, value); } + /// + /// Sets the value or a binding to the specified property. + /// + public void SetValueRaw(DotvvmPropertyId property, object? value) + { + if (property.CanUseFastAccessors) + properties.Set(property, value); + else + property.PropertyInstance.SetValue(this, value); + } + /// /// Gets the binding set to a specified property. Returns null if the property is not set or if the value is not a binding. /// @@ -294,7 +348,7 @@ internal IEnumerable GetDataContextHierarchy() var c = this; while (c != null) { - if (c.properties.TryGet(Internal.IsControlBindingTargetProperty, out var x) && (bool)x!) + if (c.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_IsControlBindingTarget, out var x) && (bool)x!) { return c; } @@ -309,7 +363,7 @@ internal IEnumerable GetDataContextHierarchy() public DotvvmBindableObject? GetClosestControlBindingTarget(out int numberOfDataContextChanges) => (Parent ?? this).GetClosestWithPropertyValue( out numberOfDataContextChanges, - (control, _) => control.properties.TryGet(Internal.IsControlBindingTargetProperty, out var x) && (bool)x!); + (control, _) => control.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_IsControlBindingTarget, out var x) && (bool)x!); /// /// Gets the closest control binding target and returns number of DataContext changes since the target. Returns null if the control is not found. @@ -374,8 +428,9 @@ public bool HasBinding(DotvvmProperty property) /// public IEnumerable> GetAllBindings() { - return Properties.Where(p => p.Value is IBinding) - .Select(p => new KeyValuePair(p.Key, (IBinding)p.Value!)); + foreach (var (propId, value) in properties) + if (value is IBinding binding) + yield return new KeyValuePair(propId.PropertyInstance, binding); } /// @@ -411,7 +466,7 @@ public DotvvmBindableObject GetRoot() protected internal virtual DotvvmBindableObject CloneControl() { var newThis = (DotvvmBindableObject)this.MemberwiseClone(); - this.properties.CloneInto(ref newThis.properties); + this.properties.CloneInto(ref newThis.properties, newThis); return newThis; } diff --git a/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs b/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs index d973295d26..a961e1aecb 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs @@ -193,13 +193,16 @@ public static TControl AddAttribute(this TControl control, string attr public static TControl AddAttribute(this TControl control, string attribute, ValueOrBinding? value) where TControl : IControlWithHtmlAttributes { - return AddAttribute(control, attribute, value?.UnwrapToObject()); + if (value is { } v) + control.Attributes.Add(attribute, v.UnwrapToObject()); + return control; } /// Appends a value into the specified html attribute. If the attribute already exists, the old and new values are merged. Returns for fluent API usage. public static TControl AddAttribute(this TControl control, string attribute, ValueOrBinding value) where TControl : IControlWithHtmlAttributes { - return AddAttribute(control, attribute, value.UnwrapToObject()); + control.Attributes.Add(attribute, value.UnwrapToObject()); + return control; } /// Appends a list of css attributes to the control. If the attributes already exist, the old and new values are merged. Returns for fluent API usage. @@ -215,8 +218,7 @@ public static TControl AddAttributes(this TControl control, IE public static TControl AddAttributes(this TControl control, VirtualPropertyGroupDictionary attributes) where TControl : IControlWithHtmlAttributes { - foreach (var a in attributes.RawValues) - AddAttribute(control, a.Key, a.Value); + control.Attributes.CopyFrom(attributes); return control; } @@ -224,9 +226,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. @@ -235,7 +237,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 c9db28e7e0..7a1715626b 100644 --- a/src/Framework/Framework/Controls/DotvvmControl.cs +++ b/src/Framework/Framework/Controls/DotvvmControl.cs @@ -80,13 +80,13 @@ public static readonly DotvvmProperty ClientIDProperty ValueOrBinding? EnsureClientId() { - if (!IsPropertySet(IDProperty)) + if (!properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ID)) { return null; } - if (IsPropertySet(ClientIDProperty)) + if (properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ClientID, out var rawClientId)) { - return GetValueOrBinding(ClientIDProperty); + return ValueOrBinding.FromBoxedValue(rawClientId); } var id = CreateClientId(); @@ -189,7 +189,7 @@ public virtual void Render(IHtmlWriter writer, IDotvvmRequestContext context) writer.SetErrorContext(this); - if (properties.Contains(PostBack.UpdateProperty)) + if (properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.PostBack_Update)) { AddDotvvmUniqueIdAttribute(); } @@ -219,7 +219,7 @@ protected void AddDotvvmUniqueIdAttribute() { throw new DotvvmControlException(this, "Postback.Update cannot be set on property which don't render html attributes."); } - htmlAttributes.Attributes.Set("data-dotvvm-id", GetDotvvmUniqueId().UnwrapToObject()); + htmlAttributes.Attributes.SetInternal(DotvvmPropertyIdAssignment.GroupMembers.data_dotvvm_id, GetDotvvmUniqueId().UnwrapToObject()); } protected struct RenderState @@ -231,16 +231,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 == DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_IncludeInPage) r.IncludeInPage = val; - else if (property == DotvvmControl.DataContextProperty) + else if (property == DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext) 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; } @@ -263,20 +266,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)); } } @@ -461,7 +464,7 @@ public DotvvmControl GetNamingContainer() /// public static bool IsNamingContainer(DotvvmBindableObject control) { - return (bool)Internal.IsNamingContainerProperty.GetValue(control)!; + return control.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_IsNamingContainer, out var value) && (bool)value!; } /// @@ -506,7 +509,7 @@ public ValueOrBinding GetDotvvmUniqueId(ValueOrBinding prefix = // build the client ID JoinValuesOrBindings(GetUniqueIdFragments(), prefix, suffix); - private ValueOrBinding JoinValuesOrBindings(IReadOnlyList fragments, ValueOrBinding prefix, ValueOrBinding suffix) + private ValueOrBinding JoinValuesOrBindings(List fragments, ValueOrBinding prefix, ValueOrBinding suffix) { if (fragments.Count == 1 && prefix.ValueIsNullOrEmpty() && suffix.ValueIsNullOrEmpty()) { @@ -573,14 +576,15 @@ private ValueOrBinding JoinValuesOrBindings(IReadOnlyList fragm { var fragments = new List { - Internal.UniqueIDProperty.GetValue(this) + properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID) }; - foreach (var ancestor in GetAllAncestors()) + for (var ancestor = Parent; ancestor is {}; ancestor = ancestor.Parent) { if (IsNamingContainer(ancestor)) { fragments.Add( - Internal.ClientIDFragmentProperty.GetValue(ancestor) ?? Internal.UniqueIDProperty.GetValue(ancestor) + ancestor.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_ClientIDFragment) ?? + ancestor.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID) ); } } @@ -590,10 +594,9 @@ private ValueOrBinding JoinValuesOrBindings(IReadOnlyList fragm private List? GetClientIdFragments() { - var rawId = IDProperty.GetValue(this); - + var rawId = properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ID); // can't generate ID from nothing - if (rawId == null) return null; + if (rawId is null) return null; var fragments = new List { rawId }; if (ClientIDMode == ClientIDMode.Static) @@ -604,7 +607,7 @@ private ValueOrBinding JoinValuesOrBindings(IReadOnlyList fragm DotvvmControl? childContainer = null; var searchingForIdElement = false; - foreach (var ancestor in GetAllAncestors()) + for (var ancestor = Parent; ancestor is {}; ancestor = ancestor.Parent) { if (ancestor is not DotvvmControl ancestorControl) { @@ -619,11 +622,11 @@ private ValueOrBinding JoinValuesOrBindings(IReadOnlyList fragm } searchingForIdElement = false; - if (Internal.ClientIDFragmentProperty.GetValue(ancestorControl) is {} clientIdExpression) + if (ancestorControl.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_ClientIDFragment) is {} clientIdExpression) { fragments.Add(clientIdExpression); } - else if (ancestorControl.GetValueRaw(IDProperty) is var ancestorId && ancestorId is not null or "") + else if (ancestorControl.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ID) is var ancestorId && ancestorId is not null or "") { // add the ID fragment fragments.Add(ancestorId); @@ -635,9 +638,9 @@ private ValueOrBinding JoinValuesOrBindings(IReadOnlyList fragm } } - if (searchingForIdElement && ancestorControl.IsPropertySet(ClientIDProperty)) + if (searchingForIdElement && ancestorControl.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ClientID, out var clientId)) { - fragments.Add(ancestorControl.GetValueRaw(ClientIDProperty)); + fragments.Add(clientId); searchingForIdElement = false; } diff --git a/src/Framework/Framework/Controls/DotvvmControlCollection.cs b/src/Framework/Framework/Controls/DotvvmControlCollection.cs index 3fc4f9c28b..06d5098cbd 100644 --- a/src/Framework/Framework/Controls/DotvvmControlCollection.cs +++ b/src/Framework/Framework/Controls/DotvvmControlCollection.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using DotVVM.Framework.Binding; using DotVVM.Framework.Controls.Infrastructure; using DotVVM.Framework.Hosting; using DotVVM.Framework.Runtime; @@ -219,9 +220,8 @@ private void SetParent(DotvvmControl item) item.Parent = parent; - if (!item.properties.Contains(Internal.UniqueIDProperty) && - parent.properties.TryGet(Internal.UniqueIDProperty, out var parentId) && - parentId is not null) + if (!item.properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID) && + parent.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID) is {} parentId) { AssignUniqueIds(item, parentId); } @@ -276,17 +276,17 @@ void AssignUniqueIds(DotvvmControl item, object parentId) // if the parent is a naming container, we don't need to duplicate it's unique id into the control // when we don't do this Repeater generates different ids in server-side and client-side mode, because the // UniqueIDProperty of the parent DataItemContainer is different, but the ClientIdFragment is the same - if (item.Parent!.properties.Contains(Internal.IsNamingContainerProperty)) + if (item.Parent!.properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.Internal_IsNamingContainer)) { parentId = ""; } var id = parentId.ToString() + "a" + uniqueIdCounter.ToString(); uniqueIdCounter++; - item.properties.Set(Internal.UniqueIDProperty, id); + item.properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID, id); foreach (var c in item.Children.controls) { - if (!c.properties.Contains(Internal.UniqueIDProperty)) + if (!c.properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID)) item.Children.AssignUniqueIds(c, id); } } @@ -304,7 +304,7 @@ internal void ValidateParentsLifecycleEvents() DotvvmBindableObject? c = parent; while (c != null) { - if (c.properties.TryGet(Internal.RequestContextProperty, out var context)) + if (c.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_RequestContext, out var context)) return (IDotvvmRequestContext?)context; c = c.Parent; } diff --git a/src/Framework/Framework/Controls/DotvvmControlProperties.cs b/src/Framework/Framework/Controls/DotvvmControlProperties.cs index 5a254b4b10..b91da89647 100644 --- a/src/Framework/Framework/Controls/DotvvmControlProperties.cs +++ b/src/Framework/Framework/Controls/DotvvmControlProperties.cs @@ -1,290 +1,770 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel.Design; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using DotVVM.Framework.Binding; +using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Utils; +using Impl = DotVVM.Framework.Controls.PropertyDictionaryImpl; 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 - [FieldOffset(0)] - private object? keys; + // 1. Empty -> keys == values == null + // 2. Dictionary -> keys == null & values is Dictionary --> it falls back to traditional mutable property dictionary + // 3. Array8 or Array16 -> keys is DotvvmPropertyId[] & values is object[] -- small linear search array - [FieldOffset(0)] - private DotvvmProperty?[] keysAsArray; + // Note about unsafe code: + // There is always the possibility that the structure may get into an invalid state, for example due to multithreded writes. + // That is obviously not supported and it's a user error, and crashes are expected, but we want to avoid critical security issues like RCE. + // The idea is that "reading random memory is OK-ish, writing is not", so we insert runtime type checks + // to the places where this could happen: + // * we write into keys, but it has insufficient length (8 instead of expected 16) + // * we write into values, - it has insufficient length + // - it is of different type (array vs dictionary) + // * we return an invalid object reference and the client writes into it - [FieldOffset(8)] - private object? values; + private const MethodImplOptions Inline = MethodImplOptions.AggressiveInlining; + private const MethodImplOptions NoInlining = MethodImplOptions.NoInlining; - [FieldOffset(8)] - private object?[] valuesAsArray; - [FieldOffset(8)] - private Dictionary valuesAsDictionary; + private DotvvmPropertyId[]? keys; - [FieldOffset(16)] - private int hashSeed; + private object? values; // either object?[] or Dictionary - public void AssignBulk(DotvvmProperty?[] keys, object?[] values, int hashSeed) + private TableState state; + + /// If the keys array is owned by this structure and can be modified. Not used in the dictionary mode. + private bool ownsKeys; + /// If the values array or the values Dictionary is owned by this structure and can be modified. + private bool ownsValues; + + private readonly bool IsArrayState => ((byte)state | 1) == 3; // state == TableState.Array8 || state == TableState.Array16; + + + private object?[] valuesAsArray + { + [MethodImpl(Inline)] + readonly get + { + var value = this.values; + Impl.Assert(value!.GetType() == typeof(object[])); // safety runtime check + return Unsafe.As(value); + } + [MethodImpl(Inline)] + set => this.values = value; + } + private readonly object?[]? valuesAsArrayUnsafe + { + [MethodImpl(Inline)] + get => Unsafe.As(values); + } + + private Dictionary valuesAsDictionary + { + [MethodImpl(Inline)] + readonly get + { + var value = this.values; + Impl.Assert(value!.GetType() == typeof(Dictionary)); // safety runtime check + return Unsafe.As>(value); + } + [MethodImpl(Inline)] + set => this.values = value; + } + private readonly Dictionary? valuesAsDictionaryUnsafe + { + [MethodImpl(Inline)] + get => Unsafe.As>(values); + } + + [Conditional("DEBUG")] + private readonly void CheckInvariant() + { + switch (state) + { + case TableState.Empty: + Debug.Assert(keys is null && values is null); + break; + case TableState.Array8: + case TableState.Array16: + Debug.Assert(keys is {}); + Debug.Assert(values is object[]); + Debug.Assert(keys.Length == valuesAsArray.Length, $"keys.Length={keys.Length} != values.Length={valuesAsArray.Length}"); + Debug.Assert(keys.Length == (state == TableState.Array8 ? 8 : 16)); + for (int i = keys.Length - 1; i >= 0 ; i--) + { + var value = valuesAsArray[i]; + var p = keys[i]; + Debug.Assert(p.Id == 0 || keys.AsSpan().IndexOf(p) == i, $"Duplicate property {p} at index {i} and {keys.AsSpan().IndexOf(keys[i])}"); + if (p.Id != 0) + { + // TODO: check currently causes issues in unrelated code + // var propType = p.PropertyType; + // Debug.Assert(value is null || value is IBinding || propType.IsInstanceOfType(value), $"Property {p} has value {value} of type {value?.GetType()} which is not assignable to {propType}"); + } + else + { + Debug.Assert(valuesAsArray[i] is null, $"Zero property id at index {i} has non-null value: {valuesAsArray[i]}"); + } + } + break; + case TableState.Dictionary: + Debug.Assert(keys is null); + Debug.Assert(values is Dictionary); + break; + default: + Impl.Fail(); + break; + } + } + + public void AssignBulk(DotvvmPropertyId[] keys, object?[] values, bool ownsKeys, bool ownsValues) { - // The explicit layout is quite likely to mess 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.Length == values.Length); - if (this.values == null || this.keys == keys) + if (keys is null || values is null || !(keys.Length is 8 or 16) || keys.Length != values.Length || values.GetType() != typeof(object[])) + throwArgumentError(keys, values); + + CheckInvariant(); + if (this.values == null || Object.ReferenceEquals(this.keys, keys)) { + // empty -> fast assignment this.valuesAsArray = values; - this.keysAsArray = keys; - this.hashSeed = hashSeed; + this.keys = keys; + this.state = keys.Length switch { + 8 => TableState.Array8, + 16 => TableState.Array16, + _ => throw new NotSupportedException("Only 8 and 16 elements are supported") + }; + this.ownsKeys = ownsKeys; + this.ownsValues = ownsValues; } 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 + for (int i = 0; i < keys.Length; i++) + { + if (keys[i].Id != 0) + this.Set(keys[i]!, values[i]); + } + } + CheckInvariant(); + + [DoesNotReturn, MethodImpl(NoInlining)] + void throwArgumentError(DotvvmPropertyId[]? keys, object?[]? values) + { + ThrowHelpers.ArgumentNull(keys); + ThrowHelpers.ArgumentNull(values); + if (keys.Length is not 8 and not 16) + throw new ArgumentException($"The length of keys array must be 8 or 16.", nameof(keys)); + if (keys.Length != values.Length) + throw new ArgumentException($"The length of values array must be the same.", nameof(keys)); + // The our unsafe memory accesses are quite likely to mess up with array covariance, just make sure we don't encounter that + if (values.GetType() != typeof(object[])) + throw new ArgumentException($"The values array must be of type {typeof(object[])}.", nameof(values)); + throw new Exception("Unknown error"); + } + } + + public void AssignBulk(Dictionary values, bool owns) + { + ThrowHelpers.ArgumentNull(values); - if (this.keys == null) // TODO: is this heuristic actually useful? + CheckInvariant(); + if (this.values == null || object.ReferenceEquals(this.values, values)) + { + this.keys = null; + this.valuesAsDictionary = values; + this.ownsValues = owns; + this.state = TableState.Dictionary; + } + else + { + if (owns && values.Count >= 8) { - 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) + foreach (var (k, v) in this) { - this.values = values; - this.keys = keys; - this.hashSeed = hashSeed; - return; +#if DotNetCore + values.TryAdd(k, v); +#else + if (!values.ContainsKey(k)) + values[k] = v; +#endif } + this.values = values; + this.keys = null; + this.ownsValues = true; } - - 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); + } } } } public void ClearEverything() { + CheckInvariant(); values = null; keys = null; + state = TableState.Empty; } - 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; } + CheckInvariant(); + if (state == TableState.Array8) + { + Debug.Assert(values!.GetType() == typeof(object[])); + Debug.Assert(keys is {}); + return Impl.ContainsKey8(this.keys, p); + } + return ContainsOutlined(p); + } - if (keys == null) + private readonly bool ContainsOutlined(DotvvmPropertyId p) // doesn't need to be inlined + { + if (state == TableState.Empty) + return false; + if (state == TableState.Array16) + { + return Impl.ContainsKey16(this.keys!, p); + } + if (state == TableState.Dictionary) { - Debug.Assert(values is Dictionary); - return valuesAsDictionary.ContainsKey(p); + return valuesAsDictionary!.ContainsKey(p); } + return Impl.Fail(); + } - Debug.Assert(values is object[]); - Debug.Assert(keys is DotvvmProperty[]); - return PropertyImmutableHashtable.ContainsKey(this.keysAsArray, this.hashSeed, p); + public readonly bool ContainsPropertyGroup(DotvvmPropertyGroup group) => ContainsPropertyGroup(group.Id); + public readonly bool ContainsPropertyGroup(ushort groupId) + { + CheckInvariant(); + if (state == TableState.Array8) + { + return Impl.ContainsPropertyGroup(this.keys!, groupId); + } + return ContainsPropertyGroupOutlined(groupId); } - public bool TryGet(DotvvmProperty p, out object? value) + private readonly bool ContainsPropertyGroupOutlined(ushort groupId) { - value = null; - if (values == null) { return false; } + if (state == TableState.Empty) + return false; + if (state == TableState.Array16) + { + return Impl.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; + } - if (keys == null) + } + + public readonly int CountPropertyGroup(DotvvmPropertyGroup group) => CountPropertyGroup(group.Id); + public readonly int CountPropertyGroup(ushort groupId) + { + CheckInvariant(); + if (state == TableState.Array8) { - Debug.Assert(values is Dictionary); - return valuesAsDictionary.TryGetValue(p, out value); + return Impl.CountPropertyGroup8(this.keys!, groupId); } + return CountPropertyGroupOutlined(groupId); + } - 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; + private readonly int CountPropertyGroupOutlined(ushort groupId) + { + switch (state) + { + case TableState.Empty: + return 0; + case TableState.Array16: + return Impl.CountPropertyGroup(this.keys!, groupId); + case TableState.Dictionary: + { + int count = 0; + foreach (var key in valuesAsDictionary.Keys) + { + if (key.IsInPropertyGroup(groupId)) + count++; + } + return count; + } + default: + return Impl.Fail(); + } } - public object? GetOrThrow(DotvvmProperty p) + public void ClearPropertyGroup(ushort groupId) { - if (this.TryGet(p, out var x)) return x; - throw new KeyNotFoundException(); + if (state == TableState.Empty) + return; + else if (state == TableState.Dictionary) + { + ClearPropertyGroupOutlined(groupId); + return; + } + + Debug.Assert(state is TableState.Array16 or TableState.Array8); +#if Vectorize + var bitmap = Impl.FindGroupBitmap(this.keys!, groupId); + if (bitmap == 0) + return; + + OwnKeys(); + OwnValues(); + Impl.Assert(this.keys!.Length == this.valuesAsArray.Length); + + int index = 0; + ref var values = ref Impl.UnsafeArrayReference(this.valuesAsArray); + ref var keys = ref Impl.UnsafeArrayReference(this.keys); + do + { + int bit = BitOperations.TrailingZeroCount(bitmap); + bitmap >>= (bit + 1); + index += bit; + Debug.Assert(this.keys![index].IsInPropertyGroup(groupId), $"Key {this.keys[index]} at index {index} is not in property group {groupId}"); + Unsafe.Add(ref keys, index) = default; + Unsafe.Add(ref values, index) = null; + index++; + } while (bitmap != 0); +#else + var keys = this.keys!; + for (var i = 0; i < keys.Length; i++) + { + if (keys[i].IsInPropertyGroup(groupId)) + { + if (!ownsKeys) + keys = CloneKeys(); + if (!ownsValues) + CloneValues(); + keys[i] = default; + this.valuesAsArray[i] = null; + } + } +#endif + CheckInvariant(); } - public void Set(DotvvmProperty p, object? value) + private void ClearPropertyGroupOutlined(ushort groupId) { - if (values == null) + var dict = this.valuesAsDictionary; + // we want to avoid allocating the list if there is only one property + DotvvmPropertyId toRemove = default; + List? toRemoveRest = null; + + foreach (var (p, _) in dict) + { + if (p.IsInPropertyGroup(groupId)) + { + if (toRemove.Id == 0) + toRemove = p; + else + { + toRemoveRest ??= new List(); + toRemoveRest.Add(p); + } + } + } + + if (toRemove.Id != 0) { - Debug.Assert(keys == null); - var d = new Dictionary(); - d[p] = value; - this.values = d; + if (!ownsValues) + { + CloneValues(); + dict = this.valuesAsDictionary; + } + + dict.Remove(toRemove); } - else if (keys == null) + + if (toRemoveRest is {}) { - Debug.Assert(values is Dictionary); - valuesAsDictionary[p] = value; + Debug.Assert(ownsValues); + foreach (var p in toRemoveRest) + dict.Remove(p); } - else + CheckInvariant(); + } + + [MethodImpl(Inline)] + public readonly bool TryGet(DotvvmProperty p, out object? value) => TryGet(p.Id, out value); + [MethodImpl(Inline)] + public readonly bool TryGet(DotvvmPropertyId p, out object? value) + { + CheckInvariant(); + if (state == TableState.Array8) { - 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)) + var index = Impl.FindSlot8(this.keys!, p); + Debug.Assert(index < 8); + if (index >= 0) { - // no-op, we would be changing it to the same value +#if NET6_0_OR_GREATER + value = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArrayUnsafe!), index); +#else + value = valuesAsArrayUnsafe![index]; +#endif + return true; } else { - var d = new Dictionary(); - for (int i = 0; i < keys.Length; i++) + value = null; + return false; + } + } + return TryGetOutlined(p, out value); + } + private readonly bool TryGetOutlined(DotvvmPropertyId p, out object? value) + { + switch (state) + { + case TableState.Empty: + value = null; + return false; + case TableState.Array16: + { + var index = Impl.FindSlot16(this.keys!, p); + if (index >= 0) + { + +#if NET6_0_OR_GREATER + value = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArrayUnsafe!), index); +#else + value = valuesAsArrayUnsafe![index]; +#endif + return true; + } + else { - if (keys[i] != null) - d[keys[i]!] = values[i]; + value = null; + return false; } - d[p] = value; - this.valuesAsDictionary = d; - this.keys = null; } + case TableState.Dictionary: + return valuesAsDictionary.TryGetValue(p, out value); + default: + value = null; + return Impl.Fail(); } } - /// Tries to set value into the dictionary without overwriting anything. - public bool TryAdd(DotvvmProperty p, object? value) + public readonly object? GetOrThrow(DotvvmProperty p) => GetOrThrow(p.Id); + public readonly object? GetOrThrow(DotvvmPropertyId p) + { + if (this.TryGet(p, out var x)) return x; + return ThrowKeyNotFound(p); + } + [MethodImpl(NoInlining), DoesNotReturn] + private readonly object? ThrowKeyNotFound(DotvvmPropertyId p) => throw new KeyNotFoundException($"Property {p} was not found."); + + [MethodImpl(Inline)] + public readonly object? GetOrNull(DotvvmProperty p) => GetOrNull(p.Id); + [MethodImpl(Inline)] + public readonly object? GetOrNull(DotvvmPropertyId p) + { + TryGet(p, out var x); + return x; + } + + [MethodImpl(Inline)] + public void Set(DotvvmProperty p, object? value) => Set(p.Id, value); + // not necessarily great for inlining + public void Set(DotvvmPropertyId p, object? value) { - if (values == null) + CheckInvariant(); + if (p.MemberId == 0) { ThrowZeroPropertyId(); return; } + + if (state == TableState.Array8) { - Debug.Assert(keys == null); - var d = new Dictionary(); - d[p] = value; - this.values = d; - return true; + var keys = this.keys!; + var slot = Impl.FindSlotOrFree8(keys, p, out var exists); + Debug.Assert(slot < 8); + if (slot >= 0) + { + if (!exists) + { + if (!ownsKeys) + keys = CloneKeys(); + // arrays are always size >= 8 + Unsafe.Add(ref Impl.UnsafeArrayReference(keys), slot) = p; + } + this.OwnValues(); + Unsafe.Add(ref Impl.UnsafeArrayReference(this.valuesAsArray), slot) = value; // avoid covariance check + CheckInvariant(); + Debug.Assert(GetOrThrow(p) == value, $"{p} was not set to {value}."); + return; + } } - else if (keys == null) + + SetOutlined(p, value); + } + private void SetOutlined(DotvvmPropertyId p, object? value) + { + TailCall: + + var keys = this.keys; + if (keys is {}) { - Debug.Assert(values is Dictionary); -#if CSharp8Polyfill - if (valuesAsDictionary.TryGetValue(p, out var existingValue)) - return Object.Equals(existingValue, value); - else + Debug.Assert(values is object[]); + var slot = state == TableState.Array8 + ? Impl.FindSlotOrFree8(keys, p, out var exists) + : Impl.FindSlotOrFree16(keys, p, out exists); + Debug.Assert(slot < 16); + if (slot >= 0) { - valuesAsDictionary.Add(p, value); - return true; + Debug.Assert(slot < keys.Length && slot < valuesAsArray.Length, $"Slot {slot} is out of range for keys {keys.Length} and values {valuesAsArray.Length} (prop={p}, value={value})"); + if (!exists) + { + if (!this.ownsKeys) + keys = CloneKeys(); + OwnValues(); + keys[slot] = p; + valuesAsArray[slot] = value; + } + else if (Object.ReferenceEquals(valuesAsArrayUnsafe![slot], value)) + { + // no-op, we would be changing it to the same value + } + else + { + OwnValues(); + valuesAsArray[slot] = value; + } } -#else - if (valuesAsDictionary.TryAdd(p, value)) - return true; else - return Object.Equals(valuesAsDictionary[p], value); -#endif + { + IncreaseSize(); + goto TailCall; + } + } + else if (values == null) + { + SetEmptyToSingle(p, value); } 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); + Debug.Assert(values is Dictionary); + OwnValues(); + valuesAsDictionary[p] = value; + } + Debug.Assert(GetOrThrow(p) == value, $"{p} was not set to {value}."); + CheckInvariant(); + } + + [MethodImpl(NoInlining), DoesNotReturn] + private static void ThrowZeroPropertyId() => throw new ArgumentException("Invalid (unitialized) property id cannot be set into the DotvvmControlProperties dictionary.", "p"); + + /// Tries to set value into the dictionary without overwriting anything. + /// True if the value was added, false if the key was already present with a different value. + public bool TryAdd(DotvvmProperty p, object? value) => TryAdd(p.Id, value); + + /// Tries to set value into the dictionary without overwriting anything. + /// True if the value was added, false if the key was already present with a different value. + public bool TryAdd(DotvvmPropertyId p, object? value) + { + CheckInvariant(); + if (p.MemberId == 0) ThrowZeroPropertyId(); + + if (state == TableState.Array8) + { + Debug.Assert(values!.GetType() == typeof(object[])); + Debug.Assert(keys is {}); + var slot = Impl.FindSlotOrFree8(this.keys, p, out var exists); + Debug.Assert(slot < 8); if (slot >= 0) { - // value already exists - return Object.Equals(values[slot], value); + if (exists) + { + return Object.ReferenceEquals(valuesAsArrayUnsafe![slot], value); + } + OwnValues(); + OwnKeys(); + // arrays are always length >= 8 + Unsafe.Add(ref Impl.UnsafeArrayReference(keys), slot) = p; + Unsafe.Add(ref Impl.UnsafeArrayReference(valuesAsArray), slot) = value; // avoid covariance check + CheckInvariant(); + Debug.Assert(GetOrThrow(p) == value, $"{p} was not set to {value}."); + return true; } - else + } + return TryAddOulined(p, value); + } + + private bool TryAddOulined(DotvvmPropertyId p, object? value) + { + TailCall: + + var keys = this.keys; + if (keys != null) + { + Debug.Assert(this.values is object[]); + Debug.Assert(keys is DotvvmPropertyId[]); + var slot = this.state == TableState.Array8 + ? Impl.FindSlotOrFree8(keys, p, out var exists) + : Impl.FindSlotOrFree16(keys, p, out exists); + if (slot >= 0) { - var d = new Dictionary(); - for (int i = 0; i < keys.Length; i++) + if (exists) { - if (keys[i] != null) - d[keys[i]!] = values[i]; + // value already exists + return Object.ReferenceEquals(valuesAsArrayUnsafe![slot], value); } - d[p] = value; - this.valuesAsDictionary = d; - this.keys = null; + else + { + if (!this.ownsKeys) + keys = CloneKeys(); + OwnValues(); + keys[slot] = p; + var valuesAsArray = this.valuesAsArray; + Impl.Assert(valuesAsArray.Length > slot); + Unsafe.Add(ref Impl.UnsafeArrayReference(valuesAsArray), slot) = value; // avoid covariance check + CheckInvariant(); + Debug.Assert(GetOrThrow(p) == value, $"{p} was not set to {value}."); + return true; + } + } + else + { + IncreaseSize(); + goto TailCall; + } + } + if (this.values == null) + { + SetEmptyToSingle(p, value); + return true; + } + else + { + // System.Dictionary backend +#if CSharp8Polyfill + if (valuesAsDictionary.TryGetValue(p, out var existingValue)) + return Object.ReferenceEquals(existingValue, value); + else + { + OwnValues(); + valuesAsDictionary.Add(p, value); return true; } +#else + OwnValues(); + return valuesAsDictionary.TryAdd(p, value) || Object.ReferenceEquals(valuesAsDictionary[p], value); +#endif } } + private void SetEmptyToSingle(DotvvmPropertyId p, object? value) + { + Debug.Assert(this.keys == null); + Debug.Assert(this.values == null); + Debug.Assert(this.state == TableState.Empty); + var newKeys = new DotvvmPropertyId[Impl.AdhocTableSize]; + newKeys[0] = p; + var newValues = new object?[Impl.AdhocTableSize]; + newValues[0] = value; + + this.keys = newKeys; + this.values = newValues; + this.ownsKeys = this.ownsValues = true; + this.state = TableState.Array8; + CheckInvariant(); + } - public DotvvmControlPropertiesEnumerator GetEnumerator() + [MethodImpl(Inline)] + public readonly DotvvmControlPropertyIdEnumerator GetEnumerator() { + CheckInvariant(); + 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); + + 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]); + [MethodImpl(Inline)] + public readonly PropertyGroupEnumerable PropertyGroup(ushort groupId) => new(in this, groupId); - public bool Remove(DotvvmProperty key) + public readonly DotvvmControlPropertyIdGroupEnumerator EnumeratePropertyGroup(ushort id) => + this.keys is {} keys ? new(keys, valuesAsArray, id) : + this.values is {} ? new(valuesAsDictionary.GetEnumerator(), id) : + default; + + private static readonly DotvvmControlPropertyIdEnumerator EmptyEnumerator = new DotvvmControlPropertyIdEnumerator(Array.Empty(), Array.Empty()); + + public bool Remove(DotvvmProperty key) => Remove(key.Id); + public bool Remove(DotvvmPropertyId key) { - if (!Contains(key)) return false; - if (this.keys == null && valuesAsDictionary != null) + CheckInvariant(); + if (this.keys != null) { - return valuesAsDictionary.Remove(key); + var slot = Impl.FindSlot(this.keys, key); + if (slot < 0) + return false; + this.OwnKeys(); + this.keys[slot] = default; + this.OwnValues(); + this.valuesAsArray[slot] = default; + CheckInvariant(); + return true; } - - // move from read-only struct to mutable struct + if (this.values == null) + return false; + else { - var keysTmp = this.keysAsArray; - var valuesTmp = this.valuesAsArray; - var d = new Dictionary(); - - for (int i = 0; i < keysTmp.Length; i++) - { - if (keysTmp[i] != null && keysTmp[i] != key) - d[keysTmp[i]!] = valuesTmp[i]; - } - this.valuesAsDictionary = d; - this.keys = null; - return true; + Debug.Assert(values is Dictionary); + return valuesAsDictionary.Remove(key); } } - private static object? CloneValue(object? value) + private static object? CloneValue(object? value, DotvvmBindableObject? newParent) { if (value is DotvvmBindableObject bindableObject) - return bindableObject.CloneControl(); + { + bindableObject = bindableObject.CloneControl(); + bindableObject.Parent = newParent; + return bindableObject; + } return null; } - public int Count() + public readonly int Count() { + CheckInvariant(); 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 Impl.Count(this.keys); } - internal void CloneInto(ref DotvvmControlProperties newDict) + internal void CloneInto(ref DotvvmControlProperties newDict, DotvvmBindableObject newParent) { + CheckInvariant(); if (this.values == null) { newDict = default; @@ -293,51 +773,333 @@ internal void CloneInto(ref DotvvmControlProperties newDict) else if (this.keys == null) { var dictionary = this.valuesAsDictionary; - if (dictionary.Count > 30) + if (dictionary.Count > 16) { newDict = this; - newDict.valuesAsDictionary = new Dictionary(dictionary); + newDict.keys = null; + Dictionary? newValues = null; foreach (var (key, value) in dictionary) - if (CloneValue(value) is {} newValue) + if (CloneValue(value, newParent) is {} newValue) + { + if (newValues is null) + // ok, we have to copy it + newValues = new Dictionary(dictionary); newDict.valuesAsDictionary[key] = newValue; + } + + if (newValues is null) + { + newDict.valuesAsDictionary = dictionary; + newDict.ownsValues = false; + this.ownsValues = false; + } + else + { + newDict.valuesAsDictionary = newValues; + newDict.ownsValues = true; + } + newDict.CheckInvariant(); + CheckInvariant(); return; } - // move to immutable version if it's reasonably small. It will be probably cloned multiple times again - var properties = new DotvvmProperty[dictionary.Count]; + // move to immutable version if it's small. It will be probably cloned multiple times again + SwitchToSimdDict(); + } + + newDict = this; + newDict.ownsKeys = false; + this.ownsKeys = false; + var valuesAsArray = newDict.valuesAsArray; + for (int i = 0; i < valuesAsArray.Length; i++) + { + if (!newDict.keys![i].IsZero && valuesAsArray[i] is {} && CloneValue(valuesAsArray[i], newParent) is {} newValue) + { + // clone the array if we didn't do that already + if (newDict.values == this.values) + { + newDict.values = valuesAsArray = valuesAsArray.AsSpan().ToArray(); + newDict.ownsValues = true; + } + + valuesAsArray[i] = newValue; + } + } + + if (newDict.values == this.values) + { + this.ownsValues = false; + newDict.ownsValues = false; + } + newDict.CheckInvariant(); + CheckInvariant(); + } + + [MethodImpl(Inline)] + void OwnKeys() + { + if (this.ownsKeys) return; + CloneKeys(); + } + [MethodImpl(Inline)] + void OwnValues() + { + if (this.ownsValues) return; + CloneValues(); + } + [MethodImpl(NoInlining)] + DotvvmPropertyId[] CloneKeys() + { + CheckInvariant(); + var oldKeys = this.keys; + var newKeys = new DotvvmPropertyId[oldKeys!.Length]; + MemoryExtensions.CopyTo(oldKeys, newKeys.AsSpan()); + this.keys = newKeys; + this.ownsKeys = true; + CheckInvariant(); + return newKeys; + } + [MethodImpl(NoInlining)] + void CloneValues() + { + CheckInvariant(); + 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.ownsValues = true; + } + CheckInvariant(); + } + + /// Switch Empty -> Array8, Array8 -> Array16, or Array16 -> Dictionary + void IncreaseSize() + { + CheckInvariant(); + switch (state) + { + case TableState.Empty: + this.keys = new DotvvmPropertyId[Impl.AdhocTableSize]; + this.valuesAsArray = new object?[Impl.AdhocTableSize]; + this.state = TableState.Array8; + this.ownsKeys = this.ownsValues = true; + break; + case TableState.Array8: + var newKeys = new DotvvmPropertyId[16]; + var newValues = new object?[16]; + MemoryExtensions.CopyTo(this.keys, newKeys.AsSpan()); + MemoryExtensions.CopyTo(this.valuesAsArray, newValues.AsSpan()); + this.keys = newKeys; + this.valuesAsArray = newValues; + this.state = TableState.Array16; + this.ownsKeys = this.ownsValues = true; + break; + case TableState.Array16: + SwitchToDictionary(); + break; + case TableState.Dictionary: + break; + default: + Impl.Fail(); + break; + } + CheckInvariant(); + } + + /// Converts the internal representation to System.Collections.Generic.Dictionary + void SwitchToDictionary() + { + CheckInvariant(); + switch (state) + { + case TableState.Array8: + case TableState.Array16: + 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.state = TableState.Dictionary; + this.valuesAsDictionary = d; + this.keys = null; + this.ownsValues = true; + break; + case TableState.Empty: + this.state = TableState.Dictionary; + this.valuesAsDictionary = new Dictionary(); + this.ownsValues = true; + break; + case TableState.Dictionary: + break; + default: + Impl.Fail(); + break; + } + CheckInvariant(); + } + + /// Converts the internal representation to the DotVVM small dictionary implementation + void SwitchToSimdDict() + { + CheckInvariant(); + if (this.keys is {}) + { + // already in the small dictionary format + return; + } + else if (this.values is {}) + { + var valuesAsDictionary = this.valuesAsDictionary; + + if (valuesAsDictionary.Count > 16) + { + return; + } + + var properties = new DotvvmPropertyId[valuesAsDictionary.Count >= 8 ? 16 : 8]; var values = new object?[properties.Length]; int j = 0; - foreach (var x in this.valuesAsDictionary) + foreach (var x in valuesAsDictionary) { (properties[j], values[j]) = x; j++; } - Array.Sort(properties, values, PropertyImmutableHashtable.DotvvmPropertyComparer.Instance); - (this.hashSeed, this.keysAsArray, this.valuesAsArray) = PropertyImmutableHashtable.CreateTableWithValues(properties, values); + this.keys = properties; + this.valuesAsArray = values; + this.state = properties.Length == 8 ? TableState.Array8 : TableState.Array16; + this.ownsKeys = true; + this.ownsValues = true; + } + else + { } + CheckInvariant(); + } - newDict = this; - for (int i = 0; i < newDict.valuesAsArray.Length; i++) + public readonly struct PropertyGroupEnumerable: IEnumerable> + { + private readonly DotvvmControlProperties properties; + private readonly ushort groupId; + [MethodImpl(Inline)] + public PropertyGroupEnumerable(in DotvvmControlProperties properties, ushort groupId) { - if (CloneValue(newDict.valuesAsArray[i]) is {} newValue) + this.properties = properties; + this.groupId = groupId; + } + + [MethodImpl(Inline)] + public IEnumerator> GetEnumerator() => properties.EnumeratePropertyGroup(groupId); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public enum TableState : byte + { + Empty = 0, + Dictionary = 1, + Array8 = 2, + Array16 = 3, + } + } + + public struct DotvvmControlPropertyIdGroupEnumerator : IEnumerator> + { + private readonly DotvvmPropertyId[]? keys; + private readonly object?[]? values; + private readonly ushort groupId; + private readonly ushort bitmap; + private int index; + 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 = Impl.FindGroupBitmap(keys, groupId); + 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 = this.index + 1; + var bitmap = this.bitmap >> index; +#if NET5_0_OR_GREATER + this.index = index + BitOperations.TrailingZeroCount(bitmap); +#else + while ((bitmap & 1) == 0) { - // clone the array if we didn't do that already - if (newDict.values == this.values) - newDict.values = this.valuesAsArray.Clone(); + index++; + bitmap >>= 1; + } + this.index = index; +#endif + return bitmap != 0; + } + else + { + // `default(T)` - empty collection + if (groupId == 0) + return false; - newDict.valuesAsArray[i] = newValue; + 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 DotvvmControlPropertiesEnumerator : IEnumerator> + 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 +1107,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 +1115,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 +1125,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) @@ -420,13 +1210,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) { @@ -461,14 +1251,14 @@ public void CopyTo(DotvvmProperty[] array, int arrayIndex) { foreach (var x in control.properties) { - array[arrayIndex++] = x.Key; + array[arrayIndex++] = x.Key.PropertyInstance; } } public IEnumerator GetEnumerator() { foreach (var x in control.properties) { - yield return x.Key; + yield return x.Key.PropertyInstance; } } public bool Remove(DotvvmProperty item) => throw new NotSupportedException("Explicitly use control.Properties.Remove() instead."); 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..0018dbd2cc 100644 --- a/src/Framework/Framework/Controls/HtmlGenericControl.cs +++ b/src/Framework/Framework/Controls/HtmlGenericControl.cs @@ -90,7 +90,7 @@ public HtmlGenericControl(string? tagName, TextOrContentCapability? content, Htm /// A dictionary of html attributes that are rendered on this control's html tag. [PropertyGroup(new[] { "", "html:" })] - public VirtualPropertyGroupDictionary Attributes => new(this, AttributesGroupDescriptor); + public VirtualPropertyGroupDictionary Attributes => new(this, DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_Attributes, false); /// A dictionary of html attributes that are rendered on this control's html tag. [MarkupOptions(MappingMode = MappingMode.Attribute, AllowBinding = true, AllowHardCodedValue = true, AllowValueMerging = true, AttributeValueMerger = typeof(HtmlAttributeValueMerger), AllowAttributeWithoutValue = true)] @@ -99,7 +99,7 @@ public HtmlGenericControl(string? tagName, TextOrContentCapability? content, Htm /// A dictionary of css classes. All classes whose value is `true` will be placed in the `class` attribute. [PropertyGroup("Class-", ValueType = typeof(bool))] - public VirtualPropertyGroupDictionary CssClasses => new(this, CssClassesGroupDescriptor); + public VirtualPropertyGroupDictionary CssClasses => new(this, DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_CssClasses, false); /// A dictionary of css classes. All classes whose value is `true` will be placed in the `class` attribute. public static DotvvmPropertyGroup CssClassesGroupDescriptor = @@ -107,7 +107,7 @@ public HtmlGenericControl(string? tagName, TextOrContentCapability? content, Htm /// A dictionary of css styles which will be placed in the `style` attribute. [PropertyGroup("Style-")] - public VirtualPropertyGroupDictionary CssStyles => new(this, CssStylesGroupDescriptor); + public VirtualPropertyGroupDictionary CssStyles => new(this, DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_CssStyles, false); /// A dictionary of css styles which will be placed in the `style` attribute. public static DotvvmPropertyGroup CssStylesGroupDescriptor = @@ -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 == DotvvmPropertyIdAssignment.PropertyIds.HtmlGenericControl_Visible) r.Visible = value; - else if (prop == ClientIDProperty) + else if (prop == DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ClientID) r.ClientId = value; - else if (prop == IDProperty && value != null) + else if (prop == DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ID && value != null) r.HasId = true; - else if (prop == InnerTextProperty) + else if (prop == DotvvmPropertyIdAssignment.PropertyIds.HtmlGenericControl_InnerText) r.InnerText = value; - else if (prop == PostBack.UpdateProperty) + else if (prop == DotvvmPropertyIdAssignment.PropertyIds.PostBack_Update) r.HasPostbackUpdate = (bool)this.EvalPropertyValue(prop, value)!; - else if (prop is GroupedDotvvmProperty gp) + else if (prop.IsPropertyGroup) { - if (gp.PropertyGroup == CssClassesGroupDescriptor) + if (prop.IsInPropertyGroup(DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_CssClasses)) r.HasClass = true; - else if (gp.PropertyGroup == CssStylesGroupDescriptor) + else if (prop.IsInPropertyGroup(DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_CssStyles)) r.HasStyle = true; - else if (gp.PropertyGroup == AttributesGroupDescriptor) + else if (prop.IsInPropertyGroup(DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_Attributes)) 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/Internal.cs b/src/Framework/Framework/Controls/Internal.cs index 4d9a5c4547..28b274b5fb 100644 --- a/src/Framework/Framework/Controls/Internal.cs +++ b/src/Framework/Framework/Controls/Internal.cs @@ -82,7 +82,7 @@ public static class InternalPropertyExtensions { for (; obj != null; obj = obj.Parent) { - if (obj.properties.TryGet(Internal.DataContextTypeProperty, out var v)) + if (obj.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_DataContextType, out var v)) return (DataContextStack?)v; } return null; @@ -91,7 +91,7 @@ public static class InternalPropertyExtensions { if (inherit) return obj.GetDataContextType(); - else if (obj.properties.TryGet(Internal.DataContextTypeProperty, out var v)) + else if (obj.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_DataContextType, out var v)) return (DataContextStack?)v; else return null; @@ -101,7 +101,7 @@ public static class InternalPropertyExtensions public static TControl SetDataContextType(this TControl control, DataContextStack? stack) where TControl : DotvvmBindableObject { - control.properties.Set(Internal.DataContextTypeProperty, stack); + control.properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Internal_DataContextType, stack); return control; } } diff --git a/src/Framework/Framework/Controls/Label.cs b/src/Framework/Framework/Controls/Label.cs index 94972d2b2c..a5815301d6 100644 --- a/src/Framework/Framework/Controls/Label.cs +++ b/src/Framework/Framework/Controls/Label.cs @@ -48,7 +48,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest else if (id is {}) { // let the html generic control evaluate the binding - this.Attributes.Add("for", id.UnwrapToObject()); + this.Attributes.Add("for", id.Value.BindingOrDefault); } base.AddAttributesToRender(writer, context); diff --git a/src/Framework/Framework/Controls/Literal.cs b/src/Framework/Framework/Controls/Literal.cs index c2d69ea937..d9376a69c8 100644 --- a/src/Framework/Framework/Controls/Literal.cs +++ b/src/Framework/Framework/Controls/Literal.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.Net; using System.Runtime.CompilerServices; @@ -23,7 +24,7 @@ public sealed class Literal : HtmlGenericControl public string Text { get { return GetValue(TextProperty)?.ToString() ?? ""; } - set { SetValue(TextProperty, value); } + set { properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Literal_Text, value); } } public static readonly DotvvmProperty TextProperty = @@ -36,7 +37,7 @@ public string Text public string? FormatString { get { return (string?)GetValue(FormatStringProperty); } - set { SetValue(FormatStringProperty, value); } + set { properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Literal_FormatString, value); } } public static readonly DotvvmProperty FormatStringProperty = @@ -49,7 +50,7 @@ public string? FormatString public bool RenderSpanElement { get { return (bool)GetValue(RenderSpanElementProperty)!; } - set { SetValue(RenderSpanElementProperty, value); } + set { properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Literal_RenderSpanElement, value); } } public static readonly DotvvmProperty RenderSpanElementProperty = @@ -75,19 +76,19 @@ public Literal(string text, bool renderSpan = false) : base("span", false) public Literal(ValueOrBinding text, bool renderSpan = false): this() { - SetValue(TextProperty, text); + properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Literal_Text, text.UnwrapToObject()); RenderSpanElement = renderSpan; } public Literal(ValueOrBinding text, bool renderSpan = false): this() { - SetValue(TextProperty, text); + properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Literal_Text, text.UnwrapToObject()); RenderSpanElement = renderSpan; } public Literal(IStaticValueBinding text, bool renderSpan = false): this() { - SetBinding(TextProperty, text); + properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Literal_Text, text); RenderSpanElement = renderSpan; } @@ -118,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 == DotvvmPropertyIdAssignment.PropertyIds.Literal_Text) r.Text = value; - else if (prop == RenderSpanElementProperty) + else if (prop == DotvvmPropertyIdAssignment.PropertyIds.Literal_RenderSpanElement) r.RenderSpanElement = (bool)EvalPropertyValue(RenderSpanElementProperty, value)!; - else if (prop == FormatStringProperty) + else if (prop == DotvvmPropertyIdAssignment.PropertyIds.Literal_FormatString) 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/PropertyDictionaryImpl.cs b/src/Framework/Framework/Controls/PropertyDictionaryImpl.cs new file mode 100644 index 0000000000..d3e787f1bb --- /dev/null +++ b/src/Framework/Framework/Controls/PropertyDictionaryImpl.cs @@ -0,0 +1,586 @@ +using System; +using System.Collections.Concurrent; +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 System.Diagnostics.CodeAnalysis; + +#if Vectorize +using System.Runtime.Intrinsics; +#endif + +#if NET8_0_OR_GREATER +using UnreachableException = System.Diagnostics.UnreachableException; +#else +using UnreachableException = System.Exception; +#endif + +namespace DotVVM.Framework.Controls +{ + internal static class PropertyDictionaryImpl + { + /// 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 AdhocLargeTableSize = 16; + public const int MaxArrayTableSize = 16; + + // General implementation notes: + // * The most heavily used methods are specialized to 8-wide and 16-wide arrays, + // others might work with variable size, but divisibility by 8 is always assumed. + // 8 is the number of property IDs which fit into a 256-bit AVX register, which + // is currently available in most CPUs. + // * We check for HW support of 128-bit registers, but then use 256-bit - this is on purpose: + // Vector256 automatically falls back onto 128, if support for it isn't available, which + // is still advantageous over 8 scalar operations. Main reason is that we can use + // ARM CPUs currently have 128-bit wide SIMD registers, but can generally issue + // more vector instructions in parallel (so the compute power is similar to x86). + + private const MethodImplOptions Inline = MethodImplOptions.AggressiveInlining; + private const MethodImplOptions NoInlining = MethodImplOptions.NoInlining; + + + [MethodImpl(Inline)] + private static bool ContainsKey8(ref DotvvmPropertyId keys, DotvvmPropertyId p) + { +#if Vectorize + if (Vector128.IsHardwareAccelerated) + { + Debug.Assert(Vector256.Count == AdhocTableSize); + var v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); + return Vector256.EqualsAny(v, Vector256.Create(p.Id)); + } +#endif + return + Unsafe.Add(ref keys, 0).Id == p.Id | + Unsafe.Add(ref keys, 1).Id == p.Id | + Unsafe.Add(ref keys, 2).Id == p.Id | + Unsafe.Add(ref keys, 3).Id == p.Id | + Unsafe.Add(ref keys, 4).Id == p.Id | + Unsafe.Add(ref keys, 5).Id == p.Id | + Unsafe.Add(ref keys, 6).Id == p.Id | + Unsafe.Add(ref keys, 7).Id == p.Id; + } + + [MethodImpl(Inline)] + public static bool ContainsKey8(DotvvmPropertyId[] keys, DotvvmPropertyId p) + { + Debug.Assert(keys.Length == AdhocTableSize); + return ContainsKey8(ref UnsafeArrayReference(keys), p); + } + + [MethodImpl(Inline)] + private static bool ContainsKey16(ref DotvvmPropertyId keys, DotvvmPropertyId p) + { +#if Vectorize + if (Vector128.IsHardwareAccelerated) + { + Debug.Assert(Vector256.Count == AdhocTableSize); + // most likely, dictionary does not contain the value, so we will need to read the second vector in almost all cases + var v1 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); + var v2 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.Add(ref keys, 8))); + return Vector256.EqualsAny(v1, Vector256.Create(p.Id)) || Vector256.EqualsAny(v2, Vector256.Create(p.Id)); + } +#endif + return ContainsKey8(ref keys, p) || ContainsKey8(ref Unsafe.Add(ref keys, 8), p); + } + + public static bool ContainsKey16(DotvvmPropertyId[] keys, DotvvmPropertyId p) + { + Debug.Assert(keys.Length == AdhocLargeTableSize); + return ContainsKey16(ref UnsafeArrayReference(keys), p); + } + + [MethodImpl(Inline)] + private static int FindSlot8(ref DotvvmPropertyId keys, DotvvmPropertyId p) + { +#if Vectorize + if (Vector128.IsHardwareAccelerated) + { + Debug.Assert(Vector256.Count == AdhocTableSize); + var v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); + var eq = Vector256.Equals(v, Vector256.Create(p.Id)); + if (eq != Vector256.Zero) + { + return BitOperations.TrailingZeroCount(eq.ExtractMostSignificantBits()); + } + else + { + return -1; + } + } +#endif + if (Unsafe.Add(ref keys, 0).Id == p.Id) return 0; + if (Unsafe.Add(ref keys, 1).Id == p.Id) return 1; + if (Unsafe.Add(ref keys, 2).Id == p.Id) return 2; + if (Unsafe.Add(ref keys, 3).Id == p.Id) return 3; + if (Unsafe.Add(ref keys, 4).Id == p.Id) return 4; + if (Unsafe.Add(ref keys, 5).Id == p.Id) return 5; + if (Unsafe.Add(ref keys, 6).Id == p.Id) return 6; + if (Unsafe.Add(ref keys, 7).Id == p.Id) return 7; + + return -1; + } + + [MethodImpl(Inline)] + public static int FindSlot8(DotvvmPropertyId[] keys, DotvvmPropertyId p) + { + Debug.Assert(keys.Length == AdhocTableSize); + return FindSlot8(ref UnsafeArrayReference(keys), p); + } + + [MethodImpl(Inline)] + private static int FindSlot16(ref DotvvmPropertyId keys, DotvvmPropertyId p) + { +#if Vectorize + if (Vector128.IsHardwareAccelerated) + { + var v1 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); + var v2 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.Add(ref keys, 8))); + var eq1 = Vector256.Equals(v1, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + var eq2 = Vector256.Equals(v2, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + var eq = eq1 | (eq2 << 8); + if (eq != 0) + { + return BitOperations.TrailingZeroCount(eq); + } + else + { + return -1; + } + } +#endif + var ix = FindSlot8(ref keys, p); + if (ix >= 0) return ix; + + ix = FindSlot8(ref Unsafe.Add(ref keys, 8), p); + if (ix >= 0) return ix + 8; + + return -1; + } + + [MethodImpl(Inline)] + public static int FindSlot16(DotvvmPropertyId[] keys, DotvvmPropertyId p) + { + Debug.Assert(keys.Length == AdhocLargeTableSize); + return FindSlot16(ref UnsafeArrayReference(keys), p); + } + + public static int FindSlot(DotvvmPropertyId[] keys, DotvvmPropertyId p) + { + if (keys.Length == AdhocTableSize) + { + return FindSlot8(keys, p); + } + else if (keys.Length == AdhocLargeTableSize) + { + return FindSlot16(keys, p); + } + else + { + throw new ArgumentException("Keys must have 8 or 16 elements.", nameof(keys)); + } + } + + [MethodImpl(Inline)] + private static int FindSlotOrFree8(ref DotvvmPropertyId keys, DotvvmPropertyId p, out bool exists) + { +#if Vectorize + if (Vector128.IsHardwareAccelerated) + { + var v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); + var eq = Vector256.Equals(v, Vector256.Create(p.Id)); + if (eq != Vector256.Zero) + { + exists = true; + return BitOperations.TrailingZeroCount(eq.ExtractMostSignificantBits()); + } + exists = false; + var empty = Vector256.Equals(v, Vector256.Zero); + if (empty != Vector256.Zero) + { + return BitOperations.TrailingZeroCount(empty.ExtractMostSignificantBits()); + } + + return -1; + } +#endif + exists = true; + if (Unsafe.Add(ref keys, 0).Id == p.Id) return 0; + if (Unsafe.Add(ref keys, 1).Id == p.Id) return 1; + if (Unsafe.Add(ref keys, 2).Id == p.Id) return 2; + if (Unsafe.Add(ref keys, 3).Id == p.Id) return 3; + if (Unsafe.Add(ref keys, 4).Id == p.Id) return 4; + if (Unsafe.Add(ref keys, 5).Id == p.Id) return 5; + if (Unsafe.Add(ref keys, 6).Id == p.Id) return 6; + if (Unsafe.Add(ref keys, 7).Id == p.Id) return 7; + exists = false; + if (Unsafe.Add(ref keys, 0).Id == 0) return 0; + if (Unsafe.Add(ref keys, 1).Id == 0) return 1; + if (Unsafe.Add(ref keys, 2).Id == 0) return 2; + if (Unsafe.Add(ref keys, 3).Id == 0) return 3; + if (Unsafe.Add(ref keys, 4).Id == 0) return 4; + if (Unsafe.Add(ref keys, 5).Id == 0) return 5; + if (Unsafe.Add(ref keys, 6).Id == 0) return 6; + if (Unsafe.Add(ref keys, 7).Id == 0) return 7; + + return -1; + } + + [MethodImpl(Inline)] + private static int FindSlotOrFree16(ref DotvvmPropertyId keys, DotvvmPropertyId p, out bool exists) + { +#if Vectorize + if (Vector128.IsHardwareAccelerated) + { + var v1 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); + var v2 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.Add(ref keys, 8))); + var eq1 = Vector256.Equals(v1, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + var eq2 = Vector256.Equals(v2, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + var eq = eq1 | (eq2 << 8); + exists = eq != 0; + if (eq != 0) + return BitOperations.TrailingZeroCount(eq); + + var empty1 = Vector256.Equals(v1, Vector256.Zero).ExtractMostSignificantBits(); + var empty2 = Vector256.Equals(v2, Vector256.Zero).ExtractMostSignificantBits(); + var empty = empty1 | (empty2 << 8); + if (empty != 0) + return BitOperations.TrailingZeroCount(empty); + + return -1; + } +#endif + var ix = FindSlot16(ref keys, p); + exists = ix >= 0; + if (ix >= 0) + { + return ix; + } + ix = FindSlot16(ref keys, 0); + if (ix >= 0) + { + return ix; + } + + return -1; + } + + + [MethodImpl(Inline)] + public static int FindSlotOrFree8(DotvvmPropertyId[] keys, DotvvmPropertyId p, out bool exists) => + FindSlotOrFree8(ref UnsafeArrayReference(keys), p, out exists); + [MethodImpl(NoInlining)] + public static int FindSlotOrFree16(DotvvmPropertyId[] keys, DotvvmPropertyId p, out bool exists) => + FindSlotOrFree16(ref UnsafeArrayReference(keys), p, out exists); + + [MethodImpl(Inline)] + public static ushort FindGroupBitmap(ref DotvvmPropertyId keys, int length, ushort groupId) + { + Debug.Assert(length is AdhocTableSize or AdhocLargeTableSize); + + ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; // groupId ^ 0x8000 + + ushort bitmap = 0; +#if Vectorize + if (Vector128.IsHardwareAccelerated) + { + var v1 = Unsafe.ReadUnaligned>(in Unsafe.As(ref keys)); + bitmap = (ushort)Vector256.Equals(v1 >> 16, Vector256.Create((uint)idPrefix)).ExtractMostSignificantBits(); + if (length == 16) + { + var v2 = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keys, 8))); + bitmap |= (ushort)(Vector256.Equals(v2 >> 16, Vector256.Create((uint)idPrefix)).ExtractMostSignificantBits() << 8); + } + return bitmap; + } +#endif + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 0).TypeId == idPrefix) << 0); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 1).TypeId == idPrefix) << 1); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 2).TypeId == idPrefix) << 2); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 3).TypeId == idPrefix) << 3); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 4).TypeId == idPrefix) << 4); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 5).TypeId == idPrefix) << 5); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 6).TypeId == idPrefix) << 6); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 7).TypeId == idPrefix) << 7); + if (length == 16) + { + bitmap |= (ushort)(FindGroupBitmap(ref Unsafe.Add(ref keys, 8), AdhocTableSize, groupId) << 8); + } + return bitmap; + } + + [MethodImpl(Inline)] + public static ushort FindGroupBitmap(DotvvmPropertyId[] keys, ushort groupId) + { + return FindGroupBitmap(ref UnsafeArrayReference(keys), keys.Length, groupId); + } + + public static bool ContainsPropertyGroup(DotvvmPropertyId[] keys, ushort groupId) + { + Debug.Assert(keys.Length >= AdhocTableSize); + Debug.Assert(keys.Length % 8 == 0); + + ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; + +#if Vectorize + Debug.Assert(keys.Length % Vector256.Count == 0); + ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + if (Vector128.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; + } +#endif + 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 % 8 == 0); + Debug.Assert(keys.Length >= AdhocTableSize); + + int count = 0; +#if Vectorize + ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + + Debug.Assert(keys.Length % Vector256.Count == 0); + if (Vector128.IsHardwareAccelerated) + { + for (int i = 0; i < keys.Length; i += Vector256.Count) + { + var v = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keysRef, i))); + var notZero = Vector256.GreaterThan(v, Vector256.Create(0)).ExtractMostSignificantBits(); + count += BitOperations.PopCount(notZero); + } + return count; + } +#endif + for (int i = 0; i < keys.Length; i++) + { + count += BoolToInt(keys[i].Id != 0); + } + return count; + } + + [MethodImpl(Inline)] + private static int CountPropertyGroup8(ref DotvvmPropertyId keys, ushort groupId) + { + ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; + ref var keysInts = ref Unsafe.As(ref keys); + +#if Vectorize + if (Vector128.IsHardwareAccelerated) + { + var v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); + return BitOperations.PopCount(Vector256.Equals(v >> 16, Vector256.Create((uint)idPrefix)).ExtractMostSignificantBits()); + } +#endif + int count = 0; + count += BoolToInt(Unsafe.Add(ref keysInts, 0) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 1) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 2) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 3) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 4) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 5) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 6) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 7) >> 16 == idPrefix); + return count; + } + + [MethodImpl(Inline)] + public static int CountPropertyGroup8(DotvvmPropertyId[] keys, ushort groupId) + { + Debug.Assert(keys.Length == 8); + ref var keysRef = ref UnsafeArrayReference(keys); + return CountPropertyGroup8(ref keysRef, groupId); + } + + public static int CountPropertyGroup(DotvvmPropertyId[] keys, ushort groupId) + { + Debug.Assert(keys.Length % 8 == 0); + ref var keysRef = ref UnsafeArrayReference(keys); + + int count = 0; + for (int i = 0; i < keys.Length; i += 8) + { + count += CountPropertyGroup8(ref Unsafe.Add(ref keysRef, i), groupId); + } + return count; + } + + [MethodImpl(Inline)] + private static byte BoolToInt(bool x) => +#if NET8_0_OR_GREATER // JIT can finally optimize this to branchless code + x ? (byte)1 : (byte)0; +#else + Unsafe.As(ref x); +#endif + + [MethodImpl(Inline)] + public static ref DotvvmPropertyId UnsafeArrayReference(DotvvmPropertyId[] array) => +#if NET6_0_OR_GREATER + ref MemoryMarshal.GetArrayDataReference(array); +#else + ref array[0]; +#endif + + [MethodImpl(Inline)] + public static ref T UnsafeArrayReference(T[] array) => +#if NET6_0_OR_GREATER + ref MemoryMarshal.GetArrayDataReference(array); +#else + ref array[0]; +#endif + + 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; + if (x.Length != y.Length) return false; + + for (int i = 0; i < x.Length; i++) + if (x[i] != y[i]) return false; + return true; + } + + public int GetHashCode(DotvvmPropertyId[] obj) + { + var h = obj.Length; + foreach (var i in obj) + h = (i, h).GetHashCode(); + return h; + } + } + + [MethodImpl(Inline)] + public static void Assert([DoesNotReturnIf(false)] bool condition) + { + if (!condition) + Fail(); + } + +// #if !NETSTANDARD1_5_OR_GREATER +// [MethodImpl(Inline)] +// public static int TrailingZeroCount(uint v) +// { +// // https://graphics.stanford.edu/~seander/bithacks.html#ZerosOnRightFloatCast +// float f = (float)(v & -v); // cast the least significant bit in v to a float +// return (int)(Unsafe.As(ref f) >> 23) - 0x7f; +// } +// #endif + + [DoesNotReturn] + [MethodImpl(NoInlining)] + public static void Fail() => Fail(); + + [DoesNotReturn] + [MethodImpl(NoInlining)] + public static T Fail() => throw new UnreachableException("Assertion failed in DotVVM property dictionary. The collection state was probably corrupted by concurrent access, or by a serious DotVVM bug."); + + private static bool IsOrderedWithoutDuplicatesAndZero(DotvvmPropertyId[] keys) + { + 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)); + } + if (keys.Length > 16) + { + throw new ArgumentException("Keys must have at most 16 elements.", 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(keys, static keys => { + // pad result to 8 or 16 elements + var result = new DotvvmPropertyId[keys.Length <= 8 ? 8 : 16]; + Array.Copy(keys, result, keys.Length); + return (0, result); + }); + } + + public static (uint hashSeed, DotvvmPropertyId[] keys, T[] valueTable) CreateTableWithValues(DotvvmPropertyId[] properties, T[] values) + { + if (properties.Length != values.Length) + throw new ArgumentException("Properties and values must have the same length.", nameof(properties)); + if (properties.Length > 16) + throw new ArgumentException("Properties and values must have at most 16 elements, otherwise create a dictionary.", nameof(properties)); + + + + var (hashSeed, keys) = BuildTable(properties); + var valueTable = new T[keys.Length]; + for (int i = 0; i < properties.Length; i++) + { + valueTable[FindSlot(keys, properties[i])] = values[i]; + } + return (hashSeed, keys, valueTable); + } + + public static Action CreateBulkSetter(DotvvmProperty[] properties, object?[] values) + { + 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 > MaxArrayTableSize) + { + 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, false, false); + } + } + + public static void SetValuesToDotvvmControl(DotvvmBindableObject obj, DotvvmPropertyId[] properties, object?[] values, bool ownsKeys, bool ownsValues) + { + obj.properties.AssignBulk(properties, values, ownsKeys, ownsValues); + } + + public static void SetValuesToDotvvmControl(DotvvmBindableObject obj, Dictionary values, bool owns) + { + obj.properties.AssignBulk(values, owns); + } + } +} diff --git a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs b/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs deleted file mode 100644 index 41215deb0d..0000000000 --- a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using DotVVM.Framework.Binding; - -namespace DotVVM.Framework.Controls -{ - internal static class PropertyImmutableHashtable - { - static int HashCombine(int a, int b) => a + b; - - public static bool ContainsKey(DotvvmProperty?[] keys, int hashSeed, DotvvmProperty p) - { - var len = keys.Length; - if (len == 4) - { - return keys[0] == p | keys[1] == p | keys[2] == p | keys[3] == p; - } - - var lengthMap = len - 1; // trims the hash to be in bounds of the array - var hash = HashCombine(p.GetHashCode(), 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; - } - - public static int FindSlot(DotvvmProperty?[] keys, int hashSeed, DotvvmProperty p) - { - var len = keys.Length; - if (len == 4) - { - for (int i = 0; i < 4; i++) - { - if (keys[i] == p) return i; - } - return -1; - } - - var lengthMap = len - 1; // trims the hash to be in bounds of the array - var hash = HashCombine(p.GetHashCode(), 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; - return -1; - } - - static ConcurrentDictionary tableCache = new ConcurrentDictionary(new EqCmp()); - - class EqCmp : IEqualityComparer - { - public bool Equals(DotvvmProperty[]? x, DotvvmProperty[]? y) - { - if (object.ReferenceEquals(x, y)) return true; - if (x == null || y == null) return false; - if (x.Length != y.Length) return false; - - for (int i = 0; i < x.Length; i++) - if (x[i] != y[i]) return false; - return true; - } - - public int GetHashCode(DotvvmProperty[] obj) - { - var h = obj.Length; - foreach (var i in obj) - h = (i, h).GetHashCode(); - return h; - } - } - - // 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 (int hashSeed, DotvvmProperty?[] keys) BuildTable(DotvvmProperty[] a) - { - Debug.Assert(a.OrderBy(x => x.FullName, StringComparer.Ordinal).SequenceEqual(a)); - - // 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) - { - // just pad them to make things regular - var result = new DotvvmProperty[4]; - 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); - - while(true) - { - Debug.Assert((size & (size - 1)) == 0); - foreach (var hashSeed in hashSeeds) - { - var result = TryBuildTable(keys, size, hashSeed); - if (result != null) - { - Debug.Assert(TestTableCorrectness(keys, hashSeed, result)); - return (hashSeed, result); - } - } - - size *= 2; - - if (size <= 4) throw new InvalidOperationException("Could not build hash table"); - } - - } - }); - } - - static bool TestTableCorrectness(DotvvmProperty[] keys, int hashSeed, DotvvmProperty?[] table) - { - return keys.All(k => FindSlot(table, hashSeed, k) >= 0); - } - - /// 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) - { - var t = new DotvvmProperty?[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 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) - t[i1] = k; - else if (t[i2] == null) - 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) - { - var (hashSeed, keys) = BuildTable(properties); - var valueTable = new T[keys.Length]; - for (int i = 0; i < properties.Length; i++) - { - valueTable[FindSlot(keys, hashSeed, properties[i])] = values[i]; - } - return (hashSeed, keys, valueTable); - } - - public static Action CreateBulkSetter(DotvvmProperty[] properties, object[] values) - { - var (hashSeed, keys, valueTable) = CreateTableWithValues(properties, values); - return (obj) => obj.properties.AssignBulk(keys, valueTable, hashSeed); - } - - public class DotvvmPropertyComparer : IComparer - { - public int Compare(DotvvmProperty? a, DotvvmProperty? b) => - string.Compare(a?.FullName, b?.FullName, StringComparison.Ordinal); - - public static readonly DotvvmPropertyComparer Instance = new(); - } - } -} diff --git a/src/Framework/Framework/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj index 031e7dcacc..3aeda6ff2b 100644 --- a/src/Framework/Framework/DotVVM.Framework.csproj +++ b/src/Framework/Framework/DotVVM.Framework.csproj @@ -70,7 +70,7 @@ - + @@ -98,6 +98,9 @@ $(DefineConstants);DotNetCore + + $(DefineConstants);Vectorize + $(DefineConstants);CSharp8Polyfill;NoSpan;INTERNAL_NULLABLE_ATTRIBUTES @@ -129,8 +132,14 @@ + + + + + + diff --git a/src/Framework/Framework/Runtime/Commands/EventValidator.cs b/src/Framework/Framework/Runtime/Commands/EventValidator.cs index 3a6954201e..fdb234c854 100644 --- a/src/Framework/Framework/Runtime/Commands/EventValidator.cs +++ b/src/Framework/Framework/Runtime/Commands/EventValidator.cs @@ -6,6 +6,7 @@ using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Runtime.Commands { @@ -68,11 +69,14 @@ private FindBindingResult FindCommandBinding(string[] path, string commandId, if (resultBinding == null) { // find bindings of current control - var bindings = control.GetAllBindings() - .Where(b => b.Value is CommandBindingExpression); - foreach (var binding in bindings) + foreach (var (propertyId, rawValue) in control.properties) { + if (rawValue is not CommandBindingExpression binding) + continue; + var property = propertyId.PropertyInstance; + + infoMessage.Clear(); var bindingMatch = new FindBindingResult.BindingMatchChecklist(); @@ -88,7 +92,7 @@ private FindBindingResult FindCommandBinding(string[] path, string commandId, } //checking binding id - if (((CommandBindingExpression) binding.Value).BindingId == commandId) + if (binding.BindingId == commandId) { bindingMatch.BindingIdMatch = true; } @@ -124,23 +128,23 @@ private FindBindingResult FindCommandBinding(string[] path, string commandId, if(bindingMatch.AllMatches) { //correct binding found - resultBinding = (CommandBindingExpression)binding.Value; + resultBinding = binding; resultControl = control; - resultProperty = binding.Key; + resultProperty = property; } else { // only add information about ID mismatch if no other mismatch was found to avoid information clutter if (!bindingMatch.BindingIdMatch && infoMessage.Length == 0) { - infoMessage.Append($"Expected internal binding id: '{commandId}' Command binding id: '{((CommandBindingExpression)binding.Value).BindingId}'"); + infoMessage.Append($"Expected internal binding id: '{commandId}' Command binding id: '{binding.BindingId}'"); } if (!candidateBindings.ContainsKey(bindingMatch)) { candidateBindings.Add(bindingMatch, new CandidateBindings()); } candidateBindings[bindingMatch] - .AddBinding(new(infoMessage.ToString(), binding.Value)); + .AddBinding(new(infoMessage.ToString(), binding)); } } } diff --git a/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs b/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs index 7ba698e2bf..9cecee8dad 100644 --- a/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs +++ b/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs @@ -55,7 +55,7 @@ private void CheckRenderedResources(IDotvvmRequestContext context) { var control = stack.Pop(); - if (control.properties.TryGet(PostBack.UpdateProperty, out var val) && true.Equals(val)) + if (control.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.PostBack_Update) is true) { using (var w = new StringWriter()) { diff --git a/src/Framework/Framework/Utils/BoxingUtils.cs b/src/Framework/Framework/Utils/BoxingUtils.cs index 567fa582d6..50965808c0 100644 --- a/src/Framework/Framework/Utils/BoxingUtils.cs +++ b/src/Framework/Framework/Utils/BoxingUtils.cs @@ -26,6 +26,7 @@ public static object Box(int v) } public static object? Box(int? v) => v.HasValue ? Box(v.GetValueOrDefault()) : null; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static object? BoxGeneric(T v) { if (typeof(T).IsValueType) diff --git a/src/Framework/Framework/Utils/FunctionalExtensions.cs b/src/Framework/Framework/Utils/FunctionalExtensions.cs index d9cf6f1113..e3357f5f9a 100644 --- a/src/Framework/Framework/Utils/FunctionalExtensions.cs +++ b/src/Framework/Framework/Utils/FunctionalExtensions.cs @@ -82,11 +82,13 @@ public static IEnumerable SelectRecursively(this IEnumerable enumerable public static string StringJoin(this IEnumerable enumerable, string separator) => string.Join(separator, enumerable); +#if !DotNetCore public static void Deconstruct(this KeyValuePair pair, out K key, out V value) { key = pair.Key; value = pair.Value; } +#endif public static IEnumerable<(int, T)> Indexed(this IEnumerable enumerable) => enumerable.Select((a, b) => (b, a)); diff --git a/src/Framework/Framework/scripts/generate-property-ids.mjs b/src/Framework/Framework/scripts/generate-property-ids.mjs new file mode 100644 index 0000000000..00f550868c --- /dev/null +++ b/src/Framework/Framework/scripts/generate-property-ids.mjs @@ -0,0 +1,826 @@ +#!/usr/bin/env node + +/** + * Generates the DotvvmPropertyIdAssignment files: + * - GroupMembers.cs - for property group member names + * - TypeIds.cs - for control type IDs + * - PropertyIds.cs - for individual property IDs + * - PropertyGroupIds.cs - for property group IDs + */ + +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const predefinedNames = [ + // HTML attributes + 'accept', + 'accesskey', + 'action', + 'align', + 'allow', + 'alt', + 'aria-checked', + 'aria-controls', + 'aria-describedby', + 'aria-expanded', + 'aria-hidden', + 'aria-label', + 'aria-selected', + 'as', + 'async', + 'autocomplete', + 'autofocus', + 'border', + 'charset', + 'checked', + 'class', + 'cols', + 'colspan', + 'content', + 'contenteditable', + 'crossorigin', + 'data-bind', + 'data-dismiss', + 'data-dotvvm-id', + 'data-placement', + 'data-target', + 'data-toggle', + 'data-ui', + 'data-uitest-name', + 'dir', + 'disabled', + 'download', + 'draggable', + 'enctype', + 'for', + 'form', + 'formaction', + 'formmethod', + 'formnovalidate', + 'formtarget', + 'height', + 'hidden', + 'href', + 'hreflang', + 'http-equiv', + 'id', + 'integrity', + 'itemprop', + 'lang', + 'list', + 'loading', + 'max', + 'maxlength', + 'media', + 'method', + 'min', + 'minlength', + 'multiple', + 'name', + 'novalidate', + 'pattern', + 'ping', + 'placeholder', + 'preload', + 'readonly', + 'referrerpolicy', + 'rel', + 'required', + 'role', + 'rows', + 'sandbox', + 'scope', + 'selected', + 'size', + 'slot', + 'span', + 'spellcheck', + 'src', + 'step', + 'style', + 'tabindex', + 'target', + 'title', + 'translate', + 'type', + 'value', + 'width', + 'wrap', + + // Common CSS properties + 'background-color', + 'bottom', + 'color', + 'display', + 'font-size', + 'left', + 'line-height', + 'margin-bottom', + 'margin-right', + 'margin-top', + 'margin', + 'max-height', + 'max-width', + 'min-height', + 'min-width', + 'opacity', + 'padding-bottom', + 'padding-left', + 'padding-right', + 'padding-top', + 'padding', + 'position', + 'right', + 'top', + 'visibility', + 'z-index', + + // Common route parameter names + 'Id', + 'Name', + 'GroupId', + 'FileName', + 'UserId', + 'Slug', + 'slug', + 'Lang' +] + +// Control types with their properties +// The script will automatically assign sequential IDs +const controls = [ + { + name: 'DotvvmBindableObject', + id: 1, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: ['DataContext'] // inherited + }, + { + name: 'DotvvmControl', + id: 2, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ID', 'ClientID', 'IncludeInPage'], + slowProps: ['ClientIDMode'] // inherited + }, + { + name: 'HtmlGenericControl', + id: 3, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Visible', 'InnerText'], + slowProps: ['HtmlCapability'] // capability property + }, + { + name: 'RawLiteral', + id: 4, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: [] + }, + { + name: 'Literal', + id: 5, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Text', 'FormatString', 'RenderSpanElement'], + slowProps: [] + }, + { + name: 'ButtonBase', + id: 6, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Click', 'ClickArguments', 'Text'], + slowProps: ['Enabled', 'TextOrContentCapability'] // with fallback, capability + }, + { + name: 'Button', + id: 7, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ButtonTagName', 'IsSubmitButton'], + slowProps: [] + }, + { + name: 'LinkButton', + id: 8, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: [] + }, + { + name: 'TextBox', + id: 9, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Text', 'Changed', 'Type', 'TextInput', 'FormatString', 'SelectAllOnFocus'], + slowProps: ['Enabled', 'UpdateTextOnInput'] // inherited + }, + { + name: 'RouteLink', + id: 10, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['RouteName', 'Enabled', 'Text', 'UrlSuffix', 'Culture'], + slowProps: [] + }, + { + name: 'CheckableControlBase', + id: 11, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Text', 'CheckedValue', 'Changed', 'LabelCssClass', 'InputCssClass', 'ItemKeyBinding'], + slowProps: [] + }, + { + name: 'CheckBox', + id: 12, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Checked', 'CheckedItems', 'DisableIndeterminate'], + slowProps: [] + }, + { + name: 'RadioButton', + id: 13, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Checked', 'CheckedItem', 'GroupName'], + slowProps: [] + }, + { + name: 'Validator', + id: 14, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: ['HideWhenValid', 'InvalidCssClass', 'SetToolTipText', 'ShowErrorMessageText'] // inherited + }, + { + name: 'Validation', + id: 15, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: ['Enabled', 'Target'] // inherited + }, + { + name: 'ValidationSummary', + id: 16, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['IncludeErrorsFromChildren', 'HideWhenValid', 'IncludeErrorsFromTarget'], + slowProps: [] + }, + { + name: 'ItemsControl', + id: 17, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['DataSource'], + slowProps: [] + }, + { + name: 'Repeater', + id: 18, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['EmptyDataTemplate', 'ItemTemplate', 'RenderWrapperTag', 'SeparatorTemplate', 'WrapperTagName', 'RenderAsNamedTemplate'], + slowProps: [] + }, + { + name: 'HierarchyRepeater', + id: 19, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ItemChildrenBinding', 'ItemTemplate', 'EmptyDataTemplate', 'RenderWrapperTag', 'WrapperTagName'], + slowProps: [] + }, + { + name: 'GridView', + id: 20, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['FilterPlacement', 'EmptyDataTemplate', 'Columns', 'RowDecorators', 'HeaderRowDecorators', 'EditRowDecorators', 'SortChanged', 'ShowHeaderWhenNoData', 'InlineEditing', 'LoadData'], + slowProps: [] + }, + { + name: 'GridViewColumn', + id: 21, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['HeaderText', 'HeaderTemplate', 'FilterTemplate', 'SortExpression', 'SortAscendingHeaderCssClass', 'SortDescendingHeaderCssClass', 'AllowSorting', 'CssClass', 'IsEditable', 'HeaderCssClass', 'Width', 'Visible', 'CellDecorators', 'EditCellDecorators', 'EditTemplate', 'HeaderCellDecorators'], + slowProps: [] + }, + { + name: 'GridViewTextColumn', + id: 22, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['FormatString', 'ChangedBinding', 'ValueBinding', 'ValidatorPlacement'], + slowProps: [] + }, + { + name: 'GridViewCheckBoxColumn', + id: 23, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ValueBinding', 'ValidatorPlacement'], + slowProps: [] + }, + { + name: 'GridViewTemplateColumn', + id: 24, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ContentTemplate'], + slowProps: [] + }, + { + name: 'DataPager', + id: 25, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['DataSet', 'FirstPageTemplate', 'LastPageTemplate', 'PreviousPageTemplate', 'NextPageTemplate', 'RenderLinkForCurrentPage', 'HideWhenOnlyOnePage', 'LoadData'], + slowProps: [] + }, + { + name: 'AppendableDataPager', + id: 26, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['LoadTemplate', 'LoadingTemplate', 'EndTemplate', 'DataSet', 'LoadData'], + slowProps: [] + }, + { + name: 'SelectorBase', + id: 27, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ItemTextBinding', 'ItemValueBinding', 'SelectionChanged', 'ItemTitleBinding'], + slowProps: [] + }, + { + name: 'Selector', + id: 28, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['SelectedValue'], + slowProps: [] + }, + { + name: 'MultiSelector', + id: 29, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['SelectedValues'], + slowProps: [] + }, + { + name: 'ListBox', + id: 30, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Size'], + slowProps: [] + }, + { + name: 'ComboBox', + id: 31, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['EmptyItemText'], + slowProps: [] + }, + { + name: 'SelectorItem', + id: 32, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Text', 'Value'], + slowProps: [] + }, + { + name: 'FileUpload', + id: 33, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['UploadedFiles', 'Capture', 'MaxFileSize', 'UploadCompleted'], + slowProps: [] + }, + { + name: 'Timer', + id: 34, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Command', 'Interval', 'Enabled'], + slowProps: [] + }, + { + name: 'UpdateProgress', + id: 35, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Delay', 'IncludedQueues', 'ExcludedQueues'], + slowProps: [] + }, + { + name: 'Label', + id: 36, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['For'], + slowProps: [] + }, + { + name: 'EmptyData', + id: 37, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['WrapperTagName', 'RenderWrapperTag'], + slowProps: [] + }, + { + name: 'Content', + id: 38, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ContentPlaceHolderID'], + slowProps: [] + }, + { + name: 'TemplateHost', + id: 39, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Template'], + slowProps: [] + }, + { + name: 'AddTemplateDecorator', + id: 40, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['AfterTemplate', 'BeforeTemplate'], + slowProps: [] + }, + { + name: 'SpaContentPlaceHolder', + id: 41, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['DefaultRouteName', 'PrefixRouteName', 'UseHistoryApi'], + slowProps: [] + }, + { + name: 'ModalDialog', + id: 42, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Open', 'CloseOnBackdropClick', 'Close'], + slowProps: [] + }, + { + name: 'HtmlLiteral', + id: 43, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Html'], + slowProps: [] + }, + { + name: 'RequiredResource', + id: 44, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Name'], + slowProps: [] + }, + { + name: 'InlineScript', + id: 45, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Dependencies', 'Script'], + slowProps: [] + }, + { + name: 'RoleView', + id: 46, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Roles', 'IsMemberTemplate', 'IsNotMemberTemplate', 'HideForAnonymousUsers'], + slowProps: [] + }, + { + name: 'ClaimView', + id: 47, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Claim', 'Values', 'HasClaimTemplate', 'HideForAnonymousUsers'], + slowProps: [] + }, + { + name: 'EnvironmentView', + id: 48, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Environments', 'IsEnvironmentTemplate', 'IsNotEnvironmentTemplate'], + slowProps: [] + }, + { + name: 'JsComponent', + id: 49, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Global', 'Name', 'WrapperTagName'], + slowProps: [] + }, + { + name: 'PostBackHandler', + id: 50, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['EventName', 'Enabled'], + slowProps: [] + }, + { + name: 'SuppressPostBackHandler', + id: 51, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Suppress'], + slowProps: [] + }, + { + name: 'ConcurrencyQueueSetting', + id: 52, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['EventName', 'ConcurrencyQueue'], + slowProps: [] + }, + { + name: 'NamedCommand', + id: 53, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Name', 'Command'], + slowProps: [] + }, + { + name: 'PostBack', + id: 54, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Update', 'Handlers', 'ConcurrencyQueueSettings'], + slowProps: ['Concurrency', 'ConcurrencyQueue'] // inherited + }, + { + name: 'FormControls', + id: 55, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: ['Enabled'] // inherited + }, + { + name: 'UITests', + id: 56, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: ['GenerateStub'] // inherited + }, + { + name: 'Events', + id: 57, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], // Uses ActiveDotvvmProperty.RegisterCommandToAttribute + slowProps: [] + }, + { + name: 'Styles', + id: 58, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], // Uses CompileTimeOnlyDotvvmProperty + slowProps: [] + }, + { + name: 'Internal', + id: 59, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['UniqueID', 'IsNamingContainer', 'IsControlBindingTarget', 'PathFragment', 'IsServerOnlyDataContext', 'MarkupLineNumber', 'ClientIDFragment', 'IsMasterPageCompositionFinished', 'CurrentIndexBinding', 'ReferencedViewModuleInfo', 'UsedPropertiesInfo'], + slowProps: ['IsSpaPage', 'UseHistoryApiSpaNavigation', 'DataContextType', 'MarkupFileName', 'RequestContext'] // inherited + }, + { + name: 'RenderSettings', + id: 60, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: ['Mode'] // inherited + }, + { + name: 'DotvvmView', + id: 61, + namespace: 'DotVVM.Framework.Controls.Infrastructure', + fastProps: [], + slowProps: ['Directives'] // inherited + } +] + +const propertyGroups = [ + { type: 'HtmlGenericControl', name: 'Attributes' }, + { type: 'HtmlGenericControl', name: 'CssClasses' }, + { type: 'HtmlGenericControl', name: 'CssStyles' }, + { type: 'RouteLink', name: 'Params' }, + { type: 'RouteLink', name: 'QueryParameters' }, + { type: 'JsComponent', name: 'Props' }, + { type: 'JsComponent', name: 'Templates' } +] + +// ------------------------------------------------------------------------------------------------ + +const csharpKeywords = new Set([ + 'abstract', 'as', 'base', 'bool', 'break', 'byte', 'case', 'catch', 'char', 'checked', + 'class', 'const', 'continue', 'decimal', 'default', 'delegate', 'do', 'double', 'else', + 'enum', 'event', 'explicit', 'extern', 'false', 'finally', 'fixed', 'float', 'for', + 'foreach', 'goto', 'if', 'implicit', 'in', 'int', 'interface', 'internal', 'is', + 'lock', 'long', 'namespace', 'new', 'null', 'object', 'operator', 'out', 'override', + 'params', 'private', 'protected', 'public', 'readonly', 'ref', 'return', 'sbyte', + 'sealed', 'short', 'sizeof', 'stackalloc', 'static', 'string', 'struct', 'switch', + 'this', 'throw', 'true', 'try', 'typeof', 'uint', 'ulong', 'unchecked', 'unsafe', + 'ushort', 'using', 'virtual', 'void', 'volatile', 'while' +]) + +function csIdentifier(name) { + let identifier = name.replace(/-/g, '_') + return csharpKeywords.has(identifier) ? '@' + identifier : identifier +} + +function validateAndGeneratePropertyDefinitions() { + const propertyDefinitions = [] + const errors = [] + + for (const control of controls) { + if (control.fastProps.length > 16) { + errors.push(`Control ${control.name} has ${control.fastProps.length} fast properties, maximum is 16`) + } + if (control.slowProps.length > 16) { + errors.push(`Control ${control.name} has ${control.slowProps.length} slow properties, maximum is 16`) + } + + // properties with CanUseFastAccessors=true get even IDs (last bit zero) + control.fastProps.forEach((propName, index) => { + propertyDefinitions.push({ + typeName: control.name, + propertyName: propName, + sequentialId: (index + 1) * 2, + canUseFastAccessors: true + }) + }) + + // properties with CanUseFastAccessors=false get odd IDs (last bit one) + control.slowProps.forEach((propName, index) => { + propertyDefinitions.push({ + typeName: control.name, + propertyName: propName, + sequentialId: (index * 2) + 1, + canUseFastAccessors: false + }) + }) + } + + if (errors.length > 0) { + console.error('❌ Configuration validation failed:') + errors.forEach(error => console.error(` ${error}`)) + throw new Error('Configuration validation failed') + } + + return propertyDefinitions +} + +function generateControlTypes() { + return controls.map(control => ({ + name: control.name, + id: control.id, + namespace: control.namespace + })) +} + +function generateGroupMembersClass() { + const set = new Set() + const names = predefinedNames.filter(n => !set.has(n) && set.add(n)) + + const constants = names.map((name, index) => { + const id = index + 1 // Start from 1 + return ` public const ushort ${csIdentifier(name)} = ${id};` + }).join('\n') + + const listItems = names.map(name => { + return ` ("${name}", ${csIdentifier(name)})` + }).join(',\n') + + const switchCases = names.map(name => { + const identifier = csIdentifier(name) + return ` "${name}" => ${identifier}` + }).join(',\n') + + return `// Generated by scripts/generate-property-ids.mjs +using System; +using System.Collections.Immutable; + +namespace DotVVM.Framework.Binding; + +static partial class DotvvmPropertyIdAssignment +{ + public static class GroupMembers + { +${constants} + + public static readonly ImmutableArray<(string Name, ushort ID)> List = ImmutableArray.Create( +${listItems} + ); + + public static ushort TryGetId(ReadOnlySpan attr) => + attr switch { +${switchCases}, + _ => 0, + }; + } +} +` +} + +function generateTypeIdsClass() { + const controlTypes = generateControlTypes() + + const constants = controlTypes.map(type => { + return `public const ushort ${type.name} = ${type.id};` + }).join('\n ') + + const listItems = controlTypes.map(type => { + return `(typeof(${type.name}), ${type.name})` + }).join(',\n ') + + const usingStatements = [...new Set(controlTypes.map(t => t.namespace))].map(ns => + `using ${ns};` + ).join('\n') + + return `// Generated by scripts/generate-property-ids.mjs +using System; +using System.Collections.Immutable; +${usingStatements} + +namespace DotVVM.Framework.Binding; + +static partial class DotvvmPropertyIdAssignment +{ + public static class TypeIds + { + ${constants} + + public static readonly ImmutableArray<(Type type, ushort id)> List = ImmutableArray.Create( + ${listItems} + ); + } +} +` +} + +function generatePropertyIdsClass() { + const propertyDefinitions = validateAndGeneratePropertyDefinitions() + const controlTypes = generateControlTypes() + + const constants = propertyDefinitions.map(prop => { + const typeInfo = controlTypes.find(t => t.name === prop.typeName) + if (!typeInfo) { + throw new Error(`Type ${prop.typeName} not found in control types`) + } + + return `/// + public const uint ${prop.typeName}_${prop.propertyName} = TypeIds.${prop.typeName} << 16 | ${prop.sequentialId};` + }).join('\n\n ') + + return `// Generated by scripts/generate-property-ids.mjs +using DotVVM.Framework.Controls; +using DotVVM.Framework.Controls.Infrastructure; + +namespace DotVVM.Framework.Binding; + +static partial class DotvvmPropertyIdAssignment +{ + public static class PropertyIds + { + ${constants} + } +} +` +} + +function generatePropertyGroupIdsClass() { + const constants = propertyGroups.map((group, index) => { + const id = index + 1; + const constantName = `${group.type}_${group.name}`; + return `/// + public const ushort ${constantName} = ${id};` + }).join('\n\n ') + + return `// Generated by scripts/generate-property-ids.mjs +using DotVVM.Framework.Controls; + +namespace DotVVM.Framework.Binding; + +static partial class DotvvmPropertyIdAssignment +{ + public static class PropertyGroupIds + { + ${constants} + } +} +` +} + + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +function main() { + const baseDir = path.join(__dirname, '..') + + const files = [ + { name: 'GroupMembers', generator: generateGroupMembersClass }, + { name: 'TypeIds', generator: generateTypeIdsClass }, + { name: 'PropertyIds', generator: generatePropertyIdsClass }, + { name: 'PropertyGroupIds', generator: generatePropertyGroupIdsClass } + ] + + let hasErrors = false + + for (const file of files) { + try { + const filepath = path.join(baseDir, 'Binding', `DotvvmPropertyIdAssignment.${file.name}.cs`) + const content = file.generator().replace('\r\n', '\n') + fs.writeFileSync(filepath, content, 'utf8') + } catch (error) { + console.error(`❌ Error generating ${file.name}:`, error.message) + hasErrors = true + } + } + + if (hasErrors) { + process.exit(1) + } +} + +main() diff --git a/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj b/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj index 29d8bdccca..cad20cfe37 100644 --- a/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj +++ b/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj @@ -38,6 +38,6 @@ - + diff --git a/src/Tests/Runtime/CapabilityPropertyTests.cs b/src/Tests/Runtime/CapabilityPropertyTests.cs index 2fc85ab269..dadbc56d64 100644 --- a/src/Tests/Runtime/CapabilityPropertyTests.cs +++ b/src/Tests/Runtime/CapabilityPropertyTests.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Controls; -using DotVVM.Framework.ResourceManagement; using DotVVM.Framework.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -315,6 +313,43 @@ public void BitMoreComplexCapability_NullableValue(Type controlType) Assert.AreEqual(2, control1.GetCapability().Nullable); } + [DataTestMethod] + [DataRow(typeof(TestControl1), "Something", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl1), "SomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl1), "Visible", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl1), "ID", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl2), "Something", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl2), "SomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl2), "AnotherHtml:Visible", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl3), "Something", MappingMode.Attribute, true, false)] + [DataRow(typeof(TestControl3), "SomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl4), "Something", MappingMode.Attribute, true, false)] + [DataRow(typeof(TestControl4), "SomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl5), "SomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl5), "AnotherTest:SomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl5), "ItemSomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl6), "BindingOnly", MappingMode.Attribute, true, false)] + [DataRow(typeof(TestControl6), "ValueOrBinding", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl6), "ValueOrBindingNullable", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl6), "Nullable", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl6), "NotNullable", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl6), "Template", MappingMode.InnerElement, false, true)] + [DataRow(typeof(TestControl6), "NestedControl", MappingMode.InnerElement, false, true)] + [DataRow(typeof(TestControl6), "NestedControls", MappingMode.InnerElement, false, true)] + public void PropertyImplicitMarkupOptions(Type control, string property, MappingMode expectedMode, bool expectedAllowsBinding, bool expectedAllowsValue) + { + + var c = (DotvvmBindableObject)Activator.CreateInstance(control); + var allProps = DotvvmProperty.ResolveProperties(control).Select(p => p.Name); + var prop = DotvvmProperty.ResolveProperty(control, property); + Assert.IsNotNull(prop, $"{control.Name}.{property} is not defined. These properties exist: {string.Join(", ", allProps)}"); + + Assert.AreEqual(expectedMode, prop.MarkupOptions.MappingMode); + Assert.AreEqual(expectedAllowsBinding, prop.MarkupOptions.AllowBinding); + Assert.AreEqual(expectedAllowsValue, prop.MarkupOptions.AllowHardCodedValue); + } + + public class TestControl1: HtmlGenericControl, @@ -445,6 +480,7 @@ public sealed record TestCapability [DotvvmControlCapability] public sealed record TestNestedCapability { + [MarkupOptions(AllowBinding = true, AllowHardCodedValue = false)] public string Something { get; init; } = "abc"; public TestCapability Test { get; init; } public HtmlCapability Html { get; init; } @@ -470,6 +506,9 @@ public sealed record BitMoreComplexCapability public ValueOrBinding? ValueOrBindingNullable { get; init; } public int? Nullable { get; init; } public int NotNullable { get; init; } = 30; + public ITemplate Template { get; init; } + public HtmlGenericControl NestedControl { get; init; } + public List NestedControls { get; init; } } } diff --git a/src/Tests/Runtime/DotvvmPropertyTests.cs b/src/Tests/Runtime/DotvvmPropertyTests.cs index fef60275b3..d14f4b1cc1 100644 --- a/src/Tests/Runtime/DotvvmPropertyTests.cs +++ b/src/Tests/Runtime/DotvvmPropertyTests.cs @@ -13,9 +13,12 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; namespace DotVVM.Framework.Tests.Runtime @@ -300,10 +303,22 @@ public void DotvvmProperty_CorrectGetAndSet() if (p.PropertyInfo.PropertyType != p.PropertyType) Console.WriteLine(p); + instance.properties.Remove(p.Id); + Assert.AreEqual(p.DefaultValue, instance.GetValue(p), $"GetValue default value {p}"); + Assert.AreEqual(p.DefaultValue, instance.GetValueRaw(p.Id), $"GetValue(id) default value {p}"); + Assert.AreEqual(p.DefaultValue, p.PropertyInfo.GetValue(instance), $"Getter default value {p}"); + foreach (var example in GetExampleValues(p.PropertyType)) { instance.SetValue(p, example); + Assert.AreEqual(example, instance.GetValue(p), $"GetValue is behaving weird {p}"); + Assert.AreEqual(example, instance.GetValueRaw(p.Id), $"GetValue(id) is behaving weird {p}"); Assert.AreEqual(example, p.PropertyInfo.GetValue(instance), $"Getter is broken in {p}"); + + if (p.Id.CanUseFastAccessors) + { + Assert.AreEqual(example, instance.properties.GetOrThrow(p.Id), "$instance.properties.GetOrThrow {p}"); + } } if (p.PropertyInfo.SetMethod == null) @@ -356,5 +371,309 @@ 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 = PropertyDictionaryImpl.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); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(false, true)] + [DataRow(true, false)] + [DataRow(true, true)] + public void DotvvmProperty_ControlClone(bool manyAttributes, bool nestedControl) + { + var control = new HtmlGenericControl("div"); + control.CssStyles.Set("color", "red"); + control.Attributes.Set("something", "value"); + + if (manyAttributes) + for (int i = 0; i < 60; i++) + control.Attributes.Set("data-" + i.ToString(), i.ToString()); + + if (nestedControl) + { + control.Properties.Add(Styles.ReplaceWithProperty, new HtmlGenericControl("span") { InnerText = "23" }); + } + + var clone = (HtmlGenericControl)control.CloneControl(); + + Assert.AreEqual(control.TagName, clone.TagName); + Assert.AreEqual(control.CssStyles["color"], "red"); + + // change original + Assert.IsFalse(clone.CssStyles.ContainsKey("abc")); + control.CssStyles.Set("color", "blue"); + control.CssStyles.Set("abc", "1"); + Assert.AreEqual("red", clone.CssStyles["color"]); + Assert.IsFalse(clone.CssStyles.ContainsKey("abc")); + + if (nestedControl) + { + var nestedClone = (HtmlGenericControl)clone.Properties[Styles.ReplaceWithProperty]!; + var nestedOriginal = (HtmlGenericControl)control.Properties[Styles.ReplaceWithProperty]!; + Assert.AreEqual("23", nestedClone.InnerText); + // change clone this time + nestedClone.InnerText = "24"; + Assert.AreEqual("23", nestedOriginal.InnerText); + Assert.AreEqual("24", nestedClone.InnerText); + } + + if (manyAttributes) + { + for (int i = 0; i < 60; i++) + { + Assert.AreEqual(i.ToString(), control.Attributes["data-" + i.ToString()]); + Assert.AreEqual(i.ToString(), clone.Attributes["data-" + i.ToString()]); + } + } + } + + [TestMethod] + [DataRow(0)] + [DataRow(1)] + [DataRow(2)] + public void DotvvmProperty_VirtualDictionary_Append(int testClone) + { + var control = new HtmlGenericControl("div"); + + foreach (var i in Enumerable.Range(0, 50)) + { + control.Attributes.Set($"data-{i}", i); + + if (testClone > 0) + { + var clone = (HtmlGenericControl)control.CloneControl(); + if (testClone == 2) + (control, clone) = (clone, control); + + clone.Attributes.Set("something-else", "abc"); + Assert.AreEqual("abc", clone.Attributes["something-else"]); + clone.Attributes.Set("data-5", -1); + Assert.AreEqual(-1, clone.Attributes["data-5"]); + } + + Assert.AreEqual(i + 1, control.properties.Count()); + Assert.AreEqual(i + 1, control.Attributes.Count); + Assert.IsTrue(control.Attributes.ContainsKey("data-" + i.ToString())); + Assert.IsFalse(control.Attributes.ContainsKey("something-else")); + Assert.AreEqual(i, control.Attributes["data-" + i.ToString()]); + + XAssert.Equal(Enumerable.Range(0, i+1).Cast(), control.Attributes.Values); + } + } + +#if NET6_0_OR_GREATER + [TestMethod, Ignore] + public void DotvvmProperty_ParallelAccess_DoesntCrashProcess() + { + var properties = new DotvvmProperty[] { + DotvvmBindableObject.DataContextProperty, + DotvvmControl.IncludeInPageProperty, + HtmlGenericControl.VisibleProperty, + TextBox.EnabledProperty, + HtmlGenericControl.AttributesGroupDescriptor.GetDotvvmProperty("data-1"), + HtmlGenericControl.AttributesGroupDescriptor.GetDotvvmProperty("data-2"), + HtmlGenericControl.AttributesGroupDescriptor.GetDotvvmProperty("data-3"), + HtmlGenericControl.AttributesGroupDescriptor.GetDotvvmProperty("data-4"), + Button.EnabledProperty, + FormControls.EnabledProperty + }; + var control = new PlaceHolder(); + + var exceptions = new ConcurrentBag(); + + ThreadPool.SetMinThreads(100, 100); + Parallel.For(0, 10_000_000_000, new ParallelOptions { MaxDegreeOfParallelism = 100 }, i => { + try + { + var value = control.GetValue(properties[(i / 2) % properties.Length]); + control.properties.TryGet(properties[i % properties.Length], out value); + if (i % 2 == 0) + { + control.SetValue(properties[i % properties.Length], BoxingUtils.Box(i % 2 == 1)); + } + else + { + control.properties.TryAdd(properties[i % properties.Length], BoxingUtils.Box(i % 2 == 1)); + } + + if (i % 16 == 0) + { + switch (Random.Shared.Next(0, 4)) + { + case 0: + control = new PlaceHolder(); + break; + case 1: + control = (PlaceHolder)control.CloneControl(); + break; + case 2: + foreach (var prop in control.Properties.Keys) + control.Properties.Remove(prop); + break; + case 3: + foreach (var prop in control.properties.PropertyGroup(HtmlGenericControl.AttributesGroupDescriptor.Id)) + control.properties.Remove(prop.Key); + break; + } + } + } + catch (Exception ex) + { + exceptions.Add(ex); + control = new PlaceHolder(); + } + // if (control.properties.Count() >= 16) + // throw new Exception("Too many properties"); + }); + + var exceptionGroups = exceptions + .GroupBy(e => e.GetType().Name + ": " + e.Message) + // .GroupBy(e => e.ToString()) + .Select(g => (g.Key, g.Count())) + .OrderByDescending(g => g.Item2) + .ToList(); + foreach (var (key, count) in exceptionGroups) + { + Console.WriteLine($"{key}: {count}"); + } + if (exceptions.Count > 0) + { + Assert.Fail($"There were {exceptions.Count} exceptions thrown during the test. See the output for details."); + } + } +#endif + + [DataTestMethod] + [DataRow(0, 0, 0)] + [DataRow(1, 1, 1)] + [DataRow(30, 1, 1)] + [DataRow(1, 30, 1)] + [DataRow(1, 1, 30)] + [DataRow(1, 1, 8)] + [DataRow(1, 8, 1)] + [DataRow(8, 1, 1)] + [DataRow(5, 5, 8)] + [DataRow(5, 8, 5)] + [DataRow(8, 5, 5)] + [DataRow(5, 5, 5)] + [DataRow(8, 8, 8)] + public void PropertyGroup_Clear(int attributeCount, int classCount, int styleCount) + { + var control = new HtmlGenericControl(); + control.InnerText = "test-inner-text"; + var attributes = control.Attributes; + + (int, int, int) counts() => (control.Attributes.Count, control.CssClasses.Count, control.CssStyles.Count); + Assert.AreEqual(0, control.Attributes.Count); + Assert.AreEqual(0, control.CssClasses.Count); + Assert.AreEqual(0, control.CssStyles.Count); + + for (int i = 0; i < attributeCount; i++) + attributes.Set($"test-attribute-{i}", $"value{i}"); + + Assert.AreEqual((attributeCount, 0, 0), counts()); + + for (int i = 0; i < classCount; i++) + control.CssClasses.Add($"test-class-{i}", true); + + Assert.AreEqual((attributeCount, classCount, 0), counts()); + + + for (int i = 0; i < styleCount; i++) + control.CssStyles.Add($"test-style-{i}", $"value{i}"); + + Assert.AreEqual((attributeCount, classCount, styleCount), counts()); + + control.CssClasses.Clear(); + var checkpoint1 = control.CloneControl(); + Assert.AreEqual((attributeCount, 0, styleCount), counts()); + control.CssClasses.Add("another-class", true); + Assert.AreEqual((attributeCount, 1, styleCount), counts()); + + for (int i = 0; i < attributeCount; i++) + Assert.AreEqual(attributes[$"test-attribute-{i}"], $"value{i}"); + + for (int i = 0; i < classCount; i++) + Assert.IsFalse(control.CssClasses.ContainsKey($"test-class-{i}")); + + for (int i = 0; i < styleCount; i++) + Assert.AreEqual($"value{i}", control.CssStyles[$"test-style-{i}"]); + + control.Attributes.Clear(); + + Assert.AreEqual((0, 1, styleCount), counts()); + var checkpoint2 = control.CloneControl(); + + for (int i = 0; i < attributeCount; i++) + Assert.IsFalse(attributes.ContainsKey($"test-attribute-{i}")); + for (int i = 0; i < classCount; i++) + Assert.IsFalse(control.CssClasses.ContainsKey($"test-class-{i}")); + for (int i = 0; i < styleCount; i++) + Assert.AreEqual($"value{i}", control.CssStyles[$"test-style-{i}"]); + + Assert.IsTrue(control.CssClasses["another-class"]); + + control.CssStyles.Clear(); + + for (int i = 0; i < styleCount; i++) + Assert.IsFalse(control.CssStyles.ContainsKey($"test-style-{i}"), $"Style 'test-style-{i}' should not exist."); + + Assert.IsTrue(control.CssClasses["another-class"]); + + Assert.AreEqual("test-inner-text", control.InnerText); + + control = (HtmlGenericControl)checkpoint1; + Assert.AreEqual((attributeCount, 0, styleCount), counts()); + control = (HtmlGenericControl)checkpoint2; + Assert.AreEqual((0, 1, styleCount), counts()); + } + + [TestMethod] + public void PropertyIds_MatchGeneratedConstants() + { + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ClientID, (uint)DotvvmControl.ClientIDProperty.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_IncludeInPage, (uint)DotvvmControl.IncludeInPageProperty.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyIds.HtmlGenericControl_Visible, (uint)HtmlGenericControl.VisibleProperty.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyIds.HtmlGenericControl_InnerText, (uint)HtmlGenericControl.InnerTextProperty.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyIds.Literal_Text, (uint)Literal.TextProperty.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyIds.ButtonBase_Click, (uint)Button.ClickProperty.Id); + } + + [TestMethod] + public void PropertyGroupIds_MatchGeneratedConstants() + { + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_Attributes, HtmlGenericControl.AttributesGroupDescriptor.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_CssClasses, HtmlGenericControl.CssClassesGroupDescriptor.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_CssStyles, HtmlGenericControl.CssStylesGroupDescriptor.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.RouteLink_Params, RouteLink.ParamsGroupDescriptor.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.RouteLink_QueryParameters, RouteLink.QueryParametersGroupDescriptor.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.JsComponent_Props, JsComponent.PropsGroupDescriptor.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.JsComponent_Templates, JsComponent.TemplatesGroupDescriptor.Id); + } + + [TestMethod] + public void PropertyIds_AreUnique() + { + XAssert.Distinct(DotvvmProperty.AllProperties.Select(p => p.Id)); + } + + [TestMethod] + public void PropertyGroupIds_AreUnique() + { + XAssert.Distinct(DotvvmPropertyGroup.AllGroups.Select(g => g.Id)); + } } } 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"]); + } + } +} diff --git a/src/Tests/Runtime/testoutputs/RuntimeErrorTests.CantFindDataContextSpace.txt b/src/Tests/Runtime/testoutputs/RuntimeErrorTests.CantFindDataContextSpace.txt index 1178fad778..8e328ec381 100644 --- a/src/Tests/Runtime/testoutputs/RuntimeErrorTests.CantFindDataContextSpace.txt +++ b/src/Tests/Runtime/testoutputs/RuntimeErrorTests.CantFindDataContextSpace.txt @@ -1,2 +1,2 @@ InvalidDataContextTypeException occurred: Could not find DataContext space of '{value: False}'. The DataContextType property of the binding does not correspond to DataContextType of the HtmlGenericControl nor any of its ancestors. Control's context is (type=string, par=[int]), binding's context is (type=string). Real data context types: . - at object DotVVM.Framework.Controls.DotvvmBindableObject.EvalPropertyValue(DotvvmProperty property, object value) + at object DotVVM.Framework.Controls.DotvvmBindableObject.EvalBinding(IBinding binding, bool isDataContext)