diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 15bf44ba88..d1ec15d3ef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -131,6 +131,15 @@ jobs: # title: Analyzer Tests # github-token: ${{ secrets.GITHUB_TOKEN }} # target-framework: net7.0 + - name: Adapters.WebForms.Tests (net472) + uses: ./.github/unittest + if: matrix.os == 'windows-2022' + with: + project: src/Adapters/Tests/WebForms + name: webforms-adapters-tests + title: WebForms Adapter Tests + github-token: ${{ secrets.GITHUB_TOKEN }} + target-framework: net472 js-tests: runs-on: ubuntu-latest diff --git a/ci/scripts/Get-PublicProjects.ps1 b/ci/scripts/Get-PublicProjects.ps1 index 9669670080..f1adf33d67 100644 --- a/ci/scripts/Get-PublicProjects.ps1 +++ b/ci/scripts/Get-PublicProjects.ps1 @@ -108,5 +108,10 @@ return @( Name = "DotVVM.Tracing.MiniProfiler.Owin"; Path = "src/Tracing/MiniProfiler.Owin"; Type = "standard" + }, + [PSCustomObject]@{ + Name = "DotVVM.Adapters.WebForms"; + Path = "src/Adapters/WebForms"; + Type = "standard" } ) diff --git a/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj b/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj new file mode 100644 index 0000000000..08d3a2e33f --- /dev/null +++ b/src/Adapters/Tests/WebForms/DotVVM.Adapters.WebForms.Tests.csproj @@ -0,0 +1,28 @@ + + + + net472 + false + + + false + + + + + + + + + + + + + + + + + + + + diff --git a/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs b/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs new file mode 100644 index 0000000000..fe3cc1616d --- /dev/null +++ b/src/Adapters/Tests/WebForms/HybridRouteLinkTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Web; +using CheckTestOutput; +using DotVVM.Framework.Configuration; +using DotVVM.Framework.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotVVM.Adapters.WebForms.Tests +{ + [TestClass] + public class HybridRouteLinkTests + { + private static readonly ControlTestHelper cth = new ControlTestHelper(config: config => config.AddWebFormsAdapters()); + OutputChecker check = new OutputChecker("testoutputs"); + + [ClassInitialize] + public static void Init(TestContext testContext) + { + WebFormsRouteTableInit.EnsureInitialized(); + } + + [TestMethod] + public async Task HybridRouteLink_NoBindings() + { + HttpContext.Current = new HttpContext( + new HttpRequest("", "http://tempuri.org", ""), + new HttpResponse(new StringWriter()) + ); + + var r = await cth.RunPage(typeof(ControlTestViewModel), @" + + + + + + + "); + + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } + + [TestMethod] + public async Task HybridRouteLink_ValueBinding() + { + HttpContext.Current = new HttpContext( + new HttpRequest("", "http://tempuri.org", ""), + new HttpResponse(new StringWriter()) + ); + + var r = await cth.RunPage(typeof(ControlTestViewModel), @" + + + + "); + + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } + + [TestMethod] + public async Task HybridRouteLink_SuffixAndQueryString() + { + HttpContext.Current = new HttpContext( + new HttpRequest("", "http://tempuri.org", ""), + new HttpResponse(new StringWriter()) + ); + + var r = await cth.RunPage(typeof(ControlTestViewModel), @" + + + + + "); + + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } + } + + class ControlTestViewModel + { + public int Value { get; set; } = 15; + + public List Items { get; set; } = new() + { + new ControlTestChildViewModel() { Id = 1, Name = "one" }, + new ControlTestChildViewModel() { Id = 2, Name = "two" }, + new ControlTestChildViewModel() { Id = 3, Name = "three" } + }; + } + + class ControlTestChildViewModel + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/src/Adapters/Tests/WebForms/WebFormsRouteTableInit.cs b/src/Adapters/Tests/WebForms/WebFormsRouteTableInit.cs new file mode 100644 index 0000000000..c56508ce8b --- /dev/null +++ b/src/Adapters/Tests/WebForms/WebFormsRouteTableInit.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Web.Routing; + +namespace DotVVM.Adapters.WebForms.Tests +{ + public static class WebFormsRouteTableInit + { + + static WebFormsRouteTableInit() + { + RouteTable.Routes.Add("NoParams", new Route("", new EmptyHandler())); + RouteTable.Routes.Add("SingleParam", new Route("page/{Index}", new EmptyHandler())); + RouteTable.Routes.Add("MultipleOptionalParams", new Route("catalog/{Tag}/{SubTag}", new EmptyHandler()) { Defaults = new RouteValueDictionary(new { Tag = "xx", SubTag = "yy" })}); + } + + public static void EnsureInitialized() + { + } + + } + + public class EmptyHandler : IRouteHandler + { + public IHttpHandler GetHttpHandler(RequestContext requestContext) => throw new NotImplementedException(); + } +} diff --git a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_NoBindings.html b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_NoBindings.html new file mode 100644 index 0000000000..8c77847baa --- /dev/null +++ b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_NoBindings.html @@ -0,0 +1,12 @@ + + + + hello 1 + hello 2 + hello 3 + hello 4 + hello 5 + hello 6 + hello 6 + + diff --git a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html new file mode 100644 index 0000000000..7d179a3a4a --- /dev/null +++ b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_SuffixAndQueryString.html @@ -0,0 +1,10 @@ + + + + hello 1 + hello 2 + hello 3 + hello 4 + hello 5 + + diff --git a/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_ValueBinding.html b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_ValueBinding.html new file mode 100644 index 0000000000..cc45494e0d --- /dev/null +++ b/src/Adapters/Tests/WebForms/testoutputs/HybridRouteLinkTests.HybridRouteLink_ValueBinding.html @@ -0,0 +1,10 @@ + + + + hello 3 +
+ + + + + diff --git a/src/Adapters/WebForms/Controls/HybridRouteLink.cs b/src/Adapters/WebForms/Controls/HybridRouteLink.cs new file mode 100644 index 0000000000..0606f75011 --- /dev/null +++ b/src/Adapters/WebForms/Controls/HybridRouteLink.cs @@ -0,0 +1,57 @@ +using System; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Hosting; + +#if NETFRAMEWORK +using System.Web.Routing; +#endif + +namespace DotVVM.Adapters.WebForms.Controls +{ + /// + /// Renders a hyperlink pointing to the specified DotVVM route if such route exists; otherwise it falls back to a Web Forms route with the specified name. + /// +#if !NETFRAMEWORK + [Obsolete("This control is used only during the Web Forms migration and is not needed in .NET Core. Use the standard RouteLink control.")] +#endif + public class HybridRouteLink : CompositeControl + { + private readonly IDotvvmRequestContext context; + + public HybridRouteLink(IDotvvmRequestContext context) + { + this.context = context; + } + + public DotvvmControl GetContents( + HtmlCapability htmlCapability, + TextOrContentCapability textOrContent, + RouteLinkCapability routeLinkCapability + ) + { + if (context.Configuration.RouteTable.Contains(routeLinkCapability.RouteName)) + { + return GenerateDotvvmRouteLink(htmlCapability, textOrContent, routeLinkCapability); + } +#if NETFRAMEWORK + else if (RouteTable.Routes[routeLinkCapability.RouteName] is Route webFormsRoute) + { + return WebFormsLinkUtils.BuildWebFormsRouteLink(this, context, htmlCapability, textOrContent, routeLinkCapability, webFormsRoute); + } +#endif + else + { + throw new DotvvmControlException($"Route '{routeLinkCapability.RouteName}' does not exist."); + } + } + + private static DotvvmControl GenerateDotvvmRouteLink(HtmlCapability htmlCapability, TextOrContentCapability textOrContent, RouteLinkCapability routeLinkCapability) + { + return new RouteLink() + .SetCapability(htmlCapability) + .SetCapability(textOrContent) + .SetCapability(routeLinkCapability); + } + + } +} diff --git a/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs b/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs new file mode 100644 index 0000000000..ba730df1d9 --- /dev/null +++ b/src/Adapters/WebForms/Controls/WebFormsLinkUtils.cs @@ -0,0 +1,132 @@ +#if NETFRAMEWORK +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Routing; +using DotVVM.Framework.Utils; +using System.Web.Routing; +using System.Web; +using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Hosting; + +namespace DotVVM.Adapters.WebForms.Controls +{ + public class WebFormsLinkUtils + { + public static HtmlGenericControl BuildWebFormsRouteLink(DotvvmControl container, IDotvvmRequestContext context, HtmlCapability htmlCapability, TextOrContentCapability textOrContent, RouteLinkCapability routeLinkCapability, Route webFormsRoute) + { + var link = new HtmlGenericControl("a", textOrContent, htmlCapability); + + var parameters = BuildParameters(context, routeLinkCapability, webFormsRoute); + if (routeLinkCapability.UrlSuffix is { HasBinding: true, BindingOrDefault: IValueBinding } + || routeLinkCapability.Params.Any(p => p.Value is { HasBinding: true, BindingOrDefault: IValueBinding })) + { + // bindings are used, we have to generate client-script code + var fragments = new List { KnockoutHelper.MakeStringLiteral(context.TranslateVirtualPath("~/")) }; + + // generate binding and embed it in the function call + var routeUrlExpression = GenerateRouteUrlExpression(container, webFormsRoute, parameters); + fragments.Add(routeUrlExpression); + + // generate URL suffix + if (GenerateUrlSuffixExpression(container, routeLinkCapability) is string urlSuffix) + { + fragments.Add(urlSuffix); + } + + // render the binding and try to evaluate it on the server + link.AddAttribute("data-bind", "attr: { 'href': " + fragments.StringJoin("+") + "}"); + if (container.DataContext != null) + { + try + { + var url = context.TranslateVirtualPath(EvaluateRouteUrl(container, webFormsRoute, parameters, routeLinkCapability)); + link.SetAttribute("href", url); + } + catch (Exception ex) + { + } + } + } + else + { + // the value can be built on the server + var url = context.TranslateVirtualPath(EvaluateRouteUrl(container, webFormsRoute, parameters, routeLinkCapability)); + link.SetAttribute("href", url); + } + + return link; + } + + private static IDictionary> BuildParameters(IDotvvmRequestContext context, RouteLinkCapability routeLinkCapability, Route webFormsRoute) + { + var parameters = webFormsRoute.Defaults?.ToDictionary(t => t.Key, t => ValueOrBinding.FromBoxedValue(t.Value)) + ?? new Dictionary>(); + foreach (var param in context.Parameters) + { + parameters[param.Key] = ValueOrBinding.FromBoxedValue(param.Value); + } + + foreach (var item in routeLinkCapability.Params) + { + parameters[item.Key] = item.Value; + } + + return parameters; + } + + private static string EvaluateRouteUrl(DotvvmControl container, Route webFormsRoute, IDictionary> parameters, RouteLinkCapability routeLinkCapability) + { + // evaluate bindings on server + var routeValues = new RouteValueDictionary(); + foreach (Match param in Regex.Matches(webFormsRoute.Url, @"\{([^{}/]+)\}")) // https://referencesource.microsoft.com/#System.Web/Routing/RouteParser.cs,48 + { + var paramName = param.Groups[1].Value; + parameters.TryGetValue(paramName, out var value); + routeValues[paramName] = value.Evaluate(container) ?? ""; + } + + // generate the URL + return "~/" + + webFormsRoute.GetVirtualPath(HttpContext.Current.Request.RequestContext, routeValues)?.VirtualPath + + UrlHelper.BuildUrlSuffix(routeLinkCapability.UrlSuffix?.Evaluate(container), routeLinkCapability.QueryParameters.OrderBy(p => p.Key).Select(p => new KeyValuePair(p.Key, p.Value.Evaluate(container)))); + } + + private static string GenerateRouteUrlExpression(DotvvmControl container, Route webFormsRoute, IDictionary> parameters) + { + var parametersExpression = parameters + .OrderBy(p => p.Key) + .Select(p => $"{KnockoutHelper.MakeStringLiteral(p.Key)}: {p.Value.GetJsExpression(container)}") + .StringJoin(","); + var routeUrlExpression = $"dotvvm.buildRouteUrl({KnockoutHelper.MakeStringLiteral(webFormsRoute.Url)}, {{{parametersExpression}}})"; + return routeUrlExpression; + } + + private static string GenerateUrlSuffixExpression(DotvvmControl container, RouteLinkCapability routeLinkCapability) + { + var urlSuffixBase = routeLinkCapability.UrlSuffix?.GetJsExpression(container) ?? "\"\""; + var queryParams = routeLinkCapability.QueryParameters + .OrderBy(p => p.Key) + .Select(p => $"{KnockoutHelper.MakeStringLiteral(p.Key)}: {p.Value.GetJsExpression(container)}") + .StringJoin(","); + + // generate the function call + if (queryParams.Any()) + { + return $"dotvvm.buildUrlSuffix({urlSuffixBase}, {{{queryParams}}})"; + } + else if (urlSuffixBase != "\"\"") + { + return urlSuffixBase; + } + else + { + return null; + } + } + } +} +#endif diff --git a/src/Adapters/WebForms/DotVVM.Adapters.WebForms.csproj b/src/Adapters/WebForms/DotVVM.Adapters.WebForms.csproj new file mode 100644 index 0000000000..096c07a02b --- /dev/null +++ b/src/Adapters/WebForms/DotVVM.Adapters.WebForms.csproj @@ -0,0 +1,27 @@ + + + + $(DefaultTargetFrameworks) + DotVVM.Adapters.WebForms + + This package contains helpers for migration of ASP.NET Web Forms application to DotVVM. + $(Description) + + + + + + + True + dotvvmwizard.snk + + + + + + + + + + + diff --git a/src/Adapters/WebForms/DotvvmConfigurationExtensions.cs b/src/Adapters/WebForms/DotvvmConfigurationExtensions.cs new file mode 100644 index 0000000000..a5e6cbd2bc --- /dev/null +++ b/src/Adapters/WebForms/DotvvmConfigurationExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Adapters.WebForms.Controls; + +// ReSharper disable once CheckNamespace +namespace DotVVM.Framework.Configuration +{ + public static class DotvvmConfigurationExtensions + { + + public static void AddWebFormsAdapters(this DotvvmConfiguration config) + { + config.Markup.AddCodeControls("webforms", typeof(HybridRouteLink)); + config.Markup.Assemblies.Add(typeof(DotvvmConfigurationExtensions).Assembly.FullName); + } + } +} diff --git a/src/Adapters/WebForms/WebFormsAdaptersExtensions.cs b/src/Adapters/WebForms/WebFormsAdaptersExtensions.cs new file mode 100644 index 0000000000..30fd66e68a --- /dev/null +++ b/src/Adapters/WebForms/WebFormsAdaptersExtensions.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Web; +using DotVVM.Framework.Routing; + +#if NETFRAMEWORK +using System.Web.Routing; +#endif + +// ReSharper disable once CheckNamespace +namespace DotVVM.Framework.Hosting +{ + public static class WebFormsAdaptersExtensions + { + + /// + /// Redirects to the specified DotVVM route if such route exists; otherwise it redirects to the specified Web Forms route. + /// +#if !NETFRAMEWORK + [Obsolete("This method is used only during the Web Forms migration and is not needed in .NET Core. Use the standard RedirectToRoute method.")] +#endif + public static void RedirectToRouteHybrid(this IDotvvmRequestContext context, string routeName, object routeValues = null, string urlSuffix = null, object query = null) + { + if (context.Configuration.RouteTable.Contains(routeName)) + { + // we have DotVVM route - use it + var url = context.Configuration.RouteTable[routeName].BuildUrl(routeValues); + url += UrlHelper.BuildUrlSuffix(urlSuffix, query); + context.RedirectToUrl(url); + } +#if NETFRAMEWORK + else if (RouteTable.Routes[routeName] is Route webFormsRoute) + { + // fall back to the Web Forms route + var url = webFormsRoute.GetVirtualPath(HttpContext.Current.Request.RequestContext, new RouteValueDictionary(routeValues))!.VirtualPath; + url += UrlHelper.BuildUrlSuffix(urlSuffix, query); + context.RedirectToUrl(url); + } +#endif + else + { + throw new ArgumentException($"The route {routeName} doesn't exist!"); + } + } + } +} diff --git a/src/Adapters/WebForms/dotvvmwizard.snk b/src/Adapters/WebForms/dotvvmwizard.snk new file mode 100644 index 0000000000..34430b3c78 Binary files /dev/null and b/src/Adapters/WebForms/dotvvmwizard.snk differ diff --git a/src/DotVVM.sln b/src/DotVVM.sln index 3542ef1634..0c677c9d39 100644 --- a/src/DotVVM.sln +++ b/src/DotVVM.sln @@ -123,6 +123,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Framework.Controls.D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Framework.Controls.DynamicData", "DynamicData\DynamicData\DotVVM.Framework.Controls.DynamicData.csproj", "{9E19A537-E1B2-4D1E-A904-D99D4222474F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Adapters", "Adapters", "{11C116EC-5E5A-400A-9311-0732DD69401C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebForms", "WebForms", "{42513853-3772-46D2-94C2-965101E2406D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{05A3401A-C541-4F7C-AAD8-02A23648CD27}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Adapters.WebForms.Tests", "Adapters\Tests\WebForms\DotVVM.Adapters.WebForms.Tests.csproj", "{A6A8451E-99D8-4296-BBA9-69E1E289270A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Adapters.WebForms", "Adapters\WebForms\DotVVM.Adapters.WebForms.csproj", "{25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -697,6 +707,30 @@ Global {9E19A537-E1B2-4D1E-A904-D99D4222474F}.Release|x64.Build.0 = Release|Any CPU {9E19A537-E1B2-4D1E-A904-D99D4222474F}.Release|x86.ActiveCfg = Release|Any CPU {9E19A537-E1B2-4D1E-A904-D99D4222474F}.Release|x86.Build.0 = Release|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Debug|x64.Build.0 = Debug|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Debug|x86.Build.0 = Debug|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Release|Any CPU.Build.0 = Release|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Release|x64.ActiveCfg = Release|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Release|x64.Build.0 = Release|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Release|x86.ActiveCfg = Release|Any CPU + {A6A8451E-99D8-4296-BBA9-69E1E289270A}.Release|x86.Build.0 = Release|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Debug|x64.ActiveCfg = Debug|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Debug|x64.Build.0 = Debug|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Debug|x86.ActiveCfg = Debug|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Debug|x86.Build.0 = Debug|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|Any CPU.Build.0 = Release|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|x64.ActiveCfg = Release|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|x64.Build.0 = Release|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|x86.ActiveCfg = Release|Any CPU + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -753,6 +787,10 @@ Global {DB0AB0C3-DA5E-4B5A-9CD4-036D37B50AED} = {E57EE0B8-30FC-4702-B310-FB82C19D7473} {3209E1B1-88BB-4A95-B234-950E89EFCEE0} = {CF90322D-63BC-4047-BFEA-EE87E45020AF} {9E19A537-E1B2-4D1E-A904-D99D4222474F} = {CF90322D-63BC-4047-BFEA-EE87E45020AF} + {42513853-3772-46D2-94C2-965101E2406D} = {11C116EC-5E5A-400A-9311-0732DD69401C} + {05A3401A-C541-4F7C-AAD8-02A23648CD27} = {42513853-3772-46D2-94C2-965101E2406D} + {A6A8451E-99D8-4296-BBA9-69E1E289270A} = {05A3401A-C541-4F7C-AAD8-02A23648CD27} + {25442AA8-7E4D-47EC-8CCB-F9E2B45EB998} = {42513853-3772-46D2-94C2-965101E2406D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {61F8A195-365E-47B1-A6F2-CD3534E918F8} diff --git a/src/Framework/Framework/Controls/RouteLink.cs b/src/Framework/Framework/Controls/RouteLink.cs index 355d0233b4..d5f5b4c804 100644 --- a/src/Framework/Framework/Controls/RouteLink.cs +++ b/src/Framework/Framework/Controls/RouteLink.cs @@ -64,11 +64,17 @@ public string Text public static readonly DotvvmProperty TextProperty = DotvvmProperty.Register(c => c.Text, ""); + /// + /// Gets or sets a collection of parameters to be substituted in the route URL. If the current route contains a parameter with the same name, its value will be reused unless another value is specified here. + /// [PropertyGroup("Param-")] public VirtualPropertyGroupDictionary Params => new VirtualPropertyGroupDictionary(this, ParamsGroupDescriptor); public static DotvvmPropertyGroup ParamsGroupDescriptor = DotvvmPropertyGroup.Register("Param-", "Params"); + /// + /// Gets or sets a collection of parameters to be added in the query string. + /// [PropertyGroup("Query-")] public VirtualPropertyGroupDictionary QueryParameters => new VirtualPropertyGroupDictionary(this, QueryParametersGroupDescriptor); public static DotvvmPropertyGroup QueryParametersGroupDescriptor = @@ -87,6 +93,13 @@ public TextOrContentCapability TextOrContentCapability } ); + public RouteLinkCapability RouteLinkCapability + { + get => (RouteLinkCapability)RouteLinkCapabilityProperty.GetValue(this); + set => RouteLinkCapabilityProperty.SetValue(this, value); + } + public static readonly DotvvmCapabilityProperty RouteLinkCapabilityProperty = DotvvmCapabilityProperty.RegisterCapability(); + public RouteLink() : base("a", false) { } diff --git a/src/Framework/Framework/Controls/RouteLinkCapability.cs b/src/Framework/Framework/Controls/RouteLinkCapability.cs new file mode 100644 index 0000000000..e80e18bca8 --- /dev/null +++ b/src/Framework/Framework/Controls/RouteLinkCapability.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.ComponentModel; +using DotVVM.Framework.Binding; + +namespace DotVVM.Framework.Controls +{ + [DotvvmControlCapability()] + public sealed record RouteLinkCapability + { + [PropertyGroup("Query-")] + [DefaultValue(null)] + public IReadOnlyDictionary> QueryParameters { get; init; } = new Dictionary>(); + + [PropertyGroup("Param-")] + [DefaultValue(null)] + public IReadOnlyDictionary> Params { get; init; } = new Dictionary>(); + + public string RouteName { get; init; } = null!; + + [DefaultValue(null)] + public ValueOrBinding? UrlSuffix { get; init; } + } +} diff --git a/src/Framework/Testing/DotVVM.Framework.Testing.csproj b/src/Framework/Testing/DotVVM.Framework.Testing.csproj index c1527c56ec..bef84dc8bb 100644 --- a/src/Framework/Testing/DotVVM.Framework.Testing.csproj +++ b/src/Framework/Testing/DotVVM.Framework.Testing.csproj @@ -14,7 +14,7 @@ True - True + true dotvvmwizard.snk diff --git a/src/Tests/DotVVM.Framework.Tests.csproj b/src/Tests/DotVVM.Framework.Tests.csproj index e9c7067ff5..fa3ee599c0 100644 --- a/src/Tests/DotVVM.Framework.Tests.csproj +++ b/src/Tests/DotVVM.Framework.Tests.csproj @@ -17,7 +17,8 @@ net6.0 - True + true + true dotvvmwizard.snk diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index 803a7b58c2..825ca33160 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -1720,6 +1720,9 @@ } }, "DotVVM.Framework.Controls.RouteLink": { + "RouteLinkCapability": { + "type": "DotVVM.Framework.Controls.RouteLinkCapability, DotVVM.Framework" + }, "TextOrContentCapability": { "type": "DotVVM.Framework.Controls.TextOrContentCapability, DotVVM.Framework" }