From 0b138096454a48ffd7804218311f018d9ce29799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 12 Oct 2022 19:07:44 +0200 Subject: [PATCH 01/23] Custom handling of binary operators This is mainly necessary to support our implicit conversions in enum bitwise operators. --- .../Binding/ExpressionBuildingVisitor.cs | 1 + .../Binding/MemberExpressionFactory.cs | 172 ++++--------- .../Binding/MethodGroupExpression.cs | 3 + .../Compilation/Binding/OperatorResolution.cs | 235 ++++++++++++++++++ .../Compilation/Binding/TypeConversions.cs | 126 ++-------- .../Framework/Utils/ReflectionUtils.cs | 3 + src/Tests/Binding/BindingCompilationTests.cs | 16 +- .../Binding/JavascriptCompilationTests.cs | 24 +- 8 files changed, 342 insertions(+), 238 deletions(-) create mode 100644 src/Framework/Framework/Compilation/Binding/OperatorResolution.cs diff --git a/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs b/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs index 0169471082..881a92692a 100644 --- a/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs +++ b/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs @@ -123,6 +123,7 @@ protected override Expression VisitInterpolatedStringExpression(InterpolatedStri { // Translate to a String.Format(...) call var arguments = node.Arguments.Select((arg, index) => HandleErrors(node.Arguments[index], Visit)!).ToArray(); + ThrowOnErrors(); return memberExpressionFactory.Call(target, new[] { Expression.Constant(node.Format) }.Concat(arguments).ToArray()); } else diff --git a/src/Framework/Framework/Compilation/Binding/MemberExpressionFactory.cs b/src/Framework/Framework/Compilation/Binding/MemberExpressionFactory.cs index 186b8ad81b..075a38395c 100644 --- a/src/Framework/Framework/Compilation/Binding/MemberExpressionFactory.cs +++ b/src/Framework/Framework/Compilation/Binding/MemberExpressionFactory.cs @@ -241,6 +241,47 @@ public Expression CallMethod(Type target, BindingFlags flags, string name, Type[ return Expression.Call(method.Method, method.Arguments); } + public Expression? TryCallCustomBinaryOperator(Expression a, Expression b, string operatorName, ExpressionType operatorType) + { + if (a is null) throw new ArgumentNullException(nameof(a)); + if (b is null) throw new ArgumentNullException(nameof(b)); + if (operatorName is null) throw new ArgumentNullException(nameof(b)); + + var searchTypes = new [] { a.Type, b.Type, a.Type.UnwrapNullableType(), b.Type.UnwrapNullableType() }.OfType().Distinct().ToArray(); + + + // https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/expressions.md#1145-binary-operator-overload-resolution + // The set of candidate user-defined operators provided by X and Y for the operation operator «op»(x, y) is determined. The set consists of the union of the candidate operators provided by X and the candidate operators provided by Y, each determined using the rules of §11.4.6. + + var candidateMethods = + searchTypes + .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)) + .Where(m => m.Name == operatorName && !m.IsGenericMethod && m.GetParameters().Length == 2) + .Distinct() + .ToArray(); + + // The overload resolution rules of §11.6.4 are applied to the set of candidate operators to select the best operator with respect to the argument list (x, y), and this operator becomes the result of the overload resolution process. If overload resolution fails to select a single best operator, a binding-time error occurs. + + var matchingMethods = FindValidMethodOverloads(candidateMethods, operatorName, false, null, new[] { a, b }, null); + var liftToNull = matchingMethods.Count == 0 && (a.Type.IsNullable() || b.Type.IsNullable()); + if (liftToNull) + { + matchingMethods = FindValidMethodOverloads(candidateMethods, operatorName, false, null, new[] { a.UnwrapNullable(), b.UnwrapNullable() }, null); + } + + if (matchingMethods.Count == 0) + return null; + var overload = BestOverload(matchingMethods, operatorName); + var parameters = overload.Method.GetParameters(); + + return Expression.MakeBinary( + operatorType, + TypeConversion.EnsureImplicitConversion(a, parameters[0].ParameterType), + TypeConversion.EnsureImplicitConversion(b, parameters[1].ParameterType), + liftToNull: liftToNull, + method: overload.Method + ); + } private MethodRecognitionResult FindValidMethodOverloads(Expression? target, Type type, string name, BindingFlags flags, Type[]? typeArguments, Expression[] arguments, IDictionary? namedArgs) { @@ -250,7 +291,7 @@ private MethodRecognitionResult FindValidMethodOverloads(Expression? target, Typ if (methods.Count == 0) { // We did not find any match in regular methods => try extension methods - if (target != null) + if (target != null && flags.HasFlag(BindingFlags.Instance)) { // Change to a static call var newArguments = new[] { target }.Concat(arguments).ToArray(); @@ -270,6 +311,14 @@ private MethodRecognitionResult FindValidMethodOverloads(Expression? target, Typ } // There are multiple method candidates + return BestOverload(methods, name); + } + + private MethodRecognitionResult BestOverload(List methods, string name) + { + if (methods.Count == 1) + return methods[0]; + methods = methods.OrderBy(s => s.CastCount).ThenBy(s => s.AutomaticTypeArgCount).ThenBy(s => s.HasParamsAttribute).ToList(); var method = methods.First(); var method2 = methods.Skip(1).First(); @@ -329,6 +378,7 @@ public MethodRecognitionResult(int automaticTypeArgCount, int castCount, Express private MethodRecognitionResult? TryCallMethod(MethodInfo method, Type[]? typeArguments, Expression[] positionalArguments, IDictionary? namedArguments) { + if (positionalArguments.Contains(null)) throw new ArgumentNullException("positionalArguments[]"); var parameters = method.GetParameters(); if (!TryPrepareArguments(parameters, positionalArguments, namedArguments, out var args, out var castCount)) @@ -561,131 +611,11 @@ private static bool TryPrepareArguments(ParameterInfo[] parameters, Expression[] return null; } - public Expression EqualsMethod(Expression left, Expression right) - { - Expression? equatable = null; - Expression? theOther = null; - if (typeof(IEquatable<>).IsAssignableFrom(left.Type)) - { - equatable = left; - theOther = right; - } - else if (typeof(IEquatable<>).IsAssignableFrom(right.Type)) - { - equatable = right; - theOther = left; - } - - if (equatable != null) - { - var m = CallMethod(equatable, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Equals", null, new[] { theOther! }); - if (m != null) return m; - } - - if (left.Type.IsValueType) - { - equatable = left; - theOther = right; - } - else if (left.Type.IsValueType) - { - equatable = right; - theOther = left; - } - - if (equatable != null) - { - theOther = TypeConversion.ImplicitConversion(theOther!, equatable.Type); - if (theOther != null) return Expression.Equal(equatable, theOther); - } - - return CallMethod(left, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Equals", null, new[] { right }); - } - - public Expression CompareMethod(Expression left, Expression right) - { - Type compareType = typeof(object); - Expression? equatable = null; - Expression? theOther = null; - if (typeof(IComparable<>).IsAssignableFrom(left.Type)) - { - equatable = left; - theOther = right; - } - else if (typeof(IComparable<>).IsAssignableFrom(right.Type)) - { - equatable = right; - theOther = left; - } - else if (typeof(IComparable).IsAssignableFrom(left.Type)) - { - equatable = left; - theOther = right; - } - else if (typeof(IComparable).IsAssignableFrom(right.Type)) - { - equatable = right; - theOther = left; - } - - if (equatable != null) - { - return CallMethod(equatable, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Compare", null, new[] { theOther! }); - } - throw new NotSupportedException("IComparable is not implemented on any of specified types"); - } - public Expression GetUnaryOperator(Expression expr, ExpressionType operation) { var binder = (DynamicMetaObjectBinder)Microsoft.CSharp.RuntimeBinder.Binder.UnaryOperation( CSharpBinderFlags.None, operation, typeof(object), ExpressionHelper.GetBinderArguments(1)); return ExpressionHelper.ApplyBinder(binder, true, expr)!; } - - public Expression GetBinaryOperator(Expression left, Expression right, ExpressionType operation) - { - if (operation == ExpressionType.Coalesce) - { - // in bindings, most expressions will be nullable due to automatic null-propagation - // the null propagation visitor however runs after this, so we need to convert left to nullable - // to make the validation in Expression.Coalesce happy - var leftNullable = - left.Type.IsValueType && !left.Type.IsNullable() - ? Expression.Convert(left, typeof(Nullable<>).MakeGenericType(left.Type)) - : left; - return Expression.Coalesce(leftNullable, right); - } - if (operation == ExpressionType.Assign) - { - return UpdateMember(left, TypeConversion.ImplicitConversion(right, left.Type, true, true)!) - .NotNull($"Expression '{right}' cannot be assigned into '{left}'."); - } - - // TODO: type conversions - if (operation == ExpressionType.AndAlso) return Expression.AndAlso(left, right); - else if (operation == ExpressionType.OrElse) return Expression.OrElse(left, right); - - var binder = (DynamicMetaObjectBinder)Microsoft.CSharp.RuntimeBinder.Binder.BinaryOperation( - CSharpBinderFlags.None, operation, typeof(object), ExpressionHelper.GetBinderArguments(2)); - var result = ExpressionHelper.ApplyBinder(binder, false, left, right); - if (result != null) return result; - if (operation == ExpressionType.Equal) return EqualsMethod(left, right); - if (operation == ExpressionType.NotEqual) return Expression.Not(EqualsMethod(left, right)); - - - // try converting left to right.Type and vice versa - // needed to enum with pseudo-string literal operations - // if (TypeConversion.ImplicitConversion(left, right.Type) is {} leftConverted) - // return GetBinaryOperator(leftConverted, right, operation); - // if (TypeConversion.ImplicitConversion(right, left.Type) is {} rightConverted) - // return GetBinaryOperator(left, rightConverted, operation); - - // lift the operator - if (left.Type.IsNullable() || right.Type.IsNullable()) - return GetBinaryOperator(left.UnwrapNullable(), right.UnwrapNullable(), operation); - - throw new Exception($"could not apply { operation } binary operator to { left } and { right }"); - // TODO: comparison operators - } } } diff --git a/src/Framework/Framework/Compilation/Binding/MethodGroupExpression.cs b/src/Framework/Framework/Compilation/Binding/MethodGroupExpression.cs index 3876324916..6a8e7f9c3c 100644 --- a/src/Framework/Framework/Compilation/Binding/MethodGroupExpression.cs +++ b/src/Framework/Framework/Compilation/Binding/MethodGroupExpression.cs @@ -92,6 +92,9 @@ public Expression CreateDelegateExpression() public Expression CreateMethodCall(IEnumerable args, MemberExpressionFactory memberExpressionFactory) { var argsArray = args.ToArray(); + if (Array.FindIndex(argsArray, a => a is null || a.Type == typeof(UnknownTypeSentinel)) is var argIdx && argIdx >= 0) + throw new Exception($"Argument {argIdx} is invalid: {this.MethodName}({string.Join(", ", argsArray.Select(a => a))})"); + if (IsStatic) { return memberExpressionFactory.CallMethod(((StaticClassIdentifierExpression)Target).Type, BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy, MethodName, TypeArgs, argsArray); diff --git a/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs b/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs new file mode 100644 index 0000000000..7444041767 --- /dev/null +++ b/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs @@ -0,0 +1,235 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using DotVVM.Framework.Utils; +using FastExpressionCompiler; + +namespace DotVVM.Framework.Compilation.Binding +{ + static class OperatorResolution + { + public static Expression GetBinaryOperator( + this MemberExpressionFactory expressionFactory, + Expression left, + Expression right, + ExpressionType operation) + { + if (operation == ExpressionType.Coalesce) + { + // in bindings, most expressions will be nullable due to automatic null-propagation + // the null propagation visitor however runs after this, so we need to convert left to nullable + // to make the validation in Expression.Coalesce happy + var leftNullable = + left.Type.IsValueType && !left.Type.IsNullable() + ? Expression.Convert(left, typeof(Nullable<>).MakeGenericType(left.Type)) + : left; + return Expression.Coalesce(leftNullable, right); + } + + if (operation == ExpressionType.Assign) + { + return expressionFactory.UpdateMember(left, TypeConversion.EnsureImplicitConversion(right, left.Type, true)!) + .NotNull($"Expression '{right}' cannot be assigned into '{left}'."); + } + + // lift to nullable types when one side is `null` + if (left is ConstantExpression { Value: null } && right.Type.IsValueType) + { + left = Expression.Constant(null, right.Type.MakeNullableType()); + right = Expression.Convert(right, right.Type.MakeNullableType()); + } + if (right is ConstantExpression { Value: null } && left.Type.IsValueType) + { + left = Expression.Convert(left, left.Type.MakeNullableType()); + right = Expression.Constant(null, left.Type.MakeNullableType()); + } + + var leftType = left.Type.UnwrapNullableType(); + var rightType = right.Type.UnwrapNullableType(); + + // we only support booleans + if (operation == ExpressionType.AndAlso) + return Expression.AndAlso(TypeConversion.EnsureImplicitConversion(left, typeof(bool)), TypeConversion.EnsureImplicitConversion(right, typeof(bool))); + else if (operation == ExpressionType.OrElse) + return Expression.OrElse(TypeConversion.EnsureImplicitConversion(left, typeof(bool)), TypeConversion.EnsureImplicitConversion(right, typeof(bool))); + + // skip the slow overload resolution if possible + if (leftType == rightType && !leftType.IsEnum && leftType.IsPrimitive) + return Expression.MakeBinary(operation, left, right); + if (operation == ExpressionType.Add && leftType == typeof(string) && rightType == typeof(string)) + { + return Expression.Add(left, right, typeof(string).GetMethod("Concat", new[] { typeof(string), typeof(string) })); + } + + var customOperator = operation switch { + ExpressionType.Add => "op_Addition", + ExpressionType.Subtract => "op_Subtraction", + ExpressionType.Multiply => "op_Multiply", + ExpressionType.Divide => "op_Division", + ExpressionType.Modulo => "op_Modulus", + ExpressionType.LeftShift => "op_LeftShift", + ExpressionType.RightShift => "op_RightShift", + ExpressionType.And => "op_BitwiseAnd", + ExpressionType.Or => "op_BitwiseOr", + ExpressionType.ExclusiveOr => "op_ExclusiveOr", + ExpressionType.Equal => "op_Equality", + ExpressionType.NotEqual => "op_Inequality", + ExpressionType.GreaterThan => "op_GreaterThan", + ExpressionType.LessThan => "op_LessThan", + ExpressionType.GreaterThanOrEqual => "op_GreaterThanOrEqual", + ExpressionType.LessThanOrEqual => "op_LessThanOrEqual", + _ => null + }; + + + // Try to find user defined operator + if (customOperator != null && (!leftType.IsPrimitive || !rightType.IsPrimitive)) + { + var customOperatorExpr = expressionFactory.TryCallCustomBinaryOperator(left, right, customOperator, operation); + if (customOperatorExpr is {}) + return customOperatorExpr; + } + + if (leftType.IsEnum && rightType.IsEnum && leftType != rightType) + { + throw new InvalidOperationException($"Cannot apply {operation} operator to two different enum types: {leftType.Name}, {rightType.Name}."); + } + + // numeric operations + if (operation == ExpressionType.LeftShift) + { + return Expression.LeftShift(left, ConvertToMaybeNullable(right, typeof(int), true)); + } + else if (operation == ExpressionType.RightShift) + { + return Expression.RightShift(left, ConvertToMaybeNullable(right, typeof(int), true)); + } + + // List of types in order of precendence + var enumType = leftType.IsEnum ? leftType : rightType.IsEnum ? rightType : null; + var typeList = operation switch { + ExpressionType.Or or ExpressionType.And or ExpressionType.ExclusiveOr => + new[] { typeof(bool), enumType, typeof(int), typeof(uint), typeof(long), typeof(ulong) }, + _ => + new[] { enumType, typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) } + }; + + foreach (var commonType in typeList) + { + if (commonType == null) continue; + + var leftConverted = ConvertToMaybeNullable(left, commonType, throwExceptions: false); + var rightConverted = ConvertToMaybeNullable(right, commonType, throwExceptions: false); + + if (leftConverted != null && rightConverted != null) + { + return MakeBinary(operation, leftConverted, rightConverted); + } + } + + if (operation == ExpressionType.Add && (leftType == typeof(string) || rightType == typeof(string))) + { + return Expression.Add( + Expression.Convert(left, typeof(object)), + Expression.Convert(right, typeof(object)), + typeof(string).GetMethod("Concat", new[] { typeof(object), typeof(object) }) + ); + } + + + // if (left.Type.IsNullable() || right.Type.IsNullable()) + // return GetBinaryOperator(expressionFactory, left.UnwrapNullable(), right.UnwrapNullable(), operation); + + // as a fallback, try finding overridden Equals method + if (operation == ExpressionType.Equal) return EqualsMethod(expressionFactory, left, right); + if (operation == ExpressionType.NotEqual) return Expression.Not(EqualsMethod(expressionFactory, left, right)); + + throw new InvalidOperationException($"Cannot apply {operation} operator to types {left.Type.Name} and {right.Type.Name}."); + } + + public static Expression EqualsMethod( + MemberExpressionFactory expressionFactory, + Expression left, + Expression right + ) + { + Expression? equatable = null; + Expression? theOther = null; + if (typeof(IEquatable<>).IsAssignableFrom(left.Type)) + { + equatable = left; + theOther = right; + } + else if (typeof(IEquatable<>).IsAssignableFrom(right.Type)) + { + equatable = right; + theOther = left; + } + + if (equatable != null) + { + var m = expressionFactory.CallMethod(equatable, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Equals", null, new[] { theOther! }); + if (m != null) return m; + } + + if (left.Type.IsValueType) + { + equatable = left; + theOther = right; + } + else if (left.Type.IsValueType) + { + equatable = right; + theOther = left; + } + + if (equatable != null) + { + theOther = TypeConversion.ImplicitConversion(theOther!, equatable.Type); + if (theOther != null) return Expression.Equal(equatable, theOther); + } + + return expressionFactory.CallMethod(left, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Equals", null, new[] { right }); + } + + static Expression? ConvertToMaybeNullable( + Expression expression, + Type targetType, + bool throwExceptions + ) + { + return TypeConversion.ImplicitConversion(expression, targetType) ?? + TypeConversion.ImplicitConversion(expression, targetType.MakeNullableType(), throwExceptions); + } + + static Expression MakeBinary(ExpressionType type, Expression left, Expression right) + { + // Expression.MakeBinary doesn't handle enums, we need to convert it to the int and back + // It works however, for Equals/NotEquals + Type? enumType = null; + + if (type != ExpressionType.Equal && type != ExpressionType.NotEqual) + { + if (left.Type.UnwrapNullableType().IsEnum) + { + enumType = left.Type.UnwrapNullableType(); + left = ConvertToMaybeNullable(left, Enum.GetUnderlyingType(enumType), true)!; + } + if (right.Type.UnwrapNullableType().IsEnum) + { + enumType = right.Type.UnwrapNullableType(); + right = ConvertToMaybeNullable(right, Enum.GetUnderlyingType(enumType), true)!; + } + } + + var result = Expression.MakeBinary(type, left, right); + if (enumType != null && result.Type != typeof(bool)) + { + return Expression.Convert(result, enumType); + } + return result; + } + } +} diff --git a/src/Framework/Framework/Compilation/Binding/TypeConversions.cs b/src/Framework/Framework/Compilation/Binding/TypeConversions.cs index f9a297d5d9..474c92e551 100644 --- a/src/Framework/Framework/Compilation/Binding/TypeConversions.cs +++ b/src/Framework/Framework/Compilation/Binding/TypeConversions.cs @@ -11,78 +11,24 @@ using DotVVM.Framework.Compilation.ControlTree.Resolved; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.HelperNamespace; +using FastExpressionCompiler; namespace DotVVM.Framework.Compilation.Binding { public class TypeConversion { - private static Dictionary> ImplicitNumericConversions = new Dictionary>(); - private static readonly Dictionary typePrecedence; - - /// - /// Performs implicit conversion between two expressions depending on their type precedence - /// - /// - /// - internal static void Convert(ref Expression le, ref Expression re) - { - if (typePrecedence.ContainsKey(le.Type) && typePrecedence.ContainsKey(re.Type)) - { - if (typePrecedence[le.Type] > typePrecedence[re.Type]) re = Expression.Convert(re, le.Type); - if (typePrecedence[le.Type] < typePrecedence[re.Type]) le = Expression.Convert(le, re.Type); - } - } - - /// - /// Performs implicit conversion on an expression against a specified type - /// - /// - /// - /// - internal static Expression Convert(Expression le, Type type) - { - if (typePrecedence.ContainsKey(le.Type) && typePrecedence.ContainsKey(type)) - { - if (typePrecedence[le.Type] < typePrecedence[type]) return Expression.Convert(le, type); - } - if (le.Type.IsNullable() && Nullable.GetUnderlyingType(le.Type) == type) - { - le = Expression.Property(le, "Value"); - } - if (type.IsNullable() && Nullable.GetUnderlyingType(type) == le.Type) - { - le = Expression.Convert(le, type); - } - if (type == typeof(object)) - { - return Expression.Convert(le, type); - } - if (le.Type == typeof(object)) - { - return Expression.Convert(le, type); - } - return le; - } - - /// - /// Compares two types for implicit conversion - /// - /// The source type - /// The destination type - /// -1 if conversion is not possible, 0 if no conversion necessary, +1 if conversion possible - internal static int CanConvert(Type from, Type to) - { - if (typePrecedence.ContainsKey(@from) && typePrecedence.ContainsKey(to)) - { - return typePrecedence[to] - typePrecedence[@from]; - } - else - { - if (@from == to) return 0; - if (to.IsAssignableFrom(@from)) return 1; - } - return -1; - } + private static Dictionary ImplicitNumericConversions = new() { + [typeof(sbyte)] = new Type[] { typeof(short), typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal) }, + [typeof(byte)] = new Type[] { typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }, + [typeof(short)] = new Type[] { typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal) }, + [typeof(ushort)] = new Type[] { typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }, + [typeof(int)] = new Type[] { typeof(long), typeof(float), typeof(double), typeof(decimal) }, + [typeof(uint)] = new Type[] { typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }, + [typeof(long)] = new Type[] { typeof(float), typeof(double), typeof(decimal) }, + [typeof(ulong)] = new Type[] { typeof(float), typeof(double), typeof(decimal) }, + [typeof(char)] = new Type[] { typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }, + [typeof(float)] = new Type[] { typeof(double) }, + }; // 6.1.7 Boxing Conversions // A boxing conversion permits a value-type to be implicitly converted to a reference type. A boxing conversion exists from any non-nullable-value-type to object and dynamic, @@ -160,12 +106,14 @@ public static Expression BoxToObject(Expression src) } //TODO: Refactor ImplicitConversion usages to EnsureImplicitConversion where applicable to take advantage of nullability - public static Expression EnsureImplicitConversion(Expression src, Type destType) + public static Expression EnsureImplicitConversion(Expression src, Type destType, bool allowToString = false) => ImplicitConversion(src, destType, true, false)!; // 6.1 Implicit Conversions public static Expression? ImplicitConversion(Expression src, Type destType, bool throwException = false, bool allowToString = false) { + if (src is null) throw new ArgumentNullException(nameof(src)); + if (destType is null) throw new ArgumentNullException(nameof(destType)); if (src is MethodGroupExpression methodGroup) { return methodGroup.CreateDelegateExpression(destType, throwException); @@ -182,7 +130,7 @@ public static Expression EnsureImplicitConversion(Expression src, Type destType) { result = ToStringConversion(src); } - if (throwException && result == null) throw new InvalidOperationException($"Could not implicitly convert expression of type { src.Type } to { destType }."); + if (throwException && result == null) throw new InvalidOperationException($"Could not implicitly convert expression of type { src.Type.ToCode() } to { destType.ToCode() }."); return result; } @@ -241,42 +189,42 @@ public static bool IsStringConversionAllowed(Type fromType) { if (value >= SByte.MinValue && value <= SByte.MinValue) { - return Expression.Constant((sbyte)srcValue, typeof(sbyte)); + return Expression.Constant((sbyte)value, typeof(sbyte)); } } if (destType == typeof(byte)) { if (value >= Byte.MinValue && value <= Byte.MaxValue) { - return Expression.Constant((byte)srcValue, typeof(byte)); + return Expression.Constant((byte)value, typeof(byte)); } } if (destType == typeof(short)) { if (value >= Int16.MinValue && value <= Int16.MaxValue) { - return Expression.Constant((short)srcValue, typeof(short)); + return Expression.Constant((short)value, typeof(short)); } } if (destType == typeof(ushort)) { if (value >= UInt16.MinValue && value <= UInt16.MaxValue) { - return Expression.Constant((ushort)srcValue, typeof(ushort)); + return Expression.Constant((ushort)value, typeof(ushort)); } } if (destType == typeof(uint)) { if (value >= uint.MinValue) { - return Expression.Constant((uint)srcValue, typeof(uint)); + return Expression.Constant((uint)value, typeof(uint)); } } if (destType == typeof(ulong)) { if (value >= 0) { - return Expression.Constant((ulong)srcValue, typeof(ulong)); + return Expression.Constant((ulong)value, typeof(ulong)); } } } @@ -288,7 +236,7 @@ public static bool IsStringConversionAllowed(Type fromType) { if (value >= 0) { - return Expression.Constant((ulong)srcValue, typeof(ulong)); + return Expression.Constant((ulong)value, typeof(ulong)); } } } @@ -449,31 +397,5 @@ private static Type GetTaskType(Type taskType) else return null; } - - static TypeConversion() - { - typePrecedence = new Dictionary - { - {typeof (object), 0}, - {typeof (bool), 1}, - {typeof (byte), 2}, - {typeof (int), 3}, - {typeof (short), 4}, - {typeof (long), 5}, - {typeof (float), 6}, - {typeof (double), 7} - }; - - ImplicitNumericConversions.Add(typeof(sbyte), new List() { typeof(short), typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(byte), new List() { typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(short), new List() { typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(ushort), new List() { typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(int), new List() { typeof(long), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(uint), new List() { typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(long), new List() { typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(ulong), new List() { typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(char), new List() { typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }); - ImplicitNumericConversions.Add(typeof(float), new List() { typeof(double) }); - } } } diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index dd942ca4c3..d69347e182 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -405,6 +405,9 @@ public static bool Implements(this Type type, Type ifc, [NotNullWhen(true)] out return (concreteInterface = type.GetInterfaces().FirstOrDefault(i => isInterface(i, ifc))) != null; } + public static bool IsAssignableFromNull(this Type t) => + !t.IsValueType || t.IsNullable(); + public static bool IsNullable(this Type type) { return Nullable.GetUnderlyingType(type) != null; diff --git a/src/Tests/Binding/BindingCompilationTests.cs b/src/Tests/Binding/BindingCompilationTests.cs index 8a6d0d59a2..6b1f522ce5 100755 --- a/src/Tests/Binding/BindingCompilationTests.cs +++ b/src/Tests/Binding/BindingCompilationTests.cs @@ -585,6 +585,16 @@ public void BindingCompiler_Invalid_EnumStringComparison() }); } + [TestMethod] + public void BindingCompiler_Valid_EnumBitOps() + { + var viewModel = new TestViewModel { EnumProperty = TestEnum.A }; + Assert.AreEqual(TestEnum.A, ExecuteBinding("EnumProperty & 1", viewModel)); + Assert.AreEqual(TestEnum.B, ExecuteBinding("EnumProperty | 1", viewModel)); + Assert.AreEqual(TestEnum.B, ExecuteBinding("EnumProperty | 'B'", viewModel)); + Assert.AreEqual(TestEnum.C, ExecuteBinding("(EnumProperty | 'D') & 'C'", viewModel)); + } + [TestMethod] public void BindingCompiler_Valid_GenericMethodCall() @@ -944,7 +954,7 @@ public void BindingCompiler_MultiBlockExpression_EnumAtEnd_CorrectResult() } [TestMethod] - [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Could not implicitly convert expression of type System.Void to System.Object")] + [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Could not implicitly convert expression of type void to object")] public void BindingCompiler_MultiBlockExpression_EmptyBlockAtEnd_Throws() { TestViewModel vm = new TestViewModel { StringProp = "a" }; @@ -952,7 +962,7 @@ public void BindingCompiler_MultiBlockExpression_EmptyBlockAtEnd_Throws() } [TestMethod] - [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Could not implicitly convert expression of type System.Void to System.Object")] + [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Could not implicitly convert expression of type void to object")] public void BindingCompiler_MultiBlockExpression_WhitespaceBlockAtEnd_Throws() { TestViewModel vm = new TestViewModel { StringProp = "a" }; @@ -1050,7 +1060,7 @@ public void BindingCompiler_Errors_AssigningToType() { var aggEx = Assert.ThrowsException(() => ExecuteBinding("System.String = 123", new [] { new TestViewModel() })); var ex = aggEx.GetBaseException(); - StringAssert.Contains(ex.Message, "cannot be assigned into"); + StringAssert.Contains(ex.Message, "Could not implicitly convert expression of type int to string"); } [TestMethod] diff --git a/src/Tests/Binding/JavascriptCompilationTests.cs b/src/Tests/Binding/JavascriptCompilationTests.cs index 49191eaa39..9f0f0fca81 100644 --- a/src/Tests/Binding/JavascriptCompilationTests.cs +++ b/src/Tests/Binding/JavascriptCompilationTests.cs @@ -119,18 +119,18 @@ public void JavascriptCompilation_Parent() } [DataTestMethod] - [DataRow("2+2", "2+2", DisplayName = "2+2")] - [DataRow("2+2+2", "2+2+2", DisplayName = "2+2+2")] - [DataRow("(2+2)+2", "2+2+2", DisplayName = "(2+2)+2")] - [DataRow("2+(2+2)", "2+(2+2)", DisplayName = "2+(2+2)")] - [DataRow("2+(2*2)", "2+2*2", DisplayName = "2+(2*2)")] - [DataRow("2*(2+2)", "2*(2+2)", DisplayName = "2*(2+2)")] - [DataRow("IntProp & (2+2)", "IntProp()&2+2", DisplayName = "IntProp & (2+2)")] - [DataRow("IntProp & 2+2", "IntProp()&2+2", DisplayName = "IntProp & 2+2")] - [DataRow("IntProp & -1", "IntProp()&-1", DisplayName = "IntProp & -1")] - [DataRow("'a' + 'b'", "\"ab\"", DisplayName = "'a' + 'b'")] - [DataRow("IntProp ^ 1", "IntProp()^1", DisplayName = "IntProp ^ 1")] - [DataRow("'xx' + IntProp", "\"xx\"+IntProp()", DisplayName = "'xx' + IntProp")] + // [DataRow("2+2", "2+2", DisplayName = "2+2")] + // [DataRow("2+2+2", "2+2+2", DisplayName = "2+2+2")] + // [DataRow("(2+2)+2", "2+2+2", DisplayName = "(2+2)+2")] + // [DataRow("2+(2+2)", "2+(2+2)", DisplayName = "2+(2+2)")] + // [DataRow("2+(2*2)", "2+2*2", DisplayName = "2+(2*2)")] + // [DataRow("2*(2+2)", "2*(2+2)", DisplayName = "2*(2+2)")] + // [DataRow("IntProp & (2+2)", "IntProp()&2+2", DisplayName = "IntProp & (2+2)")] + // [DataRow("IntProp & 2+2", "IntProp()&2+2", DisplayName = "IntProp & 2+2")] + // [DataRow("IntProp & -1", "IntProp()&-1", DisplayName = "IntProp & -1")] + // [DataRow("'a' + 'b'", "\"ab\"", DisplayName = "'a' + 'b'")] + // [DataRow("IntProp ^ 1", "IntProp()^1", DisplayName = "IntProp ^ 1")] + // [DataRow("'xx' + IntProp", "\"xx\"+IntProp()", DisplayName = "'xx' + IntProp")] [DataRow("true == (IntProp == 1)", "true==(IntProp()==1)", DisplayName = "true == (IntProp == 1)")] public void JavascriptCompilation_BinaryExpressions(string expr, string expectedJs) { From 168d21ab7630cc4f0580d0e8225ed64e0ff7dc4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 21 Oct 2022 14:54:30 +0200 Subject: [PATCH 02/23] Add tests for OperatorResolution --- .../Compilation/Binding/OperatorResolution.cs | 10 ++++ src/Tests/Binding/BindingCompilationTests.cs | 22 ++++++++ .../Binding/JavascriptCompilationTests.cs | 56 +++++++++++++++++-- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs b/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs index 7444041767..ce12d428d7 100644 --- a/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs +++ b/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs @@ -46,6 +46,16 @@ public static Expression GetBinaryOperator( right = Expression.Constant(null, left.Type.MakeNullableType()); } + // lift the other side to null + if (left.Type.IsNullable() && right.Type.IsValueType && !right.Type.IsNullable()) + { + right = Expression.Convert(right, right.Type.MakeNullableType()); + } + else if (right.Type.IsNullable() && left.Type.IsValueType && !left.Type.IsNullable()) + { + left = Expression.Convert(left, left.Type.MakeNullableType()); + } + var leftType = left.Type.UnwrapNullableType(); var rightType = right.Type.UnwrapNullableType(); diff --git a/src/Tests/Binding/BindingCompilationTests.cs b/src/Tests/Binding/BindingCompilationTests.cs index 6b1f522ce5..2a13089016 100755 --- a/src/Tests/Binding/BindingCompilationTests.cs +++ b/src/Tests/Binding/BindingCompilationTests.cs @@ -1113,6 +1113,22 @@ public void Error_DifferentDataContext() ); } + [DataTestMethod] + [DataRow("IntProp + 1L", 101L)] + [DataRow("1L + IntProp", 101L)] + [DataRow("1L + UIntProp", 3_000_000_001L)] + [DataRow("1 + UIntProp", (uint)3_000_000_001)] + [DataRow("ShortProp", short.MaxValue)] + [DataRow("ShortProp - 1", short.MaxValue - 1)] + [DataRow("DoubleProp - 1", 0.5)] + [DataRow("DoubleProp + ShortProp", short.MaxValue + 1.5)] + [DataRow("NullableDoubleProp + ShortProp", null)] + public void BindingCompiler_OperatorType(string expr, object expectedResult) + { + var vm = new TestViewModel { IntProp = 100, DoubleProp = 1.5 }; + Assert.AreEqual(expectedResult, ExecuteBinding(expr, vm)); + } +>>>>>>> ba2cf6708 (Add tests for OperatorResolution) } class TestViewModel { @@ -1147,6 +1163,12 @@ class TestViewModel public TestViewModel2[] VmArray => new TestViewModel2[] { new TestViewModel2() }; public int[] IntArray { get; set; } public decimal DecimalProp { get; set; } + public byte ByteProp { get; set; } = 255; + public sbyte SByteProp { get; set; } = 127; + public short ShortProp { get; set; } = 32767; + public ushort UShortProp { get; set; } = 65535; + public uint UIntProp { get; set; } = 3_000_000_000; + public double? NullableDoubleProp { get; set; } public ReadOnlyCollection ReadOnlyCollection => new ReadOnlyCollection(new[] { 1, 2, 3 }); diff --git a/src/Tests/Binding/JavascriptCompilationTests.cs b/src/Tests/Binding/JavascriptCompilationTests.cs index 9f0f0fca81..d6da0eafda 100644 --- a/src/Tests/Binding/JavascriptCompilationTests.cs +++ b/src/Tests/Binding/JavascriptCompilationTests.cs @@ -58,11 +58,57 @@ public string CompileBinding(Func, Expression> ex return JavascriptTranslator.FormatKnockoutScript(jsExpression); } - [TestMethod] - public void JavascriptCompilation_EnumComparison() - { - var js = CompileBinding($"_this == 'Local'", typeof(DateTimeKind)); - Assert.AreEqual("$data==\"Local\"", js); + [DataTestMethod] + [DataRow("_this == 'Local'", "$data==\"Local\"")] + [DataRow("'Local' == _this", "\"Local\"==$data")] + [DataRow("_this == 2", "$data==\"Local\"")] + [DataRow("2 == _this", "\"Local\"==$data")] + [DataRow("_this & 'Local'", "dotvvm.translations.enums.fromInt(dotvvm.translations.enums.toInt($data,\"G0/GAE51KlQlMR5T\")&2,\"G0/GAE51KlQlMR5T\")")] + [DataRow("'Local' & _this", "dotvvm.translations.enums.fromInt(2&dotvvm.translations.enums.toInt($data,\"G0/GAE51KlQlMR5T\"),\"G0/GAE51KlQlMR5T\")")] + [DataRow("_this | 'Local'", "dotvvm.translations.enums.fromInt(dotvvm.translations.enums.toInt($data,\"G0/GAE51KlQlMR5T\")|2,\"G0/GAE51KlQlMR5T\")")] + [DataRow("_this & 1", "dotvvm.translations.enums.fromInt(dotvvm.translations.enums.toInt($data,\"G0/GAE51KlQlMR5T\")&1,\"G0/GAE51KlQlMR5T\")")] + [DataRow("1 & _this", "dotvvm.translations.enums.fromInt(1&dotvvm.translations.enums.toInt($data,\"G0/GAE51KlQlMR5T\"),\"G0/GAE51KlQlMR5T\")")] + public void JavascriptCompilation_EnumOperators(string expr, string expectedJs) + { + var js = CompileBinding(expr, typeof(DateTimeKind)); + Assert.AreEqual(expectedJs, js); + } + + [DataTestMethod] + [DataRow("StringProp + StringProp", "(StringProp()??\"\")+(StringProp()??\"\")")] + [DataRow("StringProp + null", "StringProp()??\"\"")] + [DataRow("null + StringProp", "StringProp()??\"\"")] + [DataRow("'' + StringProp", "StringProp()??\"\"")] + [DataRow("BoolProp + StringProp", "BoolProp()+(StringProp()??\"\")")] + [DataRow("IntProp + IntProp + 'aa'", "IntProp()+IntProp()+\"aa\"")] + [DataRow("DoubleProp + 'aa'", "DoubleProp()+\"aa\"")] + [DataRow("'a' + DoubleProp", "\"a\"+DoubleProp()")] + [DataRow("'a' + NullableIntProp + null", "\"a\"+(NullableIntProp()??\"\")")] + public void JavascriptCompilation_StringPlus(string expr, string expectedJs) + { + var js = CompileBinding(expr, typeof(TestViewModel)); + Assert.AreEqual(expectedJs, js); + } + + [DataTestMethod] + [DataRow("NullableIntProp + NullableDoubleProp", "NullableIntProp()+NullableDoubleProp()")] + [DataRow("NullableIntProp & NullableIntProp", "NullableIntProp()&NullableIntProp()")] + [DataRow("NullableIntProp | NullableIntProp", "NullableIntProp()|NullableIntProp()")] + [DataRow("NullableIntProp ^ NullableIntProp", "NullableIntProp()^NullableIntProp()")] + [DataRow("NullableIntProp + 10", "NullableIntProp()+10")] + [DataRow("NullableIntProp - 10L", "NullableIntProp()-10")] + [DataRow("NullableIntProp / 10.0", "NullableIntProp()/10.0")] + [DataRow("10.0 / NullableIntProp", "10.0/NullableIntProp()")] + [DataRow("null / NullableIntProp", "null/NullableIntProp()")] + [DataRow("null == NullableIntProp", "null==NullableIntProp()")] + [DataRow("10 > NullableIntProp", "10>NullableIntProp()")] + [DataRow("NullableDoubleProp < 10", "NullableDoubleProp()<10")] + [DataRow("10+null", "10+null")] + [DataRow("10+null", "10+null")] + public void JavascriptCompilation_NullableOps(string expr, string expectedJs) + { + var js = CompileBinding(expr, typeof(TestViewModel)); + Assert.AreEqual(expectedJs, js); } [TestMethod] From 81c264bd9f2f377d41ae30d7b3b8e8e11fefea33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 1 Jul 2023 11:55:17 +0200 Subject: [PATCH 03/23] Fix ToString conversion in assignment --- .../Framework/Binding/BindingFactory.cs | 2 +- .../Binding/ExpressionBuildingVisitor.cs | 2 +- .../ExpressionNullPropagationVisitor.cs | 8 ++++---- .../Binding/GeneralBindingPropertyResolvers.cs | 2 +- .../Binding/MemberExpressionFactory.cs | 2 +- .../Compilation/Binding/OperatorResolution.cs | 6 +++--- .../Compilation/Binding/TypeConversions.cs | 2 +- src/Framework/Testing/BindingTestHelper.cs | 2 +- src/Tests/Binding/BindingCompilationTests.cs | 3 +-- src/Tests/Binding/ImplicitConversionTests.cs | 18 +++++++++--------- 10 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/Framework/Framework/Binding/BindingFactory.cs b/src/Framework/Framework/Binding/BindingFactory.cs index dc6ae7d584..6b60c2b842 100644 --- a/src/Framework/Framework/Binding/BindingFactory.cs +++ b/src/Framework/Framework/Binding/BindingFactory.cs @@ -44,7 +44,7 @@ public static IBinding CreateBinding(this BindingCompilationService service, Typ if (ctor == null) throw new NotSupportedException($"Could not find .ctor(BindingCompilationService service, object[] properties) on binding '{type.FullName}'."); var bindingServiceParam = Expression.Parameter(typeof(BindingCompilationService)); var propertiesParam = Expression.Parameter(typeof(object?[])); - var expression = Expression.New(ctor, bindingServiceParam, TypeConversion.ImplicitConversion(propertiesParam, ctor.GetParameters()[1].ParameterType, throwException: true)!); + var expression = Expression.New(ctor, bindingServiceParam, TypeConversion.EnsureImplicitConversion(propertiesParam, ctor.GetParameters()[1].ParameterType)); return Expression.Lambda>(expression, bindingServiceParam, propertiesParam).CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression); })(service, properties); } diff --git a/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs b/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs index 881a92692a..e0d8aced28 100644 --- a/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs +++ b/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs @@ -295,7 +295,7 @@ protected override Expression VisitAssemblyQualifiedName(AssemblyQualifiedNameBi protected override Expression VisitConditionalExpression(ConditionalExpressionBindingParserNode node) { - var condition = HandleErrors(node.ConditionExpression, n => TypeConversion.ImplicitConversion(Visit(n), typeof(bool), true)); + var condition = HandleErrors(node.ConditionExpression, n => TypeConversion.EnsureImplicitConversion(Visit(n), typeof(bool))); var trueExpr = HandleErrors(node.TrueExpression, Visit)!; var falseExpr = HandleErrors(node.FalseExpression, Visit)!; ThrowOnErrors(); diff --git a/src/Framework/Framework/Compilation/Binding/ExpressionNullPropagationVisitor.cs b/src/Framework/Framework/Compilation/Binding/ExpressionNullPropagationVisitor.cs index 2c0f9c1729..51ead1e4ee 100644 --- a/src/Framework/Framework/Compilation/Binding/ExpressionNullPropagationVisitor.cs +++ b/src/Framework/Framework/Compilation/Binding/ExpressionNullPropagationVisitor.cs @@ -123,8 +123,8 @@ protected override Expression VisitConditional(ConditionalExpression node) if (ifTrue.Type != ifFalse.Type) { var nullable = ifTrue.Type.IsNullable() ? ifTrue.Type : ifFalse.Type; - ifTrue = TypeConversion.ImplicitConversion(ifTrue, nullable, throwException: true)!; - ifFalse = TypeConversion.ImplicitConversion(ifFalse, nullable, throwException: true)!; + ifTrue = TypeConversion.EnsureImplicitConversion(ifTrue, nullable); + ifFalse = TypeConversion.EnsureImplicitConversion(ifFalse, nullable); } return Expression.Condition(test, ifTrue, ifFalse); }); @@ -181,7 +181,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node) { return CheckForNull(Visit(node.Arguments.First()), index => { - var convertedIndex = TypeConversion.ImplicitConversion(index, node.Method.GetParameters().First().ParameterType, throwException: true)!; + var convertedIndex = TypeConversion.EnsureImplicitConversion(index, node.Method.GetParameters().First().ParameterType); return Expression.Call(target, node.Method, new[] { convertedIndex }.Concat(node.Arguments.Skip(1))); }); }, suppress: node.Object?.Type?.IsNullable() ?? true); @@ -244,7 +244,7 @@ protected Expression CheckForNull(Expression? parameter, Func TypeConversion.ImplicitConversion(a, elm, throwException: true)!) + .Select(a => TypeConversion.EnsureImplicitConversion(a, elm)) .ToArray(); args[i] = NewArrayExpression.NewArrayInit(elm, converted); } diff --git a/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs b/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs index ce12d428d7..3665478c9f 100644 --- a/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs +++ b/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs @@ -30,7 +30,7 @@ public static Expression GetBinaryOperator( if (operation == ExpressionType.Assign) { - return expressionFactory.UpdateMember(left, TypeConversion.EnsureImplicitConversion(right, left.Type, true)!) + return expressionFactory.UpdateMember(left, TypeConversion.EnsureImplicitConversion(right, left.Type, true)) .NotNull($"Expression '{right}' cannot be assigned into '{left}'."); } @@ -110,11 +110,11 @@ public static Expression GetBinaryOperator( // numeric operations if (operation == ExpressionType.LeftShift) { - return Expression.LeftShift(left, ConvertToMaybeNullable(right, typeof(int), true)); + return Expression.LeftShift(left, ConvertToMaybeNullable(right, typeof(int), true)!); } else if (operation == ExpressionType.RightShift) { - return Expression.RightShift(left, ConvertToMaybeNullable(right, typeof(int), true)); + return Expression.RightShift(left, ConvertToMaybeNullable(right, typeof(int), true)!); } // List of types in order of precendence diff --git a/src/Framework/Framework/Compilation/Binding/TypeConversions.cs b/src/Framework/Framework/Compilation/Binding/TypeConversions.cs index 474c92e551..0b2d47c600 100644 --- a/src/Framework/Framework/Compilation/Binding/TypeConversions.cs +++ b/src/Framework/Framework/Compilation/Binding/TypeConversions.cs @@ -107,7 +107,7 @@ public static Expression BoxToObject(Expression src) //TODO: Refactor ImplicitConversion usages to EnsureImplicitConversion where applicable to take advantage of nullability public static Expression EnsureImplicitConversion(Expression src, Type destType, bool allowToString = false) - => ImplicitConversion(src, destType, true, false)!; + => ImplicitConversion(src, destType, throwException: true, allowToString: allowToString)!; // 6.1 Implicit Conversions public static Expression? ImplicitConversion(Expression src, Type destType, bool throwException = false, bool allowToString = false) diff --git a/src/Framework/Testing/BindingTestHelper.cs b/src/Framework/Testing/BindingTestHelper.cs index ebbd42e81c..4d11ca65d8 100644 --- a/src/Framework/Testing/BindingTestHelper.cs +++ b/src/Framework/Testing/BindingTestHelper.cs @@ -75,7 +75,7 @@ public Expression ParseBinding(string expression, DataContextStack context, Type var parsedExpression = ExpressionBuilder.ParseWithLambdaConversion(expression, context, BindingParserOptions.Value.AddImports(imports), expectedType); return TypeConversion.MagicLambdaConversion(parsedExpression, expectedType) ?? - TypeConversion.ImplicitConversion(parsedExpression, expectedType, true, true)!; + TypeConversion.EnsureImplicitConversion(parsedExpression, expectedType, allowToString: true)!; } /// Returns JavaScript code to which the translates. diff --git a/src/Tests/Binding/BindingCompilationTests.cs b/src/Tests/Binding/BindingCompilationTests.cs index 2a13089016..2d7010d0ba 100755 --- a/src/Tests/Binding/BindingCompilationTests.cs +++ b/src/Tests/Binding/BindingCompilationTests.cs @@ -1060,7 +1060,7 @@ public void BindingCompiler_Errors_AssigningToType() { var aggEx = Assert.ThrowsException(() => ExecuteBinding("System.String = 123", new [] { new TestViewModel() })); var ex = aggEx.GetBaseException(); - StringAssert.Contains(ex.Message, "Could not implicitly convert expression of type int to string"); + StringAssert.Contains(ex.Message, "Expression '123' cannot be assigned into 'System.String'."); } [TestMethod] @@ -1128,7 +1128,6 @@ public void BindingCompiler_OperatorType(string expr, object expectedResult) var vm = new TestViewModel { IntProp = 100, DoubleProp = 1.5 }; Assert.AreEqual(expectedResult, ExecuteBinding(expr, vm)); } ->>>>>>> ba2cf6708 (Add tests for OperatorResolution) } class TestViewModel { diff --git a/src/Tests/Binding/ImplicitConversionTests.cs b/src/Tests/Binding/ImplicitConversionTests.cs index 94a56465ed..8400dac2d3 100644 --- a/src/Tests/Binding/ImplicitConversionTests.cs +++ b/src/Tests/Binding/ImplicitConversionTests.cs @@ -13,30 +13,30 @@ public class ImplicitConversionTests [TestMethod] public void Conversion_IntToNullableDouble() { - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(int)), typeof(double?), throwException: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(int)), typeof(double?)); } [TestMethod] public void Conversion_DoubleNullable() { - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(double)), typeof(double?), throwException: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(double)), typeof(double?)); } [TestMethod] public void Conversion_IntToDouble() { - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(int)), typeof(double), throwException: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(int)), typeof(double)); } [TestMethod] public void Conversion_ValidToString() { - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(DateTime)), typeof(string), throwException: true, allowToString: true); - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(int)), typeof(string), throwException: true, allowToString: true); - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(string)), typeof(string), throwException: true, allowToString: true); - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(double)), typeof(string), throwException: true, allowToString: true); - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(TimeSpan)), typeof(string), throwException: true, allowToString: true); - TypeConversion.ImplicitConversion(Expression.Parameter(typeof(Tuple)), typeof(string), throwException: true, allowToString: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(DateTime)), typeof(string), allowToString: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(int)), typeof(string), allowToString: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(string)), typeof(string), allowToString: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(double)), typeof(string), allowToString: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(TimeSpan)), typeof(string), allowToString: true); + TypeConversion.EnsureImplicitConversion(Expression.Parameter(typeof(Tuple)), typeof(string), allowToString: true); } [TestMethod] From a98d13a98e7bcfb7cb75eeef97f6d4cd3333db59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 1 Jul 2023 15:36:04 +0200 Subject: [PATCH 04/23] Fix equality operator resolution --- .../Compilation/Binding/OperatorResolution.cs | 77 +++++++------------ .../Tests/Tests/DotVVM.Samples.Tests.csproj | 4 +- src/Tests/Binding/BindingCompilationTests.cs | 36 ++++++++- .../Binding/JavascriptCompilationTests.cs | 30 +++++--- .../Parser/Binding/BindingParserTests.cs | 2 +- 5 files changed, 81 insertions(+), 68 deletions(-) diff --git a/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs b/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs index 3665478c9f..ba475be74d 100644 --- a/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs +++ b/src/Framework/Framework/Compilation/Binding/OperatorResolution.cs @@ -93,7 +93,6 @@ public static Expression GetBinaryOperator( _ => null }; - // Try to find user defined operator if (customOperator != null && (!leftType.IsPrimitive || !rightType.IsPrimitive)) { @@ -107,18 +106,30 @@ public static Expression GetBinaryOperator( throw new InvalidOperationException($"Cannot apply {operation} operator to two different enum types: {leftType.Name}, {rightType.Name}."); } - // numeric operations - if (operation == ExpressionType.LeftShift) + if (operation is ExpressionType.Equal or ExpressionType.NotEqual && !leftType.IsValueType && !rightType.IsValueType) { - return Expression.LeftShift(left, ConvertToMaybeNullable(right, typeof(int), true)!); + // https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/expressions.md#11117-reference-type-equality-operators + // Every class type C implicitly provides the following predefined reference type equality operators: + // bool operator ==(C x, C y); + // bool operator !=(C x, C y); + return ReferenceEquality(left, right, operation == ExpressionType.NotEqual); } - else if (operation == ExpressionType.RightShift) + + if (operation is ExpressionType.LeftShift or ExpressionType.RightShift) { - return Expression.RightShift(left, ConvertToMaybeNullable(right, typeof(int), true)!); + // https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/expressions.md#1110-shift-operators + // * shift operators always take int32 as the second argument + var rightConverted = ConvertToMaybeNullable(right, typeof(int), true)!; + // * the first argument is int, uint, long, ulong (in this order) + var leftConverted = ConvertToMaybeNullable(left, typeof(int), false) ?? ConvertToMaybeNullable(left, typeof(uint), false) ?? ConvertToMaybeNullable(left, typeof(long), false) ?? ConvertToMaybeNullable(left, typeof(ulong), false)!; + if (leftConverted is null) + throw new InvalidOperationException($"Cannot apply {operation} operator to type {leftType.ToCode()}. The type must be convertible to an integer or have a custom operator defined."); + return operation == ExpressionType.LeftShift ? Expression.LeftShift(leftConverted, rightConverted) : Expression.RightShift(leftConverted, rightConverted); } // List of types in order of precendence var enumType = leftType.IsEnum ? leftType : rightType.IsEnum ? rightType : null; + // all operators have defined "overloads" for two var typeList = operation switch { ExpressionType.Or or ExpressionType.And or ExpressionType.ExclusiveOr => new[] { typeof(bool), enumType, typeof(int), typeof(uint), typeof(long), typeof(ulong) }, @@ -152,56 +163,22 @@ public static Expression GetBinaryOperator( // if (left.Type.IsNullable() || right.Type.IsNullable()) // return GetBinaryOperator(expressionFactory, left.UnwrapNullable(), right.UnwrapNullable(), operation); - // as a fallback, try finding overridden Equals method - if (operation == ExpressionType.Equal) return EqualsMethod(expressionFactory, left, right); - if (operation == ExpressionType.NotEqual) return Expression.Not(EqualsMethod(expressionFactory, left, right)); - throw new InvalidOperationException($"Cannot apply {operation} operator to types {left.Type.Name} and {right.Type.Name}."); } - public static Expression EqualsMethod( - MemberExpressionFactory expressionFactory, - Expression left, - Expression right - ) + static Expression ReferenceEquality(Expression left, Expression right, bool not) { - Expression? equatable = null; - Expression? theOther = null; - if (typeof(IEquatable<>).IsAssignableFrom(left.Type)) - { - equatable = left; - theOther = right; - } - else if (typeof(IEquatable<>).IsAssignableFrom(right.Type)) - { - equatable = right; - theOther = left; - } - - if (equatable != null) + // * It is a binding-time error to use the predefined reference type equality operators to compare two references that are known to be different at binding-time. For example, if the binding-time types of the operands are two class types, and if neither derives from the other, then it would be impossible for the two operands to reference the same object. Thus, the operation is considered a binding-time error. + var leftT = left.Type; + var rightT = right.Type; + if (leftT != rightT && !(leftT.IsAssignableFrom(rightT) || rightT.IsAssignableFrom(leftT))) { - var m = expressionFactory.CallMethod(equatable, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Equals", null, new[] { theOther! }); - if (m != null) return m; + if (!leftT.IsInterface && rightT.IsInterface) + throw new InvalidOperationException($"Cannot compare types {leftT.ToCode()} and {rightT.ToCode()}, because the classes are unrelated."); + if (leftT.IsSealed || rightT.IsSealed) + throw new InvalidOperationException($"Cannot compare types {leftT.ToCode()} and {rightT.ToCode()}, because {(leftT.IsSealed ? leftT : rightT).ToCode(stripNamespace: true)} is sealed and does not implement {rightT.ToCode(stripNamespace: true)}."); } - - if (left.Type.IsValueType) - { - equatable = left; - theOther = right; - } - else if (left.Type.IsValueType) - { - equatable = right; - theOther = left; - } - - if (equatable != null) - { - theOther = TypeConversion.ImplicitConversion(theOther!, equatable.Type); - if (theOther != null) return Expression.Equal(equatable, theOther); - } - - return expressionFactory.CallMethod(left, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Equals", null, new[] { right }); + return not ? Expression.ReferenceNotEqual(left, right) : Expression.ReferenceEqual(left, right); } static Expression? ConvertToMaybeNullable( diff --git a/src/Samples/Tests/Tests/DotVVM.Samples.Tests.csproj b/src/Samples/Tests/Tests/DotVVM.Samples.Tests.csproj index ced2bba49f..1e4167c204 100644 --- a/src/Samples/Tests/Tests/DotVVM.Samples.Tests.csproj +++ b/src/Samples/Tests/Tests/DotVVM.Samples.Tests.csproj @@ -9,8 +9,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tests/Binding/BindingCompilationTests.cs b/src/Tests/Binding/BindingCompilationTests.cs index 2d7010d0ba..d607114948 100755 --- a/src/Tests/Binding/BindingCompilationTests.cs +++ b/src/Tests/Binding/BindingCompilationTests.cs @@ -1123,11 +1123,41 @@ public void Error_DifferentDataContext() [DataRow("DoubleProp - 1", 0.5)] [DataRow("DoubleProp + ShortProp", short.MaxValue + 1.5)] [DataRow("NullableDoubleProp + ShortProp", null)] - public void BindingCompiler_OperatorType(string expr, object expectedResult) - { - var vm = new TestViewModel { IntProp = 100, DoubleProp = 1.5 }; + [DataRow("ByteProp | ByteProp", (byte)255)] + [DataRow("DateTime == DateTime", true)] + [DataRow("NullableTimeOnly == NullableTimeOnly", true)] + [DataRow("NullableTimeOnly != NullableTimeOnly", false)] + [DataRow("NullableTimeOnly == TimeOnly", false)] + [DataRow("EnumList[0] > EnumList[1]", false)] + [DataRow("EnumList[0] < EnumList[1]", true)] + [DataRow("EnumList[0] == 'A'", true)] + [DataRow("EnumList[0] < 'C'", true)] + [DataRow("(EnumList[1] | 'C') == 'C'", false)] + [DataRow("(EnumList[2] & 1) != 0", false)] + public void BindingCompiler_BinaryOperator_ResultType(string expr, object expectedResult) + { + var vm = new TestViewModel { IntProp = 100, DoubleProp = 1.5, EnumList = new () { TestEnum.A, TestEnum.B, TestEnum.C, TestEnum.D } }; Assert.AreEqual(expectedResult, ExecuteBinding(expr, vm)); } + + [TestMethod] + [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Reference equality is not defined for the types 'DotVVM.Framework.Tests.Binding.TestViewModel2' and 'DotVVM.Framework.Tests.Binding.TestViewModel'")] + public void BindingCompiler_InvalidReferenceComparison() => + ExecuteBinding("TestViewModel2 == _this", new TestViewModel()); + + [TestMethod] + [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Cannot apply Equal operator to types DateTime and Object.")] + public void BindingCompiler_InvalidStructReferenceComparison() => + ExecuteBinding("DateTime == Time", new TestViewModel()); + + [TestMethod] + [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Cannot apply Equal operator to types DateTime and Object.")] + public void BindingCompiler_InvalidStructComparison() => + ExecuteBinding("DateTime == Time", new TestViewModel()); + [TestMethod] + [ExpectedExceptionMessageSubstring(typeof(BindingPropertyException), "Cannot apply And operator to types TestEnum and Boolean")] + public void BindingCompiler_InvalidBitAndComparison() => + ExecuteBinding("EnumProperty & 2 == 0", new TestViewModel()); } class TestViewModel { diff --git a/src/Tests/Binding/JavascriptCompilationTests.cs b/src/Tests/Binding/JavascriptCompilationTests.cs index d6da0eafda..193b4103d3 100644 --- a/src/Tests/Binding/JavascriptCompilationTests.cs +++ b/src/Tests/Binding/JavascriptCompilationTests.cs @@ -165,19 +165,25 @@ public void JavascriptCompilation_Parent() } [DataTestMethod] - // [DataRow("2+2", "2+2", DisplayName = "2+2")] - // [DataRow("2+2+2", "2+2+2", DisplayName = "2+2+2")] - // [DataRow("(2+2)+2", "2+2+2", DisplayName = "(2+2)+2")] - // [DataRow("2+(2+2)", "2+(2+2)", DisplayName = "2+(2+2)")] - // [DataRow("2+(2*2)", "2+2*2", DisplayName = "2+(2*2)")] - // [DataRow("2*(2+2)", "2*(2+2)", DisplayName = "2*(2+2)")] - // [DataRow("IntProp & (2+2)", "IntProp()&2+2", DisplayName = "IntProp & (2+2)")] - // [DataRow("IntProp & 2+2", "IntProp()&2+2", DisplayName = "IntProp & 2+2")] - // [DataRow("IntProp & -1", "IntProp()&-1", DisplayName = "IntProp & -1")] - // [DataRow("'a' + 'b'", "\"ab\"", DisplayName = "'a' + 'b'")] - // [DataRow("IntProp ^ 1", "IntProp()^1", DisplayName = "IntProp ^ 1")] - // [DataRow("'xx' + IntProp", "\"xx\"+IntProp()", DisplayName = "'xx' + IntProp")] + [DataRow("2+2", "2+2", DisplayName = "2+2")] + [DataRow("2+2+2", "2+2+2", DisplayName = "2+2+2")] + [DataRow("(2+2)+2", "2+2+2", DisplayName = "(2+2)+2")] + [DataRow("2+(2+2)", "2+(2+2)", DisplayName = "2+(2+2)")] + [DataRow("2+(2*2)", "2+2*2", DisplayName = "2+(2*2)")] + [DataRow("2*(2+2)", "2*(2+2)", DisplayName = "2*(2+2)")] + [DataRow("IntProp & (2+2)", "IntProp()&2+2", DisplayName = "IntProp & (2+2)")] + [DataRow("IntProp & 2+2", "IntProp()&2+2", DisplayName = "IntProp & 2+2")] + [DataRow("IntProp & -1", "IntProp()&-1", DisplayName = "IntProp & -1")] + [DataRow("'a' + 'b'", "\"ab\"", DisplayName = "'a' + 'b'")] + [DataRow("IntProp ^ 1", "IntProp()^1", DisplayName = "IntProp ^ 1")] + [DataRow("'xx' + IntProp", "\"xx\"+IntProp()", DisplayName = "'xx' + IntProp")] [DataRow("true == (IntProp == 1)", "true==(IntProp()==1)", DisplayName = "true == (IntProp == 1)")] + [DataRow("TestViewModel2 == null", "TestViewModel2()==null", DisplayName = "TestViewModel2 == null")] + [DataRow("NullableDateOnly == null", "NullableDateOnly()==null", DisplayName = "NullableDateOnly == null")] + [DataRow("NullableDateOnly == NullableDateOnly", "NullableDateOnly()==NullableDateOnly()", DisplayName = "NullableDateOnly == NullableDateOnly")] + [DataRow("null != StringProp", "null!=StringProp()", DisplayName = "null != StringProp")] + [DataRow("(EnumProperty & 2) == 0", "dotvvm.translations.enums.fromInt(dotvvm.translations.enums.toInt(EnumProperty(),\"nEayAzHQ5xyCfSP6\")&2,\"nEayAzHQ5xyCfSP6\")==\"A\"", DisplayName = "(EnumProperty & 2) == 0")] + [DataRow("EnumProperty == 'B'", "EnumProperty()==\"B\"", DisplayName = "EnumProperty & 2 == 0")] public void JavascriptCompilation_BinaryExpressions(string expr, string expectedJs) { var js = CompileBinding(expr, new [] { typeof(TestViewModel) }); diff --git a/src/Tests/Parser/Binding/BindingParserTests.cs b/src/Tests/Parser/Binding/BindingParserTests.cs index 550ee53584..4db7414a04 100644 --- a/src/Tests/Parser/Binding/BindingParserTests.cs +++ b/src/Tests/Parser/Binding/BindingParserTests.cs @@ -803,7 +803,7 @@ public void BindingParser_GenericExpression_MultipleInside() var parser = bindingParserNodeFactory.SetupParser(originalString); var node = parser.ReadExpression(); - Assert.IsTrue(node is TypeOrFunctionReferenceBindingParserNode); + Assert.IsInstanceOfType(node, typeof(TypeOrFunctionReferenceBindingParserNode)); Assert.IsTrue(string.Equals(originalString, node.ToDisplayString())); } From e854118e45792722e3f699e0896e248cdb58c11f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 14 Jul 2023 20:06:30 +0200 Subject: [PATCH 05/23] Unified PublicSign property --- src/Framework/Testing/DotVVM.Framework.Testing.csproj | 2 +- src/Tests/DotVVM.Framework.Tests.csproj | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Framework/Testing/DotVVM.Framework.Testing.csproj b/src/Framework/Testing/DotVVM.Framework.Testing.csproj index c1527c56ec..bef84dc8bb 100644 --- a/src/Framework/Testing/DotVVM.Framework.Testing.csproj +++ b/src/Framework/Testing/DotVVM.Framework.Testing.csproj @@ -14,7 +14,7 @@ True - True + true dotvvmwizard.snk diff --git a/src/Tests/DotVVM.Framework.Tests.csproj b/src/Tests/DotVVM.Framework.Tests.csproj index e9c7067ff5..fa3ee599c0 100644 --- a/src/Tests/DotVVM.Framework.Tests.csproj +++ b/src/Tests/DotVVM.Framework.Tests.csproj @@ -17,7 +17,8 @@ net6.0 - True + true + true dotvvmwizard.snk From 5421a3ae863c1fab5b92a1f88484d0b5db2cec7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 14 Jul 2023 20:06:44 +0200 Subject: [PATCH 06/23] Added a new version of WebForms adapters --- .../DotVVM.Adapters.WebForms.Tests.csproj | 24 ++++ .../Tests/WebForms/HybridRouteLinkTests.cs | 97 +++++++++++++ .../Tests/WebForms/WebFormsRouteTableInit.cs | 31 +++++ ...eLinkTests.HybridRouteLink_NoBindings.html | 12 ++ ....HybridRouteLink_SuffixAndQueryString.html | 10 ++ ...inkTests.HybridRouteLink_ValueBinding.html | 10 ++ .../WebForms/Controls/HybridRouteLink.cs | 54 ++++++++ .../WebForms/Controls/RouteLinkCapability.cs | 24 ++++ .../WebForms/Controls/WebFormsLinkUtils.cs | 130 ++++++++++++++++++ .../WebForms/DotVVM.Adapters.WebForms.csproj | 27 ++++ .../WebForms/DotvvmConfigurationExtensions.cs | 20 +++ src/Adapters/WebForms/RedirectHelper.cs | 39 ++++++ src/Adapters/WebForms/dotvvmwizard.snk | Bin 0 -> 596 bytes src/DotVVM.sln | 38 +++++ 14 files changed, 516 insertions(+) create mode 100644 src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj create mode 100644 src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs create mode 100644 src/Adapters/Tests/WebForms/WebFormsRouteTableInit.cs create mode 100644 src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_NoBindings.html create mode 100644 src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html create mode 100644 src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_ValueBinding.html create mode 100644 src/Adapters/WebForms/Controls/HybridRouteLink.cs create mode 100644 src/Adapters/WebForms/Controls/RouteLinkCapability.cs create mode 100644 src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs create mode 100644 src/Adapters/WebForms/DotVVM.Adapters.WebForms.csproj create mode 100644 src/Adapters/WebForms/DotvvmConfigurationExtensions.cs create mode 100644 src/Adapters/WebForms/RedirectHelper.cs create mode 100644 src/Adapters/WebForms/dotvvmwizard.snk diff --git a/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj b/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj new file mode 100644 index 0000000000..b6c6e34cda --- /dev/null +++ b/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj @@ -0,0 +1,24 @@ + + + + net472 + false + + + + + + + + + + + + + + + + + + + diff --git a/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs b/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs new file mode 100644 index 0000000000..b4df903b56 --- /dev/null +++ b/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Web; +using CheckTestOutput; +using DotVVM.Framework.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotVVM.Adapters.WebForms.Tests +{ + [TestClass] + public class HybridRouteLinkTests + { + private static readonly ControlTestHelper cth = new ControlTestHelper(config: config => config.AddWebFormsAdapters()); + OutputChecker check = new OutputChecker("testoutputs"); + + [ClassInitialize] + public static void Init(TestContext testContext) + { + WebFormsRouteTableInit.EnsureInitialized(); + } + + [TestMethod] + public async Task HybridRouteLink_NoBindings() + { + HttpContext.Current = new HttpContext( + new HttpRequest("", "http://tempuri.org", ""), + new HttpResponse(new StringWriter()) + ); + + var r = await cth.RunPage(typeof(ControlTestViewModel), @" + + + + ; + ; + + "); + + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } + + [TestMethod] + public async Task HybridRouteLink_ValueBinding() + { + HttpContext.Current = new HttpContext( + new HttpRequest("", "http://tempuri.org", ""), + new HttpResponse(new StringWriter()) + ); + + var r = await cth.RunPage(typeof(ControlTestViewModel), @" + + + + "); + + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } + + [TestMethod] + public async Task HybridRouteLink_SuffixAndQueryString() + { + HttpContext.Current = new HttpContext( + new HttpRequest("", "http://tempuri.org", ""), + new HttpResponse(new StringWriter()) + ); + + var r = await cth.RunPage(typeof(ControlTestViewModel), @" + + + + + "); + + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } + } + + class ControlTestViewModel + { + public int Value { get; set; } = 15; + + public List Items { get; set; } = new() + { + new ControlTestChildViewModel() { Id = 1, Name = "one" }, + new ControlTestChildViewModel() { Id = 2, Name = "two" }, + new ControlTestChildViewModel() { Id = 3, Name = "three" } + }; + } + + class ControlTestChildViewModel + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/src/Adapters/Tests/WebForms/WebFormsRouteTableInit.cs b/src/Adapters/Tests/WebForms/WebFormsRouteTableInit.cs new file mode 100644 index 0000000000..c56508ce8b --- /dev/null +++ b/src/Adapters/Tests/WebForms/WebFormsRouteTableInit.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Web.Routing; + +namespace DotVVM.Adapters.WebForms.Tests +{ + public static class WebFormsRouteTableInit + { + + static WebFormsRouteTableInit() + { + RouteTable.Routes.Add("NoParams", new Route("", new EmptyHandler())); + RouteTable.Routes.Add("SingleParam", new Route("page/{Index}", new EmptyHandler())); + RouteTable.Routes.Add("MultipleOptionalParams", new Route("catalog/{Tag}/{SubTag}", new EmptyHandler()) { Defaults = new RouteValueDictionary(new { Tag = "xx", SubTag = "yy" })}); + } + + public static void EnsureInitialized() + { + } + + } + + public class EmptyHandler : IRouteHandler + { + public IHttpHandler GetHttpHandler(RequestContext requestContext) => throw new NotImplementedException(); + } +} diff --git a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_NoBindings.html b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_NoBindings.html new file mode 100644 index 0000000000..85bf141b2a --- /dev/null +++ b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_NoBindings.html @@ -0,0 +1,12 @@ + + + + hello 1 + hello 2 + hello 3 + hello 4); + hello 5); + hello 6 + hello 6 + + diff --git a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html new file mode 100644 index 0000000000..68abf32cc8 --- /dev/null +++ b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html @@ -0,0 +1,10 @@ + + + + hello 1 + hello 2 + hello 3 + hello 4 + hello 5 + + diff --git a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_ValueBinding.html b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_ValueBinding.html new file mode 100644 index 0000000000..98e84b165e --- /dev/null +++ b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_ValueBinding.html @@ -0,0 +1,10 @@ + + + + hello 3 +
+ + + + + diff --git a/src/Adapters/WebForms/Controls/HybridRouteLink.cs b/src/Adapters/WebForms/Controls/HybridRouteLink.cs new file mode 100644 index 0000000000..9499a84105 --- /dev/null +++ b/src/Adapters/WebForms/Controls/HybridRouteLink.cs @@ -0,0 +1,54 @@ +using DotVVM.Framework.Controls; +using DotVVM.Framework.Hosting; + +#if NETFRAMEWORK +using System.Web.Routing; +#endif + +namespace DotVVM.Adapters.WebForms.Controls +{ + public class HybridRouteLink : CompositeControl + { + private readonly IDotvvmRequestContext context; + + public HybridRouteLink(IDotvvmRequestContext context) + { + this.context = context; + } + + public DotvvmControl GetContents( + HtmlCapability htmlCapability, + TextOrContentCapability textOrContent, + RouteLinkCapability routeLinkCapability + ) + { + if (context.Configuration.RouteTable.Contains(routeLinkCapability.RouteName)) + { + return GenerateDotvvmRouteLink(htmlCapability, textOrContent, routeLinkCapability); + } +#if NETFRAMEWORK + else if (RouteTable.Routes[routeLinkCapability.RouteName] is Route webFormsRoute) + { + return WebFormsLinkUtils.BuildWebFormsRouteLink(this, context, htmlCapability, textOrContent, routeLinkCapability, webFormsRoute); + } +#endif + else + { + throw new DotvvmControlException($"Route '{routeLinkCapability.RouteName}' does not exist."); + } + } + + private static DotvvmControl GenerateDotvvmRouteLink(HtmlCapability htmlCapability, TextOrContentCapability textOrContent, RouteLinkCapability routeLinkCapability) + { + var link = new RouteLink() + .SetCapability(htmlCapability) + .SetCapability(textOrContent) + .SetProperty(r => r.RouteName, routeLinkCapability.RouteName) + .SetProperty(l => l.UrlSuffix, routeLinkCapability.UrlSuffix); + link.QueryParameters.CopyFrom(routeLinkCapability.QueryParameters); + link.Params.CopyFrom(routeLinkCapability.Params); + return link; + } + + } +} diff --git a/src/Adapters/WebForms/Controls/RouteLinkCapability.cs b/src/Adapters/WebForms/Controls/RouteLinkCapability.cs new file mode 100644 index 0000000000..8b5597d962 --- /dev/null +++ b/src/Adapters/WebForms/Controls/RouteLinkCapability.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.ComponentModel; +using DotVVM.Framework.Binding; + +namespace DotVVM.Adapters.WebForms.Controls +{ + [DotvvmControlCapability()] + public sealed class RouteLinkCapability + { + [PropertyGroup("Query-")] + [DefaultValue(null)] + public IReadOnlyDictionary> QueryParameters { get; private set; } = new Dictionary>(); + + [PropertyGroup("Param-")] + [DefaultValue(null)] + public IReadOnlyDictionary> Params { get; private set; } = new Dictionary>(); + + public string RouteName { get; private set; } + + [DefaultValue(null)] + public ValueOrBinding? UrlSuffix { get; private set; } + + } +} diff --git a/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs b/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs new file mode 100644 index 0000000000..fcea37e400 --- /dev/null +++ b/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs @@ -0,0 +1,130 @@ +#if NETFRAMEWORK +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Routing; +using DotVVM.Framework.Utils; +using System.Web.Routing; +using System.Web; +using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Hosting; + +namespace DotVVM.Adapters.WebForms.Controls +{ + public class WebFormsLinkUtils + { + public static HtmlGenericControl BuildWebFormsRouteLink(DotvvmControl container, IDotvvmRequestContext context, HtmlCapability htmlCapability, TextOrContentCapability textOrContent, RouteLinkCapability routeLinkCapability, Route webFormsRoute) + { + var link = new HtmlGenericControl("a", textOrContent, htmlCapability); + + var parameters = BuildParameters(context, routeLinkCapability, webFormsRoute); + if (routeLinkCapability.UrlSuffix is { HasBinding: true, BindingOrDefault: IValueBinding } + || routeLinkCapability.Params.Any(p => p.Value is { HasBinding: true, BindingOrDefault: IValueBinding })) + { + // bindings are used, we have to generate client-script code + var fragments = new List { KnockoutHelper.MakeStringLiteral(context.TranslateVirtualPath("~/")) }; + + // generate binding and embed it in the function call + var routeUrlExpression = GenerateRouteUrlExpression(container, webFormsRoute, parameters); + fragments.Add(routeUrlExpression); + + // generate URL suffix + if (GenerateUrlSuffixExpression(container, routeLinkCapability) is string urlSuffix) + { + fragments.Add(urlSuffix); + } + + // render the binding and try to evaluate it on the server + link.AddAttribute("data-bind", "attr: { 'href': " + fragments.StringJoin("+") + "}"); + if (container.DataContext != null) + { + try + { + var url = context.TranslateVirtualPath(EvaluateRouteUrl(container, webFormsRoute, parameters, routeLinkCapability)); + link.SetAttribute("href", url); + } + catch (Exception ex) + { + } + } + } + else + { + // the value can be built on the server + var url = context.TranslateVirtualPath(EvaluateRouteUrl(container, webFormsRoute, parameters, routeLinkCapability)); + link.SetAttribute("href", url); + } + + return link; + } + + private static IDictionary> BuildParameters(IDotvvmRequestContext context, RouteLinkCapability routeLinkCapability, Route webFormsRoute) + { + var parameters = webFormsRoute.Defaults?.ToDictionary(t => t.Key, t => ValueOrBinding.FromBoxedValue(t.Value)) + ?? new Dictionary>(); + foreach (var param in context.Parameters) + { + parameters[param.Key] = ValueOrBinding.FromBoxedValue(param.Value); + } + + foreach (var item in routeLinkCapability.Params) + { + parameters[item.Key] = item.Value; + } + + return parameters; + } + + private static string EvaluateRouteUrl(DotvvmControl container, Route webFormsRoute, IDictionary> parameters, RouteLinkCapability routeLinkCapability) + { + // evaluate bindings on server + var routeValues = new RouteValueDictionary(); + foreach (Match param in Regex.Matches(webFormsRoute.Url, @"\{([^{}/]+)\}")) // https://referencesource.microsoft.com/#System.Web/Routing/RouteParser.cs,48 + { + var paramName = param.Groups[1].Value; + parameters.TryGetValue(paramName, out var value); + routeValues[paramName] = value.Evaluate(container) ?? ""; + } + + // generate the URL + return "~/" + + webFormsRoute.GetVirtualPath(HttpContext.Current.Request.RequestContext, routeValues)?.VirtualPath + + UrlHelper.BuildUrlSuffix(routeLinkCapability.UrlSuffix?.Evaluate(container), routeLinkCapability.QueryParameters.ToDictionary(p => p.Key, p => p.Value.Evaluate(container))); + } + + private static string GenerateRouteUrlExpression(DotvvmControl container, Route webFormsRoute, IDictionary> parameters) + { + var parametersExpression = parameters + .Select(p => $"{KnockoutHelper.MakeStringLiteral(p.Key)}: {p.Value.GetJsExpression(container)}") + .StringJoin(","); + var routeUrlExpression = $"dotvvm.buildRouteUrl({KnockoutHelper.MakeStringLiteral(webFormsRoute.Url)}, {{{parametersExpression}}})"; + return routeUrlExpression; + } + + private static string GenerateUrlSuffixExpression(DotvvmControl container, RouteLinkCapability routeLinkCapability) + { + var urlSuffixBase = routeLinkCapability.UrlSuffix?.GetJsExpression(container); + var queryParams = routeLinkCapability.QueryParameters + .Select(p => $"{KnockoutHelper.MakeStringLiteral(p.Key.ToLowerInvariant())}: {p.Value.GetJsExpression(container)}") + .StringJoin(","); + + // generate the function call + if (queryParams.Any()) + { + return $"dotvvm.buildUrlSuffix({urlSuffixBase}, {{{queryParams}}})"; + } + else if (urlSuffixBase != "\"\"") + { + return urlSuffixBase; + } + else + { + return null; + } + } + } +} +#endif diff --git a/src/Adapters/WebForms/DotVVM.Adapters.WebForms.csproj b/src/Adapters/WebForms/DotVVM.Adapters.WebForms.csproj new file mode 100644 index 0000000000..096c07a02b --- /dev/null +++ b/src/Adapters/WebForms/DotVVM.Adapters.WebForms.csproj @@ -0,0 +1,27 @@ + + + + $(DefaultTargetFrameworks) + DotVVM.Adapters.WebForms + + This package contains helpers for migration of ASP.NET Web Forms application to DotVVM. + $(Description) + + + + + + + True + dotvvmwizard.snk + + + + + + + + + + + diff --git a/src/Adapters/WebForms/DotvvmConfigurationExtensions.cs b/src/Adapters/WebForms/DotvvmConfigurationExtensions.cs new file mode 100644 index 0000000000..bc366ee25f --- /dev/null +++ b/src/Adapters/WebForms/DotvvmConfigurationExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Adapters.WebForms.Controls; +using DotVVM.Framework.Configuration; + +namespace DotVVM.Adapters.WebForms +{ + public static class DotvvmConfigurationExtensions + { + + public static void AddWebFormsAdapters(this DotvvmConfiguration config) + { + config.Markup.AddCodeControls("webforms", typeof(HybridRouteLink)); + config.Markup.Assemblies.Add(typeof(DotvvmConfigurationExtensions).Assembly.FullName); + } + } +} diff --git a/src/Adapters/WebForms/RedirectHelper.cs b/src/Adapters/WebForms/RedirectHelper.cs new file mode 100644 index 0000000000..d72f09c00a --- /dev/null +++ b/src/Adapters/WebForms/RedirectHelper.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Web; +using DotVVM.Framework.Routing; + +#if NETFRAMEWORK +using System.Web.Routing; +#endif + +namespace DotVVM.Framework.Hosting +{ + public static class DotvvmRequestContextExtensions + { + public static void RedirectToRouteHybrid(this IDotvvmRequestContext context, string routeName, object routeValues = null, string urlSuffix = null, object query = null) + { + if (context.Configuration.RouteTable.Contains(routeName)) + { + // we have DotVVM route - use it + var url = context.Configuration.RouteTable[routeName].BuildUrl(routeValues); + url += UrlHelper.BuildUrlSuffix(urlSuffix, query); + context.RedirectToUrl(url); + } +#if NETFRAMEWORK + else if (RouteTable.Routes[routeName] is Route webFormsRoute) + { + // fall back to the Web Forms route + var url = webFormsRoute.GetVirtualPath(HttpContext.Current.Request.RequestContext, new RouteValueDictionary(routeValues))!.VirtualPath; + url += UrlHelper.BuildUrlSuffix(urlSuffix, query); + context.RedirectToUrl(url); + } +#endif + else + { + throw new ArgumentException($"The route {routeName} doesn't exist!"); + } + } + } +} diff --git a/src/Adapters/WebForms/dotvvmwizard.snk b/src/Adapters/WebForms/dotvvmwizard.snk new file mode 100644 index 0000000000000000000000000000000000000000..34430b3c78542a7d831ed4d07414ad71984bfefc GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa500968#YETJw3;Q4k97&RPe_F{!G|nCdoQ|$ zrfVO%BxTC4y6jeFJgW}}EZ710;wMw6UByyI)U4nVrR?g(4TQtF1WTGXtc$8%vQ6w{ zO4<7S$Z4&UwqQT~Qy~&VV$TY*XiJIP9@4C&@pbn5{C5l72V<7AU>C;R<)k=VduO-( zoFw-E*(g(7`iw3MHgQkMN;Q_j_uZchOF$+;l+uOwsw{*G={9%S9jth>31~U(KUd_q z>-{zG5=rgzcq83Y3v+h%Cht^>c^*zHI%xkR2nTtwuLJi!|6oJnxKAw|;sNm4Kx#~S zF5AjxMlkMlOg>Ey-vv5Q%hH8&>rg94f zvR?Ia7g{xSHP$s-s=kp`88Vj3++%{TQ_A_8s>#c-CD}_|85lrXiMn6s$bv<_w_`p0 zQr3 literal 0 HcmV?d00001 diff --git a/src/DotVVM.sln b/src/DotVVM.sln index 3542ef1634..0c677c9d39 100644 --- a/src/DotVVM.sln +++ b/src/DotVVM.sln @@ -123,6 +123,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Framework.Controls.D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Framework.Controls.DynamicData", "DynamicData\DynamicData\DotVVM.Framework.Controls.DynamicData.csproj", "{9E19A537-E1B2-4D1E-A904-D99D4222474F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Adapters", "Adapters", "{11C116EC-5E5A-400A-9311-0732DD69401C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebForms", "WebForms", "{42513853-3772-46D2-94C2-965101E2406D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{05A3401A-C541-4F7C-AAD8-02A23648CD27}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Adapters.WebForms.Tests", "Adapters\Tests\WebForms\DotVVM.Adapters.WebForms.Tests.csproj", "{A6A8451E-99D8-4296-BBA9-69E1E289270A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Adapters.WebForms", "Adapters\WebForms\DotVVM.Adapters.WebForms.csproj", "{25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -697,6 +707,30 @@ Global {9E19A537-E1B2-4D1E-A904-D99D4222474F}.Release|x64.Build.0 = Release|Any CPU {9E19A537-E1B2-4D1E-A904-D99D4222474F}.Release|x86.ActiveCfg = Release|Any CPU {9E19A537-E1B2-4D1E-A904-D99D4222474F}.Release|x86.Build.0 = Release|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Debug|x64.Build.0 = Debug|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Debug|x86.Build.0 = Debug|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Release|Any CPU.Build.0 = Release|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Release|x64.ActiveCfg = Release|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Release|x64.Build.0 = Release|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Release|x86.ActiveCfg = Release|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Release|x86.Build.0 = Release|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Debug|x64.ActiveCfg = Debug|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Debug|x64.Build.0 = Debug|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Debug|x86.ActiveCfg = Debug|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Debug|x86.Build.0 = Debug|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|Any CPU.Build.0 = Release|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|x64.ActiveCfg = Release|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|x64.Build.0 = Release|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|x86.ActiveCfg = Release|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -753,6 +787,10 @@ Global {DB0AB0C3-DA5E-4B5A-9CD4-036D37B50AED} = {E57EE0B8-30FC-4702-B310-FB82C19D7473} {3209E1B1-88BB-4A95-B234-950E89EFCEE0} = {CF90322D-63BC-4047-BFEA-EE87E45020AF} {9E19A537-E1B2-4D1E-A904-D99D4222474F} = {CF90322D-63BC-4047-BFEA-EE87E45020AF} + {42513853-3772-46D2-94C2-965101E2406D} = {11C116EC-5E5A-400A-9311-0732DD69401C} + {05A3401A-C541-4F7C-AAD8-02A23648CD27} = {42513853-3772-46D2-94C2-965101E2406D} + {A6A8451E-99D8-4296-BBA9-69E1E289270A} = {05A3401A-C541-4F7C-AAD8-02A23648CD27} + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998} = {42513853-3772-46D2-94C2-965101E2406D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {61F8A195-365E-47B1-A6F2-CD3534E918F8} From 49db919f5841887148c66f2b40f41aaaca6e46f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 14 Jul 2023 20:09:14 +0200 Subject: [PATCH 07/23] Web Forms package added to the CI scripts --- ci/scripts/Get-PublicProjects.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ci/scripts/Get-PublicProjects.ps1 b/ci/scripts/Get-PublicProjects.ps1 index 9669670080..f1adf33d67 100644 --- a/ci/scripts/Get-PublicProjects.ps1 +++ b/ci/scripts/Get-PublicProjects.ps1 @@ -108,5 +108,10 @@ return @( Name = "DotVVM.Tracing.MiniProfiler.Owin"; Path = "src/Tracing/MiniProfiler.Owin"; Type = "standard" + }, + [PSCustomObject]@{ + Name = "DotVVM.Adapters.WebForms"; + Path = "src/Adapters/WebForms"; + Type = "standard" } ) From e7dc3b30c502d12cca81372504b5225d5123b658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 14 Jul 2023 20:10:54 +0200 Subject: [PATCH 08/23] Test outputs fixed --- src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs | 4 ++-- .../HybridRouteLinkTests.HybridRouteLink_NoBindings.html | 4 ++-- ...uteLinkTests.HybridRouteLink_SuffixAndQueryString.html | 2 +- ...HybridRouteLinkTests.HybridRouteLink_ValueBinding.html | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs b/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs index b4df903b56..24b2565aeb 100644 --- a/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs +++ b/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs @@ -33,8 +33,8 @@ public async Task HybridRouteLink_NoBindings() - ; - ; + + "); diff --git a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_NoBindings.html b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_NoBindings.html index 85bf141b2a..8c77847baa 100644 --- a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_NoBindings.html +++ b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_NoBindings.html @@ -4,8 +4,8 @@ hello 1 hello 2 hello 3 - hello 4); - hello 5); + hello 4 + hello 5 hello 6 hello 6 diff --git a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html index 68abf32cc8..1fa9df0670 100644 --- a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html +++ b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html @@ -5,6 +5,6 @@ hello 2 hello 3 hello 4 - hello 5 + hello 5 diff --git a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_ValueBinding.html b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_ValueBinding.html index 98e84b165e..cc45494e0d 100644 --- a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_ValueBinding.html +++ b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_ValueBinding.html @@ -1,10 +1,10 @@ - hello 3 -
+ hello 3 +
- - + + From 242a02349758b9621887e0fc798d0b3e9fe717fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 14 Jul 2023 20:22:44 +0200 Subject: [PATCH 09/23] Fixed bug, test made deterministic --- ...idRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html | 2 +- src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html index 1fa9df0670..f08748f268 100644 --- a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html +++ b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html @@ -5,6 +5,6 @@ hello 2 hello 3 hello 4 - hello 5 + hello 5 diff --git a/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs b/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs index fcea37e400..19d499d457 100644 --- a/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs +++ b/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs @@ -99,6 +99,7 @@ private static string GenerateRouteUrlExpression(DotvvmControl container, Route { var parametersExpression = parameters .Select(p => $"{KnockoutHelper.MakeStringLiteral(p.Key)}: {p.Value.GetJsExpression(container)}") + .OrderBy(p => p) .StringJoin(","); var routeUrlExpression = $"dotvvm.buildRouteUrl({KnockoutHelper.MakeStringLiteral(webFormsRoute.Url)}, {{{parametersExpression}}})"; return routeUrlExpression; @@ -106,9 +107,10 @@ private static string GenerateRouteUrlExpression(DotvvmControl container, Route private static string GenerateUrlSuffixExpression(DotvvmControl container, RouteLinkCapability routeLinkCapability) { - var urlSuffixBase = routeLinkCapability.UrlSuffix?.GetJsExpression(container); + var urlSuffixBase = routeLinkCapability.UrlSuffix?.GetJsExpression(container) ?? "\"\""; var queryParams = routeLinkCapability.QueryParameters .Select(p => $"{KnockoutHelper.MakeStringLiteral(p.Key.ToLowerInvariant())}: {p.Value.GetJsExpression(container)}") + .OrderBy(p => p) .StringJoin(","); // generate the function call From cab997fc31c768e6af5a3b893b45e8a1668f2959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Wed, 26 Jul 2023 17:57:32 +0200 Subject: [PATCH 10/23] Refactoring, obsolete attributes added for .NET Core branch --- src/Adapters/WebForms/Controls/HybridRouteLink.cs | 9 ++++++++- ...RedirectHelper.cs => WebFormsAdaptersExtensions.cs} | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) rename src/Adapters/WebForms/{RedirectHelper.cs => WebFormsAdaptersExtensions.cs} (75%) diff --git a/src/Adapters/WebForms/Controls/HybridRouteLink.cs b/src/Adapters/WebForms/Controls/HybridRouteLink.cs index 9499a84105..a82d38722e 100644 --- a/src/Adapters/WebForms/Controls/HybridRouteLink.cs +++ b/src/Adapters/WebForms/Controls/HybridRouteLink.cs @@ -1,4 +1,5 @@ -using DotVVM.Framework.Controls; +using System; +using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; #if NETFRAMEWORK @@ -7,6 +8,12 @@ namespace DotVVM.Adapters.WebForms.Controls { + /// + /// Renders a hyperlink pointing to the specified DotVVM route if such route exists; otherwise it falls back to a Web Forms route with the specified name. + /// +#if !NETFRAMEWORK + [Obsolete("This control is used only during the Web Forms migration and is not needed in .NET Core. Use the standard RouteLink control.")] +#endif public class HybridRouteLink : CompositeControl { private readonly IDotvvmRequestContext context; diff --git a/src/Adapters/WebForms/RedirectHelper.cs b/src/Adapters/WebForms/WebFormsAdaptersExtensions.cs similarity index 75% rename from src/Adapters/WebForms/RedirectHelper.cs rename to src/Adapters/WebForms/WebFormsAdaptersExtensions.cs index d72f09c00a..30fd66e68a 100644 --- a/src/Adapters/WebForms/RedirectHelper.cs +++ b/src/Adapters/WebForms/WebFormsAdaptersExtensions.cs @@ -8,10 +8,18 @@ using System.Web.Routing; #endif +// ReSharper disable once CheckNamespace namespace DotVVM.Framework.Hosting { - public static class DotvvmRequestContextExtensions + public static class WebFormsAdaptersExtensions { + + /// + /// Redirects to the specified DotVVM route if such route exists; otherwise it redirects to the specified Web Forms route. + /// +#if !NETFRAMEWORK + [Obsolete("This method is used only during the Web Forms migration and is not needed in .NET Core. Use the standard RedirectToRoute method.")] +#endif public static void RedirectToRouteHybrid(this IDotvvmRequestContext context, string routeName, object routeValues = null, string urlSuffix = null, object query = null) { if (context.Configuration.RouteTable.Contains(routeName)) From 0912e53876717941b6cdd3ec7d0233f968dd88ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Wed, 26 Jul 2023 17:57:41 +0200 Subject: [PATCH 11/23] RouteLinkCapability moved to framework --- src/Framework/Framework/Controls/RouteLink.cs | 13 +++++++++++++ .../Framework}/Controls/RouteLinkCapability.cs | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) rename src/{Adapters/WebForms => Framework/Framework}/Controls/RouteLinkCapability.cs (94%) diff --git a/src/Framework/Framework/Controls/RouteLink.cs b/src/Framework/Framework/Controls/RouteLink.cs index 355d0233b4..d5f5b4c804 100644 --- a/src/Framework/Framework/Controls/RouteLink.cs +++ b/src/Framework/Framework/Controls/RouteLink.cs @@ -64,11 +64,17 @@ public string Text public static readonly DotvvmProperty TextProperty = DotvvmProperty.Register(c => c.Text, ""); + /// + /// Gets or sets a collection of parameters to be substituted in the route URL. If the current route contains a parameter with the same name, its value will be reused unless another value is specified here. + /// [PropertyGroup("Param-")] public VirtualPropertyGroupDictionary Params => new VirtualPropertyGroupDictionary(this, ParamsGroupDescriptor); public static DotvvmPropertyGroup ParamsGroupDescriptor = DotvvmPropertyGroup.Register("Param-", "Params"); + /// + /// Gets or sets a collection of parameters to be added in the query string. + /// [PropertyGroup("Query-")] public VirtualPropertyGroupDictionary QueryParameters => new VirtualPropertyGroupDictionary(this, QueryParametersGroupDescriptor); public static DotvvmPropertyGroup QueryParametersGroupDescriptor = @@ -87,6 +93,13 @@ public TextOrContentCapability TextOrContentCapability } ); + public RouteLinkCapability RouteLinkCapability + { + get => (RouteLinkCapability)RouteLinkCapabilityProperty.GetValue(this); + set => RouteLinkCapabilityProperty.SetValue(this, value); + } + public static readonly DotvvmCapabilityProperty RouteLinkCapabilityProperty = DotvvmCapabilityProperty.RegisterCapability(); + public RouteLink() : base("a", false) { } diff --git a/src/Adapters/WebForms/Controls/RouteLinkCapability.cs b/src/Framework/Framework/Controls/RouteLinkCapability.cs similarity index 94% rename from src/Adapters/WebForms/Controls/RouteLinkCapability.cs rename to src/Framework/Framework/Controls/RouteLinkCapability.cs index 8b5597d962..2e7ced5419 100644 --- a/src/Adapters/WebForms/Controls/RouteLinkCapability.cs +++ b/src/Framework/Framework/Controls/RouteLinkCapability.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using DotVVM.Framework.Binding; -namespace DotVVM.Adapters.WebForms.Controls +namespace DotVVM.Framework.Controls { [DotvvmControlCapability()] public sealed class RouteLinkCapability From c753ca1c33a77752aebeed41d405bd646050e80f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Wed, 26 Jul 2023 19:00:21 +0200 Subject: [PATCH 12/23] Extension method moved to a different namespace so the using will not be necessary --- src/Adapters/WebForms/DotvvmConfigurationExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Adapters/WebForms/DotvvmConfigurationExtensions.cs b/src/Adapters/WebForms/DotvvmConfigurationExtensions.cs index bc366ee25f..a5e6cbd2bc 100644 --- a/src/Adapters/WebForms/DotvvmConfigurationExtensions.cs +++ b/src/Adapters/WebForms/DotvvmConfigurationExtensions.cs @@ -4,9 +4,9 @@ using System.Text; using System.Threading.Tasks; using DotVVM.Adapters.WebForms.Controls; -using DotVVM.Framework.Configuration; -namespace DotVVM.Adapters.WebForms +// ReSharper disable once CheckNamespace +namespace DotVVM.Framework.Configuration { public static class DotvvmConfigurationExtensions { From 47fa35ce55ff8774e92704664e34b211cad2f9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 28 Jul 2023 14:14:29 +0200 Subject: [PATCH 13/23] Fixed warning and test errors in RouteLinkCapability --- .../Framework/Controls/RouteLinkCapability.cs | 11 +++++------ ...tionSerializationTests.SerializeDefaultConfig.json | 3 +++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Framework/Framework/Controls/RouteLinkCapability.cs b/src/Framework/Framework/Controls/RouteLinkCapability.cs index 2e7ced5419..e80e18bca8 100644 --- a/src/Framework/Framework/Controls/RouteLinkCapability.cs +++ b/src/Framework/Framework/Controls/RouteLinkCapability.cs @@ -5,20 +5,19 @@ namespace DotVVM.Framework.Controls { [DotvvmControlCapability()] - public sealed class RouteLinkCapability + public sealed record RouteLinkCapability { [PropertyGroup("Query-")] [DefaultValue(null)] - public IReadOnlyDictionary> QueryParameters { get; private set; } = new Dictionary>(); + public IReadOnlyDictionary> QueryParameters { get; init; } = new Dictionary>(); [PropertyGroup("Param-")] [DefaultValue(null)] - public IReadOnlyDictionary> Params { get; private set; } = new Dictionary>(); + public IReadOnlyDictionary> Params { get; init; } = new Dictionary>(); - public string RouteName { get; private set; } + public string RouteName { get; init; } = null!; [DefaultValue(null)] - public ValueOrBinding? UrlSuffix { get; private set; } - + public ValueOrBinding? UrlSuffix { get; init; } } } diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index 803a7b58c2..825ca33160 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -1720,6 +1720,9 @@ } }, "DotVVM.Framework.Controls.RouteLink": { + "RouteLinkCapability": { + "type": "DotVVM.Framework.Controls.RouteLinkCapability, DotVVM.Framework" + }, "TextOrContentCapability": { "type": "DotVVM.Framework.Controls.TextOrContentCapability, DotVVM.Framework" } From a3632152397670d5f64f078e7ced3f77c8d58d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 28 Jul 2023 14:47:31 +0200 Subject: [PATCH 14/23] Fixed compile error in tests --- .github/workflows/main.yml | 9 +++++++++ src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs | 1 + 2 files changed, 10 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 15bf44ba88..d1ec15d3ef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -131,6 +131,15 @@ jobs: # title: Analyzer Tests # github-token: ${{ secrets.GITHUB_TOKEN }} # target-framework: net7.0 + - name: Adapters.WebForms.Tests (net472) + uses: ./.github/unittest + if: matrix.os == 'windows-2022' + with: + project: src/Adapters/Tests/WebForms + name: webforms-adapters-tests + title: WebForms Adapter Tests + github-token: ${{ secrets.GITHUB_TOKEN }} + target-framework: net472 js-tests: runs-on: ubuntu-latest diff --git a/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs b/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs index 24b2565aeb..fe3cc1616d 100644 --- a/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs +++ b/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Web; using CheckTestOutput; +using DotVVM.Framework.Configuration; using DotVVM.Framework.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; From 59a3431e3b54e6f6ea22e863ab1e0fa7b1728ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 28 Jul 2023 15:33:52 +0200 Subject: [PATCH 15/23] Fixed test project --- .../Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj | 3 ++- src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj b/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj index b6c6e34cda..86ef296e36 100644 --- a/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj +++ b/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj @@ -9,7 +9,8 @@ - + + diff --git a/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs b/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs index 19d499d457..b4eb52ba8d 100644 --- a/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs +++ b/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs @@ -92,14 +92,14 @@ private static string EvaluateRouteUrl(DotvvmControl container, Route webFormsRo // generate the URL return "~/" + webFormsRoute.GetVirtualPath(HttpContext.Current.Request.RequestContext, routeValues)?.VirtualPath - + UrlHelper.BuildUrlSuffix(routeLinkCapability.UrlSuffix?.Evaluate(container), routeLinkCapability.QueryParameters.ToDictionary(p => p.Key, p => p.Value.Evaluate(container))); + + UrlHelper.BuildUrlSuffix(routeLinkCapability.UrlSuffix?.Evaluate(container), routeLinkCapability.QueryParameters.OrderBy(p => p.Key).Select(p => new KeyValuePair(p.Key, p.Value.Evaluate(container)))); } private static string GenerateRouteUrlExpression(DotvvmControl container, Route webFormsRoute, IDictionary> parameters) { var parametersExpression = parameters + .OrderBy(p => p.Key) .Select(p => $"{KnockoutHelper.MakeStringLiteral(p.Key)}: {p.Value.GetJsExpression(container)}") - .OrderBy(p => p) .StringJoin(","); var routeUrlExpression = $"dotvvm.buildRouteUrl({KnockoutHelper.MakeStringLiteral(webFormsRoute.Url)}, {{{parametersExpression}}})"; return routeUrlExpression; @@ -109,8 +109,8 @@ private static string GenerateUrlSuffixExpression(DotvvmControl container, Route { var urlSuffixBase = routeLinkCapability.UrlSuffix?.GetJsExpression(container) ?? "\"\""; var queryParams = routeLinkCapability.QueryParameters + .OrderBy(p => p.Key) .Select(p => $"{KnockoutHelper.MakeStringLiteral(p.Key.ToLowerInvariant())}: {p.Value.GetJsExpression(container)}") - .OrderBy(p => p) .StringJoin(","); // generate the function call From 6eaba698fc504c5005213b08abd5165188cd8ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 28 Jul 2023 16:13:32 +0200 Subject: [PATCH 16/23] Disabled deterministic build in Adapter tests --- .../Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj b/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj index 86ef296e36..08d3a2e33f 100644 --- a/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj +++ b/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj @@ -3,6 +3,9 @@ net472 false + + + false From 6aaf80412f8efb3fd2d9b8c8489438a1624af27a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 30 Jul 2023 16:10:12 +0200 Subject: [PATCH 17/23] Fixed web.config files --- src/Samples/Api.Owin/Web.config | 4 ++-- src/Samples/ApplicationInsights.Owin/Web.config | 4 ++-- src/Samples/MiniProfiler.Owin/Web.config | 6 ++++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Samples/Api.Owin/Web.config b/src/Samples/Api.Owin/Web.config index 41ff4385af..66d4915c97 100644 --- a/src/Samples/Api.Owin/Web.config +++ b/src/Samples/Api.Owin/Web.config @@ -62,8 +62,8 @@ - - + + diff --git a/src/Samples/ApplicationInsights.Owin/Web.config b/src/Samples/ApplicationInsights.Owin/Web.config index caad38616d..a7b2698ee1 100644 --- a/src/Samples/ApplicationInsights.Owin/Web.config +++ b/src/Samples/ApplicationInsights.Owin/Web.config @@ -59,8 +59,8 @@ - - + + diff --git a/src/Samples/MiniProfiler.Owin/Web.config b/src/Samples/MiniProfiler.Owin/Web.config index 0ae952b2c6..40cc6cbbf4 100644 --- a/src/Samples/MiniProfiler.Owin/Web.config +++ b/src/Samples/MiniProfiler.Owin/Web.config @@ -71,5 +71,11 @@ + + + + + + \ No newline at end of file From 8c2eb4f060613cccfae0fd2d803914a0397292fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 30 Jul 2023 16:20:26 +0200 Subject: [PATCH 18/23] Fixed ports in sample projects --- .../Properties/launchSettings.json | 19 ++---------------- .../Properties/launchSettings.json | 20 ++++--------------- .../Api.Owin/Properties/launchSettings.json | 12 +++++++++++ .../AspNetCore/Properties/launchSettings.json | 17 +--------------- .../Properties/launchSettings.json | 17 +--------------- .../Owin/Properties/launchSettings.json | 12 +++++++++++ src/Samples/Tests/Tests/seleniumconfig.json | 2 +- 7 files changed, 33 insertions(+), 66 deletions(-) create mode 100644 src/Samples/Api.Owin/Properties/launchSettings.json create mode 100644 src/Samples/Owin/Properties/launchSettings.json diff --git a/src/Samples/Api.AspNetCore/Properties/launchSettings.json b/src/Samples/Api.AspNetCore/Properties/launchSettings.json index 244e3813ad..9eac61f38a 100644 --- a/src/Samples/Api.AspNetCore/Properties/launchSettings.json +++ b/src/Samples/Api.AspNetCore/Properties/launchSettings.json @@ -1,25 +1,10 @@ { "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "swag": { + "DotVVM.Samples.BasicSamples.Api.AspNetCore": { "commandName": "Project", "launchBrowser": true, "launchUrl": "http://localhost:50001", "applicationUrl": "http://localhost:50001" } - }, - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:50001/", - "sslPort": 0 - } } -} \ No newline at end of file +} diff --git a/src/Samples/Api.AspNetCoreLatest/Properties/launchSettings.json b/src/Samples/Api.AspNetCoreLatest/Properties/launchSettings.json index af65e5130e..9eac61f38a 100644 --- a/src/Samples/Api.AspNetCoreLatest/Properties/launchSettings.json +++ b/src/Samples/Api.AspNetCoreLatest/Properties/launchSettings.json @@ -1,22 +1,10 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:5003/", - "sslPort": 0 - } - }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", + "DotVVM.Samples.BasicSamples.Api.AspNetCore": { + "commandName": "Project", "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "swag": { - "commandName": "Project" + "launchUrl": "http://localhost:50001", + "applicationUrl": "http://localhost:50001" } } } diff --git a/src/Samples/Api.Owin/Properties/launchSettings.json b/src/Samples/Api.Owin/Properties/launchSettings.json new file mode 100644 index 0000000000..8d33da700a --- /dev/null +++ b/src/Samples/Api.Owin/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "DotVVM.Samples.BasicSamples.Api.Owin": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:61453" + } + } +} diff --git a/src/Samples/AspNetCore/Properties/launchSettings.json b/src/Samples/AspNetCore/Properties/launchSettings.json index f539c094c5..70ca5a293a 100644 --- a/src/Samples/AspNetCore/Properties/launchSettings.json +++ b/src/Samples/AspNetCore/Properties/launchSettings.json @@ -1,24 +1,9 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:16018/", - "sslPort": 0 - } - }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "DotVVM.Samples.BasicSamples": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "http://localhost:5000", + "launchUrl": "http://localhost:16019", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Samples/AspNetCoreLatest/Properties/launchSettings.json b/src/Samples/AspNetCoreLatest/Properties/launchSettings.json index 5cf93a20d2..70ca5a293a 100644 --- a/src/Samples/AspNetCoreLatest/Properties/launchSettings.json +++ b/src/Samples/AspNetCoreLatest/Properties/launchSettings.json @@ -1,24 +1,9 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:16019/", - "sslPort": 0 - } - }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "DotVVM.Samples.BasicSamples": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "http://localhost:5000", + "launchUrl": "http://localhost:16019", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Samples/Owin/Properties/launchSettings.json b/src/Samples/Owin/Properties/launchSettings.json new file mode 100644 index 0000000000..15d9c829a0 --- /dev/null +++ b/src/Samples/Owin/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "DotVVM.Samples.BasicSamples.Owin": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5407" + } + } +} diff --git a/src/Samples/Tests/Tests/seleniumconfig.json b/src/Samples/Tests/Tests/seleniumconfig.json index bb70470fd1..7a5fb817d1 100644 --- a/src/Samples/Tests/Tests/seleniumconfig.json +++ b/src/Samples/Tests/Tests/seleniumconfig.json @@ -5,7 +5,7 @@ } }, "baseUrls": [ - "http://localhost:5407/" + "http://localhost:16019/" ], "testRunOptions": { "runInParallel": false, From 33bb0ed25c6a01c47a47987037e32ce3f4c1cd6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 30 Jul 2023 17:13:42 +0200 Subject: [PATCH 19/23] Removed ToLower with query params Used RouteLinkCapability --- ...uteLinkTests.HybridRouteLink_SuffixAndQueryString.html | 6 +++--- src/Adapters/WebForms/Controls/HybridRouteLink.cs | 8 ++------ src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html index f08748f268..7d179a3a4a 100644 --- a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html +++ b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html @@ -3,8 +3,8 @@ hello 1 hello 2 - hello 3 - hello 4 - hello 5 + hello 3 + hello 4 + hello 5 diff --git a/src/Adapters/WebForms/Controls/HybridRouteLink.cs b/src/Adapters/WebForms/Controls/HybridRouteLink.cs index a82d38722e..0606f75011 100644 --- a/src/Adapters/WebForms/Controls/HybridRouteLink.cs +++ b/src/Adapters/WebForms/Controls/HybridRouteLink.cs @@ -47,14 +47,10 @@ RouteLinkCapability routeLinkCapability private static DotvvmControl GenerateDotvvmRouteLink(HtmlCapability htmlCapability, TextOrContentCapability textOrContent, RouteLinkCapability routeLinkCapability) { - var link = new RouteLink() + return new RouteLink() .SetCapability(htmlCapability) .SetCapability(textOrContent) - .SetProperty(r => r.RouteName, routeLinkCapability.RouteName) - .SetProperty(l => l.UrlSuffix, routeLinkCapability.UrlSuffix); - link.QueryParameters.CopyFrom(routeLinkCapability.QueryParameters); - link.Params.CopyFrom(routeLinkCapability.Params); - return link; + .SetCapability(routeLinkCapability); } } diff --git a/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs b/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs index b4eb52ba8d..ba730df1d9 100644 --- a/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs +++ b/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs @@ -110,7 +110,7 @@ private static string GenerateUrlSuffixExpression(DotvvmControl container, Route var urlSuffixBase = routeLinkCapability.UrlSuffix?.GetJsExpression(container) ?? "\"\""; var queryParams = routeLinkCapability.QueryParameters .OrderBy(p => p.Key) - .Select(p => $"{KnockoutHelper.MakeStringLiteral(p.Key.ToLowerInvariant())}: {p.Value.GetJsExpression(container)}") + .Select(p => $"{KnockoutHelper.MakeStringLiteral(p.Key)}: {p.Value.GetJsExpression(container)}") .StringJoin(","); // generate the function call From 652e7951254d287732ab99227192fae448e47f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 1 Jul 2023 16:54:15 +0200 Subject: [PATCH 20/23] Respect derived type preference in overload resolution according to the spec, this is the rule with priority second only to "instance > extension". The change is needed to fix problems with multiple times overriden operators Easy to try demo in C# that this is how we should behave: public class C { public void M() { B.Method((int)1); } } public class A { public static void Method(int a) { } } public class B : A { public static void Method(object a) { } } --- .../Binding/MemberExpressionFactory.cs | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/src/Framework/Framework/Compilation/Binding/MemberExpressionFactory.cs b/src/Framework/Framework/Compilation/Binding/MemberExpressionFactory.cs index dc54918ca8..9a14dae933 100644 --- a/src/Framework/Framework/Compilation/Binding/MemberExpressionFactory.cs +++ b/src/Framework/Framework/Compilation/Binding/MemberExpressionFactory.cs @@ -271,7 +271,7 @@ public Expression CallMethod(Type target, BindingFlags flags, string name, Type[ if (matchingMethods.Count == 0) return null; - var overload = BestOverload(matchingMethods, operatorName); + var overload = BestOverload(matchingMethods, searchTypes, operatorName); var parameters = overload.Method.GetParameters(); return Expression.MakeBinary( @@ -285,6 +285,7 @@ public Expression CallMethod(Type target, BindingFlags flags, string name, Type[ private MethodRecognitionResult FindValidMethodOverloads(Expression? target, Type type, string name, BindingFlags flags, Type[]? typeArguments, Expression[] arguments, IDictionary? namedArgs) { + bool extensionMethods = false; var methods = FindValidMethodOverloads(type.GetAllMethods(flags), name, false, typeArguments, arguments, namedArgs); if (methods.Count == 1) return methods[0]; @@ -293,6 +294,7 @@ private MethodRecognitionResult FindValidMethodOverloads(Expression? target, Typ // We did not find any match in regular methods => try extension methods if (target != null && flags.HasFlag(BindingFlags.Instance)) { + extensionMethods = true; // Change to a static call var newArguments = new[] { target }.Concat(arguments).ToArray(); var extensions = FindValidMethodOverloads(GetAllExtensionMethods(), name, true, typeArguments, newArguments, namedArgs); @@ -311,21 +313,25 @@ private MethodRecognitionResult FindValidMethodOverloads(Expression? target, Typ } // There are multiple method candidates - return BestOverload(methods, name); + return BestOverload(methods, extensionMethods ? Type.EmptyTypes : new[] { type }, name); } - private MethodRecognitionResult BestOverload(List methods, string name) + private MethodRecognitionResult BestOverload(List methods, Type[] callingOnType, string name) { if (methods.Count == 1) return methods[0]; - methods = methods.OrderBy(s => s.CastCount).ThenBy(s => s.AutomaticTypeArgCount).ThenBy(s => s.HasParamsAttribute).ToList(); + methods = methods + .OrderBy(s => GetNearestInheritanceDistance(s.Method.DeclaringType, callingOnType)) + .ThenBy(s => s.CastCount) + .ThenBy(s => s.AutomaticTypeArgCount) + .ThenBy(s => s.HasParamsAttribute).ToList(); var method = methods.First(); var method2 = methods.Skip(1).First(); - if (method.AutomaticTypeArgCount == method2.AutomaticTypeArgCount && method.CastCount == method2.CastCount && method.HasParamsAttribute == method2.HasParamsAttribute) + if (method.AutomaticTypeArgCount == method2.AutomaticTypeArgCount && method.CastCount == method2.CastCount && method.HasParamsAttribute == method2.HasParamsAttribute && GetNearestInheritanceDistance(method.Method.DeclaringType, callingOnType) == GetNearestInheritanceDistance(method2.Method.DeclaringType, callingOnType)) { // TODO: this behavior is not completed. Implement the same behavior as in roslyn. - var foundOverloads = $"{method.Method}, {method2.Method}"; + var foundOverloads = $"{ReflectionUtils.FormatMethodInfo(method.Method, stripNamespace: true)}, {ReflectionUtils.FormatMethodInfo(method2.Method, stripNamespace: true)}"; throw new InvalidOperationException($"Found ambiguous overloads of method '{name}'. The following overloads were found: {foundOverloads}."); } return method; @@ -354,6 +360,40 @@ private List FindValidMethodOverloads(IEnumerable t.ToCode()))}'."); + return distance; + } + sealed class MethodRecognitionResult { public MethodRecognitionResult(int automaticTypeArgCount, int castCount, Expression[] arguments, MethodInfo method, int paramsArrayCount, bool isExtension, bool hasParamsAttribute) From ab73b7692168db8b8891ca0f3d3c5c8134aa2b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Tue, 1 Aug 2023 21:23:53 +0200 Subject: [PATCH 21/23] Allow samples to start in dotnet invariant culture mode when System.Globalization.Invariant runtime switch or DOTNET_SYSTEM_GLOBALIZATION_INVARIANT env variables are set, only invariant culture is supported. This mode useful to supress some "incompatible ICU" errors. Obviously samples relying on cultures won't work, this isn't attempt to make it fully work, just to make it easier to debug dotvvm on older .NET Core versions --- src/Samples/Common/DotvvmStartup.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index c041796624..8bd8555114 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -39,9 +39,27 @@ public class DotvvmStartup : IDotvvmStartup public const string GitHubTokenEnvName = "GITHUB_TOKEN"; public const string GitHubTokenConfigName = "githubApiToken"; + private bool IsInInvariantCultureMode() + { + // Makes the samples run even if only invariant culture is enabled + // This is useful for testing older versions of .NET Core which rely on ICU which is no longer installed + try + { + new System.Globalization.CultureInfo("en-US"); + return false; + } + catch (System.Globalization.CultureNotFoundException) + { + return true; + } + } + public void Configure(DotvvmConfiguration config, string applicationPath) { - config.DefaultCulture = "en-US"; + if (!IsInInvariantCultureMode()) + { + config.DefaultCulture = "en-US"; + } AddControls(config); AddStyles(config); From c355c57b72be2c077dade8259de4cf33c51c0563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Tue, 1 Aug 2023 22:52:15 +0200 Subject: [PATCH 22/23] Run module initialization in dotvvm.init event This allows using dotvvm.state (etc.) already in the module constructor. resolves #1592 --- .../ResourceManagement/ViewModuleInitResource.cs | 10 +++++++++- .../ViewModulesServerSideTests.IncludeViewModule.html | 4 +++- ...ulesServerSideTests.IncludeViewModuleInControl.html | 4 +++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Framework/Framework/ResourceManagement/ViewModuleInitResource.cs b/src/Framework/Framework/ResourceManagement/ViewModuleInitResource.cs index bcb2965f2b..8e97077f08 100644 --- a/src/Framework/Framework/ResourceManagement/ViewModuleInitResource.cs +++ b/src/Framework/Framework/ResourceManagement/ViewModuleInitResource.cs @@ -29,7 +29,15 @@ public ViewModuleInitResource(string[] referencedModules, string name, string vi this.ReferencedModules = referencedModules.ToArray(); this.Dependencies = dependencies; - this.registrationScript = string.Join("\r\n", this.ReferencedModules.Select(m => $"dotvvm.viewModules.init({KnockoutHelper.MakeStringLiteral(m)}, {KnockoutHelper.MakeStringLiteral(viewId)}, document.body);")); + var initCalls = this.ReferencedModules.Select(m => $"dotvvm.viewModules.init({KnockoutHelper.MakeStringLiteral(m)}, {KnockoutHelper.MakeStringLiteral(viewId)}, document.body);"); + + // Run the module init in the init event + // * dotvvm.state will be available + // * executed before applying bindings to the controls, so the page module will initialize before control modules + this.registrationScript = + "dotvvm.events.init.subscribeOnce(() => {\n" + + " " + string.Join("\n", initCalls) + "\n" + + "})"; } public void Render(IHtmlWriter writer, IDotvvmRequestContext context, string resourceName) diff --git a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html index 46df5cdf5a..f92075b260 100644 --- a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html +++ b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html @@ -15,7 +15,9 @@ - + import * as m0 from '/dotvvmResource/viewModule/viewModule';dotvvm.viewModules.registerMany({'viewModule': m0}); - +
From 7ecda72b75fe7dd05b6818785b00068af4be2867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Tue, 1 Aug 2023 21:19:44 +0200 Subject: [PATCH 23/23] Allow hardcoded values and resource bindings in IncludeInPage, Visible properties It only produced a warning, but still, hardcoded values and resource bindings are already supported in both properties. --- src/Framework/Framework/Controls/DotvvmControl.cs | 2 +- src/Framework/Framework/Controls/HtmlGenericControl.cs | 2 +- ...figurationSerializationTests.SerializeDefaultConfig.json | 6 ++---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Framework/Framework/Controls/DotvvmControl.cs b/src/Framework/Framework/Controls/DotvvmControl.cs index e4b60e573f..b03c192a39 100644 --- a/src/Framework/Framework/Controls/DotvvmControl.cs +++ b/src/Framework/Framework/Controls/DotvvmControl.cs @@ -115,7 +115,7 @@ public ClientIDMode ClientIDMode /// /// Essentially wraps Knockout's 'if' binding. /// - [MarkupOptions(AllowHardCodedValue = false)] + [MarkupOptions] public bool IncludeInPage { get { return (bool)GetValue(IncludeInPageProperty)!; } diff --git a/src/Framework/Framework/Controls/HtmlGenericControl.cs b/src/Framework/Framework/Controls/HtmlGenericControl.cs index db33cd9501..b2b807914b 100644 --- a/src/Framework/Framework/Controls/HtmlGenericControl.cs +++ b/src/Framework/Framework/Controls/HtmlGenericControl.cs @@ -133,7 +133,7 @@ public string? InnerText /// /// Gets or sets whether the control is visible. When set to false, `style="display: none"` will be added to this control. /// - [MarkupOptions(AllowHardCodedValue = false)] + [MarkupOptions] public bool Visible { get { return (bool)GetValue(VisibleProperty)!; } diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index 825ca33160..192cc06a4c 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -564,8 +564,7 @@ }, "IncludeInPage": { "type": "System.Boolean", - "defaultValue": true, - "onlyBindings": true + "defaultValue": true } }, "DotVVM.Framework.Controls.EmptyData": { @@ -1016,8 +1015,7 @@ }, "Visible": { "type": "System.Boolean", - "defaultValue": true, - "onlyBindings": true + "defaultValue": true } }, "DotVVM.Framework.Controls.HtmlLiteral": {