Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backported fix 1385 #1437

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
640fed9
AsString() Enums support added
Feb 22, 2022
2f5933d
args[1].Clone() removed
Feb 28, 2022
c4355ea
Enum->string implicit conversion uses ToEnumString
exyi Mar 24, 2022
42a14fa
Fixed ToEnumString nullability
tomasherceg May 17, 2022
8208a84
Fixed problems in tests
tomasherceg May 17, 2022
d354646
Fix ToEnumString performance
exyi May 20, 2022
3915952
Fixed build after cherry-picking
acizmarik Jul 27, 2022
ac4f7fe
Fixed ToEnumString to work with flags
tomasherceg May 17, 2022
c14879e
Rewritten flag support using Newtonsoft.Json
tomasherceg Jun 4, 2022
18ff810
Moved `EnumToString` JS translation into binding helper namespace
acizmarik Jun 4, 2022
b3e7688
Removed extension `ToEnumString` from `ReflectionUtils`
acizmarik Jun 4, 2022
60ba129
Fixed possible issues with nullability
acizmarik Jun 4, 2022
9fbc923
Add binding property with information about used properties
exyi Mar 4, 2022
db5c63b
Added support for `Validator.Value` unwrapping
acizmarik May 17, 2022
7749188
Added sample and test for complex expression in `Validator.Value`
acizmarik May 17, 2022
e6b0bd8
Utilize caching mechanism to avoid repeated binding derive calls
acizmarik May 31, 2022
324c196
Provided more concrete specification for unwrappable methods
acizmarik May 31, 2022
7705da8
Added test for validating complex expressions in grid cells
acizmarik May 31, 2022
851d67a
Removed direct reference and leaving it for DotVVM to automatically d…
acizmarik Jun 4, 2022
8e217b3
Throw whenever value binding can not be resolved
acizmarik Jun 4, 2022
49c6e07
Removed `renderEvenInServerRenderingMode` parameter
acizmarik Jun 4, 2022
431c407
Added support for unwrapping also `ToEnumString`
acizmarik Jun 4, 2022
28b060e
Fixed comment
acizmarik Jun 4, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Framework/Framework/Binding/BindingProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.CodeAnalysis;
using DotVVM.Framework.Binding.Expressions;
using DotVVM.Framework.Compilation.ControlTree;
using System.Reflection;

namespace DotVVM.Framework.Binding.Properties
{
Expand Down Expand Up @@ -232,4 +233,6 @@ public sealed record IsNullOrWhitespaceBindingExpression(IBinding Binding);
public sealed record IsNullOrEmptyBindingExpression(IBinding Binding);
/// <summary> Contains the same binding as this binding but converted to a string. </summary>
public sealed record ExpectedAsStringBindingExpression(IBinding Binding);
/// <summary> Contains references to the .NET properties referenced in the binding. MainProperty is the property on the root node (modulo conversions and simple expressions). </summary>
public sealed record ReferencedViewModelPropertiesBindingProperty(PropertyInfo? MainProperty, PropertyInfo[] OtherProperties, IValueBinding UnwrappedBindingExpression);
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ public ValueBindingExpression(BindingCompilationService service, IEnumerable<obj
}

private protected MaybePropValue<KnockoutExpressionBindingProperty> knockoutExpressions;
private protected MaybePropValue<ReferencedViewModelPropertiesBindingProperty> referencedPropertyExpressions;

private protected override void StoreProperty(object p)
{
if (p is KnockoutExpressionBindingProperty knockoutExpressions)
this.knockoutExpressions.SetValue(new(knockoutExpressions));
if (p is ReferencedViewModelPropertiesBindingProperty referencedPropertyExpressions)
this.referencedPropertyExpressions.SetValue(new(referencedPropertyExpressions));
else
base.StoreProperty(p);
}
Expand All @@ -49,6 +52,8 @@ private protected override void StoreProperty(object p)
{
if (type == typeof(KnockoutExpressionBindingProperty))
return knockoutExpressions.GetValue(this).GetValue(errorMode, this, type);
if (type == typeof(ReferencedViewModelPropertiesBindingProperty))
return referencedPropertyExpressions.GetValue(this).GetValue(errorMode, this, type);
return base.GetProperty(type, errorMode);
}

Expand Down
11 changes: 11 additions & 0 deletions src/Framework/Framework/Binding/HelperNamespace/Enums.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using DotVVM.Framework.Utils;

namespace DotVVM.Framework.Binding.HelperNamespace
{
Expand All @@ -9,5 +10,15 @@ public static string[] GetNames<TEnum>()
{
return Enum.GetNames(typeof(TEnum));
}

public static string? ToEnumString<T>(T? instance) where T : struct, Enum
{
return ReflectionUtils.ToEnumString(instance);
}

public static string ToEnumString<T>(this T instance) where T : struct, Enum
{
return ReflectionUtils.ToEnumString<T>(instance);
}
}
}
14 changes: 12 additions & 2 deletions src/Framework/Framework/Binding/ValueOrBindingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,21 @@ public static ValueOrBinding<IList<T>> GetItems<T>(this ValueOrBinding<IBaseGrid
public static ValueOrBinding<string> AsString<T>(this ValueOrBinding<T> v)
{
if (v.BindingOrDefault is IBinding binding)
return new ValueOrBinding<string>(
return new(
binding.GetProperty<ExpectedAsStringBindingExpression>().Binding
);
else if (v.ValueOrDefault is null)
{
return new("");
}
else if (typeof(T).IsValueType && typeof(T).UnwrapNullableType().IsEnum)
{
return new(ReflectionUtils.ToEnumString(typeof(T), v.ValueOrDefault.ToString() ?? ""));
}
else
return new ValueOrBinding<string>("" + v.ValueOrDefault);
{
return new(v.ValueOrDefault.ToString() ?? "");
}
}
/// <summary> Returns ValueOrBinding with the value of `a is object`. The resulting binding is cached, so it's safe to use this method at runtime. </summary>
public static ValueOrBinding<bool> NotNull<T>(this ValueOrBinding<T> v) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Collections.Concurrent;
using DotVVM.Framework.Binding.HelperNamespace;

namespace DotVVM.Framework.Compilation.Binding
{
Expand Down Expand Up @@ -429,5 +430,49 @@ public IsMoreThanZeroBindingProperty IsMoreThanZero(ParsedExpressionBindingPrope
Expression.GreaterThan(expr.Expression, Expression.Constant(0))
));
}

public ReferencedViewModelPropertiesBindingProperty GetReferencedViewModelProperties(IValueBinding binding, ParsedExpressionBindingProperty expression)
{
var allProperties = new List<PropertyInfo>();
var expr = expression.Expression;

expr.ForEachMember(m => {
if (m is PropertyInfo property)
{
allProperties.Add(property);
}
});

while (true)
{
// unwrap type conversions, negations, ...
if (expr is UnaryExpression unary)
expr = unary.Operand;
// unwrap some method invocations
else if (expr is MethodCallExpression boxCall && boxCall.Method.DeclaringType == typeof(BoxingUtils))
expr = boxCall.Arguments.First();
else if (expr is MethodCallExpression { Method.Name: nameof(DateTimeExtensions.ToBrowserLocalTime) } dtMethodCall && dtMethodCall.Method.DeclaringType == typeof(DateTimeExtensions))
expr = dtMethodCall.Object ?? dtMethodCall.Arguments.First();
else if (expr is MethodCallExpression { Method.Name: nameof(object.ToString) } toStringMethodCall)
expr = toStringMethodCall.Object ?? toStringMethodCall.Arguments.First();
else if (expr is MethodCallExpression { Method.Name: nameof(Enums.ToEnumString) } toEnumStringMethodCall && toEnumStringMethodCall.Method.DeclaringType == typeof(Enums))
expr = toEnumStringMethodCall.Object ?? toEnumStringMethodCall.Arguments.First();
// unwrap binary operation with a constant
else if (expr is BinaryExpression { Right.NodeType: ExpressionType.Constant } binaryLeft)
expr = binaryLeft.Left;
else if (expr is BinaryExpression { Left.NodeType: ExpressionType.Constant } binaryRight)
expr = binaryRight.Right;
else
break;
}
var mainProperty = (expr as MemberExpression)?.Member as PropertyInfo;
var unwrappedBinding = binding.DeriveBinding(expr);

return new(
mainProperty,
allProperties.ToArray(),
unwrappedBinding
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using DotVVM.Framework.Compilation.Javascript.Ast;
using DotVVM.Framework.Compilation.ControlTree.Resolved;
using DotVVM.Framework.Binding;
using DotVVM.Framework.Binding.HelperNamespace;

namespace DotVVM.Framework.Compilation.Binding
{
Expand Down Expand Up @@ -189,6 +190,10 @@ public static bool IsStringConversionAllowed(Type fromType)

public static Expression? ToStringConversion(Expression src)
{
if (src.Type.UnwrapNullableType().IsEnum)
{
return Expression.Call(typeof(Enums), "ToEnumString", new [] { src.Type.UnwrapNullableType() }, src);
}
var toStringMethod = src.Type.GetMethod("ToString", Type.EmptyTypes);
if (toStringMethod?.DeclaringType == typeof(object))
toStringMethod = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ private void AddDefaultToStringTranslations()
{
AddMethodTranslator(typeof(object), "ToString", new PrimitiveToStringTranslator(), 0);
AddMethodTranslator(typeof(Convert), "ToString", new PrimitiveToStringTranslator(), 1, true);
AddMethodTranslator(typeof(Enums), "ToEnumString", parameterCount: 1, translator: new GenericMethodCompiler(
args => args[1]
));

AddMethodTranslator(typeof(DateTime).GetMethod("ToString", Type.EmptyTypes), new GenericMethodCompiler(
args => new JsIdentifierExpression("dotvvm").Member("globalize").Member("bindingDateToString")
Expand Down
31 changes: 17 additions & 14 deletions src/Framework/Framework/Controls/HtmlGenericControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using FastExpressionCompiler;

namespace DotVVM.Framework.Controls
{
Expand Down Expand Up @@ -368,25 +369,27 @@ private void AddHtmlAttribute(IHtmlWriter writer, string name, object? value)
}
else if (value is Enum enumValue)
{
writer.AddAttribute(name, enumValue.ToEnumString());
writer.AddAttribute(name, ReflectionUtils.ToEnumString(enumValue.GetType(), enumValue.ToString()));
}
else if (value is Guid)
{
writer.AddAttribute(name, value.ToString());
}
else if (ReflectionUtils.IsNumericType(value.GetType()))
{
writer.AddAttribute(name, Convert.ToString(value, CultureInfo.InvariantCulture));
}
else
{
}

private static string AttributeValueToString(object? value) =>
value switch {
null => "",
string str => str,
Enum enumValue => ReflectionUtils.ToEnumString(enumValue.GetType(), enumValue.ToString()),
Guid guid => guid.ToString(),
_ when ReflectionUtils.IsNumericType(value.GetType()) => Convert.ToString(value, CultureInfo.InvariantCulture) ?? "",
System.Collections.IEnumerable =>
throw new NotSupportedException($"Attribute value of type '{value.GetType().ToCode(stripNamespace: true)}' is not supported. Consider concatenating the values into a string or use the HtmlGenericControl.AttributeList if you need to pass multiple values."),
_ =>

// DateTime and related are not supported here intentionally.
// It is not clear in which format it should be rendered - on some places, the HTML specs requires just yyyy-MM-dd,
// but in case of Web Components, the users may want to pass the whole date, or use a specific format

throw new NotSupportedException($"Attribute value of type '{value.GetType().FullName}' is not supported. Please convert the value to string, e. g. by using ToString()");
}
}
throw new NotSupportedException($"Attribute value of type '{value.GetType().FullName}' is not supported. Please convert the value to string, e. g. by using ToString()")
};

private void AddHtmlAttributesToRender(ref RenderState r, IHtmlWriter writer)
{
Expand Down
18 changes: 13 additions & 5 deletions src/Framework/Framework/Controls/KnockoutHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ public static class KnockoutHelper
var expression = control.GetValueBinding(property);
if (expression != null && (!control.RenderOnServer || renderEvenInServerRenderingMode))
{
writer.AddKnockoutDataBind(name, expression.GetKnockoutBindingExpression(control));
if (valueUpdate != null)
{
writer.AddKnockoutDataBind("valueUpdate", $"'{valueUpdate}'");
}
writer.AddKnockoutDataBind(name, control, expression, valueUpdate);
}
else
{
Expand All @@ -38,6 +34,18 @@ public static class KnockoutHelper
}
}

/// <summary>
/// Adds the data-bind attribute to the next HTML element that is being rendered.
/// </summary>
public static void AddKnockoutDataBind(this IHtmlWriter writer, string name, DotvvmBindableObject control, IValueBinding expression, string? valueUpdate = null)
{
writer.AddKnockoutDataBind(name, expression.GetKnockoutBindingExpression(control));
if (valueUpdate != null)
{
writer.AddKnockoutDataBind("valueUpdate", $"'{valueUpdate}'");
}
}

[Obsolete("Use the AddKnockoutDataBind(this IHtmlWriter writer, string name, IValueBinding valueBinding, DotvvmControl control) or AddKnockoutDataBind(this IHtmlWriter writer, string name, string expression) overload")]
public static void AddKnockoutDataBind(this IHtmlWriter writer, string name, IValueBinding valueBinding)
{
Expand Down
19 changes: 18 additions & 1 deletion src/Framework/Framework/Controls/Validator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
using DotVVM.Framework.Binding.Expressions;
using Microsoft.Extensions.DependencyInjection;
using DotVVM.Framework.Configuration;
using DotVVM.Framework.Binding.Properties;
using DotVVM.Framework.Compilation.Binding;
using System.Linq.Expressions;

namespace DotVVM.Framework.Controls
{
Expand Down Expand Up @@ -83,7 +86,21 @@ public class Validator : HtmlGenericControl

private static void AddValidatedValue(IHtmlWriter writer, IDotvvmRequestContext context, DotvvmProperty prop, DotvvmControl control)
{
writer.AddKnockoutDataBind("dotvvm-validation", control, ValueProperty, renderEvenInServerRenderingMode: true);
const string validationDataBindName = "dotvvm-validation";

var binding = control.GetValueBinding(ValueProperty);
if (binding is not null)
{
var referencedPropertyExpressions = binding.GetProperty<ReferencedViewModelPropertiesBindingProperty>();
var unwrappedPropertyExpression = referencedPropertyExpressions.UnwrappedBindingExpression;

// We were able to unwrap the the provided expression
writer.AddKnockoutDataBind(validationDataBindName, control, unwrappedPropertyExpression);
}
else
{
throw new DotvvmControlException($"Could not resolve {nameof(ValueProperty)} to a valid value binding.");
}

// render options
var bindingGroup = new KnockoutBindingGroup();
Expand Down
32 changes: 30 additions & 2 deletions src/Framework/Framework/Utils/ReflectionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
using System.Runtime.Serialization;
using System.Threading.Tasks;
using DotVVM.Framework.Binding;
using DotVVM.Framework.Configuration;
using FastExpressionCompiler;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using RecordExceptions;

namespace DotVVM.Framework.Utils
Expand Down Expand Up @@ -472,10 +475,18 @@ public static string GetTypeHash(this Type type)
member is TypeInfo type ? type.AsType() :
throw new NotImplementedException($"Could not get return type of member {member.GetType().FullName}");

public static string ToEnumString<T>(this T instance) where T : Enum
[return: NotNullIfNotNull("instance")]
public static string? ToEnumString<T>(T? instance) where T : struct, Enum
{
return ToEnumString(instance.GetType(), instance.ToString());
if (instance == null)
return null;

var name = instance.ToString()!;
if (!EnumInfo<T>.HasEnumMemberField)
return name;
return ToEnumString(typeof(T), name);
}

public static string ToEnumString(Type enumType, string name)
{
var field = enumType.GetField(name);
Expand All @@ -490,6 +501,23 @@ public static string ToEnumString(Type enumType, string name)
return name;
}

internal static class EnumInfo<T> where T: struct, Enum
{
internal static readonly bool HasEnumMemberField;

static EnumInfo()
{
foreach (var field in typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static))
{
if (field.IsDefined(typeof(EnumMemberAttribute), false))
{
HasEnumMemberField = true;
break;
}
}
}
}

public static Type GetDelegateType(MethodInfo methodInfo)
{
return Expression.GetDelegateType(methodInfo.GetParameters().Select(a => a.ParameterType).Append(methodInfo.ReturnType).ToArray());
Expand Down
1 change: 1 addition & 0 deletions src/Samples/Common/DotVVM.Samples.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
<None Remove="Views\FeatureSamples\Serialization\EnumSerializationCoercion.dothtml" />
<None Remove="Views\FeatureSamples\Serialization\SerializationDateTimeOffset.dothtml" />
<None Remove="Views\FeatureSamples\StringInterpolation\StringInterpolation.dothtml" />
<None Remove="Views\FeatureSamples\Validation\ValidatorValueComplexExpressions.dothtml" />
<None Remove="Views\FeatureSamples\ViewModules\Incrementer.dotcontrol" />
<None Remove="Views\FeatureSamples\ViewModules\IncrementerInRepeater.dothtml" />
<None Remove="Views\FeatureSamples\ViewModules\InnerModuleControl.dotcontrol" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.ComponentModel.DataAnnotations;
using DotVVM.Framework.ViewModel;

namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.Validation
{
public class ValidatorValueComplexExpressionsViewModel : DotvvmViewModelBase
{
[Required]
public DateTime DateTime { get; set; }

public TestDto[] Collection { get; set; } = new[]
{
new TestDto() { Id = 1, Description = "DateTime is not null", DateTime = DateTime.Now },
new TestDto() { Id = 1, Description = "DateTime is null", DateTime = null }
};
}

public class TestDto
{
public int Id { get; set; }

public string Description { get; set; }

[Required]
public DateTime? DateTime { get; set; }
}
}

Loading