diff --git a/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs b/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs deleted file mode 100644 index 906f4ee9ba..0000000000 --- a/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace DotVVM.Framework.ViewModel; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] -public class CustomPrimitiveTypeAttribute : Attribute -{ -} diff --git a/src/Framework/Core/ViewModel/IDotvvmPrimitiveType.cs b/src/Framework/Core/ViewModel/IDotvvmPrimitiveType.cs new file mode 100644 index 0000000000..58752d780d --- /dev/null +++ b/src/Framework/Core/ViewModel/IDotvvmPrimitiveType.cs @@ -0,0 +1,8 @@ +namespace DotVVM.Framework.ViewModel; + +/// +/// Marker interface instructing DotVVM to treat the type as a primitive type. +/// The type is required to have a static TryParse(string, [IFormatProvider,] out T) method and expected to implement ToString() method which is compatible with the TryParse method. +/// Primitive types are then serialized as string in client-side view models. +/// +public interface IDotvvmPrimitiveType { } diff --git a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs index 3ba35f50e1..ee4fb33e77 100644 --- a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs +++ b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs @@ -21,7 +21,7 @@ internal CustomPrimitiveTypeRegistration(Type type) { if (ReflectionUtils.IsCollection(type) || ReflectionUtils.IsDictionary(type)) { - throw new DotvvmConfigurationException($"The type {type} is marked with {nameof(CustomPrimitiveTypeAttribute)}, but it cannot be used as a custom primitive type. Custom primitive types cannot be collections, dictionaries, and cannot be primitive types already supported by DotVVM."); + throw new DotvvmConfigurationException($"The type {type} implements {nameof(IDotvvmPrimitiveType)}, but it cannot be used as a custom primitive type. Custom primitive types cannot be collections, dictionaries, and cannot be primitive types already supported by DotVVM."); } Type = type; @@ -38,7 +38,7 @@ internal static Func ResolveTryParseMethod(Type type) new[] { typeof(string), typeof(IFormatProvider), type.MakeByRefType() }, null) ?? type.GetMethod("TryParse", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy, null, new[] { typeof(string), type.MakeByRefType() }, null) - ?? throw new DotvvmConfigurationException($"The type {type} is marked with {nameof(CustomPrimitiveTypeAttribute)} but it does not contain a public static method TryParse(string, IFormatProvider, out {type}) or TryParse(string, out {type})!"); + ?? throw new DotvvmConfigurationException($"The type {type} implements {nameof(IDotvvmPrimitiveType)} but it does not contain a public static method TryParse(string, IFormatProvider, out {type}) or TryParse(string, out {type})!"); var inputParameter = Expression.Parameter(typeof(string), "arg"); var resultVariable = Expression.Variable(type, "result"); diff --git a/src/Framework/Framework/Controls/HtmlGenericControl.cs b/src/Framework/Framework/Controls/HtmlGenericControl.cs index 0c5559268b..b77fe47295 100644 --- a/src/Framework/Framework/Controls/HtmlGenericControl.cs +++ b/src/Framework/Framework/Controls/HtmlGenericControl.cs @@ -13,6 +13,7 @@ using System.Text; using DotVVM.Framework.Compilation.Javascript; using FastExpressionCompiler; +using DotVVM.Framework.ViewModel; namespace DotVVM.Framework.Controls { @@ -405,7 +406,10 @@ private static string AttributeValueToString(object? value) => Enum enumValue => ReflectionUtils.ToEnumString(enumValue.GetType(), enumValue.ToString()), Guid guid => guid.ToString(), _ when ReflectionUtils.IsNumericType(value.GetType()) => Convert.ToString(value, CultureInfo.InvariantCulture) ?? "", - _ when ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(value.GetType()) is {} registration => registration.ToStringMethod(value), + IDotvvmPrimitiveType => value switch { + IFormattable f => f.ToString(null, CultureInfo.InvariantCulture), + _ => value.ToString() ?? "" + }, 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."), _ => diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index 6819a9b96a..55d3e33ef4 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -26,6 +26,7 @@ using DotVVM.Framework.Compilation; using DotVVM.Framework.Routing; using DotVVM.Framework.ViewModel; +using System.Diagnostics; namespace DotVVM.Framework.Utils { @@ -307,7 +308,7 @@ public record TypeConvertException(object Value, Type Type, Exception InnerExcep typeof (decimal) }; // mapping of server-side types to their client-side representation - private static readonly ConcurrentDictionary CustomPrimitiveTypes = new(); + private static readonly ConcurrentDictionary CustomPrimitiveTypes = new(); public static IEnumerable GetNumericTypes() { @@ -374,31 +375,30 @@ public static bool IsDotvvmNativePrimitiveType(Type type) /// Returns true if the type is a custom primitive type. public static bool IsCustomPrimitiveType(Type type) { - return TryGetCustomPrimitiveTypeRegistration(type) is { }; + return typeof(IDotvvmPrimitiveType).IsAssignableFrom(type); } /// Returns a custom primitive type registration for the given type, or null if the type is not a custom primitive type. public static CustomPrimitiveTypeRegistration? TryGetCustomPrimitiveTypeRegistration(Type type) { - return CustomPrimitiveTypes.GetOrAdd(type, TryDiscoverCustomPrimitiveType); + if (IsCustomPrimitiveType(type)) + return CustomPrimitiveTypes.GetOrAdd(type, DiscoverCustomPrimitiveType); + else + return null; } /// Returns true the type is serialized as a JavaScript primitive (not object nor array) public static bool IsPrimitiveType(Type type) { return PrimitiveTypes.Contains(type) - || (IsNullableType(type) && IsDotvvmNativePrimitiveType(type.UnwrapNullableType())) + || (IsNullableType(type) && IsPrimitiveType(type.UnwrapNullableType())) || type.IsEnum - || TryGetCustomPrimitiveTypeRegistration(type) is {}; + || IsCustomPrimitiveType(type); } - private static CustomPrimitiveTypeRegistration? TryDiscoverCustomPrimitiveType(Type type) + private static CustomPrimitiveTypeRegistration DiscoverCustomPrimitiveType(Type type) { - var attribute = type.GetCustomAttribute(); - if (attribute == null) - { - return null; - } + Debug.Assert(typeof(IDotvvmPrimitiveType).IsAssignableFrom(type)); return new CustomPrimitiveTypeRegistration(type); } diff --git a/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs b/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs index ba2a5344eb..ad7e0388cd 100644 --- a/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs +++ b/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs @@ -81,7 +81,7 @@ private IEnumerable ValidateViewModel(object? viewMode if (propertyResult != ValidationResult.Success) { var propertyPath = - ReflectionUtils.IsCustomPrimitiveType(viewModelType) ? pathPrefix.TrimEnd('/') : pathPrefix + property.Name; + viewModel is IDotvvmPrimitiveType ? pathPrefix.TrimEnd('/') : pathPrefix + property.Name; yield return new ViewModelValidationError(rule.ErrorMessage, propertyPath, rootObject); } } diff --git a/src/Samples/Common/Controls/ControlWithCustomPrimitiveTypeProperties.cs b/src/Samples/Common/Controls/ControlWithCustomPrimitiveTypeProperties.cs index 3c60ca605d..2b513d6a87 100644 --- a/src/Samples/Common/Controls/ControlWithCustomPrimitiveTypeProperties.cs +++ b/src/Samples/Common/Controls/ControlWithCustomPrimitiveTypeProperties.cs @@ -42,8 +42,7 @@ public DotvvmControl GetContents( } - [CustomPrimitiveType] - public struct Point : IFormattable + public struct Point : IFormattable, IDotvvmPrimitiveType { public int X { get; set; } public int Y { get; set; } diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs index 413017224a..ae9b2a155d 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs @@ -4,8 +4,7 @@ namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes { - [CustomPrimitiveType] - public record SampleId : TypeId + public record SampleId : TypeId, IDotvvmPrimitiveType { public SampleId(Guid idValue) : base(idValue) { diff --git a/src/Tests/Binding/BindingCompilationTests.cs b/src/Tests/Binding/BindingCompilationTests.cs index 1f8a36e831..11d6fce6ab 100755 --- a/src/Tests/Binding/BindingCompilationTests.cs +++ b/src/Tests/Binding/BindingCompilationTests.cs @@ -1201,11 +1201,10 @@ public async Task GetStringPropAsync() } - [CustomPrimitiveType] record struct VehicleNumber( [property: Range(100, 999)] int Value - ) + ): IDotvvmPrimitiveType { public override string ToString() => Value.ToString(); public static bool TryParse(string s, out VehicleNumber result)