mirror of
https://gitee.com/LongbowEnterprise/BootstrapBlazor.git
synced 2024-11-29 18:49:08 +08:00
perf(Tab): improve create tab item from url performance (#4445)
* 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 <argo@live.ca>
This commit is contained in:
parent
14871c4ed6
commit
71ee1ab035
@ -104,3 +104,4 @@ scrlang
|
||||
Validata
|
||||
Validatable
|
||||
noselect
|
||||
Urls
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>8.10.2</Version>
|
||||
<Version>8.10.3-beta01</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -39,9 +39,9 @@ public class TimePickerSetting : ComponentBase
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
protected override void OnInitialized()
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
base.OnInitialized();
|
||||
base.OnParametersSet();
|
||||
|
||||
if (Option != null)
|
||||
{
|
||||
|
@ -11,7 +11,7 @@ using System.Collections.ObjectModel;
|
||||
namespace BootstrapBlazor.Components;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// BootstrapBlazorAuthorizeView 组件
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// OnInitializedAsync 方法
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override async Task OnInitializedAsync()
|
||||
@ -68,7 +66,7 @@ public class BootstrapBlazorAuthorizeView : ComponentBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BuildRenderTree 方法
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
/// <param name="builder"></param>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
/// <summary>
|
||||
/// Provides an abstraction over <see cref="RouteTable"/>.
|
||||
/// the legacy route matching logic is removed.
|
||||
/// </summary>
|
||||
internal interface IRouteTable
|
||||
{
|
||||
void Route(RouteContext routeContext);
|
||||
}
|
||||
#endif
|
@ -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<string, RouteConstraint> _cachedConstraints
|
||||
= new ConcurrentDictionary<string, RouteConstraint>();
|
||||
|
||||
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}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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?}`.
|
||||
/// </summary>
|
||||
/// <param name="constraint">String representation of the constraint</param>
|
||||
/// <returns>Type-specific RouteConstraint object</returns>
|
||||
private static RouteConstraint? CreateRouteConstraint(string constraint)
|
||||
{
|
||||
switch (constraint)
|
||||
{
|
||||
case "bool":
|
||||
return new TypeRouteConstraint<bool>(bool.TryParse);
|
||||
case "datetime":
|
||||
return new TypeRouteConstraint<DateTime>((string str, out DateTime result)
|
||||
=> DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result));
|
||||
case "decimal":
|
||||
return new TypeRouteConstraint<decimal>((string str, out decimal result)
|
||||
=> decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "double":
|
||||
return new TypeRouteConstraint<double>((string str, out double result)
|
||||
=> double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "float":
|
||||
return new TypeRouteConstraint<float>((string str, out float result)
|
||||
=> float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "guid":
|
||||
return new TypeRouteConstraint<Guid>(Guid.TryParse);
|
||||
case "int":
|
||||
return new TypeRouteConstraint<int>((string str, out int result)
|
||||
=> int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
|
||||
case "long":
|
||||
return new TypeRouteConstraint<long>((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
|
||||
|
@ -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<string, object>? Parameters { get; set; }
|
||||
|
@ -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<string, object> 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<string, object>(StringComparer.Ordinal);
|
||||
AddDefaultValues(parameters, templateIndex, Template.Segments);
|
||||
}
|
||||
if (UnusedRouteParameterNames?.Length > 0)
|
||||
{
|
||||
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < UnusedRouteParameterNames.Length; i++)
|
||||
{
|
||||
parameters[UnusedRouteParameterNames[i]] = null;
|
||||
}
|
||||
}
|
||||
context.Handler = Handler;
|
||||
context.Parameters = parameters;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddDefaultValues(Dictionary<string, object> 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
|
||||
|
@ -5,7 +5,6 @@ using System.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing;
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal readonly struct RouteKey : IEquatable<RouteKey>
|
||||
{
|
||||
@ -61,4 +60,3 @@ internal readonly struct RouteKey : IEquatable<RouteKey>
|
||||
return HashCode.Combine(AppAssembly, AdditionalAssemblies.Count);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -7,234 +7,6 @@ using System.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing;
|
||||
|
||||
#if NET5_0
|
||||
/// <summary>
|
||||
/// Resolves components for an application.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static class RouteTableFactory
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Key, RouteTable> Cache =
|
||||
new ConcurrentDictionary<Key, RouteTable>();
|
||||
public static readonly IComparer<RouteEntry> RoutePrecedence = Comparer<RouteEntry>.Create(RouteComparison);
|
||||
|
||||
public static RouteTable Create(IEnumerable<Assembly> 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<Type> componentTypes)
|
||||
{
|
||||
var templatesByHandler = new Dictionary<Type, string[]>();
|
||||
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<RouteAttribute>(inherit: false);
|
||||
|
||||
var templates = routeAttributes.Select(t => t.Template).ToArray();
|
||||
templatesByHandler.Add(componentType, templates);
|
||||
}
|
||||
return Create(templatesByHandler);
|
||||
}
|
||||
|
||||
internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler)
|
||||
{
|
||||
var routes = new List<RouteEntry>();
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<Key>
|
||||
{
|
||||
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
|
||||
/// <summary>
|
||||
/// Resolves components for an application.
|
||||
/// </summary>
|
||||
@ -468,4 +240,3 @@ internal static class RouteTableFactory
|
||||
};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -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; }
|
||||
|
@ -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<char> 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
|
||||
|
@ -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<RouteConstraint>();
|
||||
}
|
||||
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<RouteConstraint>();
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
/// <summary>
|
||||
/// A route constraint that requires the value to be parseable as a specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to which the value must be parseable.</typeparam>
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal class TypeRouteConstraint<T> : 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
|
@ -7,7 +7,6 @@ using System.Globalization;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing;
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
/// <summary>
|
||||
/// Shared logic for parsing tokens from route values and querystring values.
|
||||
/// </summary>
|
||||
@ -183,4 +182,3 @@ internal abstract class UrlValueConstraint
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -57,12 +57,12 @@ public partial class Tab : IHandlerException
|
||||
private List<TabItem> TabItems => _dragged ? _draggedItems : _items;
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 是否为排除地址 默认为 false
|
||||
/// 获得/设置 是否为排除地址 默认 false
|
||||
/// </summary>
|
||||
private bool Excluded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 是否为卡片样式
|
||||
/// 获得/设置 是否为卡片样式 默认 false
|
||||
/// </summary>
|
||||
[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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,15 @@ public class TabTest : BootstrapBlazorTestBase
|
||||
Assert.Contains("Tab1-Content", cut.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TabItem_Null()
|
||||
{
|
||||
var cut = Context.RenderComponent<TabItem>(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");
|
||||
|
Loading…
Reference in New Issue
Block a user