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)