From 71ee1ab035a20f4b5f0b9d3aab7211faf32b5162 Mon Sep 17 00:00:00 2001 From: Diego2098 <82756760+kimdiego2098@users.noreply.github.com> Date: Sun, 13 Oct 2024 20:49:43 +0800 Subject: [PATCH] perf(Tab): improve create tab item from url performance (#4445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(tab): 创建TabItem时不需要渲染TabItem组件 * 添加复现demo * 更新复现demo * refactor: 增加不渲染设置 * refactor: 重构代码 * refactor: 更新私有变量 * chore: 增加拼写字典配置 * refactor: 精简代码 * refactor: 更改参数生效生命周期 * refactor: 增加部分 Tab 参数 * doc: 更新文档 * doc: 更新示例 * refactor: 增加部分 Tab 参数 * test: 增加单元测试 * refactor: 精简代码 * doc: 更新文档 * chore: 移除测试工程 * revert: 撤销代码更改 * chore: bump version 8.10.3-beta01 * test: 更新单元测试 * test: 更新单元测试 * test: 更新单元测试 --------- Co-authored-by: Argo-AsicoTech --- exclusion.dic | 1 + src/BootstrapBlazor/BootstrapBlazor.csproj | 2 +- .../DateTimePicker/TimePickerSetting.cs | 4 +- .../Tab/BootstrapBlazorAuthorizeView.cs | 12 +- .../Components/Tab/Route/IRouteTable.cs | 16 -- .../Components/Tab/Route/RouteConstraint.cs | 81 ------- .../Components/Tab/Route/RouteContext.cs | 2 - .../Components/Tab/Route/RouteEntry.cs | 141 +---------- .../Components/Tab/Route/RouteKey.cs | 2 - .../Components/Tab/Route/RouteTable.cs | 4 - .../Components/Tab/Route/RouteTableFactory.cs | 229 ------------------ .../Components/Tab/Route/RouteTemplate.cs | 6 - .../Tab/Route/StringSegmentAccumulator.cs | 2 - .../Components/Tab/Route/TemplateSegment.cs | 141 ----------- .../Tab/Route/TypeRouteConstraint.cs | 51 ---- .../Tab/Route/UrlValueConstraint.cs | 2 - .../Components/Tab/Tab.razor.cs | 10 +- src/BootstrapBlazor/Components/Tab/TabItem.cs | 3 +- test/UnitTest/Components/TabTest.cs | 27 ++- 19 files changed, 35 insertions(+), 701 deletions(-) delete mode 100644 src/BootstrapBlazor/Components/Tab/Route/IRouteTable.cs delete mode 100644 src/BootstrapBlazor/Components/Tab/Route/TypeRouteConstraint.cs diff --git a/exclusion.dic b/exclusion.dic index 64f190d35..f0da15ba6 100644 --- a/exclusion.dic +++ b/exclusion.dic @@ -104,3 +104,4 @@ scrlang Validata Validatable noselect +Urls diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index a6e885610..d2e064a13 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@ - 8.10.2 + 8.10.3-beta01 diff --git a/src/BootstrapBlazor/Components/DateTimePicker/TimePickerSetting.cs b/src/BootstrapBlazor/Components/DateTimePicker/TimePickerSetting.cs index e95e2ae5f..03f3ed90f 100644 --- a/src/BootstrapBlazor/Components/DateTimePicker/TimePickerSetting.cs +++ b/src/BootstrapBlazor/Components/DateTimePicker/TimePickerSetting.cs @@ -39,9 +39,9 @@ public class TimePickerSetting : ComponentBase /// /// /// - protected override void OnInitialized() + protected override void OnParametersSet() { - base.OnInitialized(); + base.OnParametersSet(); if (Option != null) { diff --git a/src/BootstrapBlazor/Components/Tab/BootstrapBlazorAuthorizeView.cs b/src/BootstrapBlazor/Components/Tab/BootstrapBlazorAuthorizeView.cs index fbf60ae68..01b83611c 100644 --- a/src/BootstrapBlazor/Components/Tab/BootstrapBlazorAuthorizeView.cs +++ b/src/BootstrapBlazor/Components/Tab/BootstrapBlazorAuthorizeView.cs @@ -11,7 +11,7 @@ using System.Collections.ObjectModel; namespace BootstrapBlazor.Components; /// -/// +/// BootstrapBlazorAuthorizeView 组件 /// public class BootstrapBlazorAuthorizeView : ComponentBase { @@ -49,16 +49,14 @@ public class BootstrapBlazorAuthorizeView : ComponentBase [Inject] private IAuthorizationService? AuthorizationService { get; set; } -#if NET6_0_OR_GREATER [Inject] [NotNull] private NavigationManager? NavigationManager { get; set; } -#endif private bool Authorized { get; set; } /// - /// OnInitializedAsync 方法 + /// /// /// protected override async Task OnInitializedAsync() @@ -68,7 +66,7 @@ public class BootstrapBlazorAuthorizeView : ComponentBase } /// - /// BuildRenderTree 方法 + /// /// /// protected override void BuildRenderTree(RenderTreeBuilder builder) @@ -82,9 +80,7 @@ public class BootstrapBlazorAuthorizeView : ComponentBase { builder.AddAttribute(index++, kv.Key, kv.Value); } -#if NET6_0_OR_GREATER BuildQueryParameters(); -#endif builder.CloseComponent(); } else @@ -92,7 +88,6 @@ public class BootstrapBlazorAuthorizeView : ComponentBase builder.AddContent(0, NotAuthorized); } -#if NET6_0_OR_GREATER void BuildQueryParameters() { var queryParameterSupplier = QueryParameterValueSupplier.ForType(Type); @@ -106,6 +101,5 @@ public class BootstrapBlazorAuthorizeView : ComponentBase queryParameterSupplier.RenderParametersFromQueryString(builder, query); } } -#endif } } diff --git a/src/BootstrapBlazor/Components/Tab/Route/IRouteTable.cs b/src/BootstrapBlazor/Components/Tab/Route/IRouteTable.cs deleted file mode 100644 index bad599078..000000000 --- a/src/BootstrapBlazor/Components/Tab/Route/IRouteTable.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// Website: https://www.blazor.zone or https://argozhang.github.io/ - -namespace Microsoft.AspNetCore.Components.Routing; - -#if NET5_0 -/// -/// Provides an abstraction over . -/// the legacy route matching logic is removed. -/// -internal interface IRouteTable -{ - void Route(RouteContext routeContext); -} -#endif diff --git a/src/BootstrapBlazor/Components/Tab/Route/RouteConstraint.cs b/src/BootstrapBlazor/Components/Tab/Route/RouteConstraint.cs index 3b3067e24..7139243ab 100644 --- a/src/BootstrapBlazor/Components/Tab/Route/RouteConstraint.cs +++ b/src/BootstrapBlazor/Components/Tab/Route/RouteConstraint.cs @@ -5,86 +5,6 @@ namespace Microsoft.AspNetCore.Components.Routing; [ExcludeFromCodeCoverage] -#if NET5_0 -internal abstract class RouteConstraint -{ - // note: the things that prevent this cache from growing unbounded is that - // we're the only caller to this code path, and the fact that there are only - // 8 possible instances that we create. - // - // The values passed in here for parsing are always static text defined in route attributes. - private static readonly ConcurrentDictionary _cachedConstraints - = new ConcurrentDictionary(); - - public abstract bool Match(string pathSegment, out object? convertedValue); - - public static RouteConstraint Parse(string template, string segment, string constraint) - { - if (string.IsNullOrEmpty(constraint)) - { - throw new ArgumentException($"Malformed segment '{segment}' in route '{template}' contains an empty constraint."); - } - - if (_cachedConstraints.TryGetValue(constraint, out var cachedInstance)) - { - return cachedInstance; - } - else - { - var newInstance = CreateRouteConstraint(constraint); - if (newInstance != null) - { - // We've done to the work to create the constraint now, but it's possible - // we're competing with another thread. GetOrAdd can ensure only a single - // instance is returned so that any extra ones can be GC'ed. - return _cachedConstraints.GetOrAdd(constraint, newInstance); - } - else - { - throw new ArgumentException($"Unsupported constraint '{constraint}' in route '{template}'."); - } - } - } - - /// - /// Creates a structured RouteConstraint object given a string that contains - /// the route constraint. A constraint is the place after the colon in a - /// parameter definition, for example `{age:int?}`. - /// - /// String representation of the constraint - /// Type-specific RouteConstraint object - private static RouteConstraint? CreateRouteConstraint(string constraint) - { - switch (constraint) - { - case "bool": - return new TypeRouteConstraint(bool.TryParse); - case "datetime": - return new TypeRouteConstraint((string str, out DateTime result) - => DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)); - case "decimal": - return new TypeRouteConstraint((string str, out decimal result) - => decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result)); - case "double": - return new TypeRouteConstraint((string str, out double result) - => double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result)); - case "float": - return new TypeRouteConstraint((string str, out float result) - => float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result)); - case "guid": - return new TypeRouteConstraint(Guid.TryParse); - case "int": - return new TypeRouteConstraint((string str, out int result) - => int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result)); - case "long": - return new TypeRouteConstraint((string str, out long result) - => long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result)); - default: - return null; - } - } -} -#else internal static class RouteConstraint { public static UrlValueConstraint Parse(string template, string segment, string constraint) @@ -116,4 +36,3 @@ internal static class RouteConstraint _ => null, }; } -#endif diff --git a/src/BootstrapBlazor/Components/Tab/Route/RouteContext.cs b/src/BootstrapBlazor/Components/Tab/Route/RouteContext.cs index 41e77c24f..685b87a85 100644 --- a/src/BootstrapBlazor/Components/Tab/Route/RouteContext.cs +++ b/src/BootstrapBlazor/Components/Tab/Route/RouteContext.cs @@ -23,9 +23,7 @@ internal class RouteContext public string[] Segments { get; } -#if NET6_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif public Type? Handler { get; set; } public IReadOnlyDictionary? Parameters { get; set; } diff --git a/src/BootstrapBlazor/Components/Tab/Route/RouteEntry.cs b/src/BootstrapBlazor/Components/Tab/Route/RouteEntry.cs index d32e78dc5..b14a2c3e3 100644 --- a/src/BootstrapBlazor/Components/Tab/Route/RouteEntry.cs +++ b/src/BootstrapBlazor/Components/Tab/Route/RouteEntry.cs @@ -9,144 +9,6 @@ using System.Diagnostics; namespace Microsoft.AspNetCore.Components.Routing; [ExcludeFromCodeCoverage] -#if NET5_0 -[DebuggerDisplay("Handler = {Handler}, Template = {Template}")] -internal class RouteEntry -{ - public RouteEntry(RouteTemplate template, Type handler, string[] unusedRouteParameterNames) - { - Template = template; - UnusedRouteParameterNames = unusedRouteParameterNames; - Handler = handler; - } - - public RouteTemplate Template { get; } - - public string[] UnusedRouteParameterNames { get; } - - public Type Handler { get; } - - internal void Match(RouteContext context) - { - var pathIndex = 0; - var templateIndex = 0; - Dictionary parameters = null; - // We will iterate over the path segments and the template segments until we have consumed - // one of them. - // There are three cases we need to account here for: - // * Path is shorter than template -> - // * This can match only if we have t-p optional parameters at the end. - // * Path and template have the same number of segments - // * This can happen when the catch-all segment matches 1 segment - // * This can happen when an optional parameter has been specified. - // * This can happen when the route only contains literals and parameters. - // * Path is longer than template -> This can only match if the parameter has a catch-all at the end. - // * We still need to iterate over all the path segments if the catch-all is constrained. - // * We still need to iterate over all the template/path segments before the catch-all - while (pathIndex < context.Segments.Length && templateIndex < Template.Segments.Length) - { - var pathSegment = context.Segments[pathIndex]; - var templateSegment = Template.Segments[templateIndex]; - - var matches = templateSegment.Match(pathSegment, out var match); - if (!matches) - { - // A constraint or literal didn't match - return; - } - - if (!templateSegment.IsCatchAll) - { - // We were dealing with a literal or a parameter, so just advance both cursors. - pathIndex++; - templateIndex++; - - if (templateSegment.IsParameter) - { - parameters ??= new(StringComparer.OrdinalIgnoreCase); - parameters[templateSegment.Value] = match; - } - } - else - { - if (templateSegment.Constraints.Length == 0) - { - - // Unconstrained catch all, we can stop early - parameters ??= new(StringComparer.OrdinalIgnoreCase); - parameters[templateSegment.Value] = string.Join('/', context.Segments, pathIndex, context.Segments.Length - pathIndex); - - // Mark the remaining segments as consumed. - pathIndex = context.Segments.Length; - - // Catch-alls are always last. - templateIndex++; - - // We are done, so break out of the loop. - break; - } - else - { - // For constrained catch-alls, we advance the path index but keep the template index on the catch-all. - pathIndex++; - if (pathIndex == context.Segments.Length) - { - parameters ??= new(StringComparer.OrdinalIgnoreCase); - parameters[templateSegment.Value] = string.Join('/', context.Segments, templateIndex, context.Segments.Length - templateIndex); - - // This is important to signal that we consumed the entire template. - templateIndex++; - } - } - } - } - - var hasRemainingOptionalSegments = templateIndex < Template.Segments.Length && - RemainingSegmentsAreOptional(pathIndex, Template.Segments); - - if ((pathIndex == context.Segments.Length && templateIndex == Template.Segments.Length) || hasRemainingOptionalSegments) - { - if (hasRemainingOptionalSegments) - { - parameters ??= new Dictionary(StringComparer.Ordinal); - AddDefaultValues(parameters, templateIndex, Template.Segments); - } - if (UnusedRouteParameterNames?.Length > 0) - { - parameters ??= new Dictionary(StringComparer.Ordinal); - for (var i = 0; i < UnusedRouteParameterNames.Length; i++) - { - parameters[UnusedRouteParameterNames[i]] = null; - } - } - context.Handler = Handler; - context.Parameters = parameters; - } - } - - private void AddDefaultValues(Dictionary parameters, int templateIndex, TemplateSegment[] segments) - { - for (var i = templateIndex; i < segments.Length; i++) - { - var currentSegment = segments[i]; - parameters[currentSegment.Value] = null; - } - } - - private bool RemainingSegmentsAreOptional(int index, TemplateSegment[] segments) - { - for (var i = index; index < segments.Length - 1; index++) - { - if (!segments[i].IsOptional) - { - return false; - } - } - - return segments[^1].IsOptional || segments[^1].IsCatchAll; - } -} -#else [DebuggerDisplay("Handler = {Handler}, Template = {Template}")] internal class RouteEntry { @@ -284,4 +146,5 @@ internal class RouteEntry return segments[^1].IsOptional || segments[^1].IsCatchAll; } } -#endif + +#nullable restore warnings diff --git a/src/BootstrapBlazor/Components/Tab/Route/RouteKey.cs b/src/BootstrapBlazor/Components/Tab/Route/RouteKey.cs index a619b7447..d35d2c2aa 100644 --- a/src/BootstrapBlazor/Components/Tab/Route/RouteKey.cs +++ b/src/BootstrapBlazor/Components/Tab/Route/RouteKey.cs @@ -5,7 +5,6 @@ using System.Reflection; namespace Microsoft.AspNetCore.Components.Routing; -#if NET6_0_OR_GREATER [ExcludeFromCodeCoverage] internal readonly struct RouteKey : IEquatable { @@ -61,4 +60,3 @@ internal readonly struct RouteKey : IEquatable return HashCode.Combine(AppAssembly, AdditionalAssemblies.Count); } } -#endif diff --git a/src/BootstrapBlazor/Components/Tab/Route/RouteTable.cs b/src/BootstrapBlazor/Components/Tab/Route/RouteTable.cs index 94ab0bc29..c2f6018ec 100644 --- a/src/BootstrapBlazor/Components/Tab/Route/RouteTable.cs +++ b/src/BootstrapBlazor/Components/Tab/Route/RouteTable.cs @@ -5,11 +5,7 @@ namespace Microsoft.AspNetCore.Components.Routing; [ExcludeFromCodeCoverage] -#if NET5_0 -internal class RouteTable : IRouteTable -#else internal class RouteTable -#endif { public RouteTable(RouteEntry[] routes) { diff --git a/src/BootstrapBlazor/Components/Tab/Route/RouteTableFactory.cs b/src/BootstrapBlazor/Components/Tab/Route/RouteTableFactory.cs index e6b86ea3f..ab6c3b17d 100644 --- a/src/BootstrapBlazor/Components/Tab/Route/RouteTableFactory.cs +++ b/src/BootstrapBlazor/Components/Tab/Route/RouteTableFactory.cs @@ -7,234 +7,6 @@ using System.Reflection; namespace Microsoft.AspNetCore.Components.Routing; -#if NET5_0 -/// -/// Resolves components for an application. -/// -[ExcludeFromCodeCoverage] -internal static class RouteTableFactory - { - private static readonly ConcurrentDictionary Cache = - new ConcurrentDictionary(); - public static readonly IComparer RoutePrecedence = Comparer.Create(RouteComparison); - - public static RouteTable Create(IEnumerable assemblies) - { - var key = new Key(assemblies.OrderBy(a => a.FullName).ToArray()); - if (Cache.TryGetValue(key, out var resolvedComponents)) - { - return resolvedComponents; - } - - var componentTypes = key.Assemblies.SelectMany(a => a.ExportedTypes.Where(t => typeof(IComponent).IsAssignableFrom(t))); - var routeTable = Create(componentTypes); - Cache.TryAdd(key, routeTable); - return routeTable; - } - - internal static RouteTable Create(IEnumerable componentTypes) - { - var templatesByHandler = new Dictionary(); - foreach (var componentType in componentTypes) - { - // We're deliberately using inherit = false here. - // - // RouteAttribute is defined as non-inherited, because inheriting a route attribute always causes an - // ambiguity. You end up with two components (base class and derived class) with the same route. - var routeAttributes = componentType.GetCustomAttributes(inherit: false); - - var templates = routeAttributes.Select(t => t.Template).ToArray(); - templatesByHandler.Add(componentType, templates); - } - return Create(templatesByHandler); - } - - internal static RouteTable Create(Dictionary templatesByHandler) - { - var routes = new List(); - foreach (var keyValuePair in templatesByHandler) - { - var parsedTemplates = keyValuePair.Value.Select(v => TemplateParser.ParseTemplate(v)).ToArray(); - var allRouteParameterNames = parsedTemplates - .SelectMany(GetParameterNames) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - foreach (var parsedTemplate in parsedTemplates) - { - var unusedRouteParameterNames = allRouteParameterNames - .Except(GetParameterNames(parsedTemplate), StringComparer.OrdinalIgnoreCase) - .ToArray(); - var entry = new RouteEntry(parsedTemplate, keyValuePair.Key, unusedRouteParameterNames); - routes.Add(entry); - } - } - - return new RouteTable(routes.OrderBy(id => id, RoutePrecedence).ToArray()); - } - - private static string[] GetParameterNames(RouteTemplate routeTemplate) - { - return routeTemplate.Segments - .Where(s => s.IsParameter) - .Select(s => s.Value) - .ToArray(); - } - - /// - /// Route precedence algorithm. - /// We collect all the routes and sort them from most specific to - /// less specific. The specificity of a route is given by the specificity - /// of its segments and the position of those segments in the route. - /// * A literal segment is more specific than a parameter segment. - /// * A parameter segment with more constraints is more specific than one with fewer constraints - /// * Segment earlier in the route are evaluated before segments later in the route. - /// For example: - /// /Literal is more specific than /Parameter - /// /Route/With/{parameter} is more specific than /{multiple}/With/{parameters} - /// /Product/{id:int} is more specific than /Product/{id} - /// - /// Routes can be ambiguous if: - /// They are composed of literals and those literals have the same values (case insensitive) - /// They are composed of a mix of literals and parameters, in the same relative order and the - /// literals have the same values. - /// For example: - /// * /literal and /Literal - /// /{parameter}/literal and /{something}/literal - /// /{parameter:constraint}/literal and /{something:constraint}/literal - /// - /// To calculate the precedence we sort the list of routes as follows: - /// * Shorter routes go first. - /// * A literal wins over a parameter in precedence. - /// * For literals with different values (case insensitive) we choose the lexical order - /// * For parameters with different numbers of constraints, the one with more wins - /// If we get to the end of the comparison routing we've detected an ambiguous pair of routes. - /// - internal static int RouteComparison(RouteEntry x, RouteEntry y) - { - if (ReferenceEquals(x, y)) - { - return 0; - } - - var xTemplate = x.Template; - var yTemplate = y.Template; - var minSegments = Math.Min(xTemplate.Segments.Length, yTemplate.Segments.Length); - var currentResult = 0; - for (var i = 0; i < minSegments; i++) - { - var xSegment = xTemplate.Segments[i]; - var ySegment = yTemplate.Segments[i]; - - var xRank = GetRank(xSegment); - var yRank = GetRank(ySegment); - - currentResult = xRank.CompareTo(yRank); - - // If they are both literals we can disambiguate - if ((xRank, yRank) == (0, 0)) - { - currentResult = StringComparer.OrdinalIgnoreCase.Compare(xSegment.Value, ySegment.Value); - } - - if (currentResult != 0) - { - break; - } - } - - if (currentResult == 0) - { - currentResult = xTemplate.Segments.Length.CompareTo(yTemplate.Segments.Length); - } - - if (currentResult == 0) - { - throw new InvalidOperationException($@"The following routes are ambiguous: - '{x.Template.TemplateText}' in '{x.Handler.FullName}' - '{y.Template.TemplateText}' in '{y.Handler.FullName}' - "); - } - - return currentResult; - } - - private static int GetRank(TemplateSegment xSegment) - { - return xSegment switch - { - // Literal - { IsParameter: false } => 0, - // Parameter with constraints - { IsParameter: true, IsCatchAll: false, Constraints: { Length: > 0 } } => 1, - // Parameter without constraints - { IsParameter: true, IsCatchAll: false, Constraints: { Length: 0 } } => 2, - // Catch all parameter with constraints - { IsParameter: true, IsCatchAll: true, Constraints: { Length: > 0 } } => 3, - // Catch all parameter without constraints - { IsParameter: true, IsCatchAll: true, Constraints: { Length: 0 } } => 4, - // The segment is not correct - _ => throw new InvalidOperationException($"Unknown segment definition '{xSegment}.") - }; - } - - private readonly struct Key : IEquatable - { - public readonly Assembly[] Assemblies; - - public Key(Assembly[] assemblies) - { - Assemblies = assemblies; - } - - public override bool Equals(object? obj) - { - return obj is Key other ? base.Equals(other) : false; - } - - public bool Equals(Key other) - { - if (Assemblies == null && other.Assemblies == null) - { - return true; - } - else if ((Assemblies == null) || (other.Assemblies == null)) - { - return false; - } - else if (Assemblies.Length != other.Assemblies.Length) - { - return false; - } - - for (var i = 0; i < Assemblies.Length; i++) - { - if (!Assemblies[i].Equals(other.Assemblies[i])) - { - return false; - } - } - - return true; - } - - public override int GetHashCode() - { - var hash = new HashCode(); - - if (Assemblies != null) - { - for (var i = 0; i < Assemblies.Length; i++) - { - hash.Add(Assemblies[i]); - } - } - - return hash.ToHashCode(); - } - } - } -#else /// /// Resolves components for an application. /// @@ -468,4 +240,3 @@ internal static class RouteTableFactory }; } } -#endif diff --git a/src/BootstrapBlazor/Components/Tab/Route/RouteTemplate.cs b/src/BootstrapBlazor/Components/Tab/Route/RouteTemplate.cs index cde3c57bb..bd07dc010 100644 --- a/src/BootstrapBlazor/Components/Tab/Route/RouteTemplate.cs +++ b/src/BootstrapBlazor/Components/Tab/Route/RouteTemplate.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Website: https://www.blazor.zone or https://argozhang.github.io/ - using System.Diagnostics; namespace Microsoft.AspNetCore.Components.Routing; @@ -16,10 +15,6 @@ internal class RouteTemplate TemplateText = templateText; Segments = segments; -#if NET5_0 - OptionalSegmentsCount = segments.Count(template => template.IsOptional); - ContainsCatchAllSegment = segments.Any(template => template.IsCatchAll); -#else for (var i = 0; i < segments.Length; i++) { var segment = segments[i]; @@ -32,7 +27,6 @@ internal class RouteTemplate ContainsCatchAllSegment = true; } } -#endif } public string TemplateText { get; } diff --git a/src/BootstrapBlazor/Components/Tab/Route/StringSegmentAccumulator.cs b/src/BootstrapBlazor/Components/Tab/Route/StringSegmentAccumulator.cs index 1fd988a30..b1e4cd8fd 100644 --- a/src/BootstrapBlazor/Components/Tab/Route/StringSegmentAccumulator.cs +++ b/src/BootstrapBlazor/Components/Tab/Route/StringSegmentAccumulator.cs @@ -4,7 +4,6 @@ namespace Microsoft.AspNetCore.Components.Routing; -#if NET6_0_OR_GREATER // This is very similar to Microsoft.Extensions.Primitives.StringValues, except it works in terms // of ReadOnlyMemory rather than string, so the querystring handling logic doesn't need to // allocate per-value when tracking things that will be parsed as value types. @@ -64,4 +63,3 @@ internal struct StringSegmentAccumulator } } } -#endif diff --git a/src/BootstrapBlazor/Components/Tab/Route/TemplateSegment.cs b/src/BootstrapBlazor/Components/Tab/Route/TemplateSegment.cs index b889439f5..93f2ab7a9 100644 --- a/src/BootstrapBlazor/Components/Tab/Route/TemplateSegment.cs +++ b/src/BootstrapBlazor/Components/Tab/Route/TemplateSegment.cs @@ -5,146 +5,6 @@ namespace Microsoft.AspNetCore.Components.Routing; [ExcludeFromCodeCoverage] -#if NET5_0 -internal class TemplateSegment -{ - public TemplateSegment(string template, string segment, bool isParameter) - { - IsParameter = isParameter; - - IsCatchAll = isParameter && segment.StartsWith('*'); - - if (IsCatchAll) - { - // Only one '*' currently allowed - Value = segment[1..]; - - var invalidCharacterIndex = Value.IndexOf('*'); - if (invalidCharacterIndex != -1) - { - throw new InvalidOperationException($"Invalid template '{template}'. A catch-all parameter may only have one '*' at the beginning of the segment."); - } - } - else - { - Value = segment; - } - - // Process segments that parameters that do not contain a token separating a type constraint. - if (IsParameter) - { - if (Value.IndexOf(':') < 0) - { - - // Set the IsOptional flag to true for segments that contain - // a parameter with no type constraints but optionality set - // via the '?' token. - var questionMarkIndex = Value.IndexOf('?'); - if (questionMarkIndex == Value.Length - 1) - { - IsOptional = true; - Value = Value[0..^1]; - } - // If the `?` optional marker shows up in the segment but not at the very end, - // then throw an error. - else if (questionMarkIndex >= 0) - { - throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}'. '?' character can only appear at the end of parameter name."); - } - - Constraints = Array.Empty(); - } - else - { - var tokens = Value.Split(':'); - if (tokens[0].Length == 0) - { - throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}' has no name before the constraints list."); - } - - Value = tokens[0]; - IsOptional = tokens[^1].EndsWith('?'); - if (IsOptional) - { - tokens[^1] = tokens[^1][0..^1]; - } - - Constraints = new RouteConstraint[tokens.Length - 1]; - for (var i = 1; i < tokens.Length; i++) - { - Constraints[i - 1] = RouteConstraint.Parse(template, segment, tokens[i]); - } - } - } - else - { - Constraints = Array.Empty(); - } - - if (IsParameter) - { - if (IsOptional && IsCatchAll) - { - throw new InvalidOperationException($"Invalid segment '{segment}' in route '{template}'. A catch-all parameter cannot be marked optional."); - } - - // Moving the check for this here instead of TemplateParser so we can allow catch-all. - // We checked for '*' up above specifically for catch-all segments, this one checks for all others - if (Value.IndexOf('*') != -1) - { - throw new InvalidOperationException($"Invalid template '{template}'. The character '*' in parameter segment '{{{segment}}}' is not allowed."); - } - } - } - - // The value of the segment. The exact text to match when is a literal. - // The parameter name when its a segment - public string Value { get; } - - public bool IsParameter { get; } - - public bool IsOptional { get; } - - public bool IsCatchAll { get; } - - public RouteConstraint[] Constraints { get; } - - public bool Match(string pathSegment, out object? matchedParameterValue) - { - if (IsParameter) - { - matchedParameterValue = pathSegment; - - foreach (var constraint in Constraints) - { - if (!constraint.Match(pathSegment, out matchedParameterValue)) - { - return false; - } - } - - return true; - } - else - { - matchedParameterValue = null; - return string.Equals(Value, pathSegment, StringComparison.OrdinalIgnoreCase); - } - } - - public override string ToString() => this switch - { - { IsParameter: true, IsOptional: false, IsCatchAll: false, Constraints: { Length: 0 } } => $"{{{Value}}}", - { IsParameter: true, IsOptional: false, IsCatchAll: false, Constraints: { Length: > 0 } } => $"{{{Value}:{string.Join(':', Constraints.Select(c => c.ToString()))}}}", - { IsParameter: true, IsOptional: true, Constraints: { Length: 0 } } => $"{{{Value}?}}", - { IsParameter: true, IsOptional: true, Constraints: { Length: > 0 } } => $"{{{Value}:{string.Join(':', Constraints.Select(c => c.ToString()))}?}}", - { IsParameter: true, IsCatchAll: true, Constraints: { Length: 0 } } => $"{{*{Value}}}", - { IsParameter: true, IsCatchAll: true, Constraints: { Length: > 0 } } => $"{{*{Value}:{string.Join(':', Constraints.Select(c => c.ToString()))}?}}", - { IsParameter: false } => Value, - _ => throw new InvalidOperationException("Invalid template segment.") - }; -} -#else internal class TemplateSegment { public TemplateSegment(string template, string segment, bool isParameter) @@ -283,4 +143,3 @@ internal class TemplateSegment _ => throw new InvalidOperationException("Invalid template segment.") }; } -#endif diff --git a/src/BootstrapBlazor/Components/Tab/Route/TypeRouteConstraint.cs b/src/BootstrapBlazor/Components/Tab/Route/TypeRouteConstraint.cs deleted file mode 100644 index 3295a461c..000000000 --- a/src/BootstrapBlazor/Components/Tab/Route/TypeRouteConstraint.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// Website: https://www.blazor.zone or https://argozhang.github.io/ - -namespace Microsoft.AspNetCore.Components.Routing; - -#if NET5_0 -/// -/// A route constraint that requires the value to be parseable as a specified type. -/// -/// The type to which the value must be parseable. -[ExcludeFromCodeCoverage] -internal class TypeRouteConstraint : RouteConstraint -{ - public delegate bool TryParseDelegate(string str, [MaybeNullWhen(false)] out T result); - - private readonly TryParseDelegate _parser; - - public TypeRouteConstraint(TryParseDelegate parser) - { - _parser = parser; - } - - public override bool Match(string pathSegment, out object? convertedValue) - { - if (_parser(pathSegment, out var result)) - { - convertedValue = result; - return true; - } - else - { - convertedValue = null; - return false; - } - } - - public override string ToString() => typeof(T) switch - { - var x when x == typeof(bool) => "bool", - var x when x == typeof(DateTime) => "datetime", - var x when x == typeof(decimal) => "decimal", - var x when x == typeof(double) => "double", - var x when x == typeof(float) => "float", - var x when x == typeof(Guid) => "guid", - var x when x == typeof(int) => "int", - var x when x == typeof(long) => "long", - var x => x.Name.ToLowerInvariant() - }; -} -#endif diff --git a/src/BootstrapBlazor/Components/Tab/Route/UrlValueConstraint.cs b/src/BootstrapBlazor/Components/Tab/Route/UrlValueConstraint.cs index c3f3ee164..5ed244204 100644 --- a/src/BootstrapBlazor/Components/Tab/Route/UrlValueConstraint.cs +++ b/src/BootstrapBlazor/Components/Tab/Route/UrlValueConstraint.cs @@ -7,7 +7,6 @@ using System.Globalization; namespace Microsoft.AspNetCore.Components.Routing; -#if NET6_0_OR_GREATER /// /// Shared logic for parsing tokens from route values and querystring values. /// @@ -183,4 +182,3 @@ internal abstract class UrlValueConstraint } } } -#endif diff --git a/src/BootstrapBlazor/Components/Tab/Tab.razor.cs b/src/BootstrapBlazor/Components/Tab/Tab.razor.cs index 96c31ca50..f938f33ef 100644 --- a/src/BootstrapBlazor/Components/Tab/Tab.razor.cs +++ b/src/BootstrapBlazor/Components/Tab/Tab.razor.cs @@ -57,12 +57,12 @@ public partial class Tab : IHandlerException private List TabItems => _dragged ? _draggedItems : _items; /// - /// 获得/设置 是否为排除地址 默认为 false + /// 获得/设置 是否为排除地址 默认 false /// private bool Excluded { get; set; } /// - /// 获得/设置 是否为卡片样式 + /// 获得/设置 是否为卡片样式 默认 false /// [Parameter] public bool IsCard { get; set; } @@ -388,10 +388,10 @@ public partial class Tab : IHandlerException var requestUrl = Navigator.ToBaseRelativePath(Navigator.Uri); // 判断是否排除 - var urls = ExcludeUrls ?? []; + var routes = ExcludeUrls ?? []; Excluded = requestUrl == "" - ? urls.Any(u => u is "" or "/") - : urls.Any(u => u != "/" && requestUrl.StartsWith(u.TrimStart('/'), StringComparison.OrdinalIgnoreCase)); + ? routes.Any(u => u is "" or "/") + : routes.Any(u => u != "/" && requestUrl.StartsWith(u.TrimStart('/'), StringComparison.OrdinalIgnoreCase)); if (!Excluded) { // 地址相同参数不同需要重新渲染 TabItem diff --git a/src/BootstrapBlazor/Components/Tab/TabItem.cs b/src/BootstrapBlazor/Components/Tab/TabItem.cs index 1ddf2b646..35fd027d2 100644 --- a/src/BootstrapBlazor/Components/Tab/TabItem.cs +++ b/src/BootstrapBlazor/Components/Tab/TabItem.cs @@ -144,7 +144,8 @@ public class TabItem : ComponentBase { parameters[nameof(Url)] = url?.ToString()?.TrimStart('/') ?? ""; } - _ = item.SetParametersAsync(ParameterView.FromDictionary(parameters!)); + var pv = ParameterView.FromDictionary(parameters); + pv.SetParameterProperties(item); return item; } } diff --git a/test/UnitTest/Components/TabTest.cs b/test/UnitTest/Components/TabTest.cs index 50ce8488c..d444cf44c 100644 --- a/test/UnitTest/Components/TabTest.cs +++ b/test/UnitTest/Components/TabTest.cs @@ -40,6 +40,15 @@ public class TabTest : BootstrapBlazorTestBase Assert.Contains("Tab1-Content", cut.Markup); } + [Fact] + public void TabItem_Null() + { + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.Text, "Test"); + }); + } + [Fact] public void TabItemCreate_Ok() { @@ -265,19 +274,19 @@ public class TabTest : BootstrapBlazorTestBase navMan.NavigateTo("/"); cut.SetParametersAndRender(pb => { - pb.Add(a => a.ExcludeUrls, new String[] { "/" }); + pb.Add(a => a.ExcludeUrls, ["/"]); }); navMan.NavigateTo("/"); cut.SetParametersAndRender(pb => { - pb.Add(a => a.ExcludeUrls, new String[] { "" }); + pb.Add(a => a.ExcludeUrls, [""]); }); navMan.NavigateTo("/Cat"); cut.SetParametersAndRender(pb => { - pb.Add(a => a.ExcludeUrls, new String[] { "/", "Cat" }); + pb.Add(a => a.ExcludeUrls, ["/", "Cat"]); }); navMan.NavigateTo("/"); @@ -288,7 +297,7 @@ public class TabTest : BootstrapBlazorTestBase })); cut.SetParametersAndRender(pb => { - pb.Add(a => a.ExcludeUrls, new String[] { "/Test" }); + pb.Add(a => a.ExcludeUrls, ["/Test"]); }); cut.InvokeAsync(() => cut.Instance.CloseCurrentTab()); @@ -369,7 +378,7 @@ public class TabTest : BootstrapBlazorTestBase { var instance = cut.Instance; var mi = instance.GetType().GetMethod("GetMenuItem", BindingFlags.Instance | BindingFlags.NonPublic)!; - mi.Invoke(instance, new object[] { "/" }); + mi.Invoke(instance, ["/"]); }); } @@ -387,7 +396,7 @@ public class TabTest : BootstrapBlazorTestBase { var instance = cut.Instance; var mi = instance.GetType().GetMethod("GetMenuItem", BindingFlags.Instance | BindingFlags.NonPublic)!; - mi.Invoke(instance, new object[] { "/" }); + mi.Invoke(instance, ["/"]); }); } @@ -485,13 +494,15 @@ public class TabTest : BootstrapBlazorTestBase cut.DoesNotContain("Tab2-Content"); // 点击第二个 TabItem - var item = cut.FindAll(".tabs-item").Last(); + var items = cut.FindAll(".tabs-item"); + var item = items[items.Count - 1]; cut.InvokeAsync(() => item.Click()); cut.Contains("Tab1-Content"); cut.Contains("Tab2-Content"); // 再点击第一个 TabItem - item = cut.FindAll(".tabs-item").First(); + items = cut.FindAll(".tabs-item"); + item = items[0]; cut.InvokeAsync(() => item.Click()); cut.Contains("Tab1-Content"); cut.Contains("Tab2-Content");