+ {
+ 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 Parse(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 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/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/Basic.dothtml b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml
new file mode 100644
index 0000000000..4f2f1123c4
--- /dev/null
+++ b/src/Samples/Common/Views/FeatureSamples/CustomPrimitiveTypes/Basic.dothtml
@@ -0,0 +1,63 @@
+@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes.BasicViewModel, DotVVM.Samples.Common
+
+
+
+
+
+
+ Custom primitive types
+
+
+
+ Selected item ID: {{value: SelectedItemId}}
+
+
+
+
+
+
+ Selected nullable item ID: {{value: SelectedItemNullableId}}
+
+
+
+
+
+
+ Route parameter: {{value: IdInRoute}}
+
+ Query parameter: {{value: IdInQuery}}
+
+
+
+
+
+
+
+
+
+
+ Static command result: {{value: StaticCommandResult}}
+
+
+
+ Binding with JS translation: {{value: $"My id values are {SelectedItemId.IdValue.ToString()} and {SelectedItemNullableId.IdValue.ToString().ToUpper()}"}}
+
+
+
+
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 43cd0c94b0..d1c5806bce 100644
--- a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs
+++ b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs
@@ -237,6 +237,10 @@ 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_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
new file mode 100644
index 0000000000..1d05584bc1
--- /dev/null
+++ b/src/Samples/Tests/Tests/Feature/CustomPrimitiveTypesTests.cs
@@ -0,0 +1,169 @@
+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");
+ });
+ }
+
+ [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");
+ });
+ }
+ }
+}
diff --git a/src/Tests/Binding/BindingCompilationTests.cs b/src/Tests/Binding/BindingCompilationTests.cs
index d607114948..48c3bc9e0d 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
{
@@ -1199,6 +1200,8 @@ class TestViewModel
public uint UIntProp { get; set; } = 3_000_000_000;
public double? NullableDoubleProp { get; set; }
+ public VehicleNumber? VehicleNumber { get; set; }
+
public ReadOnlyCollection ReadOnlyCollection => new ReadOnlyCollection(new[] { 1, 2, 3 });
public string SetStringProp(string a, int b)
@@ -1258,6 +1261,29 @@ public async Task GetStringPropAsync()
public int MethodWithOverloads(int a, int b) => a + b;
}
+
+ 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)
+ {
+ 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 193b4103d3..81de562a92 100644
--- a/src/Tests/Binding/JavascriptCompilationTests.cs
+++ b/src/Tests/Binding/JavascriptCompilationTests.cs
@@ -1277,6 +1277,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
@@ -1321,4 +1335,5 @@ public enum TestEnum
public TestEnum Enum { get; set; }
public string String { get; set; }
}
+
}
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