From 59ac901cae5bff5c873b62ec1555a2900d25d93b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Thu, 27 Oct 2022 12:40:06 +0200 Subject: [PATCH 01/32] Prepared sample page for type ids --- .../Common/DotVVM.Samples.Common.csproj | 1 + src/Samples/Common/DotvvmStartup.cs | 1 + .../CompositeControls/BasicSampleViewModel.cs | 1 + .../CustomPrimitiveTypes/BasicViewModel.cs | 134 ++++++++++++++++++ .../CustomPrimitiveTypes/Basic.dothtml | 32 +++++ 5 files changed, 169 insertions(+) create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs create mode 100644 src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index 22023293b8..00c932322f 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -94,6 +94,7 @@ + diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index c041796624..c38a4964ed 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -196,6 +196,7 @@ private static void AddRoutes(DotvvmConfiguration config) config.RouteTable.Add("FeatureSamples_ParameterBinding_OptionalParameterBinding2", "FeatureSamples/ParameterBinding/OptionalParameterBinding2/{Id?}", "Views/FeatureSamples/ParameterBinding/OptionalParameterBinding.dothtml", new { Id = 300 }); config.RouteTable.Add("FeatureSamples_Validation_Localization", "FeatureSamples/Validation/Localization", "Views/FeatureSamples/Validation/Localization.dothtml", presenterFactory: LocalizablePresenter.BasedOnQuery("lang")); config.RouteTable.Add("FeatureSamples_Localization_Globalize", "FeatureSamples/Localization/Globalize", "Views/FeatureSamples/Localization/Globalize.dothtml", presenterFactory: LocalizablePresenter.BasedOnQuery("lang")); + config.RouteTable.Add("FeatureSamples_CustomPrimitiveTypes_Basic", "FeatureSamples/CustomPrimitiveTypes/Basic/{Id?}", "Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml"); config.RouteTable.AutoDiscoverRoutes(new DefaultRouteStrategy(config)); diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CompositeControls/BasicSampleViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/CompositeControls/BasicSampleViewModel.cs index 7ba58f7dc9..ebff37cfe4 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CompositeControls/BasicSampleViewModel.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CompositeControls/BasicSampleViewModel.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using DotVVM.Framework.ViewModel; +using DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes; namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CompositeControls { diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs new file mode 100644 index 0000000000..b647cd7463 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.ViewModel; +using DotVVM.Samples.Common.ViewModels.FeatureSamples.CompositeControls; +using Newtonsoft.Json; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes +{ + public class BasicViewModel : DotVVM.Samples.BasicSamples.ViewModels.ComplexSamples.SPA.SiteViewModel + { + + [FromRoute("id")] + public SampleId IdInRoute { get; set; } + + [FromQuery("id")] + public SampleId? IdInQuery { get; set; } + + [Required] + public SampleId SelectedItemId { get; set; } + + [Required] + public SampleId? SelectedItemNullableId { get; set; } + + + // použít typové ID ve viewmodelu + // [Required] validace + // musí fungovat i když je nullable + + // vybírání hodnot v comboboxu, checkboxu + opět s podporou nullable + + + // routing - + + // [FromRoute] a [FromQuery] + + // JS translations + + public List Items { get; set; } = new List + { + new SampleItem() { Id = SampleId.CreateExisting(new Guid("96c37b99-5fd5-448c-8a64-977ae11b8b8b")), Text = "Item 1" }, + new SampleItem() { Id = SampleId.CreateExisting(new Guid("c2654a1f-3781-49a8-911b-c7346db166e0")), Text = "Item 2" }, + new SampleItem() { Id = SampleId.CreateExisting(new Guid("e467a201-9ab7-4cd5-adbf-66edd03f6ae1")), Text = "Item 3" }, + }; + + } + + public class SampleItem + { + public SampleId Id { get; set; } + public string Text { get; set; } + } + + + [JsonConverter(typeof(TypeIdJsonConverter))] + public record SampleId : TypeId + { + public SampleId(Guid idValue) : base(idValue) + { + } + } + + public abstract record TypeId : ITypeId + where TId : TypeId + { + public Guid IdValue { get; } + + protected TypeId(Guid idValue) + { + if (idValue == default) throw new ArgumentException(nameof(idValue)); + IdValue = idValue; + } + + public static TId CreateNew() + { + var guid = Guid.NewGuid(); + return (TId)Activator.CreateInstance(typeof(TId), args: guid)!; + } + + public static TId CreateExisting(Guid idValue) + { + if (idValue == default) throw new ArgumentException(nameof(idValue)); + return (TId)Activator.CreateInstance(typeof(TId), args: idValue)!; + } + + public override string ToString() + { + return $"{GetType()} {{{IdValue}}}"; + } + } + + public interface ITypeId + { + Guid IdValue { get; } + } + + public class TypeIdJsonConverter : JsonConverter where TId : TypeId + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(TId); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String) + { + var idText = reader.ReadAsString(); + var idValue = Guid.Parse(idText); + return TypeId.CreateExisting(idValue); + } + else if (reader.TokenType == JsonToken.Null) + { + return null; + } + else + { + throw new JsonSerializationException($"Token {reader.TokenType} cannot be deserialized as TypeId!"); + } + + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue((value as ITypeId)?.IdValue); + } + + } + +} + diff --git a/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml new file mode 100644 index 0000000000..29a8b76560 --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml @@ -0,0 +1,32 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes.BasicViewModel, DotVVM.Samples.Common +@masterPage Views/ComplexSamples/SPA/site.dotmaster + + + +

+ Selected item ID: {{value: SelectedItemId}} +
+ +

+ +

+ Selected nullable item ID: {{value: SelectedItemNullableId}} +
+ +

+ +

+ Route parameter: {{value: IdInRoute}} +
+ Query parameter: {{value: IdInQuery}} +

+ + +
+ From 29cbda67e8e0f936695ae34a68d1fac7e831f5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Thu, 27 Oct 2022 13:20:57 +0200 Subject: [PATCH 02/32] Proof of concept of type metadata for custom primitive types --- .../CustomPrimitiveTypeRegistration.cs | 18 ++++++++++++ .../Configuration/DotvvmConfiguration.cs | 8 ++++++ .../DotvvmRuntimeConfiguration.cs | 10 +++++++ .../Framework/Utils/ReflectionUtils.cs | 22 +++++++++------ .../ViewModelTypeMetadataSerializer.cs | 25 +++++++++-------- .../ApplicationInsights.Owin/Web.config | 4 +-- src/Samples/Common/DotvvmStartup.cs | 3 ++ .../CustomPrimitiveTypes/BasicViewModel.cs | 3 +- .../CustomPrimitiveTypes/Basic.dothtml | 28 +++++++++++++++---- 9 files changed, 92 insertions(+), 29 deletions(-) create mode 100644 src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs diff --git a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs new file mode 100644 index 0000000000..d1ae9219c7 --- /dev/null +++ b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs @@ -0,0 +1,18 @@ +using System; + +namespace DotVVM.Framework.Configuration +{ + public class CustomPrimitiveTypeRegistration + { + // TODO: think about better name + public Type Type { get; } + + public Type ClientSidePrimitiveType { get; } + + public CustomPrimitiveTypeRegistration(Type type, Type clientSidePrimitiveType) + { + Type = type; + ClientSidePrimitiveType = clientSidePrimitiveType; + } + } +} diff --git a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs index e777b96c61..31e3d2708d 100644 --- a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs @@ -38,6 +38,11 @@ public sealed class DotvvmConfiguration private bool isFrozen; public const string DotvvmControlTagPrefix = "dot"; + /// + /// Fired when the configuration routine is completely initialized and frozen. + /// + public event Action ConfigurationReady; + /// /// Gets or sets the application physical path. /// @@ -167,6 +172,8 @@ public void Freeze() _routeConstraints.Freeze(); Styles.Freeze(); FreezableList.Freeze(ref _compiledViewsAssemblies); + + ConfigurationReady?.Invoke(); } [JsonIgnore] @@ -228,6 +235,7 @@ internal DotvvmConfiguration(IServiceProvider services) RouteTable = new DotvvmRouteTable(this); _styles = new StyleRepository(this); + ConfigurationReady += () => ReflectionUtils.RegisterCustomPrimitiveTypes(Runtime.CustomPrimitiveTypes); } private static ServiceCollection CreateDefaultServiceCollection() diff --git a/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs index e024df0b94..027266861b 100644 --- a/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs @@ -16,12 +16,21 @@ public class DotvvmRuntimeConfiguration public IList GlobalFilters => _globalFilters; private IList _globalFilters; + /// + /// Gets types that are treated as primitive by DotVVM components and serialization. + /// + [JsonIgnore()] + public IList CustomPrimitiveTypes => _customPrimitiveTypes; + private IList _customPrimitiveTypes; + + /// /// Initializes a new instance of the class. /// public DotvvmRuntimeConfiguration() { _globalFilters = new FreezableList(); + _customPrimitiveTypes = new FreezableList(); } private bool isFrozen = false; @@ -35,6 +44,7 @@ public void Freeze() { this.isFrozen = true; FreezableList.Freeze(ref _globalFilters); + FreezableList.Freeze(ref _customPrimitiveTypes); } } } diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index dd942ca4c3..2cf2d09d90 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -293,6 +293,8 @@ public record TypeConvertException(object Value, Type Type, Exception InnerExcep typeof (double), typeof (decimal) }; + // mapping of server-side types to their client-side representation + internal static readonly Dictionary CustomPrimitiveTypes = new Dictionary(); public static IEnumerable GetNumericTypes() { @@ -348,21 +350,14 @@ public static bool IsCollection(Type type) return type != typeof(string) && IsEnumerable(type) && !IsDictionary(type); } - public static bool IsPrimitiveType(Type type) + public static bool IsPrimitiveType(this Type type) { return PrimitiveTypes.Contains(type) + || CustomPrimitiveTypes.ContainsKey(type) || (IsNullableType(type) && IsPrimitiveType(type.UnwrapNullableType())) || type.IsEnum; } - public static bool IsSerializationSupported(this Type type, bool includeNullables) - { - if (includeNullables) - return IsPrimitiveType(type); - - return PrimitiveTypes.Contains(type); - } - public static bool IsNullableType(Type type) { return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); @@ -611,5 +606,14 @@ public static IEnumerable GetBaseTypesAndInterfaces(Type type) type = baseType; } } + + internal static void RegisterCustomPrimitiveTypes(IList customPrimitiveTypeRegistrations) + { + // TODO: validation + foreach (var registration in customPrimitiveTypeRegistrations) + { + CustomPrimitiveTypes.Add(registration.Type, registration.ClientSidePrimitiveType); + } + } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs index 3cc4fc524e..ab2b01b169 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs @@ -148,8 +148,21 @@ private ObjectMetadataWithDependencies BuildObjectTypeMetadata(ViewModelSerializ internal JToken GetTypeIdentifier(Type type, HashSet dependentObjectTypes, HashSet dependentEnumTypes) { - if (type.IsSerializationSupported(includeNullables: false)) + if (type.IsEnum) { + dependentEnumTypes.Add(type); + return GetEnumTypeName(type); + } + else if (ReflectionUtils.IsNullable(type)) + { + return GetNullableTypeIdentifier(type, dependentObjectTypes, dependentEnumTypes); + } + else if (type.IsPrimitiveType()) // we intentionally detect this after handling enums and nullable types + { + if (ReflectionUtils.CustomPrimitiveTypes.TryGetValue(type, out var clientSideType)) + { + return GetTypeIdentifier(clientSideType, dependentObjectTypes, dependentEnumTypes); + } return GetPrimitiveTypeName(type); } else if (type == typeof(object)) @@ -162,15 +175,6 @@ internal JToken GetTypeIdentifier(Type type, HashSet dependentObjectTypes, var keyValuePair = typeof(KeyValuePair<,>).MakeGenericType(attrs); return new JArray(GetTypeIdentifier(keyValuePair, dependentObjectTypes, dependentEnumTypes)); } - else if (type.IsEnum) - { - dependentEnumTypes.Add(type); - return GetEnumTypeName(type); - } - else if (ReflectionUtils.IsNullable(type)) - { - return GetNullableTypeIdentifier(type, dependentObjectTypes, dependentEnumTypes); - } else if (ReflectionUtils.IsCollection(type)) { return new JArray(GetTypeIdentifier(ReflectionUtils.GetEnumerableType(type)!, dependentObjectTypes, dependentEnumTypes)); @@ -226,7 +230,6 @@ private JObject BuildEnumTypeMetadata(Type type) private string GetPrimitiveTypeName(Type type) => type.Name.ToString(); - readonly struct ObjectMetadataWithDependencies { public JObject Metadata { get; } 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/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index c38a4964ed..89e5c3105f 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -31,6 +31,7 @@ using DotVVM.Samples.Common.ViewModels.FeatureSamples.BindingVariables; using DotVVM.Samples.Common.Views.ControlSamples.TemplateHost; using DotVVM.Framework.ResourceManagement; +using DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes; namespace DotVVM.Samples.BasicSamples { @@ -80,6 +81,8 @@ public void Configure(DotvvmConfiguration config, string applicationPath) config.AssertConfigurationIsValid(); config.RouteTable.Add("Errors_Routing_NonExistingView", "Errors/Routing/NonExistingView", "Views/Errors/Routing/NonExistingView.dothml"); + + config.Runtime.CustomPrimitiveTypes.Add(new CustomPrimitiveTypeRegistration(typeof(SampleId), typeof(Guid?))); } private void LoadSampleConfiguration(DotvvmConfiguration config, string applicationPath) diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs index b647cd7463..633469c68a 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs @@ -5,12 +5,11 @@ using System.Text; using System.Threading.Tasks; using DotVVM.Framework.ViewModel; -using DotVVM.Samples.Common.ViewModels.FeatureSamples.CompositeControls; using Newtonsoft.Json; namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes { - public class BasicViewModel : DotVVM.Samples.BasicSamples.ViewModels.ComplexSamples.SPA.SiteViewModel + public class BasicViewModel : DotvvmViewModelBase { [FromRoute("id")] diff --git a/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml index 29a8b76560..c6ceee5e7c 100644 --- a/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml +++ b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml @@ -1,8 +1,26 @@ @viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes.BasicViewModel, DotVVM.Samples.Common -@masterPage Views/ComplexSamples/SPA/site.dotmaster - + + + + + Conditional css classes + + +

Selected item ID: {{value: SelectedItemId}}
@@ -26,7 +44,7 @@
Query parameter: {{value: IdInQuery}}

- - -
+ + + From 304a4ecd38ef707deead869c562728ea4cfa1097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Thu, 27 Oct 2022 13:43:54 +0200 Subject: [PATCH 03/32] Custom primitive types URL and route parameter support --- .../Framework/Utils/ReflectionUtils.cs | 6 ++++ .../CustomPrimitiveTypes/BasicViewModel.cs | 35 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index 2cf2d09d90..c83d4ca0de 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -22,6 +22,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using RecordExceptions; +using System.ComponentModel; namespace DotVVM.Framework.Utils { @@ -228,6 +229,11 @@ public static bool IsAssignableToGenericType(this Type givenType, Type genericTy return Convert.ChangeType(long.Parse(str2, numberStyle & NumberStyles.Integer, CultureInfo.InvariantCulture), type, CultureInfo.InvariantCulture); } + if (TypeDescriptor.GetConverter(type) is { } converter && converter.CanConvertFrom(value.GetType())) + { + return converter.ConvertTo(value, type); + } + // convert try { diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs index 633469c68a..62ec647aef 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; +using DotVVM.Core.Storage; using DotVVM.Framework.ViewModel; using Newtonsoft.Json; @@ -53,7 +56,7 @@ public class SampleItem public string Text { get; set; } } - + [TypeConverter(typeof(TypeIdConverter))] [JsonConverter(typeof(TypeIdJsonConverter))] public record SampleId : TypeId { @@ -62,6 +65,36 @@ public SampleId(Guid idValue) : base(idValue) } } + public class TypeIdConverter : TypeConverter + where TId : TypeId + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string) || sourceType == typeof(Guid) || sourceType == typeof(Guid?); + } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return destinationType == typeof(TId); + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (value is string stringValue) + { + return TypeId.CreateExisting(new Guid(stringValue)); + } + else if (value is Guid guidValue) + { + return TypeId.CreateExisting(guidValue); + } + else + { + return null; + } + } + } + public abstract record TypeId : ITypeId where TId : TypeId { From 9c728cb0596e504fc13a5f9426481dd83c53edbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Thu, 27 Oct 2022 14:03:20 +0200 Subject: [PATCH 04/32] Custom primitive types - added sample for commands and fixed JSON converter --- .../CustomPrimitiveTypes/BasicViewModel.cs | 24 +++++++++++++++++-- .../CustomPrimitiveTypes/Basic.dothtml | 14 ++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs index 62ec647aef..4aec4ff4c5 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; @@ -48,6 +49,26 @@ public class BasicViewModel : DotvvmViewModelBase new SampleItem() { Id = SampleId.CreateExisting(new Guid("e467a201-9ab7-4cd5-adbf-66edd03f6ae1")), Text = "Item 3" }, }; + public SampleId StaticCommandResult { get; set; } + + [AllowStaticCommand] + public SampleId StaticCommandWithSampleId(SampleId? current) + { + if (!Items.Any(i => i.Id == current)) + { + throw new Exception("The 'current' parameter didn't deserialize correctly."); + } + return SampleId.CreateExisting(new Guid("54162c7e-cdcc-4585-aa92-2e78be3f0c75")); + } + + public void CommandWithSampleId(SampleId current) + { + if (!Items.Any(i => i.Id == current)) + { + throw new Exception("The 'current' parameter didn't deserialize correctly."); + } + } + } public class SampleItem @@ -140,7 +161,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist { if (reader.TokenType == JsonToken.String) { - var idText = reader.ReadAsString(); + var idText = (string)reader.Value; var idValue = Guid.Parse(idText); return TypeId.CreateExisting(idValue); } @@ -152,7 +173,6 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist { throw new JsonSerializationException($"Token {reader.TokenType} cannot be deserialized as TypeId!"); } - } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) diff --git a/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml index c6ceee5e7c..b334973b6e 100644 --- a/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml +++ b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml @@ -28,6 +28,7 @@ ItemTextBinding="{value: Text}" ItemValueBinding="{value: Id}" SelectedValue="{value: SelectedItemId}" /> +

@@ -37,6 +38,7 @@ ItemTextBinding="{value: Text}" ItemValueBinding="{value: Id}" SelectedValue="{value: SelectedItemNullableId}" /> +

@@ -44,7 +46,17 @@
Query parameter: {{value: IdInQuery}}

- +

+ +

+ +

+ +
+ +
+ Static command result: {{value: StaticCommandResult}} +

From 83237f50580598a8814f5d63c5a1d6d9ce16f2ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Thu, 27 Oct 2022 14:20:24 +0200 Subject: [PATCH 05/32] Fixed issue in validation when property is not found on the client side --- .../Framework/Resources/Scripts/validation/validation.ts | 3 ++- .../FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Framework/Framework/Resources/Scripts/validation/validation.ts b/src/Framework/Framework/Resources/Scripts/validation/validation.ts index 6c62f2b541..7f66479f80 100644 --- a/src/Framework/Framework/Resources/Scripts/validation/validation.ts +++ b/src/Framework/Framework/Resources/Scripts/validation/validation.ts @@ -338,9 +338,10 @@ export function addErrors(errors: ValidationErrorDescriptor[], options: AddError try { // find the property const property = evaluator.traverseContext(root, propertyPath); + ValidationError.attach(prop.errorMessage, propertyPath, property); } catch (err) { - logWarning("validation", err); + logWarning("validation", `Unable to find viewmodel property ${propertyPath}. If you've added this validation error using Context.AddModelError, make sure the property path is correct and that the object hasn't been removed from the viewmodel.`, err); } } diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs index 4aec4ff4c5..5f9ab39984 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using DotVVM.Core.Storage; using DotVVM.Framework.ViewModel; +using DotVVM.Framework.ViewModel.Validation; using Newtonsoft.Json; namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes @@ -67,6 +68,12 @@ public void CommandWithSampleId(SampleId current) { throw new Exception("The 'current' parameter didn't deserialize correctly."); } + if (current == Items[0].Id) + { + this.AddModelError(vm => vm.SelectedItemId, "Valid property path"); + this.AddModelError(vm => vm.SelectedItemId.IdValue, "Invalid property path"); + Context.FailOnInvalidModelState(); + } } } From b9f860441bf62cc7691ea525a1f9ddec43df8f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Thu, 27 Oct 2022 14:27:02 +0200 Subject: [PATCH 06/32] Custom primitive types - added sample for JS translation --- src/Samples/Common/DotvvmStartup.cs | 4 ++++ .../Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index 89e5c3105f..481848c6bb 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -83,6 +83,10 @@ public void Configure(DotvvmConfiguration config, string applicationPath) config.RouteTable.Add("Errors_Routing_NonExistingView", "Errors/Routing/NonExistingView", "Views/Errors/Routing/NonExistingView.dothml"); config.Runtime.CustomPrimitiveTypes.Add(new CustomPrimitiveTypeRegistration(typeof(SampleId), typeof(Guid?))); + config.Markup.JavascriptTranslator.MethodCollection + .AddPropertyGetterTranslator(typeof(ITypeId), nameof(ITypeId.IdValue), + new GenericMethodCompiler(args => args[0]) + ); } private void LoadSampleConfiguration(DotvvmConfiguration config, string applicationPath) diff --git a/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml index b334973b6e..abb9d1f37b 100644 --- a/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml +++ b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml @@ -57,6 +57,10 @@
Static command result: {{value: StaticCommandResult}}

+ +

+ Binding with JS translation: {{value: $"My id values are {SelectedItemId.IdValue.ToString()} and {SelectedItemNullableId.IdValue.ToString().ToUpper()}"}} +

From 06e33f5fa089e27ec1b5550bf024337f57fe78b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Thu, 27 Oct 2022 15:31:44 +0200 Subject: [PATCH 07/32] Refactoring in samples --- .../CustomPrimitiveTypes/BasicViewModel.cs | 126 ------------------ .../CustomPrimitiveTypes/ITypeId.cs | 11 ++ .../CustomPrimitiveTypes/SampleId.cs | 14 ++ .../CustomPrimitiveTypes/SampleItem.cs | 10 ++ .../CustomPrimitiveTypes/TypeId.cs | 55 ++++++++ .../TypeIdJsonConverter.cs | 39 ++++++ 6 files changed, 129 insertions(+), 126 deletions(-) create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/ITypeId.cs create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleItem.cs create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeId.cs create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdJsonConverter.cs diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs index 5f9ab39984..b026083166 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/BasicViewModel.cs @@ -29,20 +29,6 @@ public class BasicViewModel : DotvvmViewModelBase [Required] public SampleId? SelectedItemNullableId { get; set; } - - // použít typové ID ve viewmodelu - // [Required] validace - // musí fungovat i když je nullable - - // vybírání hodnot v comboboxu, checkboxu + opět s podporou nullable - - - // routing - - - // [FromRoute] a [FromQuery] - - // JS translations - public List Items { get; set; } = new List { new SampleItem() { Id = SampleId.CreateExisting(new Guid("96c37b99-5fd5-448c-8a64-977ae11b8b8b")), Text = "Item 1" }, @@ -77,117 +63,5 @@ public void CommandWithSampleId(SampleId current) } } - - public class SampleItem - { - public SampleId Id { get; set; } - public string Text { get; set; } - } - - [TypeConverter(typeof(TypeIdConverter))] - [JsonConverter(typeof(TypeIdJsonConverter))] - public record SampleId : TypeId - { - public SampleId(Guid idValue) : base(idValue) - { - } - } - - public class TypeIdConverter : TypeConverter - where TId : TypeId - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string) || sourceType == typeof(Guid) || sourceType == typeof(Guid?); - } - - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - return destinationType == typeof(TId); - } - - public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) - { - if (value is string stringValue) - { - return TypeId.CreateExisting(new Guid(stringValue)); - } - else if (value is Guid guidValue) - { - return TypeId.CreateExisting(guidValue); - } - else - { - return null; - } - } - } - - public abstract record TypeId : ITypeId - where TId : TypeId - { - public Guid IdValue { get; } - - protected TypeId(Guid idValue) - { - if (idValue == default) throw new ArgumentException(nameof(idValue)); - IdValue = idValue; - } - - public static TId CreateNew() - { - var guid = Guid.NewGuid(); - return (TId)Activator.CreateInstance(typeof(TId), args: guid)!; - } - - public static TId CreateExisting(Guid idValue) - { - if (idValue == default) throw new ArgumentException(nameof(idValue)); - return (TId)Activator.CreateInstance(typeof(TId), args: idValue)!; - } - - public override string ToString() - { - return $"{GetType()} {{{IdValue}}}"; - } - } - - public interface ITypeId - { - Guid IdValue { get; } - } - - public class TypeIdJsonConverter : JsonConverter where TId : TypeId - { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(TId); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.String) - { - var idText = (string)reader.Value; - var idValue = Guid.Parse(idText); - return TypeId.CreateExisting(idValue); - } - else if (reader.TokenType == JsonToken.Null) - { - return null; - } - else - { - throw new JsonSerializationException($"Token {reader.TokenType} cannot be deserialized as TypeId!"); - } - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue((value as ITypeId)?.IdValue); - } - - } - } diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/ITypeId.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/ITypeId.cs new file mode 100644 index 0000000000..205da1b3a8 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/ITypeId.cs @@ -0,0 +1,11 @@ +using System; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes +{ + public interface ITypeId + { + Guid IdValue { get; } + } + +} + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs new file mode 100644 index 0000000000..f15a049ce1 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes +{ + public record SampleId : TypeId + { + public SampleId(Guid idValue) : base(idValue) + { + } + } + +} + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleItem.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleItem.cs new file mode 100644 index 0000000000..b2f7dc8a0e --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleItem.cs @@ -0,0 +1,10 @@ +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes +{ + public class SampleItem + { + public SampleId Id { get; set; } + public string Text { get; set; } + } + +} + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeId.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeId.cs new file mode 100644 index 0000000000..5456c39413 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeId.cs @@ -0,0 +1,55 @@ +using System; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes +{ + public abstract record TypeId : ITypeId + where TId : TypeId + { + public Guid IdValue { get; } + + protected TypeId(Guid idValue) + { + if (idValue == default) throw new ArgumentException(nameof(idValue)); + IdValue = idValue; + } + + public static TId CreateNew() + { + var guid = Guid.NewGuid(); + return (TId)Activator.CreateInstance(typeof(TId), args: guid)!; + } + + public static TId CreateExisting(Guid idValue) + { + if (idValue == default) throw new ArgumentException(nameof(idValue)); + return (TId)Activator.CreateInstance(typeof(TId), args: idValue)!; + } + + public static TId ParseValue(object? value) + { + if (value is string stringValue) + { + return CreateExisting(new Guid(stringValue)); + } + else if (value is Guid guidValue) + { + return CreateExisting(guidValue); + } + else if (value == null) + { + return null; + } + else + { + throw new NotSupportedException($"Cannot parse TypeId from {value.GetType()}!"); + } + } + + public override string ToString() + { + return $"{GetType()} {{{IdValue}}}"; + } + } + +} + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdJsonConverter.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdJsonConverter.cs new file mode 100644 index 0000000000..c8f07d7ac2 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdJsonConverter.cs @@ -0,0 +1,39 @@ +using System; +using Newtonsoft.Json; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes +{ + public class TypeIdJsonConverter : JsonConverter where TId : TypeId + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(TId); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String) + { + var idText = (string)reader.Value; + var idValue = Guid.Parse(idText); + return TypeId.CreateExisting(idValue); + } + else if (reader.TokenType == JsonToken.Null) + { + return null; + } + else + { + throw new JsonSerializationException($"Token {reader.TokenType} cannot be deserialized as TypeId!"); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue((value as ITypeId)?.IdValue); + } + + } + +} + From a6118f1c724e52c480dadd6e1cbd0d384bcc53c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Thu, 27 Oct 2022 15:32:05 +0200 Subject: [PATCH 08/32] Fixed issue with converters in static commands --- .../CustomPrimitiveTypeRegistration.cs | 53 ++++++++++++++++--- .../DefaultSerializerSettingsProvider.cs | 33 ++++++++---- .../Framework/Utils/ReflectionUtils.cs | 44 ++++++++++++--- .../ViewModelTypeMetadataSerializer.cs | 4 +- src/Samples/Common/DotvvmStartup.cs | 4 +- 5 files changed, 110 insertions(+), 28 deletions(-) diff --git a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs index d1ae9219c7..e739608a29 100644 --- a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs +++ b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs @@ -1,18 +1,57 @@ using System; +using Newtonsoft.Json; namespace DotVVM.Framework.Configuration { - public class CustomPrimitiveTypeRegistration + public class CustomPrimitiveTypeRegistration : CustomPrimitiveTypeRegistration { - // TODO: think about better name - public Type Type { get; } - public Type ClientSidePrimitiveType { get; } + /// + /// Gets a function which can parse the value from string or other type that may appear in route parameters collection. + /// + public Func ParseValue { get; } - public CustomPrimitiveTypeRegistration(Type type, Type clientSidePrimitiveType) + /// + public override Type ServerSideType => typeof(T); + + public CustomPrimitiveTypeRegistration(Type clientSideType, Func parseValue, JsonConverter? jsonConverter = null) + : base(clientSideType, jsonConverter) { - Type = type; - ClientSidePrimitiveType = clientSidePrimitiveType; + ParseValue = parseValue; } + + /// + public override object? ConvertToServerSideType(object? value) => ParseValue(value); + } + + public abstract class CustomPrimitiveTypeRegistration + { + + /// + /// Gets a JsonConverter which can read and write the client-side representation of the value. + /// + public JsonConverter? JsonConverter { get; } + + /// + /// Gets a type which will be used for this custom type on the client side. Only types from ReflectionUtils.PrimitiveTypes (or their nullable versions) are supported. + /// + public Type ClientSideType { get; } + + /// + /// Gets a type which appears in viewmodels that is treated as a primitive type by DotVVM. + /// + public abstract Type ServerSideType { get; } + + /// + /// Parses the value from string or other type that may appear in route parameters collection. + /// + public abstract object? ConvertToServerSideType(object? value); + + protected CustomPrimitiveTypeRegistration(Type clientSideType, JsonConverter? jsonConverter = null) + { + ClientSideType = clientSideType; + JsonConverter = jsonConverter; + } + } } diff --git a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs index 4f60311a81..92a9776134 100644 --- a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs +++ b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel.Serialization; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -21,18 +22,21 @@ public JsonSerializerSettings GetSettingsCopy() private JsonSerializerSettings CreateSettings() { + var converters = new List + { + new DotvvmDateTimeConverter(), + new DotvvmDateOnlyConverter(), + new DotvvmTimeOnlyConverter(), + new StringEnumConverter(), + new DotvvmDictionaryConverter(), + new DotvvmByteArrayConverter() + }; + converters.AddRange(ReflectionUtils.GetCustomPrimitiveTypeJsonConverters()); + return new JsonSerializerSettings() { DateTimeZoneHandling = DateTimeZoneHandling.Unspecified, - Converters = new List - { - new DotvvmDateTimeConverter(), - new DotvvmDateOnlyConverter(), - new DotvvmTimeOnlyConverter(), - new StringEnumConverter(), - new DotvvmDictionaryConverter(), - new DotvvmByteArrayConverter() - }, + Converters = converters, MaxDepth = defaultMaxSerializationDepth }; } @@ -42,11 +46,20 @@ public static DefaultSerializerSettingsProvider Instance get { if (instance == null) - instance = new DefaultSerializerSettingsProvider(); + { + lock (instanceLocker) + { + if (instance == null) + { + instance = new DefaultSerializerSettingsProvider(); + } + } + } return instance; } } private static DefaultSerializerSettingsProvider? instance; + private static object instanceLocker = new(); private DefaultSerializerSettingsProvider() { diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index c83d4ca0de..3735ee5d06 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -229,14 +229,15 @@ public static bool IsAssignableToGenericType(this Type givenType, Type genericTy return Convert.ChangeType(long.Parse(str2, numberStyle & NumberStyles.Integer, CultureInfo.InvariantCulture), type, CultureInfo.InvariantCulture); } - if (TypeDescriptor.GetConverter(type) is { } converter && converter.CanConvertFrom(value.GetType())) - { - return converter.ConvertTo(value, type); - } - // convert try { + // custom primitive types + if (CustomPrimitiveTypes.TryGetValue(type, out var registration)) + { + return registration.ConvertToServerSideType(value); + } + return Convert.ChangeType(value, type, CultureInfo.InvariantCulture); } catch (Exception e) @@ -300,7 +301,7 @@ public record TypeConvertException(object Value, Type Type, Exception InnerExcep typeof (decimal) }; // mapping of server-side types to their client-side representation - internal static readonly Dictionary CustomPrimitiveTypes = new Dictionary(); + internal static readonly Dictionary CustomPrimitiveTypes = new Dictionary(); public static IEnumerable GetNumericTypes() { @@ -613,13 +614,40 @@ public static IEnumerable GetBaseTypesAndInterfaces(Type type) } } + private static volatile bool customPrimitiveTypesRegistered = false; internal static void RegisterCustomPrimitiveTypes(IList customPrimitiveTypeRegistrations) { - // TODO: validation foreach (var registration in customPrimitiveTypeRegistrations) { - CustomPrimitiveTypes.Add(registration.Type, registration.ClientSidePrimitiveType); + if (IsPrimitiveType(registration.ServerSideType) + || IsCollection(registration.ServerSideType) + || IsDictionary(registration.ServerSideType)) + { + throw new DotvvmConfigurationException($"The type {registration.ServerSideType} cannot be used as a custom primitive type. Custom primitive types cannot be collections, dictionaries, and cannot be primitive types already supported by DotVVM."); + } + if (CustomPrimitiveTypes.ContainsKey(registration.ServerSideType)) + { + throw new DotvvmConfigurationException($"The type {registration.ServerSideType} is already registered as a custom primitive type."); + } + if (!PrimitiveTypes.Contains(registration.ClientSideType.UnwrapNullableType())) + { + throw new DotvvmConfigurationException($"The custom primitive type {registration.ServerSideType} cannot use {registration.ClientSideType} as a client-side equivalent. Only primitive types (strings, numbers, Guid, date and time types) are supported."); + } + + CustomPrimitiveTypes.Add(registration.ServerSideType, registration); + } + customPrimitiveTypesRegistered = true; + } + + internal static IEnumerable GetCustomPrimitiveTypeJsonConverters() + { + if (!customPrimitiveTypesRegistered) + { + throw new InvalidOperationException("Cannot access DefaultSerializerSettingsProvider.Instance before DotvvmConfiguration was initialized!"); } + return CustomPrimitiveTypes.Values + .Where(t => t.JsonConverter != null) + .Select(t => t.JsonConverter!); } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs index ab2b01b169..b909b06ed7 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs @@ -159,9 +159,9 @@ internal JToken GetTypeIdentifier(Type type, HashSet dependentObjectTypes, } else if (type.IsPrimitiveType()) // we intentionally detect this after handling enums and nullable types { - if (ReflectionUtils.CustomPrimitiveTypes.TryGetValue(type, out var clientSideType)) + if (ReflectionUtils.CustomPrimitiveTypes.TryGetValue(type, out var registration)) { - return GetTypeIdentifier(clientSideType, dependentObjectTypes, dependentEnumTypes); + return GetTypeIdentifier(registration.ClientSideType, dependentObjectTypes, dependentEnumTypes); } return GetPrimitiveTypeName(type); } diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index 481848c6bb..e3a4647c3d 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -82,7 +82,9 @@ public void Configure(DotvvmConfiguration config, string applicationPath) config.RouteTable.Add("Errors_Routing_NonExistingView", "Errors/Routing/NonExistingView", "Views/Errors/Routing/NonExistingView.dothml"); - config.Runtime.CustomPrimitiveTypes.Add(new CustomPrimitiveTypeRegistration(typeof(SampleId), typeof(Guid?))); + config.Runtime.CustomPrimitiveTypes.Add( + new CustomPrimitiveTypeRegistration(typeof(Guid?), SampleId.ParseValue, new TypeIdJsonConverter()) + ); config.Markup.JavascriptTranslator.MethodCollection .AddPropertyGetterTranslator(typeof(ITypeId), nameof(ITypeId.IdValue), new GenericMethodCompiler(args => args[0]) From 05afdb0487ac6d2c45107fa50c5201d64bef6bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 5 Nov 2022 16:36:49 +0100 Subject: [PATCH 09/32] Configuration of custom primitive types changed to work via attributes --- .../CustomPrimitiveTypeAttribute.cs | 25 +++++++++ .../CustomPrimitiveTypeRegistration.cs | 52 ++++--------------- .../DefaultSerializerSettingsProvider.cs | 36 ++++--------- .../Configuration/DotvvmConfiguration.cs | 9 ---- .../DotvvmRuntimeConfiguration.cs | 12 +---- .../ICustomPrimitiveTypeConverter.cs | 13 +++++ .../Framework/Utils/ReflectionUtils.cs | 32 ++++++++++-- src/Samples/Common/DotvvmStartup.cs | 3 -- .../CustomPrimitiveTypes/SampleId.cs | 4 +- .../TypeIdPrimitiveTypeConverter.cs | 9 ++++ 10 files changed, 100 insertions(+), 95 deletions(-) create mode 100644 src/Framework/Framework/Configuration/CustomPrimitiveTypeAttribute.cs create mode 100644 src/Framework/Framework/Configuration/ICustomPrimitiveTypeConverter.cs create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdPrimitiveTypeConverter.cs diff --git a/src/Framework/Framework/Configuration/CustomPrimitiveTypeAttribute.cs b/src/Framework/Framework/Configuration/CustomPrimitiveTypeAttribute.cs new file mode 100644 index 0000000000..9c1b0ec25c --- /dev/null +++ b/src/Framework/Framework/Configuration/CustomPrimitiveTypeAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace DotVVM.Framework.Configuration; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public class CustomPrimitiveTypeAttribute : Attribute +{ + /// + /// Gets a type which will be used for this custom type on the client side. Only types from ReflectionUtils.PrimitiveTypes (or their nullable versions) are supported. + /// + public Type ClientSideType { get; } + + /// + /// Gets a type implementing ICustomPrimitiveTypeConverter which converts the value from string or other type that may appear in route parameters collection. + /// + public Type ConverterType { get; } + + public CustomPrimitiveTypeAttribute(Type clientSideType, Type converterType) + { + // types are validated later as we don't know on which type the attribute is applied so the error message wouldn't tell the user where is the problem + ClientSideType = clientSideType; + ConverterType = converterType; + } + +} diff --git a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs index e739608a29..9f4178f090 100644 --- a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs +++ b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs @@ -1,57 +1,23 @@ using System; -using Newtonsoft.Json; namespace DotVVM.Framework.Configuration { - public class CustomPrimitiveTypeRegistration : CustomPrimitiveTypeRegistration + public sealed class CustomPrimitiveTypeRegistration { + private readonly Func convertFunction; - /// - /// Gets a function which can parse the value from string or other type that may appear in route parameters collection. - /// - public Func ParseValue { get; } - - /// - public override Type ServerSideType => typeof(T); - - public CustomPrimitiveTypeRegistration(Type clientSideType, Func parseValue, JsonConverter? jsonConverter = null) - : base(clientSideType, jsonConverter) - { - ParseValue = parseValue; - } - - /// - public override object? ConvertToServerSideType(object? value) => ParseValue(value); - } - - public abstract class CustomPrimitiveTypeRegistration - { - - /// - /// Gets a JsonConverter which can read and write the client-side representation of the value. - /// - public JsonConverter? JsonConverter { get; } - - /// - /// Gets a type which will be used for this custom type on the client side. Only types from ReflectionUtils.PrimitiveTypes (or their nullable versions) are supported. - /// public Type ClientSideType { get; } - /// - /// Gets a type which appears in viewmodels that is treated as a primitive type by DotVVM. - /// - public abstract Type ServerSideType { get; } - - /// - /// Parses the value from string or other type that may appear in route parameters collection. - /// - public abstract object? ConvertToServerSideType(object? value); - - protected CustomPrimitiveTypeRegistration(Type clientSideType, JsonConverter? jsonConverter = null) + public Type ServerSideType { get; } + + internal CustomPrimitiveTypeRegistration(Type serverSideType, Type clientSideType, Func convertFunction) { + ServerSideType = serverSideType; ClientSideType = clientSideType; - JsonConverter = jsonConverter; + this.convertFunction = convertFunction; } + public object? ConvertToServerSideType(object? value) => convertFunction(value); + } } diff --git a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs index 92a9776134..ca4bead977 100644 --- a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs +++ b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel.Serialization; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -22,21 +21,17 @@ public JsonSerializerSettings GetSettingsCopy() private JsonSerializerSettings CreateSettings() { - var converters = new List - { - new DotvvmDateTimeConverter(), - new DotvvmDateOnlyConverter(), - new DotvvmTimeOnlyConverter(), - new StringEnumConverter(), - new DotvvmDictionaryConverter(), - new DotvvmByteArrayConverter() - }; - converters.AddRange(ReflectionUtils.GetCustomPrimitiveTypeJsonConverters()); - - return new JsonSerializerSettings() - { + return new JsonSerializerSettings() { DateTimeZoneHandling = DateTimeZoneHandling.Unspecified, - Converters = converters, + Converters = new List + { + new DotvvmDateTimeConverter(), + new DotvvmDateOnlyConverter(), + new DotvvmTimeOnlyConverter(), + new StringEnumConverter(), + new DotvvmDictionaryConverter(), + new DotvvmByteArrayConverter() + }, MaxDepth = defaultMaxSerializationDepth }; } @@ -46,20 +41,11 @@ public static DefaultSerializerSettingsProvider Instance get { if (instance == null) - { - lock (instanceLocker) - { - if (instance == null) - { - instance = new DefaultSerializerSettingsProvider(); - } - } - } + instance = new DefaultSerializerSettingsProvider(); return instance; } } private static DefaultSerializerSettingsProvider? instance; - private static object instanceLocker = new(); private DefaultSerializerSettingsProvider() { diff --git a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs index 31e3d2708d..c74415ad62 100644 --- a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs @@ -38,11 +38,6 @@ public sealed class DotvvmConfiguration private bool isFrozen; public const string DotvvmControlTagPrefix = "dot"; - /// - /// Fired when the configuration routine is completely initialized and frozen. - /// - public event Action ConfigurationReady; - /// /// Gets or sets the application physical path. /// @@ -172,8 +167,6 @@ public void Freeze() _routeConstraints.Freeze(); Styles.Freeze(); FreezableList.Freeze(ref _compiledViewsAssemblies); - - ConfigurationReady?.Invoke(); } [JsonIgnore] @@ -234,8 +227,6 @@ internal DotvvmConfiguration(IServiceProvider services) Markup = new DotvvmMarkupConfiguration(new Lazy(() => ServiceProvider.GetRequiredService>().Value)); RouteTable = new DotvvmRouteTable(this); _styles = new StyleRepository(this); - - ConfigurationReady += () => ReflectionUtils.RegisterCustomPrimitiveTypes(Runtime.CustomPrimitiveTypes); } private static ServiceCollection CreateDefaultServiceCollection() diff --git a/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs index 027266861b..ed48b91494 100644 --- a/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs @@ -15,22 +15,13 @@ public class DotvvmRuntimeConfiguration [JsonIgnore()] public IList GlobalFilters => _globalFilters; private IList _globalFilters; - - /// - /// Gets types that are treated as primitive by DotVVM components and serialization. - /// - [JsonIgnore()] - public IList CustomPrimitiveTypes => _customPrimitiveTypes; - private IList _customPrimitiveTypes; - - + /// /// Initializes a new instance of the class. /// public DotvvmRuntimeConfiguration() { _globalFilters = new FreezableList(); - _customPrimitiveTypes = new FreezableList(); } private bool isFrozen = false; @@ -44,7 +35,6 @@ public void Freeze() { this.isFrozen = true; FreezableList.Freeze(ref _globalFilters); - FreezableList.Freeze(ref _customPrimitiveTypes); } } } diff --git a/src/Framework/Framework/Configuration/ICustomPrimitiveTypeConverter.cs b/src/Framework/Framework/Configuration/ICustomPrimitiveTypeConverter.cs new file mode 100644 index 0000000000..c03a58afc4 --- /dev/null +++ b/src/Framework/Framework/Configuration/ICustomPrimitiveTypeConverter.cs @@ -0,0 +1,13 @@ +namespace DotVVM.Framework.Configuration +{ + /// + /// Represents a converter which can convert the value from string or other type that may appear in route parameters collection. + /// + public interface ICustomPrimitiveTypeConverter + { + /// + /// Converts the value from string or other type that may appear in route parameters collection. + /// + object? Convert(object? value); + } +} diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index 3735ee5d06..4ee23f43f9 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -23,6 +23,7 @@ using Newtonsoft.Json.Converters; using RecordExceptions; using System.ComponentModel; +using DotVVM.Framework.Compilation; namespace DotVVM.Framework.Utils { @@ -301,7 +302,7 @@ public record TypeConvertException(object Value, Type Type, Exception InnerExcep typeof (decimal) }; // mapping of server-side types to their client-side representation - internal static readonly Dictionary CustomPrimitiveTypes = new Dictionary(); + internal static readonly ConcurrentDictionary CustomPrimitiveTypes = new(); public static IEnumerable GetNumericTypes() { @@ -360,9 +361,34 @@ public static bool IsCollection(Type type) public static bool IsPrimitiveType(this Type type) { return PrimitiveTypes.Contains(type) - || CustomPrimitiveTypes.ContainsKey(type) || (IsNullableType(type) && IsPrimitiveType(type.UnwrapNullableType())) - || type.IsEnum; + || type.IsEnum + || CustomPrimitiveTypes.GetOrAdd(type, TryDiscoverCustomPrimitiveType) is {}; + } + + private static CustomPrimitiveTypeRegistration? TryDiscoverCustomPrimitiveType(Type type) + { + var attribute = type.GetCustomAttribute(); + if (attribute == null) + { + return null; + } + + if (IsCollection(type) || 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."); + } + if (!PrimitiveTypes.Contains(attribute.ClientSideType.UnwrapNullableType())) + { + throw new DotvvmConfigurationException($"The type {type} is marked with {nameof(CustomPrimitiveTypeAttribute)} but its {nameof(CustomPrimitiveTypeAttribute.ClientSideType)} is not valid. The client-side representation can only be one of the built-in primitive types (string, numbers, date and time types), or a nullable version of such type."); + } + if (!typeof(ICustomPrimitiveTypeConverter).IsAssignableFrom(attribute.ConverterType)) + { + throw new DotvvmConfigurationException($"The type {type} is marked with {nameof(CustomPrimitiveTypeAttribute)} but its {nameof(CustomPrimitiveTypeAttribute.ConverterType)} doesn't implement the {nameof(ICustomPrimitiveTypeConverter)} interface!"); + } + + var converter = (ICustomPrimitiveTypeConverter)Activator.CreateInstance(attribute.ConverterType); + return new CustomPrimitiveTypeRegistration(type, attribute.ClientSideType, converter.Convert); } public static bool IsNullableType(Type type) diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index e3a4647c3d..ffcd70271e 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -82,9 +82,6 @@ public void Configure(DotvvmConfiguration config, string applicationPath) config.RouteTable.Add("Errors_Routing_NonExistingView", "Errors/Routing/NonExistingView", "Views/Errors/Routing/NonExistingView.dothml"); - config.Runtime.CustomPrimitiveTypes.Add( - new CustomPrimitiveTypeRegistration(typeof(Guid?), SampleId.ParseValue, new TypeIdJsonConverter()) - ); config.Markup.JavascriptTranslator.MethodCollection .AddPropertyGetterTranslator(typeof(ITypeId), nameof(ITypeId.IdValue), new GenericMethodCompiler(args => args[0]) diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs index f15a049ce1..32bc077666 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs @@ -1,14 +1,16 @@ using System; +using DotVVM.Framework.Configuration; using Newtonsoft.Json; namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes { + [CustomPrimitiveType(typeof(Guid?), typeof(TypeIdPrimitiveTypeConverter))] + [JsonConverter(typeof(TypeIdJsonConverter))] public record SampleId : TypeId { public SampleId(Guid idValue) : base(idValue) { } } - } diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdPrimitiveTypeConverter.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdPrimitiveTypeConverter.cs new file mode 100644 index 0000000000..7b781d577f --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdPrimitiveTypeConverter.cs @@ -0,0 +1,9 @@ +using DotVVM.Framework.Configuration; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes; + +public class TypeIdPrimitiveTypeConverter : ICustomPrimitiveTypeConverter + where TId : TypeId +{ + public object Convert(object value) => TypeId.ParseValue(value); +} From 50e38a901c17a880ea2fb7a318399a69b231a9f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 5 Nov 2022 16:44:34 +0100 Subject: [PATCH 10/32] Removed unnecessary changes in the code --- .../Configuration/DefaultSerializerSettingsProvider.cs | 3 ++- src/Framework/Framework/Configuration/DotvvmConfiguration.cs | 1 + .../Framework/Configuration/DotvvmRuntimeConfiguration.cs | 2 +- .../ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs | 3 ++- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs index ca4bead977..4f60311a81 100644 --- a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs +++ b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs @@ -21,7 +21,8 @@ public JsonSerializerSettings GetSettingsCopy() private JsonSerializerSettings CreateSettings() { - return new JsonSerializerSettings() { + return new JsonSerializerSettings() + { DateTimeZoneHandling = DateTimeZoneHandling.Unspecified, Converters = new List { diff --git a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs index c74415ad62..e777b96c61 100644 --- a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs @@ -227,6 +227,7 @@ internal DotvvmConfiguration(IServiceProvider services) Markup = new DotvvmMarkupConfiguration(new Lazy(() => ServiceProvider.GetRequiredService>().Value)); RouteTable = new DotvvmRouteTable(this); _styles = new StyleRepository(this); + } private static ServiceCollection CreateDefaultServiceCollection() diff --git a/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs index ed48b91494..e024df0b94 100644 --- a/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs @@ -15,7 +15,7 @@ public class DotvvmRuntimeConfiguration [JsonIgnore()] public IList GlobalFilters => _globalFilters; private IList _globalFilters; - + /// /// Initializes a new instance of the class. /// diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs index b909b06ed7..a5aca8f68d 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs @@ -159,7 +159,7 @@ internal JToken GetTypeIdentifier(Type type, HashSet dependentObjectTypes, } else if (type.IsPrimitiveType()) // we intentionally detect this after handling enums and nullable types { - if (ReflectionUtils.CustomPrimitiveTypes.TryGetValue(type, out var registration)) + if (ReflectionUtils.CustomPrimitiveTypes.TryGetValue(type, out var registration) && registration is {}) { return GetTypeIdentifier(registration.ClientSideType, dependentObjectTypes, dependentEnumTypes); } @@ -230,6 +230,7 @@ private JObject BuildEnumTypeMetadata(Type type) private string GetPrimitiveTypeName(Type type) => type.Name.ToString(); + readonly struct ObjectMetadataWithDependencies { public JObject Metadata { get; } From f1175ba013307f782e8c06ea504743719ac21ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 5 Nov 2022 17:22:19 +0100 Subject: [PATCH 11/32] UI tests added --- src/Samples/Common/DotvvmStartup.cs | 4 + .../CustomPrimitiveTypes/SampleId.cs | 2 + .../CustomPrimitiveTypes/TypeId.cs | 2 +- .../CustomPrimitiveTypes/Basic.dothtml | 51 +++++----- .../Abstractions/SamplesRouteUrls.designer.cs | 1 + .../Feature/CustomPrimitiveTypesTests.cs | 94 +++++++++++++++++++ 6 files changed, 126 insertions(+), 28 deletions(-) create mode 100644 src/Samples/Tests/Tests/Feature/CustomPrimitiveTypesTests.cs diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index ffcd70271e..8d75faf5ba 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -86,6 +86,10 @@ public void Configure(DotvvmConfiguration config, string applicationPath) .AddPropertyGetterTranslator(typeof(ITypeId), nameof(ITypeId.IdValue), new GenericMethodCompiler(args => args[0]) ); + config.Markup.JavascriptTranslator.MethodCollection + .AddMethodTranslator(typeof(SampleId), nameof(ToString), + new GenericMethodCompiler(args => args[0]) + ); } private void LoadSampleConfiguration(DotvvmConfiguration config, string applicationPath) diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs index 32bc077666..16e521e0f3 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs @@ -11,6 +11,8 @@ public record SampleId : TypeId public SampleId(Guid idValue) : base(idValue) { } + + public override string ToString() => base.ToString(); } } diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeId.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeId.cs index 5456c39413..a1027f4dd2 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeId.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeId.cs @@ -47,7 +47,7 @@ public static TId ParseValue(object? value) public override string ToString() { - return $"{GetType()} {{{IdValue}}}"; + return IdValue.ToString(); } } diff --git a/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml index abb9d1f37b..4f2f1123c4 100644 --- a/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml +++ b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml @@ -5,61 +5,58 @@ - Conditional css classes - + Custom primitive types

- Selected item ID: {{value: SelectedItemId}} + Selected item ID: {{value: SelectedItemId}}
- + SelectedValue="{value: SelectedItemId}" + data-ui="selected-item-combo"/> +

- Selected nullable item ID: {{value: SelectedItemNullableId}} + Selected nullable item ID: {{value: SelectedItemNullableId}}
- + SelectedValue="{value: SelectedItemNullableId}" + EmptyItemText="none" + data-ui="selected-item-nullable-combo"/> +

- Route parameter: {{value: IdInRoute}} + Route parameter: {{value: IdInRoute}}
- Query parameter: {{value: IdInQuery}} + Query parameter: {{value: IdInQuery}}

- +

- +
- +
- Static command result: {{value: StaticCommandResult}} + Static command result: {{value: StaticCommandResult}}

- Binding with JS translation: {{value: $"My id values are {SelectedItemId.IdValue.ToString()} and {SelectedItemNullableId.IdValue.ToString().ToUpper()}"}} + Binding with JS translation: {{value: $"My id values are {SelectedItemId.IdValue.ToString()} and {SelectedItemNullableId.IdValue.ToString().ToUpper()}"}}

diff --git a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs index 43cd0c94b0..a54b2cb7cc 100644 --- a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs +++ b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs @@ -237,6 +237,7 @@ public partial class SamplesRouteUrls public const string FeatureSamples_CompositeControls_BasicSample = "FeatureSamples/CompositeControls/BasicSample"; public const string FeatureSamples_CompositeControls_ControlPropertyNamingConflict = "FeatureSamples/CompositeControls/ControlPropertyNamingConflict"; public const string FeatureSamples_ConditionalCssClasses_ConditionalCssClasses = "FeatureSamples/ConditionalCssClasses/ConditionalCssClasses"; + public const string FeatureSamples_CustomPrimitiveTypes_Basic = "FeatureSamples/CustomPrimitiveTypes/Basic"; public const string FeatureSamples_CustomResponseProperties_SimpleExceptionFilter = "FeatureSamples/CustomResponseProperties/SimpleExceptionFilter"; public const string FeatureSamples_DateTimeSerialization_DateTimeSerialization = "FeatureSamples/DateTimeSerialization/DateTimeSerialization"; public const string FeatureSamples_DependencyInjection_ViewModelScopedService = "FeatureSamples/DependencyInjection/ViewModelScopedService"; diff --git a/src/Samples/Tests/Tests/Feature/CustomPrimitiveTypesTests.cs b/src/Samples/Tests/Tests/Feature/CustomPrimitiveTypesTests.cs new file mode 100644 index 0000000000..59a1d64529 --- /dev/null +++ b/src/Samples/Tests/Tests/Feature/CustomPrimitiveTypesTests.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Samples.Tests.Base; +using DotVVM.Testing.Abstractions; +using OpenQA.Selenium; +using Riganti.Selenium.Core; +using Riganti.Selenium.DotVVM; +using Xunit.Abstractions; +using Xunit; + +namespace DotVVM.Samples.Tests.Feature +{ + public class CustomPrimitiveTypesTests : AppSeleniumTest + { + public CustomPrimitiveTypesTests(ITestOutputHelper output) : base(output) + { + } + + [Theory] + [InlineData("", "", "")] + [InlineData("/96c37b99-5fd5-448c-8a64-977ae11b8b8b?Id=c2654a1f-3781-49a8-911b-c7346db166e0", "96c37b99-5fd5-448c-8a64-977ae11b8b8b", "c2654a1f-3781-49a8-911b-c7346db166e0")] + public void Feature_CustomPrimitiveTypes_Basic(string urlSuffix, string expectedRouteParam, string expectedQueryParam) + { + RunInAllBrowsers(browser => + { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_CustomPrimitiveTypes_Basic + urlSuffix); + + var selectedItem = browser.Single("selected-item", SelectByDataUi); + var selectedItemCombo = browser.Single("selected-item-combo", SelectByDataUi); + var selectedItemValidator = browser.Single("selected-item-validator", SelectByDataUi); + var selectedItemNullable = browser.Single("selected-item-nullable", SelectByDataUi); + var selectedItemNullableCombo = browser.Single("selected-item-nullable-combo", SelectByDataUi); + var selectedItemNullableValidator = browser.Single("selected-item-nullable-validator", SelectByDataUi); + var idFromRoute = browser.Single("id-from-route", SelectByDataUi); + var idFromQuery = browser.Single("id-from-query", SelectByDataUi); + var routeLink = browser.Single("routelink", SelectByDataUi); + var command = browser.Single("command", SelectByDataUi); + var staticCommand = browser.Single("static-command", SelectByDataUi); + var staticCommandResult = browser.Single("static-command-result", SelectByDataUi); + var binding = browser.Single("binding", SelectByDataUi); + + // check route link + AssertUI.TextEquals(idFromRoute, expectedRouteParam); + AssertUI.TextEquals(idFromQuery, expectedQueryParam); + AssertUI.Attribute(routeLink, "href", v => v.Contains(urlSuffix)); + + // select in first list + AssertUI.TextEquals(binding, "My id values are and"); + AssertUI.TextEquals(selectedItem, ""); + selectedItemCombo.Select(0); + AssertUI.TextEquals(selectedItem, "96c37b99-5fd5-448c-8a64-977ae11b8b8b"); + selectedItemCombo.Select(1); + AssertUI.TextEquals(selectedItem, "c2654a1f-3781-49a8-911b-c7346db166e0"); + AssertUI.TextEquals(binding, "My id values are c2654a1f-3781-49a8-911b-c7346db166e0 and"); + + // select in second list + AssertUI.TextEquals(selectedItemNullable, ""); + selectedItemNullableCombo.Select(3); + AssertUI.TextEquals(selectedItemNullable, "e467a201-9ab7-4cd5-adbf-66edd03f6ae1"); + AssertUI.TextEquals(binding, "My id values are c2654a1f-3781-49a8-911b-c7346db166e0 and E467A201-9AB7-4CD5-ADBF-66EDD03F6AE1"); + selectedItemNullableCombo.Select(0); + AssertUI.TextEquals(selectedItemNullable, ""); + + // command and validation + AssertUI.IsNotDisplayed(selectedItemValidator); + AssertUI.IsNotDisplayed(selectedItemNullableValidator); + command.Click(); + + AssertUI.IsNotDisplayed(selectedItemValidator); + AssertUI.IsDisplayed(selectedItemNullableValidator); + AssertUI.TextEquals(selectedItemNullableValidator, "The SelectedItemNullableId field is required."); + selectedItemCombo.Select(0); + selectedItemNullableCombo.Select(1); + command.Click(); + + AssertUI.IsDisplayed(selectedItemValidator); + AssertUI.IsNotDisplayed(selectedItemNullableValidator); + AssertUI.TextEquals(selectedItemValidator, "Valid property path Invalid property path"); + selectedItemCombo.Select(1); + command.Click(); + + AssertUI.IsNotDisplayed(selectedItemValidator); + AssertUI.IsNotDisplayed(selectedItemNullableValidator); + + // static command + staticCommand.Click(); + AssertUI.TextEquals(staticCommandResult, "54162c7e-cdcc-4585-aa92-2e78be3f0c75"); + }); + } + } +} From 02a61b88e0b2e5dcd3d4abe88ada48f35e487422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 5 Nov 2022 17:52:14 +0100 Subject: [PATCH 12/32] Fixed nullability issues --- src/Framework/Framework/Utils/ReflectionUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index 4ee23f43f9..e556fea802 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -234,7 +234,7 @@ public static bool IsAssignableToGenericType(this Type givenType, Type genericTy try { // custom primitive types - if (CustomPrimitiveTypes.TryGetValue(type, out var registration)) + if (CustomPrimitiveTypes.TryGetValue(type, out var registration) && registration is {}) { return registration.ConvertToServerSideType(value); } @@ -387,7 +387,7 @@ public static bool IsPrimitiveType(this Type type) throw new DotvvmConfigurationException($"The type {type} is marked with {nameof(CustomPrimitiveTypeAttribute)} but its {nameof(CustomPrimitiveTypeAttribute.ConverterType)} doesn't implement the {nameof(ICustomPrimitiveTypeConverter)} interface!"); } - var converter = (ICustomPrimitiveTypeConverter)Activator.CreateInstance(attribute.ConverterType); + var converter = (ICustomPrimitiveTypeConverter)Activator.CreateInstance(attribute.ConverterType)!; return new CustomPrimitiveTypeRegistration(type, attribute.ClientSideType, converter.Convert); } From 4d637d1731caeb821c421ad20e75ac6bb96ff63a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 20 Nov 2022 15:09:50 +0100 Subject: [PATCH 13/32] Handled exception when creating custom primitive type converter --- src/Framework/Framework/Utils/ReflectionUtils.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index e556fea802..38137ef0a3 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -387,8 +387,15 @@ public static bool IsPrimitiveType(this Type type) throw new DotvvmConfigurationException($"The type {type} is marked with {nameof(CustomPrimitiveTypeAttribute)} but its {nameof(CustomPrimitiveTypeAttribute.ConverterType)} doesn't implement the {nameof(ICustomPrimitiveTypeConverter)} interface!"); } - var converter = (ICustomPrimitiveTypeConverter)Activator.CreateInstance(attribute.ConverterType)!; - return new CustomPrimitiveTypeRegistration(type, attribute.ClientSideType, converter.Convert); + try + { + var converter = (ICustomPrimitiveTypeConverter)Activator.CreateInstance(attribute.ConverterType)!; + return new CustomPrimitiveTypeRegistration(type, attribute.ClientSideType, converter.Convert); + } + catch (Exception ex) + { + throw new DotvvmCompilationException($"The converter for a custom primitive type {type} couldn't be created. Make sure the class {attribute.ConverterType} has a default constructor.", ex); + } } public static bool IsNullableType(Type type) From 5688dbc1bbb47725db8b48b20a191714b7e7159e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 20 Nov 2022 15:27:54 +0100 Subject: [PATCH 14/32] Feature flag added for static command serialization --- .../Configuration/DotvvmExperimentalFeaturesConfiguration.cs | 4 ++++ src/Samples/Common/DotvvmStartup.cs | 1 + 2 files changed, 5 insertions(+) diff --git a/src/Framework/Framework/Configuration/DotvvmExperimentalFeaturesConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmExperimentalFeaturesConfiguration.cs index e40edf078f..ce2a2bd621 100644 --- a/src/Framework/Framework/Configuration/DotvvmExperimentalFeaturesConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmExperimentalFeaturesConfiguration.cs @@ -40,11 +40,15 @@ public class DotvvmExperimentalFeaturesConfiguration [JsonProperty("knockoutDeferUpdates")] public DotvvmFeatureFlag KnockoutDeferUpdates { get; private set; } = new DotvvmFeatureFlag("KnockoutDeferUpdates"); + [JsonProperty("useDotvvmSerializationForStaticCommandArguments")] + public DotvvmGlobalFeatureFlag UseDotvvmSerializationForStaticCommandArguments { get; private set; } = new DotvvmGlobalFeatureFlag(); + public void Freeze() { LazyCsrfToken.Freeze(); ServerSideViewModelCache.Freeze(); ExplicitAssemblyLoading.Freeze(); + UseDotvvmSerializationForStaticCommandArguments.Freeze(); } } diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index 8d75faf5ba..6594f91726 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -43,6 +43,7 @@ public class DotvvmStartup : IDotvvmStartup public void Configure(DotvvmConfiguration config, string applicationPath) { config.DefaultCulture = "en-US"; + config.ExperimentalFeatures.UseDotvvmSerializationForStaticCommandArguments.Enable(); AddControls(config); AddStyles(config); From b4ac339022d084cc938b457a51c58d72971beb92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Wed, 23 Nov 2022 20:13:53 +0100 Subject: [PATCH 15/32] CustomPrimitiveTypeAttribute moved to DotVVM.Core package --- .../ViewModel}/CustomPrimitiveTypeAttribute.cs | 2 +- src/Framework/Framework/Utils/ReflectionUtils.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) rename src/Framework/{Framework/Configuration => Core/ViewModel}/CustomPrimitiveTypeAttribute.cs (95%) diff --git a/src/Framework/Framework/Configuration/CustomPrimitiveTypeAttribute.cs b/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs similarity index 95% rename from src/Framework/Framework/Configuration/CustomPrimitiveTypeAttribute.cs rename to src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs index 9c1b0ec25c..d6b3610c09 100644 --- a/src/Framework/Framework/Configuration/CustomPrimitiveTypeAttribute.cs +++ b/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace DotVVM.Framework.Configuration; +namespace DotVVM.Framework.ViewModel; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] public class CustomPrimitiveTypeAttribute : Attribute diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index 38137ef0a3..106c7e4ecf 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -24,6 +24,7 @@ using RecordExceptions; using System.ComponentModel; using DotVVM.Framework.Compilation; +using DotVVM.Framework.ViewModel; namespace DotVVM.Framework.Utils { From e890f53a4ce994ccb954b1e9f22f24cf0176c51d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Wed, 23 Nov 2022 20:50:42 +0100 Subject: [PATCH 16/32] Create an universal JsonConverter for all custom primitive types --- src/Framework/Core/DotVVM.Core.csproj | 3 ++ .../ICustomPrimitiveTypeConverter.cs | 18 +++++++ .../CustomPrimitiveTypeRegistration.cs | 31 +++++++++-- .../DefaultSerializerSettingsProvider.cs | 3 +- .../ICustomPrimitiveTypeConverter.cs | 13 ----- .../Framework/Utils/ReflectionUtils.cs | 2 +- .../CustomPrimitiveTypeJsonConverter.cs | 53 +++++++++++++++++++ .../CustomPrimitiveTypes/SampleId.cs | 4 +- .../TypeIdJsonConverter.cs | 39 -------------- .../TypeIdPrimitiveTypeConverter.cs | 6 ++- 10 files changed, 108 insertions(+), 64 deletions(-) create mode 100644 src/Framework/Core/ViewModel/ICustomPrimitiveTypeConverter.cs delete mode 100644 src/Framework/Framework/Configuration/ICustomPrimitiveTypeConverter.cs create mode 100644 src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs delete mode 100644 src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdJsonConverter.cs diff --git a/src/Framework/Core/DotVVM.Core.csproj b/src/Framework/Core/DotVVM.Core.csproj index 76ef2cabbe..02fcfe1111 100644 --- a/src/Framework/Core/DotVVM.Core.csproj +++ b/src/Framework/Core/DotVVM.Core.csproj @@ -29,6 +29,9 @@ all + + + $(DefineConstants);RELEASE diff --git a/src/Framework/Core/ViewModel/ICustomPrimitiveTypeConverter.cs b/src/Framework/Core/ViewModel/ICustomPrimitiveTypeConverter.cs new file mode 100644 index 0000000000..38d17b99e5 --- /dev/null +++ b/src/Framework/Core/ViewModel/ICustomPrimitiveTypeConverter.cs @@ -0,0 +1,18 @@ +namespace DotVVM.Framework.ViewModel +{ + /// + /// Represents a converter which can convert the value from string or other type that may appear in route parameters collection. + /// + public interface ICustomPrimitiveTypeConverter + { + /// + /// Converts the value from its client-side representation or other type that may appear in route parameters collection to the registered custom primitive type. + /// + object? ToCustomPrimitiveType(object? value); + + /// + /// Converts the value from the registered custom primitive type to its client-side representation. + /// + object? FromCustomPrimitiveType(object? value); + } +} diff --git a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs index 9f4178f090..5c5d55e5f8 100644 --- a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs +++ b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs @@ -1,23 +1,44 @@ using System; +using DotVVM.Framework.ViewModel; namespace DotVVM.Framework.Configuration { public sealed class CustomPrimitiveTypeRegistration { - private readonly Func convertFunction; + private readonly Func convertToServerSideType; + private readonly Func convertToClientSideType; public Type ClientSideType { get; } public Type ServerSideType { get; } - - internal CustomPrimitiveTypeRegistration(Type serverSideType, Type clientSideType, Func convertFunction) + + internal CustomPrimitiveTypeRegistration(Type serverSideType, Type clientSideType, Func convertToServerSideType, Func convertToClientSideType) { ServerSideType = serverSideType; ClientSideType = clientSideType; - this.convertFunction = convertFunction; + this.convertToServerSideType = convertToServerSideType; + this.convertToClientSideType = convertToClientSideType; + } + + public object? ConvertToServerSideType(object? value) + { + var result = convertToServerSideType(value); + if (result != null && !ServerSideType.IsAssignableFrom(result.GetType())) + { + throw new Exception($"The {nameof(ICustomPrimitiveTypeConverter.ToCustomPrimitiveType)} for type {ServerSideType} returned an incompatible type {result?.GetType()}! Expected type: {ServerSideType}"); + } + return result; } - public object? ConvertToServerSideType(object? value) => convertFunction(value); + public object? ConvertToClientSideType(object? value) + { + var result = convertToClientSideType(value); + if (result != null && !ClientSideType.IsAssignableFrom(result.GetType())) + { + throw new Exception($"The {nameof(ICustomPrimitiveTypeConverter.FromCustomPrimitiveType)} for type {ServerSideType} returned an incompatible type {result?.GetType()}! Expected type: {ClientSideType}"); + } + return result; + } } } diff --git a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs index 4f60311a81..bdad703563 100644 --- a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs +++ b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs @@ -31,7 +31,8 @@ private JsonSerializerSettings CreateSettings() new DotvvmTimeOnlyConverter(), new StringEnumConverter(), new DotvvmDictionaryConverter(), - new DotvvmByteArrayConverter() + new DotvvmByteArrayConverter(), + new DotvvmCustomPrimitiveTypeConverter() }, MaxDepth = defaultMaxSerializationDepth }; diff --git a/src/Framework/Framework/Configuration/ICustomPrimitiveTypeConverter.cs b/src/Framework/Framework/Configuration/ICustomPrimitiveTypeConverter.cs deleted file mode 100644 index c03a58afc4..0000000000 --- a/src/Framework/Framework/Configuration/ICustomPrimitiveTypeConverter.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DotVVM.Framework.Configuration -{ - /// - /// Represents a converter which can convert the value from string or other type that may appear in route parameters collection. - /// - public interface ICustomPrimitiveTypeConverter - { - /// - /// Converts the value from string or other type that may appear in route parameters collection. - /// - object? Convert(object? value); - } -} diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index 106c7e4ecf..4fff7a35a6 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -391,7 +391,7 @@ public static bool IsPrimitiveType(this Type type) try { var converter = (ICustomPrimitiveTypeConverter)Activator.CreateInstance(attribute.ConverterType)!; - return new CustomPrimitiveTypeRegistration(type, attribute.ClientSideType, converter.Convert); + return new CustomPrimitiveTypeRegistration(type, attribute.ClientSideType, converter.ToCustomPrimitiveType, converter.FromCustomPrimitiveType); } catch (Exception ex) { diff --git a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs new file mode 100644 index 0000000000..ecff551d43 --- /dev/null +++ b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Text; +using DotVVM.Framework.Utils; +using DotVVM.Framework.ViewModel; +using Newtonsoft.Json; + +namespace DotVVM.Framework.ViewModel.Serialization +{ + public class DotvvmCustomPrimitiveTypeConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return ReflectionUtils.CustomPrimitiveTypes.TryGetValue(objectType, out var result) && result is { }; + } + + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType is JsonToken.String + or JsonToken.Boolean + or JsonToken.Integer + or JsonToken.Float + or JsonToken.Date) + { + var registration = ReflectionUtils.CustomPrimitiveTypes[objectType]; + return registration.ConvertToServerSideType(reader.Value); + } + else if (reader.TokenType == JsonToken.Null) + { + return null; + } + else + { + throw new JsonSerializationException($"Token {reader.TokenType} cannot be deserialized as {objectType}! Primitive value in JSON was expected."); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + } + else + { + var registration = ReflectionUtils.CustomPrimitiveTypes[value.GetType()]!; + writer.WriteValue(registration.ConvertToClientSideType(value)); + } + } + + + } +} diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs index 16e521e0f3..aaf70c3771 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs @@ -1,11 +1,9 @@ using System; -using DotVVM.Framework.Configuration; -using Newtonsoft.Json; +using DotVVM.Framework.ViewModel; namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes { [CustomPrimitiveType(typeof(Guid?), typeof(TypeIdPrimitiveTypeConverter))] - [JsonConverter(typeof(TypeIdJsonConverter))] public record SampleId : TypeId { public SampleId(Guid idValue) : base(idValue) diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdJsonConverter.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdJsonConverter.cs deleted file mode 100644 index c8f07d7ac2..0000000000 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdJsonConverter.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes -{ - public class TypeIdJsonConverter : JsonConverter where TId : TypeId - { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(TId); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.String) - { - var idText = (string)reader.Value; - var idValue = Guid.Parse(idText); - return TypeId.CreateExisting(idValue); - } - else if (reader.TokenType == JsonToken.Null) - { - return null; - } - else - { - throw new JsonSerializationException($"Token {reader.TokenType} cannot be deserialized as TypeId!"); - } - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue((value as ITypeId)?.IdValue); - } - - } - -} - diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdPrimitiveTypeConverter.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdPrimitiveTypeConverter.cs index 7b781d577f..2b78ef02e9 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdPrimitiveTypeConverter.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdPrimitiveTypeConverter.cs @@ -1,9 +1,11 @@ -using DotVVM.Framework.Configuration; +using DotVVM.Framework.Utils; +using DotVVM.Framework.ViewModel; namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes; public class TypeIdPrimitiveTypeConverter : ICustomPrimitiveTypeConverter where TId : TypeId { - public object Convert(object value) => TypeId.ParseValue(value); + public object FromCustomPrimitiveType(object value) => (value as TypeId)?.IdValue; + public object ToCustomPrimitiveType(object value) => TypeId.ParseValue(value); } From f0fe7d4d9ea9130ce0e00d1429157d946633ac15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 11 Dec 2022 11:53:48 +0100 Subject: [PATCH 17/32] Fixed nullability error --- .../ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs index ecff551d43..c2002567b5 100644 --- a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs @@ -22,7 +22,7 @@ or JsonToken.Integer or JsonToken.Float or JsonToken.Date) { - var registration = ReflectionUtils.CustomPrimitiveTypes[objectType]; + var registration = ReflectionUtils.CustomPrimitiveTypes[objectType]!; return registration.ConvertToServerSideType(reader.Value); } else if (reader.TokenType == JsonToken.Null) From 876d386a1ff147ebe2364d1ef3ae39bfd37295de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 30 Jul 2023 11:09:09 +0200 Subject: [PATCH 18/32] Fixed issues after rebase --- .../Scripts/validation/validation.ts | 2 +- .../Framework/Utils/ReflectionUtils.cs | 36 ------------------- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/src/Framework/Framework/Resources/Scripts/validation/validation.ts b/src/Framework/Framework/Resources/Scripts/validation/validation.ts index 7f66479f80..5edd0037b7 100644 --- a/src/Framework/Framework/Resources/Scripts/validation/validation.ts +++ b/src/Framework/Framework/Resources/Scripts/validation/validation.ts @@ -341,7 +341,7 @@ export function addErrors(errors: ValidationErrorDescriptor[], options: AddError ValidationError.attach(prop.errorMessage, propertyPath, property); } catch (err) { - logWarning("validation", `Unable to find viewmodel property ${propertyPath}. If you've added this validation error using Context.AddModelError, make sure the property path is correct and that the object hasn't been removed from the viewmodel.`, err); + logWarning("validation", err); } } diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index 4fff7a35a6..f567376253 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -647,41 +647,5 @@ public static IEnumerable GetBaseTypesAndInterfaces(Type type) type = baseType; } } - - private static volatile bool customPrimitiveTypesRegistered = false; - internal static void RegisterCustomPrimitiveTypes(IList customPrimitiveTypeRegistrations) - { - foreach (var registration in customPrimitiveTypeRegistrations) - { - if (IsPrimitiveType(registration.ServerSideType) - || IsCollection(registration.ServerSideType) - || IsDictionary(registration.ServerSideType)) - { - throw new DotvvmConfigurationException($"The type {registration.ServerSideType} cannot be used as a custom primitive type. Custom primitive types cannot be collections, dictionaries, and cannot be primitive types already supported by DotVVM."); - } - if (CustomPrimitiveTypes.ContainsKey(registration.ServerSideType)) - { - throw new DotvvmConfigurationException($"The type {registration.ServerSideType} is already registered as a custom primitive type."); - } - if (!PrimitiveTypes.Contains(registration.ClientSideType.UnwrapNullableType())) - { - throw new DotvvmConfigurationException($"The custom primitive type {registration.ServerSideType} cannot use {registration.ClientSideType} as a client-side equivalent. Only primitive types (strings, numbers, Guid, date and time types) are supported."); - } - - CustomPrimitiveTypes.Add(registration.ServerSideType, registration); - } - customPrimitiveTypesRegistered = true; - } - - internal static IEnumerable GetCustomPrimitiveTypeJsonConverters() - { - if (!customPrimitiveTypesRegistered) - { - throw new InvalidOperationException("Cannot access DefaultSerializerSettingsProvider.Instance before DotvvmConfiguration was initialized!"); - } - return CustomPrimitiveTypes.Values - .Where(t => t.JsonConverter != null) - .Select(t => t.JsonConverter!); - } } } From e53148baa253576b4e020a3f624557a82e87b5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 30 Jul 2023 12:37:58 +0200 Subject: [PATCH 19/32] Support for dotvvm serialization of static command arguments --- .../DefaultSerializerSettingsProvider.cs | 2 ++ .../DotvvmExperimentalFeaturesConfiguration.cs | 2 +- .../Framework/Hosting/StaticCommandExecutor.cs | 12 +++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs index bdad703563..d66ed10b8e 100644 --- a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs +++ b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs @@ -54,5 +54,7 @@ private DefaultSerializerSettingsProvider() JsonConvert.DefaultSettings = () => new JsonSerializerSettings() { MaxDepth = defaultMaxSerializationDepth }; Settings = CreateSettings(); } + + public static JsonSerializer CreateJsonSerializer() => JsonSerializer.Create(Instance.Settings); } } diff --git a/src/Framework/Framework/Configuration/DotvvmExperimentalFeaturesConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmExperimentalFeaturesConfiguration.cs index ce2a2bd621..cc292d6547 100644 --- a/src/Framework/Framework/Configuration/DotvvmExperimentalFeaturesConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmExperimentalFeaturesConfiguration.cs @@ -41,7 +41,7 @@ public class DotvvmExperimentalFeaturesConfiguration public DotvvmFeatureFlag KnockoutDeferUpdates { get; private set; } = new DotvvmFeatureFlag("KnockoutDeferUpdates"); [JsonProperty("useDotvvmSerializationForStaticCommandArguments")] - public DotvvmGlobalFeatureFlag UseDotvvmSerializationForStaticCommandArguments { get; private set; } = new DotvvmGlobalFeatureFlag(); + public DotvvmGlobalFeatureFlag UseDotvvmSerializationForStaticCommandArguments { get; private set; } = new DotvvmGlobalFeatureFlag("UseDotvvmSerializationForStaticCommandArguments"); public void Freeze() { diff --git a/src/Framework/Framework/Hosting/StaticCommandExecutor.cs b/src/Framework/Framework/Hosting/StaticCommandExecutor.cs index 0b21851b38..d9a4d51727 100644 --- a/src/Framework/Framework/Hosting/StaticCommandExecutor.cs +++ b/src/Framework/Framework/Hosting/StaticCommandExecutor.cs @@ -10,6 +10,7 @@ using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel; using DotVVM.Framework.ViewModel.Validation; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DotVVM.Framework.Hosting @@ -21,6 +22,7 @@ public class StaticCommandExecutor private readonly IViewModelProtector viewModelProtector; private readonly IStaticCommandArgumentValidator validator; private readonly DotvvmConfiguration configuration; + private readonly JsonSerializer jsonDeserializer; public StaticCommandExecutor(IStaticCommandServiceLoader serviceLoader, IViewModelProtector viewModelProtector, IStaticCommandArgumentValidator validator, DotvvmConfiguration configuration) { @@ -28,6 +30,14 @@ public StaticCommandExecutor(IStaticCommandServiceLoader serviceLoader, IViewMod this.viewModelProtector = viewModelProtector; this.validator = validator; this.configuration = configuration; + if (configuration.ExperimentalFeatures.UseDotvvmSerializationForStaticCommandArguments.Enabled) + { + this.jsonDeserializer = DefaultSerializerSettingsProvider.CreateJsonSerializer(); + } + else + { + this.jsonDeserializer = JsonSerializer.Create(); + } } #pragma warning restore CS0618 @@ -56,7 +66,7 @@ IDotvvmRequestContext context { var (value, path) = a.Type switch { StaticCommandParameterType.Argument => - ((object?)arguments.Dequeue().ToObject((Type)a.Arg!), argumentValidationPaths?.Dequeue()), + ((object?)arguments.Dequeue().ToObject((Type)a.Arg!, this.jsonDeserializer), argumentValidationPaths?.Dequeue()), StaticCommandParameterType.Constant or StaticCommandParameterType.DefaultValue => (a.Arg, null), StaticCommandParameterType.Inject => From 1ed4119328d3690dee594be94d336fe593e18a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 30 Jul 2023 13:03:54 +0200 Subject: [PATCH 20/32] Validate contents of custom primitive types --- src/Framework/Framework/Utils/ReflectionUtils.cs | 11 +++++++++-- .../Serialization/ViewModelTypeMetadataSerializer.cs | 2 +- .../ViewModel/Validation/ViewModelValidator.cs | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index f567376253..8c4ef46013 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -359,11 +359,18 @@ public static bool IsCollection(Type type) return type != typeof(string) && IsEnumerable(type) && !IsDictionary(type); } - public static bool IsPrimitiveType(this Type type) + /// Returns true if the type is a primitive type natively supported by DotVVM. "Primitive" means that it is serialized as a JavaScript primitive (not object nor array) + public static bool IsDotvvmNativePrimitiveType(Type type) { return PrimitiveTypes.Contains(type) || (IsNullableType(type) && IsPrimitiveType(type.UnwrapNullableType())) - || type.IsEnum + || type.IsEnum; + } + + /// Returns true the type is serialized as a JavaScript primitive (not object nor array) + public static bool IsPrimitiveType(Type type) + { + return IsDotvvmNativePrimitiveType(type) || CustomPrimitiveTypes.GetOrAdd(type, TryDiscoverCustomPrimitiveType) is {}; } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs index a5aca8f68d..dbc0a81abe 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs @@ -157,7 +157,7 @@ internal JToken GetTypeIdentifier(Type type, HashSet dependentObjectTypes, { return GetNullableTypeIdentifier(type, dependentObjectTypes, dependentEnumTypes); } - else if (type.IsPrimitiveType()) // we intentionally detect this after handling enums and nullable types + else if (ReflectionUtils.IsPrimitiveType(type)) // we intentionally detect this after handling enums and nullable types { if (ReflectionUtils.CustomPrimitiveTypes.TryGetValue(type, out var registration) && registration is {}) { diff --git a/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs b/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs index a952027888..ec9ef061ee 100644 --- a/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs +++ b/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs @@ -43,7 +43,7 @@ private IEnumerable ValidateViewModel(object? viewMode } if (alreadyValidated.Contains(viewModel)) yield break; var viewModelType = viewModel.GetType(); - if (ReflectionUtils.IsPrimitiveType(viewModelType) || ReflectionUtils.IsNullableType(viewModelType)) + if (ReflectionUtils.IsDotvvmNativePrimitiveType(viewModelType)) { yield break; } From c1f8830d0dd084f57d0037262cb568c6296541d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 30 Jul 2023 13:19:08 +0200 Subject: [PATCH 21/32] ICustomPrimitiveTypeConverter replaced by ToString and TryParse methods on the type --- .../ViewModel/CustomPrimitiveTypeAttribute.cs | 17 ---- .../ICustomPrimitiveTypeConverter.cs | 18 ----- .../CustomPrimitiveTypeRegistration.cs | 77 +++++++++++++------ .../Framework/Utils/ReflectionUtils.cs | 32 ++------ .../CustomPrimitiveTypeJsonConverter.cs | 10 ++- .../ViewModelTypeMetadataSerializer.cs | 2 +- .../CustomPrimitiveTypes/SampleId.cs | 5 +- .../CustomPrimitiveTypes/TypeId.cs | 10 +-- .../TypeIdPrimitiveTypeConverter.cs | 11 --- 9 files changed, 77 insertions(+), 105 deletions(-) delete mode 100644 src/Framework/Core/ViewModel/ICustomPrimitiveTypeConverter.cs delete mode 100644 src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdPrimitiveTypeConverter.cs diff --git a/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs b/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs index d6b3610c09..906f4ee9ba 100644 --- a/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs +++ b/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs @@ -5,21 +5,4 @@ namespace DotVVM.Framework.ViewModel; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] public class CustomPrimitiveTypeAttribute : Attribute { - /// - /// Gets a type which will be used for this custom type on the client side. Only types from ReflectionUtils.PrimitiveTypes (or their nullable versions) are supported. - /// - public Type ClientSideType { get; } - - /// - /// Gets a type implementing ICustomPrimitiveTypeConverter which converts the value from string or other type that may appear in route parameters collection. - /// - public Type ConverterType { get; } - - public CustomPrimitiveTypeAttribute(Type clientSideType, Type converterType) - { - // types are validated later as we don't know on which type the attribute is applied so the error message wouldn't tell the user where is the problem - ClientSideType = clientSideType; - ConverterType = converterType; - } - } diff --git a/src/Framework/Core/ViewModel/ICustomPrimitiveTypeConverter.cs b/src/Framework/Core/ViewModel/ICustomPrimitiveTypeConverter.cs deleted file mode 100644 index 38d17b99e5..0000000000 --- a/src/Framework/Core/ViewModel/ICustomPrimitiveTypeConverter.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace DotVVM.Framework.ViewModel -{ - /// - /// Represents a converter which can convert the value from string or other type that may appear in route parameters collection. - /// - public interface ICustomPrimitiveTypeConverter - { - /// - /// Converts the value from its client-side representation or other type that may appear in route parameters collection to the registered custom primitive type. - /// - object? ToCustomPrimitiveType(object? value); - - /// - /// Converts the value from the registered custom primitive type to its client-side representation. - /// - object? FromCustomPrimitiveType(object? value); - } -} diff --git a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs index 5c5d55e5f8..c3dbd625f4 100644 --- a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs +++ b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs @@ -1,44 +1,75 @@ using System; +using System.Globalization; +using System.Reflection; +using DotVVM.Framework.Routing; +using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel; namespace DotVVM.Framework.Configuration { public sealed class CustomPrimitiveTypeRegistration { - private readonly Func convertToServerSideType; - private readonly Func convertToClientSideType; + public Type Type { get; } - public Type ClientSideType { get; } + public Func TryParseMethod { get; } - public Type ServerSideType { get; } + public Func ToStringMethod { get; } - internal CustomPrimitiveTypeRegistration(Type serverSideType, Type clientSideType, Func convertToServerSideType, Func convertToClientSideType) + internal CustomPrimitiveTypeRegistration(Type type) { - ServerSideType = serverSideType; - ClientSideType = clientSideType; - this.convertToServerSideType = convertToServerSideType; - this.convertToClientSideType = convertToClientSideType; + 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."); + } + + Type = type; + + TryParseMethod = ResolveTryParseMethod(type); + ToStringMethod = ResolveToStringMethod(type); } - public object? ConvertToServerSideType(object? value) + internal static Func ResolveTryParseMethod(Type type) { - var result = convertToServerSideType(value); - if (result != null && !ServerSideType.IsAssignableFrom(result.GetType())) - { - throw new Exception($"The {nameof(ICustomPrimitiveTypeConverter.ToCustomPrimitiveType)} for type {ServerSideType} returned an incompatible type {result?.GetType()}! Expected type: {ServerSideType}"); - } - return result; + var tryParseMethod = type.GetMethod("TryParse", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy, null, + 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})!"); + + var hasFormatProvider = tryParseMethod.GetParameters().Length == 3; + return arg => { + var args = hasFormatProvider + ? new object[] { arg, CultureInfo.InvariantCulture, null } + : new object[] { arg, null }; + return (bool)tryParseMethod.Invoke(null, args) ? new ParseResult(args[args.Length - 1]) : ParseResult.Failed; + }; } - public object? ConvertToClientSideType(object? value) + internal static Func ResolveToStringMethod(Type type) { - var result = convertToClientSideType(value); - if (result != null && !ClientSideType.IsAssignableFrom(result.GetType())) - { - throw new Exception($"The {nameof(ICustomPrimitiveTypeConverter.FromCustomPrimitiveType)} for type {ServerSideType} returned an incompatible type {result?.GetType()}! Expected type: {ClientSideType}"); - } - return result; + var toStringMethod = type.GetMethod("ToString", BindingFlags.Public | BindingFlags.Instance, null, + new[] { typeof(string), typeof(IFormatProvider) }, null) + ?? type.GetMethod("ToString", BindingFlags.Public | BindingFlags.Instance, null, + new[] { typeof(IFormatProvider) }, null) + ?? type.GetMethod("ToString", BindingFlags.Public | BindingFlags.Instance, null, + new Type[] { }, null) + ?? throw new DotvvmConfigurationException($"The type {type} is marked with {nameof(CustomPrimitiveTypeAttribute)} but it does not contain a public method ToString(string, IFormatProvider), ToString(IFormatProvider), or ToString()!"); + var parameterCount = toStringMethod.GetParameters().Length; + return arg => { + var args = parameterCount switch { + 2 => new object?[] { null, CultureInfo.InvariantCulture }, + 1 => new object?[] { CultureInfo.InvariantCulture }, + _ => new object?[] { } + }; + return (string)toStringMethod.Invoke(arg, args); + }; } + public record ParseResult(object? Result = null) + { + public bool Successful { get; init; } = true; + + public static readonly ParseResult Failed = new ParseResult() { Successful = false }; + } } } diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index 8c4ef46013..1a6d314770 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -24,6 +24,7 @@ using RecordExceptions; using System.ComponentModel; using DotVVM.Framework.Compilation; +using DotVVM.Framework.Routing; using DotVVM.Framework.ViewModel; namespace DotVVM.Framework.Utils @@ -235,9 +236,12 @@ public static bool IsAssignableToGenericType(this Type givenType, Type genericTy try { // custom primitive types - if (CustomPrimitiveTypes.TryGetValue(type, out var registration) && registration is {}) + if (CustomPrimitiveTypes.TryGetValue(type, out var registration) && registration is { }) { - return registration.ConvertToServerSideType(value); + var result = registration.TryParseMethod(Convert.ToString(value, CultureInfo.InvariantCulture)); + return result.Successful + ? result.Result + : throw new TypeConvertException(value, type, new Exception("The TryParse method of a custom primitive type failed to parse the value.")); } return Convert.ChangeType(value, type, CultureInfo.InvariantCulture); @@ -381,29 +385,7 @@ public static bool IsPrimitiveType(Type type) { return null; } - - if (IsCollection(type) || 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."); - } - if (!PrimitiveTypes.Contains(attribute.ClientSideType.UnwrapNullableType())) - { - throw new DotvvmConfigurationException($"The type {type} is marked with {nameof(CustomPrimitiveTypeAttribute)} but its {nameof(CustomPrimitiveTypeAttribute.ClientSideType)} is not valid. The client-side representation can only be one of the built-in primitive types (string, numbers, date and time types), or a nullable version of such type."); - } - if (!typeof(ICustomPrimitiveTypeConverter).IsAssignableFrom(attribute.ConverterType)) - { - throw new DotvvmConfigurationException($"The type {type} is marked with {nameof(CustomPrimitiveTypeAttribute)} but its {nameof(CustomPrimitiveTypeAttribute.ConverterType)} doesn't implement the {nameof(ICustomPrimitiveTypeConverter)} interface!"); - } - - try - { - var converter = (ICustomPrimitiveTypeConverter)Activator.CreateInstance(attribute.ConverterType)!; - return new CustomPrimitiveTypeRegistration(type, attribute.ClientSideType, converter.ToCustomPrimitiveType, converter.FromCustomPrimitiveType); - } - catch (Exception ex) - { - throw new DotvvmCompilationException($"The converter for a custom primitive type {type} couldn't be created. Make sure the class {attribute.ConverterType} has a default constructor.", ex); - } + return new CustomPrimitiveTypeRegistration(type); } public static bool IsNullableType(Type type) diff --git a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs index c2002567b5..8e58f32200 100644 --- a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Text; using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel; @@ -23,7 +24,12 @@ or JsonToken.Float or JsonToken.Date) { var registration = ReflectionUtils.CustomPrimitiveTypes[objectType]!; - return registration.ConvertToServerSideType(reader.Value); + var parseResult = registration.TryParseMethod(Convert.ToString(reader.Value, CultureInfo.InvariantCulture)); + if (!parseResult.Successful) + { + throw new JsonSerializationException($"The value '{reader.Value}' cannot be deserialized as {objectType} because its TryParse method wasn't able to parse the value!"); + } + return parseResult.Result; } else if (reader.TokenType == JsonToken.Null) { @@ -44,7 +50,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s else { var registration = ReflectionUtils.CustomPrimitiveTypes[value.GetType()]!; - writer.WriteValue(registration.ConvertToClientSideType(value)); + writer.WriteValue(registration.ToStringMethod(value)); } } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs index dbc0a81abe..25308923eb 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs @@ -161,7 +161,7 @@ internal JToken GetTypeIdentifier(Type type, HashSet dependentObjectTypes, { if (ReflectionUtils.CustomPrimitiveTypes.TryGetValue(type, out var registration) && registration is {}) { - return GetTypeIdentifier(registration.ClientSideType, dependentObjectTypes, dependentEnumTypes); + return GetPrimitiveTypeName(typeof(string)); } return GetPrimitiveTypeName(type); } diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs index aaf70c3771..413017224a 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/SampleId.cs @@ -1,16 +1,15 @@ using System; +using System.Security.Cryptography; using DotVVM.Framework.ViewModel; namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes { - [CustomPrimitiveType(typeof(Guid?), typeof(TypeIdPrimitiveTypeConverter))] + [CustomPrimitiveType] public record SampleId : TypeId { public SampleId(Guid idValue) : base(idValue) { } - - public override string ToString() => base.ToString(); } } diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeId.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeId.cs index a1027f4dd2..081b1775ab 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeId.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeId.cs @@ -25,7 +25,7 @@ public static TId CreateExisting(Guid idValue) return (TId)Activator.CreateInstance(typeof(TId), args: idValue)!; } - public static TId ParseValue(object? value) + public static TId Parse(object? value) { if (value is string stringValue) { @@ -45,10 +45,10 @@ public static TId ParseValue(object? value) } } - public override string ToString() - { - return IdValue.ToString(); - } + public static bool TryParse(string id, out TId result) + => (result = Guid.TryParse(id, out var r) ? CreateExisting(r) : null) is not null; + + public sealed override string ToString() => IdValue.ToString(); } } diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdPrimitiveTypeConverter.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdPrimitiveTypeConverter.cs deleted file mode 100644 index 2b78ef02e9..0000000000 --- a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TypeIdPrimitiveTypeConverter.cs +++ /dev/null @@ -1,11 +0,0 @@ -using DotVVM.Framework.Utils; -using DotVVM.Framework.ViewModel; - -namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes; - -public class TypeIdPrimitiveTypeConverter : ICustomPrimitiveTypeConverter - where TId : TypeId -{ - public object FromCustomPrimitiveType(object value) => (value as TypeId)?.IdValue; - public object ToCustomPrimitiveType(object value) => TypeId.ParseValue(value); -} From 3290fc70f743ab6a600b7a16622f6a28c797f8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 30 Jul 2023 14:11:57 +0200 Subject: [PATCH 22/32] IsCustomPrimitiveTypes function added --- src/Framework/Framework/Utils/ReflectionUtils.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index 1a6d314770..9c0a8aa7ff 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -371,6 +371,12 @@ public static bool IsDotvvmNativePrimitiveType(Type type) || type.IsEnum; } + /// Returns true if the type is a custom primitive type. + public static bool IsCustomPrimitiveType(Type type) + { + return CustomPrimitiveTypes.GetOrAdd(type, TryDiscoverCustomPrimitiveType) is { }; + } + /// Returns true the type is serialized as a JavaScript primitive (not object nor array) public static bool IsPrimitiveType(Type type) { From 079c9669a42d2f7aa679fbbf3844fa4adfd79f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 30 Jul 2023 14:15:24 +0200 Subject: [PATCH 23/32] CustomPrimitiveType ToString and Parse JS translations --- ...ustomPrimitiveTypesConversionTranslator.cs | 27 +++++++++++++++++++ .../Javascript/JavascriptTranslator.cs | 1 + src/Tests/Binding/BindingCompilationTests.cs | 27 +++++++++++++++++++ .../Binding/JavascriptCompilationTests.cs | 15 +++++++++++ 4 files changed, 70 insertions(+) create mode 100644 src/Framework/Framework/Compilation/Javascript/CustomPrimitiveTypesConversionTranslator.cs diff --git a/src/Framework/Framework/Compilation/Javascript/CustomPrimitiveTypesConversionTranslator.cs b/src/Framework/Framework/Compilation/Javascript/CustomPrimitiveTypesConversionTranslator.cs new file mode 100644 index 0000000000..3122cc384f --- /dev/null +++ b/src/Framework/Framework/Compilation/Javascript/CustomPrimitiveTypesConversionTranslator.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using DotVVM.Framework.Compilation.Javascript.Ast; +using DotVVM.Framework.Utils; + +namespace DotVVM.Framework.Compilation.Javascript +{ + public class CustomPrimitiveTypesConversionTranslator : IJavascriptMethodTranslator + { + public JsExpression? TryTranslateCall(LazyTranslatedExpression? context, LazyTranslatedExpression[] arguments, MethodInfo method) + { + var type = context?.OriginalExpression.Type ?? method.DeclaringType; + type = type.UnwrapNullableType(); + if (method.Name is "ToString" or "Parse" && ReflectionUtils.IsCustomPrimitiveType(type)) + { + if (method.Name == "ToString" && arguments.Length == 0 && context is {}) + { + return context.JsExpression(); + } + else if (method.Name == "Parse" && arguments.Length == 1 && context is null) + { + return arguments[0].JsExpression(); + } + } + return null; + } + } +} diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslator.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslator.cs index ab4145bb40..098be7b437 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslator.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslator.cs @@ -248,6 +248,7 @@ public JavascriptTranslatorConfiguration() { Translators.Add(MethodCollection = new JavascriptTranslatableMethodCollection()); Translators.Add(new DelegateInvokeMethodTranslator()); + Translators.Add(new CustomPrimitiveTypesConversionTranslator()); } public JsExpression? TryTranslateCall(LazyTranslatedExpression? context, LazyTranslatedExpression[] arguments, MethodInfo method) => diff --git a/src/Tests/Binding/BindingCompilationTests.cs b/src/Tests/Binding/BindingCompilationTests.cs index 8a6d0d59a2..1f8a36e831 100755 --- a/src/Tests/Binding/BindingCompilationTests.cs +++ b/src/Tests/Binding/BindingCompilationTests.cs @@ -22,6 +22,7 @@ using System.Runtime.Serialization; using CheckTestOutput; using DotVVM.Framework.Tests.Runtime; +using System.ComponentModel.DataAnnotations; namespace DotVVM.Framework.Tests.Binding { @@ -1138,6 +1139,8 @@ class TestViewModel public int[] IntArray { get; set; } public decimal DecimalProp { get; set; } + public VehicleNumber? VehicleNumber { get; set; } + public ReadOnlyCollection ReadOnlyCollection => new ReadOnlyCollection(new[] { 1, 2, 3 }); public string SetStringProp(string a, int b) @@ -1197,6 +1200,30 @@ public async Task GetStringPropAsync() public int MethodWithOverloads(int a, int b) => a + b; } + + [CustomPrimitiveType] + record struct VehicleNumber( + [property: Range(100, 999)] + int Value + ) + { + public override string ToString() => Value.ToString(); + public static bool TryParse(string s, out VehicleNumber result) + { + if (int.TryParse(s, out var i)) + { + result = new VehicleNumber(i); + return true; + } + else + { + result = default!; + return false; + } + } + public static VehicleNumber Parse(string s) => new VehicleNumber(int.Parse(s)); + } + class TestLambdaCompilation { public string StringProp { get; set; } diff --git a/src/Tests/Binding/JavascriptCompilationTests.cs b/src/Tests/Binding/JavascriptCompilationTests.cs index 49191eaa39..3ebea1dc06 100644 --- a/src/Tests/Binding/JavascriptCompilationTests.cs +++ b/src/Tests/Binding/JavascriptCompilationTests.cs @@ -1225,6 +1225,20 @@ public void JavascriptCompilation_StringFunctions(string input, string expected) var result = CompileBinding(input, new[] { new NamespaceImport("DotVVM.Framework.Binding.HelperNamespace") }, typeof(TestViewModel)); Assert.AreEqual(expected, result); } + + [TestMethod] + public void JavascriptCompilation_CustomPrimitiveToString() + { + var result = CompileBinding("VehicleNumber.ToString()", typeof(TestViewModel)); + Assert.AreEqual("VehicleNumber", result); + } + + [TestMethod] + public void JavascriptCompilation_CustomPrimitiveParse() + { + var result = CompileBinding("VehicleNumber == DotVVM.Framework.Tests.Binding.VehicleNumber.Parse('123')", typeof(TestViewModel)); + Assert.AreEqual("VehicleNumber()==\"123\"", result); + } } public class TestExtensionParameterConflictViewModel @@ -1269,4 +1283,5 @@ public enum TestEnum public TestEnum Enum { get; set; } public string String { get; set; } } + } From 36011bc474b24559a30fd6c34408d22b59173402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 30 Jul 2023 14:59:40 +0200 Subject: [PATCH 24/32] UI tests added CustomPrimitiveTypes collection changed to private --- .../Framework/Controls/HtmlGenericControl.cs | 1 + .../Framework/Utils/ReflectionUtils.cs | 15 ++-- .../CustomPrimitiveTypeJsonConverter.cs | 6 +- .../ViewModelTypeMetadataSerializer.cs | 2 +- ...ontrolWithCustomPrimitiveTypeProperties.cs | 69 +++++++++++++++++ .../Common/DotVVM.Samples.Common.csproj | 3 + .../RouteLinkViewModel.cs | 28 +++++++ .../CustomPrimitiveTypes/TextBoxViewModel.cs | 24 ++++++ .../UsedInControlsViewModel.cs | 19 +++++ .../CustomPrimitiveTypes/RouteLink.dothtml | 49 ++++++++++++ .../CustomPrimitiveTypes/TextBox.dothtml | 30 ++++++++ .../UsedInControls.dothtml | 29 +++++++ .../Abstractions/SamplesRouteUrls.designer.cs | 3 + .../Feature/CustomPrimitiveTypesTests.cs | 75 +++++++++++++++++++ 14 files changed, 344 insertions(+), 9 deletions(-) create mode 100644 src/Samples/Common/Controls/ControlWithCustomPrimitiveTypeProperties.cs create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/RouteLinkViewModel.cs create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TextBoxViewModel.cs create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/UsedInControlsViewModel.cs create mode 100644 src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/RouteLink.dothtml create mode 100644 src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/TextBox.dothtml create mode 100644 src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/UsedInControls.dothtml diff --git a/src/Framework/Framework/Controls/HtmlGenericControl.cs b/src/Framework/Framework/Controls/HtmlGenericControl.cs index db33cd9501..0c5559268b 100644 --- a/src/Framework/Framework/Controls/HtmlGenericControl.cs +++ b/src/Framework/Framework/Controls/HtmlGenericControl.cs @@ -405,6 +405,7 @@ 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), 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 9c0a8aa7ff..0f20d4d2e5 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -236,7 +236,7 @@ public static bool IsAssignableToGenericType(this Type givenType, Type genericTy try { // custom primitive types - if (CustomPrimitiveTypes.TryGetValue(type, out var registration) && registration is { }) + if (TryGetCustomPrimitiveTypeRegistration(type) is { } registration) { var result = registration.TryParseMethod(Convert.ToString(value, CultureInfo.InvariantCulture)); return result.Successful @@ -307,7 +307,7 @@ public record TypeConvertException(object Value, Type Type, Exception InnerExcep typeof (decimal) }; // mapping of server-side types to their client-side representation - internal static readonly ConcurrentDictionary CustomPrimitiveTypes = new(); + private static readonly ConcurrentDictionary CustomPrimitiveTypes = new(); public static IEnumerable GetNumericTypes() { @@ -374,14 +374,19 @@ public static bool IsDotvvmNativePrimitiveType(Type type) /// Returns true if the type is a custom primitive type. public static bool IsCustomPrimitiveType(Type type) { - return CustomPrimitiveTypes.GetOrAdd(type, TryDiscoverCustomPrimitiveType) is { }; + return TryGetCustomPrimitiveTypeRegistration(type) is { }; + } + + /// 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); } /// Returns true the type is serialized as a JavaScript primitive (not object nor array) public static bool IsPrimitiveType(Type type) { - return IsDotvvmNativePrimitiveType(type) - || CustomPrimitiveTypes.GetOrAdd(type, TryDiscoverCustomPrimitiveType) is {}; + return IsDotvvmNativePrimitiveType(type) || TryGetCustomPrimitiveTypeRegistration(type) is {}; } private static CustomPrimitiveTypeRegistration? TryDiscoverCustomPrimitiveType(Type type) diff --git a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs index 8e58f32200..4989f999c4 100644 --- a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs @@ -12,7 +12,7 @@ public class DotvvmCustomPrimitiveTypeConverter : JsonConverter { public override bool CanConvert(Type objectType) { - return ReflectionUtils.CustomPrimitiveTypes.TryGetValue(objectType, out var result) && result is { }; + return ReflectionUtils.IsCustomPrimitiveType(objectType); } public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) @@ -23,7 +23,7 @@ or JsonToken.Integer or JsonToken.Float or JsonToken.Date) { - var registration = ReflectionUtils.CustomPrimitiveTypes[objectType]!; + var registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(objectType)!; var parseResult = registration.TryParseMethod(Convert.ToString(reader.Value, CultureInfo.InvariantCulture)); if (!parseResult.Successful) { @@ -49,7 +49,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s } else { - var registration = ReflectionUtils.CustomPrimitiveTypes[value.GetType()]!; + var registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(value.GetType())!; writer.WriteValue(registration.ToStringMethod(value)); } } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs index 25308923eb..c70c202aa1 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs @@ -159,7 +159,7 @@ internal JToken GetTypeIdentifier(Type type, HashSet dependentObjectTypes, } else if (ReflectionUtils.IsPrimitiveType(type)) // we intentionally detect this after handling enums and nullable types { - if (ReflectionUtils.CustomPrimitiveTypes.TryGetValue(type, out var registration) && registration is {}) + if (ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(type) is {}) { return GetPrimitiveTypeName(typeof(string)); } diff --git a/src/Samples/Common/Controls/ControlWithCustomPrimitiveTypeProperties.cs b/src/Samples/Common/Controls/ControlWithCustomPrimitiveTypeProperties.cs new file mode 100644 index 0000000000..0eaf59838b --- /dev/null +++ b/src/Samples/Common/Controls/ControlWithCustomPrimitiveTypeProperties.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Text; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Controls; +using DotVVM.Framework.ViewModel; + +namespace DotVVM.Samples.Common.Controls +{ + public class ControlWithCustomPrimitiveTypeProperties : CompositeControl + { + public DotvvmControl GetContents( + HtmlCapability htmlCapability, + Point pointValue, + Point pointValue2, + IValueBinding pointBinding, + ValueOrBinding pointValueOrBinding, + ValueOrBinding pointValueOrBinding2, + ValueOrBinding pointValueOrBinding3, + ValueOrBinding pointValueOrBinding4) + { + return new HtmlGenericControl("ul", htmlCapability) + .AppendChildren( + new HtmlGenericControl("li") + .AppendChildren(new Literal(pointValue.ToString())), + new HtmlGenericControl("li") + .AppendChildren(new Literal(pointValue2.ToString())), + new HtmlGenericControl("li") + .AppendChildren(new Literal(pointBinding)), + new HtmlGenericControl("li") + .AppendChildren(new Literal(pointValueOrBinding)), + new HtmlGenericControl("li") + .AppendChildren(new Literal(pointValueOrBinding2)), + new HtmlGenericControl("li") + .AppendChildren(new Literal(pointValueOrBinding3)), + new HtmlGenericControl("li") + .AppendChildren(new Literal(pointValueOrBinding4)) + ); + } + + } + + [CustomPrimitiveType] + public struct Point + { + public int X { get; set; } + public int Y { get; set; } + + public static bool TryParse(string value, out Point result) + { + var parts = value.Split(','); + if (int.TryParse(parts[0], out var x) && int.TryParse(parts[1], out var y)) + { + result = new Point { X = x, Y = y }; + return true; + } + else + { + result = default; + return false; + } + } + + public static Point Parse(string value) => TryParse(value, out var result) ? result : throw new FormatException(); + + public override string ToString() => $"{X},{Y}"; + } +} diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index 00c932322f..4602ac2f7a 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -95,6 +95,9 @@ + + + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/RouteLinkViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/RouteLinkViewModel.cs new file mode 100644 index 0000000000..49d8dff42a --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/RouteLinkViewModel.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.ViewModel; +using DotVVM.Framework.Hosting; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes +{ + public class RouteLinkViewModel : DotvvmViewModelBase + { + + public SampleId Id1 { get; set; } = new SampleId(new Guid("D7682DE1-B985-4B4B-B2BF-C349192AD9C9")); + + public SampleId Id2 { get; set; } = new SampleId(new Guid("6F5E8011-BD12-477D-9E82-A7A1CE836773")); + + public SampleId Null { get; set; } + + public void ChangeIds() + { + Null = Id1; + Id1 = Id2; + Id2 = null; + } + } +} + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TextBoxViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TextBoxViewModel.cs new file mode 100644 index 0000000000..529019d2c2 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/TextBoxViewModel.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.ViewModel; +using DotVVM.Framework.Hosting; +using DotVVM.Samples.Common.Controls; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes +{ + public class TextBoxViewModel : DotvvmViewModelBase + { + + public Point Point { get; set; } = new Point() { X = 15, Y = 32 }; + + [Required] + [RegularExpression(@"^\d+,\d+$")] + public Point Null { get; set; } + + } +} + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/UsedInControlsViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/UsedInControlsViewModel.cs new file mode 100644 index 0000000000..29a6e722f9 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/CustomPrimitiveTypes/UsedInControlsViewModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.ViewModel; +using DotVVM.Framework.Hosting; +using DotVVM.Samples.Common.Controls; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes +{ + public class UsedInControlsViewModel : DotvvmViewModelBase + { + + public Point Point { get; set; } = new Point() { X = 1, Y = 2 }; + + } +} + diff --git a/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/RouteLink.dothtml b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/RouteLink.dothtml new file mode 100644 index 0000000000..d8b6a07510 --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/RouteLink.dothtml @@ -0,0 +1,49 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes.RouteLinkViewModel, DotVVM.Samples.Common + + + + + + + + + + +

RouteLink

+ +

Client

+ + + +

Server

+ + + + + + + + + diff --git a/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/TextBox.dothtml b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/TextBox.dothtml new file mode 100644 index 0000000000..561aafb630 --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/TextBox.dothtml @@ -0,0 +1,30 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes.TextBoxViewModel, DotVVM.Samples.Common + + + + + + + + + +

TextBox

+ + + + + + +
    +
  • Point X: {{resource: Point.X}}
  • +
  • Point Y: {{resource: Point.Y}}
  • +
  • Null X: {{resource: Null.X}}
  • +
  • Null Y: {{resource: Null.Y}}
  • +
+ + + + + + + diff --git a/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/UsedInControls.dothtml b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/UsedInControls.dothtml new file mode 100644 index 0000000000..0878d2cba7 --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/UsedInControls.dothtml @@ -0,0 +1,29 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes.UsedInControlsViewModel, DotVVM.Samples.Common + + + + + + + + + +

Custom primitive types used in properties

+ + + +
{{value: Point.ToString()}}
+
{{value: Point}}
+ + + + + diff --git a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs index a54b2cb7cc..d1c5806bce 100644 --- a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs +++ b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs @@ -238,6 +238,9 @@ public partial class SamplesRouteUrls public const string FeatureSamples_CompositeControls_ControlPropertyNamingConflict = "FeatureSamples/CompositeControls/ControlPropertyNamingConflict"; public const string FeatureSamples_ConditionalCssClasses_ConditionalCssClasses = "FeatureSamples/ConditionalCssClasses/ConditionalCssClasses"; public const string FeatureSamples_CustomPrimitiveTypes_Basic = "FeatureSamples/CustomPrimitiveTypes/Basic"; + public const string FeatureSamples_CustomPrimitiveTypes_RouteLink = "FeatureSamples/CustomPrimitiveTypes/RouteLink"; + public const string FeatureSamples_CustomPrimitiveTypes_TextBox = "FeatureSamples/CustomPrimitiveTypes/TextBox"; + public const string FeatureSamples_CustomPrimitiveTypes_UsedInControls = "FeatureSamples/CustomPrimitiveTypes/UsedInControls"; public const string FeatureSamples_CustomResponseProperties_SimpleExceptionFilter = "FeatureSamples/CustomResponseProperties/SimpleExceptionFilter"; public const string FeatureSamples_DateTimeSerialization_DateTimeSerialization = "FeatureSamples/DateTimeSerialization/DateTimeSerialization"; public const string FeatureSamples_DependencyInjection_ViewModelScopedService = "FeatureSamples/DependencyInjection/ViewModelScopedService"; diff --git a/src/Samples/Tests/Tests/Feature/CustomPrimitiveTypesTests.cs b/src/Samples/Tests/Tests/Feature/CustomPrimitiveTypesTests.cs index 59a1d64529..1d05584bc1 100644 --- a/src/Samples/Tests/Tests/Feature/CustomPrimitiveTypesTests.cs +++ b/src/Samples/Tests/Tests/Feature/CustomPrimitiveTypesTests.cs @@ -90,5 +90,80 @@ public void Feature_CustomPrimitiveTypes_Basic(string urlSuffix, string expected AssertUI.TextEquals(staticCommandResult, "54162c7e-cdcc-4585-aa92-2e78be3f0c75"); }); } + + [Fact] + public void Feature_CustomPrimitiveTypes_RouteLink() + { + RunInAllBrowsers(browser => + { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_CustomPrimitiveTypes_RouteLink); + + var links = browser.FindElements("a").ThrowIfDifferentCountThan(4); + + AssertUI.Attribute(links[0], "href", u => u.EndsWith("/FeatureSamples/CustomPrimitiveTypes/Basic/d7682de1-b985-4b4b-b2bf-c349192ad9c9?Id=6f5e8011-bd12-477d-9e82-a7a1ce836773")); + AssertUI.Attribute(links[1], "href", u => u.EndsWith("/FeatureSamples/CustomPrimitiveTypes/Basic/d7682de1-b985-4b4b-b2bf-c349192ad9c9?Id=6f5e8011-bd12-477d-9e82-a7a1ce836773")); + AssertUI.Attribute(links[2], "href", u => u.EndsWith("/FeatureSamples/CustomPrimitiveTypes/Basic/d7682de1-b985-4b4b-b2bf-c349192ad9c9?Id=6f5e8011-bd12-477d-9e82-a7a1ce836773")); + AssertUI.Attribute(links[3], "href", u => u.EndsWith("/FeatureSamples/CustomPrimitiveTypes/Basic/d7682de1-b985-4b4b-b2bf-c349192ad9c9?Id=6f5e8011-bd12-477d-9e82-a7a1ce836773")); + + browser.Single("input[type=button]").Click(); + + AssertUI.Attribute(links[0], "href", u => u.EndsWith("/FeatureSamples/CustomPrimitiveTypes/Basic/6f5e8011-bd12-477d-9e82-a7a1ce836773?Null=d7682de1-b985-4b4b-b2bf-c349192ad9c9")); + AssertUI.Attribute(links[2], "href", u => u.EndsWith("/FeatureSamples/CustomPrimitiveTypes/Basic/6f5e8011-bd12-477d-9e82-a7a1ce836773?Null=d7682de1-b985-4b4b-b2bf-c349192ad9c9")); + }); + } + + + [Fact] + public void Feature_CustomPrimitiveTypes_TextBox() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_CustomPrimitiveTypes_TextBox); + + var textboxes = browser.FindElements("input[type=text]").ThrowIfDifferentCountThan(2); + + AssertUI.Value(textboxes[0], "15,32"); + AssertUI.Value(textboxes[1], "0,0"); + + textboxes[0].Clear().SendKeys("1,2"); + browser.Single("input[type=button]").Click(); + + AssertUI.Value(textboxes[0], "1,2"); + + var items = browser.FindElements(".results li").ThrowIfDifferentCountThan(4); + AssertUI.TextEquals(items[0], "Point X: 1"); + AssertUI.TextEquals(items[1], "Point Y: 2"); + AssertUI.TextEquals(items[2], "Null X: 0"); + AssertUI.TextEquals(items[3], "Null Y: 0"); + + textboxes[1].Clear().SendKeys("xxx"); + browser.Single("input[type=button]").Click(); + + browser.FindElements(".validation li").ThrowIfSequenceEmpty(); + }); + } + + [Fact] + public void Feature_CustomPrimitiveTypes_UsedInControls() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_CustomPrimitiveTypes_UsedInControls); + + var items = browser.FindElements("li").ThrowIfDifferentCountThan(7); + AssertUI.TextEquals(items[0], "12,13"); + AssertUI.TextEquals(items[1], "1,2"); + AssertUI.TextEquals(items[2], "1,2"); + AssertUI.TextEquals(items[3], "1,34"); + AssertUI.TextEquals(items[4], "1,2"); + AssertUI.TextEquals(items[5], "1,2"); + AssertUI.TextEquals(items[6], "12,3"); + + var ul = browser.Single("ul"); + AssertUI.Attribute(ul, "data-value", "1,2"); + AssertUI.Attribute(ul, "data-resource", "1,2"); + + AssertUI.TextEquals(browser.Single(".tostring"), "1,2"); + AssertUI.TextEquals(browser.Single(".implicit-tostring"), "1,2"); + }); + } } } From 6b51cb7a3b5c2f002d2b2550d0b04ff0d89582b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 30 Jul 2023 15:05:40 +0200 Subject: [PATCH 25/32] Fixed unnecessary changes --- .../Framework/Resources/Scripts/validation/validation.ts | 1 - src/Samples/ApplicationInsights.Owin/Web.config | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Framework/Framework/Resources/Scripts/validation/validation.ts b/src/Framework/Framework/Resources/Scripts/validation/validation.ts index 5edd0037b7..6c62f2b541 100644 --- a/src/Framework/Framework/Resources/Scripts/validation/validation.ts +++ b/src/Framework/Framework/Resources/Scripts/validation/validation.ts @@ -338,7 +338,6 @@ export function addErrors(errors: ValidationErrorDescriptor[], options: AddError try { // find the property const property = evaluator.traverseContext(root, propertyPath); - ValidationError.attach(prop.errorMessage, propertyPath, property); } catch (err) { logWarning("validation", err); diff --git a/src/Samples/ApplicationInsights.Owin/Web.config b/src/Samples/ApplicationInsights.Owin/Web.config index a7b2698ee1..7063dfb4f1 100644 --- a/src/Samples/ApplicationInsights.Owin/Web.config +++ b/src/Samples/ApplicationInsights.Owin/Web.config @@ -59,8 +59,8 @@
- - + + @@ -106,4 +106,4 @@ - \ No newline at end of file + From b65d7ed808193e9b596f7f20e29e9943e17adea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 30 Jul 2023 15:06:46 +0200 Subject: [PATCH 26/32] Removed unnecessary change in DotVVM.Core project --- src/Framework/Core/DotVVM.Core.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Framework/Core/DotVVM.Core.csproj b/src/Framework/Core/DotVVM.Core.csproj index 02fcfe1111..76ef2cabbe 100644 --- a/src/Framework/Core/DotVVM.Core.csproj +++ b/src/Framework/Core/DotVVM.Core.csproj @@ -29,9 +29,6 @@ all - - - $(DefineConstants);RELEASE From 1e1a3ea6818d28fcc407bf32a636e4051de6c92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 30 Jul 2023 15:13:02 +0200 Subject: [PATCH 27/32] Fix deserialization of Dictionary into pre-initialized dict --- .../ViewModel/Serialization/DotvvmDictionaryConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs index be529f1b08..ecd16b9500 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs @@ -69,7 +69,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s if (value is null) throw new Exception($"Could not deserialize object with path '{reader.Path}' as IEnumerable."); foreach (var item in value) { - dict.Add(keyProp.GetValue(item)!, valueProp.GetValue(item)); + dict[keyProp.GetValue(item)!] = valueProp.GetValue(item); } return dict; } From 4ffbb69288e9a7dce118255a3fffa473c9c04164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 30 Jul 2023 15:22:24 +0200 Subject: [PATCH 28/32] Allow validation of properties inside custom primitives --- .../Framework/Utils/ReflectionUtils.cs | 7 ++++-- .../StaticCommandArgumentValidator.cs | 2 +- .../Validation/ViewModelValidator.cs | 7 +++--- src/Framework/Testing/DotvvmTestHelper.cs | 1 + .../Binding/StaticCommandExecutorTests.cs | 23 ++++++++++++++++++- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index 0f20d4d2e5..b7d321ba73 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -367,7 +367,7 @@ public static bool IsCollection(Type type) public static bool IsDotvvmNativePrimitiveType(Type type) { return PrimitiveTypes.Contains(type) - || (IsNullableType(type) && IsPrimitiveType(type.UnwrapNullableType())) + || (IsNullableType(type) && IsDotvvmNativePrimitiveType(type.UnwrapNullableType())) || type.IsEnum; } @@ -386,7 +386,10 @@ public static bool IsCustomPrimitiveType(Type type) /// Returns true the type is serialized as a JavaScript primitive (not object nor array) public static bool IsPrimitiveType(Type type) { - return IsDotvvmNativePrimitiveType(type) || TryGetCustomPrimitiveTypeRegistration(type) is {}; + return PrimitiveTypes.Contains(type) + || (IsNullableType(type) && IsDotvvmNativePrimitiveType(type.UnwrapNullableType())) + || type.IsEnum + || TryGetCustomPrimitiveTypeRegistration(type) is {}; } private static CustomPrimitiveTypeRegistration? TryDiscoverCustomPrimitiveType(Type type) diff --git a/src/Framework/Framework/ViewModel/Validation/StaticCommandArgumentValidator.cs b/src/Framework/Framework/ViewModel/Validation/StaticCommandArgumentValidator.cs index 989e114d13..2d2ecf7a50 100644 --- a/src/Framework/Framework/ViewModel/Validation/StaticCommandArgumentValidator.cs +++ b/src/Framework/Framework/ViewModel/Validation/StaticCommandArgumentValidator.cs @@ -52,7 +52,7 @@ public StaticCommandArgumentValidator(IViewModelValidator viewModelValidator, IV } } // validate data annotations in the object - if (argument is {} && ReflectionUtils.IsComplexType(argument.GetType())) + if (argument is {} && !ReflectionUtils.IsDotvvmNativePrimitiveType(argument.GetType())) { var errors = this.viewModelValidator.ValidateViewModel(args[i]); foreach (var e in errors) diff --git a/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs b/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs index ec9ef061ee..ba2a5344eb 100644 --- a/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs +++ b/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs @@ -47,7 +47,6 @@ private IEnumerable ValidateViewModel(object? viewMode { yield break; } - alreadyValidated.Add(viewModel); if (ReflectionUtils.IsEnumerable(viewModelType)) @@ -81,7 +80,9 @@ private IEnumerable ValidateViewModel(object? viewMode var propertyResult = rule.SourceValidationAttribute?.GetValidationResult(value, context); if (propertyResult != ValidationResult.Success) { - yield return new ViewModelValidationError(rule.ErrorMessage, pathPrefix + property.Name, rootObject); + var propertyPath = + ReflectionUtils.IsCustomPrimitiveType(viewModelType) ? pathPrefix.TrimEnd('/') : pathPrefix + property.Name; + yield return new ViewModelValidationError(rule.ErrorMessage, propertyPath, rootObject); } } } @@ -89,7 +90,7 @@ private IEnumerable ValidateViewModel(object? viewMode // inspect objects if (value != null) { - if (ReflectionUtils.IsComplexType(property.Type)) + if (!ReflectionUtils.IsDotvvmNativePrimitiveType(property.Type)) { // complex objects foreach (var error in ValidateViewModel(value, alreadyValidated, rootObject, pathPrefix + property.Name + "/")) diff --git a/src/Framework/Testing/DotvvmTestHelper.cs b/src/Framework/Testing/DotvvmTestHelper.cs index 018e9a22fe..dd3bc7d07c 100644 --- a/src/Framework/Testing/DotvvmTestHelper.cs +++ b/src/Framework/Testing/DotvvmTestHelper.cs @@ -101,6 +101,7 @@ public static void RegisterMoqServices(IServiceCollection services) private static Lazy _defaultConfig = new Lazy(() => { var config = CreateConfiguration(); + config.ExperimentalFeatures.UseDotvvmSerializationForStaticCommandArguments.Enable(); config.RouteTable.Add("TestRoute", "TestRoute", "TestView.dothtml"); config.Freeze(); return config; diff --git a/src/Tests/Binding/StaticCommandExecutorTests.cs b/src/Tests/Binding/StaticCommandExecutorTests.cs index db7a158e6f..754c02f595 100644 --- a/src/Tests/Binding/StaticCommandExecutorTests.cs +++ b/src/Tests/Binding/StaticCommandExecutorTests.cs @@ -43,7 +43,7 @@ StaticCommandInvocationPlan CreatePlan(Expression methodExpr) async Task Invoke(StaticCommandInvocationPlan plan, params (object value, string path)[] arguments) { var context = DotvvmTestHelper.CreateContext(config, requestType: DotvvmRequestType.StaticCommand); - var a = arguments.Select(t => JToken.FromObject(t.value)); + var a = arguments.Select(t => JToken.FromObject(t.value, DefaultSerializerSettingsProvider.CreateJsonSerializer())); var p = arguments.Select(t => t.path); return await executor.Execute(plan, a, p, context); } @@ -226,5 +226,26 @@ internal static void AutomaticValidation(ViewModelInstance viewModel, [RegularEx ms.AddArgumentError(() => viewModel.Property, "manual-error"); ms.FailOnInvalidModelState(); } + + [TestMethod] + public async Task Validation_CustomPrimitives() + { + var vm = new TestViewModel { VehicleNumber = new(1) }; + var plan = CreatePlan(() => CustomPrimitivesValidation(vm, new VehicleNumber(1))); + var modelState = await InvokeExpectingErrors(plan, (vm, "/VM"), (new VehicleNumber(321), "/Argument")); + + Assert.AreEqual(2, modelState.Errors.Count, $"Unexpected errors: {string.Join(", ", modelState.Errors)}"); + Assert.IsTrue(modelState.Errors[0].IsResolved); + Assert.IsTrue(modelState.Errors[1].IsResolved); + Assert.AreEqual("The field Value must be between 100 and 999.", modelState.Errors[0].ErrorMessage); + Assert.AreEqual("Vehicle must have lucky number.", modelState.Errors[1].ErrorMessage); + Assert.AreEqual("/VM/VehicleNumber", modelState.Errors[0].PropertyPath); + Assert.AreEqual("/Argument", modelState.Errors[1].PropertyPath); + } + + [AllowStaticCommand(StaticCommandValidation.Automatic)] + internal static void CustomPrimitivesValidation(TestViewModel viewModel, [RegularExpression(@"\d\d7", ErrorMessage = "Vehicle must have lucky number.")] VehicleNumber vehicle) + { + } } } From be55d5a3fee55d4ebbf45aee438ab439cd10c7b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 30 Jul 2023 16:03:09 +0200 Subject: [PATCH 29/32] Covered all overloads of ToString and TryParse --- .../CustomPrimitiveTypeRegistration.cs | 55 ++++++++++--------- ...ontrolWithCustomPrimitiveTypeProperties.cs | 14 +++-- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs index c3dbd625f4..8a9f55c06f 100644 --- a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs +++ b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs @@ -1,5 +1,7 @@ using System; using System.Globalization; +using System.Linq; +using System.Linq.Expressions; using System.Reflection; using DotVVM.Framework.Routing; using DotVVM.Framework.Utils; @@ -25,7 +27,9 @@ internal CustomPrimitiveTypeRegistration(Type type) Type = type; TryParseMethod = ResolveTryParseMethod(type); - ToStringMethod = ResolveToStringMethod(type); + ToStringMethod = typeof(IFormattable).IsAssignableFrom(type) + ? obj => ((IFormattable)obj).ToString(null, CultureInfo.InvariantCulture) + : obj => obj.ToString(); } internal static Func ResolveTryParseMethod(Type type) @@ -36,33 +40,30 @@ internal static Func ResolveTryParseMethod(Type type) 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})!"); - var hasFormatProvider = tryParseMethod.GetParameters().Length == 3; - return arg => { - var args = hasFormatProvider - ? new object[] { arg, CultureInfo.InvariantCulture, null } - : new object[] { arg, null }; - return (bool)tryParseMethod.Invoke(null, args) ? new ParseResult(args[args.Length - 1]) : ParseResult.Failed; - }; - } + var inputParameter = Expression.Parameter(typeof(string), "arg"); + var resultVariable = Expression.Variable(type, "result"); - internal static Func ResolveToStringMethod(Type type) - { - var toStringMethod = type.GetMethod("ToString", BindingFlags.Public | BindingFlags.Instance, null, - new[] { typeof(string), typeof(IFormatProvider) }, null) - ?? type.GetMethod("ToString", BindingFlags.Public | BindingFlags.Instance, null, - new[] { typeof(IFormatProvider) }, null) - ?? type.GetMethod("ToString", BindingFlags.Public | BindingFlags.Instance, null, - new Type[] { }, null) - ?? throw new DotvvmConfigurationException($"The type {type} is marked with {nameof(CustomPrimitiveTypeAttribute)} but it does not contain a public method ToString(string, IFormatProvider), ToString(IFormatProvider), or ToString()!"); - var parameterCount = toStringMethod.GetParameters().Length; - return arg => { - var args = parameterCount switch { - 2 => new object?[] { null, CultureInfo.InvariantCulture }, - 1 => new object?[] { CultureInfo.InvariantCulture }, - _ => new object?[] { } - }; - return (string)toStringMethod.Invoke(arg, args); - }; + var arguments = new Expression?[] + { + inputParameter, + tryParseMethod.GetParameters().Length == 3 + ? Expression.Constant(CultureInfo.InvariantCulture) + : null, + resultVariable + } + .Where(a => a != null) + .ToArray(); + var call = Expression.Call(tryParseMethod, arguments); + + var body = Expression.Block( + new[] { resultVariable }, + Expression.Condition( + Expression.IsTrue(call), + Expression.New(typeof(ParseResult).GetConstructor(new[] { typeof(object) })!, Expression.Convert(resultVariable, typeof(object))), + Expression.Constant(ParseResult.Failed) + ) + ); + return Expression.Lambda>(body, inputParameter).Compile(); } public record ParseResult(object? Result = null) diff --git a/src/Samples/Common/Controls/ControlWithCustomPrimitiveTypeProperties.cs b/src/Samples/Common/Controls/ControlWithCustomPrimitiveTypeProperties.cs index 0eaf59838b..3c60ca605d 100644 --- a/src/Samples/Common/Controls/ControlWithCustomPrimitiveTypeProperties.cs +++ b/src/Samples/Common/Controls/ControlWithCustomPrimitiveTypeProperties.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Text; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; @@ -23,9 +24,9 @@ public DotvvmControl GetContents( return new HtmlGenericControl("ul", htmlCapability) .AppendChildren( new HtmlGenericControl("li") - .AppendChildren(new Literal(pointValue.ToString())), + .AppendChildren(new Literal(pointValue.ToString(null, null))), new HtmlGenericControl("li") - .AppendChildren(new Literal(pointValue2.ToString())), + .AppendChildren(new Literal(pointValue2.ToString(null, null))), new HtmlGenericControl("li") .AppendChildren(new Literal(pointBinding)), new HtmlGenericControl("li") @@ -42,12 +43,12 @@ public DotvvmControl GetContents( } [CustomPrimitiveType] - public struct Point + public struct Point : IFormattable { public int X { get; set; } public int Y { get; set; } - public static bool TryParse(string value, out Point result) + public static bool TryParse(string value, IFormatProvider formatProvider, out Point result) { var parts = value.Split(','); if (int.TryParse(parts[0], out var x) && int.TryParse(parts[1], out var y)) @@ -62,8 +63,11 @@ public static bool TryParse(string value, out Point result) } } - public static Point Parse(string value) => TryParse(value, out var result) ? result : throw new FormatException(); + public static Point Parse(string value) => TryParse(value, CultureInfo.InvariantCulture, out var result) ? result : throw new FormatException(); + // note: both implementations of ToString must return the same result - they should not depend on the current culture public override string ToString() => $"{X},{Y}"; + + public string ToString(string format, IFormatProvider formatProvider) => $"{X},{Y}"; } } From 391274070f2e979d29c9782a820c287a7de0a8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 30 Jul 2023 16:59:26 +0200 Subject: [PATCH 30/32] Fixed build warnings --- .../Javascript/CustomPrimitiveTypesConversionTranslator.cs | 2 +- .../Framework/Configuration/CustomPrimitiveTypeRegistration.cs | 3 ++- src/Framework/Framework/Utils/ReflectionUtils.cs | 2 +- .../Serialization/CustomPrimitiveTypeJsonConverter.cs | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Framework/Framework/Compilation/Javascript/CustomPrimitiveTypesConversionTranslator.cs b/src/Framework/Framework/Compilation/Javascript/CustomPrimitiveTypesConversionTranslator.cs index 3122cc384f..d5052c1446 100644 --- a/src/Framework/Framework/Compilation/Javascript/CustomPrimitiveTypesConversionTranslator.cs +++ b/src/Framework/Framework/Compilation/Javascript/CustomPrimitiveTypesConversionTranslator.cs @@ -8,7 +8,7 @@ public class CustomPrimitiveTypesConversionTranslator : IJavascriptMethodTransla { public JsExpression? TryTranslateCall(LazyTranslatedExpression? context, LazyTranslatedExpression[] arguments, MethodInfo method) { - var type = context?.OriginalExpression.Type ?? method.DeclaringType; + var type = context?.OriginalExpression.Type ?? method.DeclaringType!; type = type.UnwrapNullableType(); if (method.Name is "ToString" or "Parse" && ReflectionUtils.IsCustomPrimitiveType(type)) { diff --git a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs index 8a9f55c06f..3ba35f50e1 100644 --- a/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs +++ b/src/Framework/Framework/Configuration/CustomPrimitiveTypeRegistration.cs @@ -29,7 +29,7 @@ internal CustomPrimitiveTypeRegistration(Type type) TryParseMethod = ResolveTryParseMethod(type); ToStringMethod = typeof(IFormattable).IsAssignableFrom(type) ? obj => ((IFormattable)obj).ToString(null, CultureInfo.InvariantCulture) - : obj => obj.ToString(); + : obj => obj.ToString()!; } internal static Func ResolveTryParseMethod(Type type) @@ -52,6 +52,7 @@ internal static Func ResolveTryParseMethod(Type type) resultVariable } .Where(a => a != null) + .Cast() .ToArray(); var call = Expression.Call(tryParseMethod, arguments); diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index b7d321ba73..6819a9b96a 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -238,7 +238,7 @@ public static bool IsAssignableToGenericType(this Type givenType, Type genericTy // custom primitive types if (TryGetCustomPrimitiveTypeRegistration(type) is { } registration) { - var result = registration.TryParseMethod(Convert.ToString(value, CultureInfo.InvariantCulture)); + var result = registration.TryParseMethod(Convert.ToString(value, CultureInfo.InvariantCulture)!); return result.Successful ? result.Result : throw new TypeConvertException(value, type, new Exception("The TryParse method of a custom primitive type failed to parse the value.")); diff --git a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs index 4989f999c4..377bf56a5d 100644 --- a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs @@ -24,7 +24,7 @@ or JsonToken.Float or JsonToken.Date) { var registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(objectType)!; - var parseResult = registration.TryParseMethod(Convert.ToString(reader.Value, CultureInfo.InvariantCulture)); + var parseResult = registration.TryParseMethod(Convert.ToString(reader.Value, CultureInfo.InvariantCulture)!); if (!parseResult.Successful) { throw new JsonSerializationException($"The value '{reader.Value}' cannot be deserialized as {objectType} because its TryParse method wasn't able to parse the value!"); From b37dca9abe84171f468805b4df0c0512e47d282c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Tue, 1 Aug 2023 21:44:07 +0200 Subject: [PATCH 31/32] Switch custom primitive types to a marker interface This allows for efficient checks if a value is primitive without the need for cache hashtable containing all types we encounter --- .../ViewModel/CustomPrimitiveTypeAttribute.cs | 1 + .../Core/ViewModel/IDotvvmPrimitiveType.cs | 8 +++++++ .../CustomPrimitiveTypeRegistration.cs | 4 ++-- .../Framework/Controls/HtmlGenericControl.cs | 6 ++++- .../Framework/Utils/ReflectionUtils.cs | 22 +++++++++---------- .../Validation/ViewModelValidator.cs | 2 +- ...ontrolWithCustomPrimitiveTypeProperties.cs | 3 +-- .../CustomPrimitiveTypes/SampleId.cs | 3 +-- src/Tests/Binding/BindingCompilationTests.cs | 3 +-- 9 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 src/Framework/Core/ViewModel/IDotvvmPrimitiveType.cs diff --git a/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs b/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs index 906f4ee9ba..7c2a2cdb9f 100644 --- a/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs +++ b/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs @@ -3,6 +3,7 @@ namespace DotVVM.Framework.ViewModel; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +[Obsolete("Implement IDotvvmPrimitiveType.", error: true)] 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..e494803b84 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, TryDiscoverCustomPrimitiveType); + 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 TryDiscoverCustomPrimitiveType(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) From a9561eaa1694166a9cbe07d1e9ce80ad1623533e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 2 Aug 2023 18:54:32 +0200 Subject: [PATCH 32/32] Remove the CustomPrimitiveType attribute --- .../Core/ViewModel/CustomPrimitiveTypeAttribute.cs | 9 --------- src/Framework/Framework/Utils/ReflectionUtils.cs | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs diff --git a/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs b/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs deleted file mode 100644 index 7c2a2cdb9f..0000000000 --- a/src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace DotVVM.Framework.ViewModel; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] -[Obsolete("Implement IDotvvmPrimitiveType.", error: true)] -public class CustomPrimitiveTypeAttribute : Attribute -{ -} diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index e494803b84..55d3e33ef4 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -382,7 +382,7 @@ public static bool IsCustomPrimitiveType(Type type) public static CustomPrimitiveTypeRegistration? TryGetCustomPrimitiveTypeRegistration(Type type) { if (IsCustomPrimitiveType(type)) - return CustomPrimitiveTypes.GetOrAdd(type, TryDiscoverCustomPrimitiveType); + return CustomPrimitiveTypes.GetOrAdd(type, DiscoverCustomPrimitiveType); else return null; } @@ -396,7 +396,7 @@ public static bool IsPrimitiveType(Type type) || IsCustomPrimitiveType(type); } - private static CustomPrimitiveTypeRegistration TryDiscoverCustomPrimitiveType(Type type) + private static CustomPrimitiveTypeRegistration DiscoverCustomPrimitiveType(Type type) { Debug.Assert(typeof(IDotvvmPrimitiveType).IsAssignableFrom(type)); return new CustomPrimitiveTypeRegistration(type);