Routing
与标准ASP.NETMVC一样,Blazor路由是一种用于检查浏览器的URL并将其与要呈现的页面进行匹配的技术。
路由比简单地将URL与页面匹配要灵活得多。它允许我们基于文本模式进行匹配,例如,上图中的两个URL将映射到相同的组件,并传入上下文的ID(在本例中为1或4)。
Simulated navigation 模拟导航
当Blazor应用程序在同一个应用程序中导航到新的URL时,它实际上并不是在传统的WWW意义上导航。不向服务器发送请求新页面的内容的请求。相反,Blazor会重写浏览器的URL,然后呈现相关内容。
另请注意,当导航到解析为与当前页面相同类型的组件的新URL时,在导航之前不会销毁该组件,也不会执行OnInitialized*
生命周期方法。导航被简单地视为对组件参数的更改。
Defining routes
要定义路由,我们只需在任何组件的顶部添加@page
声明。
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
如果我们打开为此视图生成的源代码(在obj\Debug\netcoreapp3.0\Razor\Pages\Index.razor.g.cs)中,我们看到@page
指令编译为以下代码。
[Microsoft.AspNetCore.Components.LayoutAttribute(typeof(MainLayout))]
[Microsoft.AspNetCore.Components.RouteAttribute("/")]
public class Index : Microsoft.AspNetCore.Components.ComponentBase
{
}
@page
指令在组件的类上生成RouteAttribute
。在启动期间,Blazor扫描使用RouteAttribute
装饰的类,并相应地构建其路由定义。
Route discovery
路由发现由Blazor在其默认项目模板中自动执行。如果我们查看App.razor
文件,我们将看到路由器组件的使用。
… other code …
<Router AppAssembly="typeof(Startup).Assembly">
… other code …
</Router>
… other code …
Router组件扫描指定程序集中实现**IComponent**
的所有类,然后在类上反映,看看是否用任何**RouteAttribute**
属性修饰了它。对于它找到的每个RouteAttribute
,它都会解析其URL模板字符串,并将URL到组件的关系添加到其内部路由表中。
这意味着单个组件可以用零个、一个或多个RouteAttribute
属性(@page
声明)进行修饰。零的组件不能通过URL访问,而多个的组件可以通过它指定的任何URL模板访问。
@page "/"
@page "/greeting"
@page "/HelloWorld"
@page "/hello-world"
<h1>Hello, world!</h1>
页面也可以在组件库中定义。
Route parameters
到目前为止,我们已经了解了如何将静态网址链接到Blazor组件。静态URL只对静态内容有用,如果我们希望同一组件根据URL中的信息(如客户ID)呈现不同的视图,则需要使用路由参数。
添加组件的@page
声明时,通过将路由参数的名称用一对{
大括号}
括起来,可以在URL中定义路由参数。
@page "/customer/{CustomerId}
Capturing a parameter value
捕获参数值就像添加同名属性并用[Parameter]
属性修饰它一样简单。
@page "/"
@page "/customer/{CustomerId}"
<h1>
Customer:
@if (string.IsNullOrEmpty(CustomerId))
{
@:None
}
else
{
@CustomerId
}
</h1>
<h3>Select a customer</h3>
<ul>
<li><a href="/customer/Microsoft">Microsoft</a></li>
<li><a href="/customer/Google">Google</a></li>
<li><a href="/customer/IBM">IBM</a></li>
</ul>
@code {
[Parameter]
public string CustomerId { get; set; }
}
请注意,当导航到解析为与当前页面相同类型的组件的新URL时,在导航之前不会销毁该组件,也不会执行OnInitialized*
生命周期方法。导航被简单地视为对组件参数的更改。
Constraining route parameters 约束路由参数
除了能够指定包含参数的URL模板外,还可以确保Blazor仅在参数值满足特定条件时才将URL与组件匹配。
例如,在采购订单编号始终为整数的应用程序中,只有当OrderNumber
的URL值实际上是数字时,我们才希望URL中的参数与用于显示采购订单的组件匹配。
要定义参数的约束,它的后缀是冒号,然后是约束类型。例如:int
只有在组件的URL在正确位置包含有效的整数值时,才会匹配该组件的URL。
@page "/"
@page "/purchase-order/{OrderNumber:int}"
<h1>
Order number:
@if (!OrderNumber.HasValue)
{
@:None
}
else
{
@OrderNumber
}
</h1>
<h3>Select a purchase order</h3>
<ul>
<li><a href="/purchase-order/1/">Order 1</a></li>
<li><a href="/purchase-order/2/">Order 2</a></li>
<li><a href="/purchase-order/42/">Order 42</a></li>
</ul>
@code {
[Parameter]
public int? OrderNumber { get; set; }
}
Constraint types
Constraint | .NET type | Valid | Invalid |
---|---|---|---|
:bool | System.Boolean | - true - false |
- 1 - Hello |
:datetime | System.DateTime | - 2001-01-01 - 02-29-2000 |
- 29-02-2000 |
:decimal | System.Decimal | - 2.34 - 0.234 |
- 2,34 - ૦.૨૩૪ |
:double | System.Double | - 2.34 - 0.234 |
- 2,34 - ૦.૨૩૪ |
:float | System.Single | - 2.34 - 0.234 |
- 2,34 - ૦.૨૩૪ |
:guid | System.Guid | - 99303dc9-8c76-42d9-9430-de3ee1ac25d0 |
- {99303dc9-8c76-42d9-9430-de3ee1ac25d0} |
:int | System.Int32 | - -1 - 42 - 299792458 |
- 12.34 - ૨૩ |
:long | System.Int64 | - -1 - 42 - 299792458 |
- 12.34 - ૨૩ |
Localization
Blazor约束当前不支持本地化。
- 只有格式为
0..9
的数字才被认为是有效的,而不是来自非英语语言,如૦..૯
(古吉拉特语)。 - 日期的格式仅为
MM-dd-yyyy
、MM-dd-yy
或ISO格式yyyy-MM-dd
。 -
Unsupported constraint types
Blazor约束不支持以下约束类型,但希望将来会支持:
贪婪参数 Greedy parameters
在ASP.NETMVC中,可以提供以星号开头的参数名称,并捕获包含正斜杠的URL块。/articles/{Subject}/{*TheRestOfTheURL}
- 正则表达式
Blazor当前不支持基于正则表达式约束参数的功能。
- 枚举
目前不可能将参数约束为与枚举的值匹配。
- 自定义约束
Optional router parameters
Blazor不显式地支持可选的路由参数,但是通过在组件上添加多个@page
声明就可以很容易地实现等效的路由参数。例如,更改标准的Counter.razor页面以添加额外的URL。
@page "/counter"
@page "/counter/{CurrentCount:int}"
将int currentCount
字段更改为参数,如下所示[Parameter] public int CurrentCount { get; set; }
然后用CurrentCount
替换对currentCount
的所有引用。还可以在页面上添加一些导航,以便我们可以快速测试我们的路线
@page "/counter"
@page "/counter/{CurrentCount:int}"
<h1>Counter</h1>
<p>Current count: @CurrentCount</p>
<button class="btn btn-primary" @onclick=IncrementCount>Click me</button>
<ul>
<li><a href="/counter/42">Navigate to /counter/42</a></li>
<li><a href="/counter/123">Navigate to /counter/123</a></li>
<li><a href="/counter/">Navigate to /counter</a></li>
</ul>
@code {
[Parameter]
public int CurrentCount { get; set; }
void IncrementCount()
{
CurrentCount++;
}
}
当我们运行这个应用程序时,我们看到我们可以导航到/Counter
(不需要参数)或/Counter/AnyNumber
(指定了参数值)。当URL中没有指定值时,将使用属性类型的默认值。
Specifying a default value for optional parameters
如果我们希望参数的默认值不是C#默认值,该怎么办呢?例如,当没有为CurrentCount
指定值时,我们可能希望它默认为1
而不是0
。
首先,我们需要将参数属性的类型更改为可为空,这样我们就可以区分/Counter/0
和Just/Counter
-之间的区别,然后如果该属性为空,则将默认值分配给该属性。
[Parameter]
public int? CurrentCount { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
CurrentCount = CurrentCount ?? 1;
}
乍一看,这似乎是可行的,导航到/Counter
实际上会将CurrentCount
值默认为1
。
但是,这仅在第一次显示页面时起作用。如果我们现在使用其中一个链接导航到/Counter
,而没有首先导航到另一个页面(比如Home),我们将看到CurrentCount
缺省为NULL。
当组件是@page
并且Blazor应用程序导航到呈现同一页面的新URL时,Blazor不会创建组件的新实例来呈现页面,而是将其视为参数已更改的相同页面。因此,只有在第一次创建页面时才会执行OnInitialized
。有关详细信息,请参见组件生命周期。
Previous URL | Current URL | Counter.OnInit executed |
---|---|---|
/ | /counter | Yes – Different page |
/counter | /counter/42 | No – Same page |
/counter/42 | counter/123 | No – Same page |
/counter/123 | /counter | No – Same page |
/counter | /counter/123 | No – Same page |
/counter/123 | /counter | No – Same page |
/counter | / | Yes – Different page |
正确的解决方案是默认SetParametersAsync
中的值-只要参数更改,并且它们的值被推入组件的属性中(例如在导航期间),就会调用该方法。
[Parameter]
public int? CurrentCount { get; set; }
public async override Task SetParametersAsync(ParameterView parameters)
{
await base.SetParametersAsync(parameters);
CurrentCount = CurrentCount ?? 1;
}
404 - Not found
当Blazor无法将URL与组件匹配时,我们可能希望告诉它要显示什么内容。Router
组件有一个名为NotFound
的RenderFragment参数,它是一个RenderFragment
。当尝试访问路由器无法与任何组件匹配的URL时,将显示在Router
组件的此参数中定义的任何Razor标记。
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" />
</Found>
<NotFound>
<div class="content">
<h1>PAGE NOT FOUND</h1>
<p>
The page you have requested could not be found. <a href="/">Return to the home page.</a>
</p>
</div>
</NotFound>
</Router>
Navigating our app via HTML
链接到Blazor组件中的路由的最简单方法是使用HTML超链接。
<a href="/Counter">This works just fine</a>
Blazor组件中的超链接会被自动截取。当用户单击超链接时,浏览器不会向服务器发送请求,而是Blazor将更新浏览器中的URL,并呈现与新地址相关联的任何页面。
Using the NavLink component
Blazor还包括一个用于呈现超链接的组件,该组件还支持在Address与URL匹配时更改HTML元素的CSS类。
如果我们查看默认Blazor应用程序中的/Shared/NavMenu.razor组件,我们将看到如下所示的标记:
<NavLink class="nav-link" href="counter">
<span class="oi oi-home" aria-hidden="true"></span> Counter
</NavLink>
NavLink
组件使用HTML超链接装饰其子内容。所有属性(如class
、href
等)都通过属性Splatting直接呈现给<a>
元素。NavLink
组件有两个参数可提供附加行为。ActiveClass
参数指定当浏览器的URL与href
属性的URL匹配时,将哪个CSS类应用于呈现的<a>
元素。如果未指定,Blazor将应用名为“active”的CSS类。
URL matching
Match
参数标识应该如何将浏览器的URL与href
进行比较,以便决定是否应该将ActiveClass
添加到元素的class
属性。
在新的Blazor应用程序中编辑/Pages/Counter.razor文件,以便可以从三个URL访问该文件。
@page "/counter"
@page "/counter/1"
@page "/counter/2"
然后编辑/Shared/NavMenu.razor组件,使计数器菜单项有两个子菜单链接。
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter" Match=@NavLinkMatch.All>
<span class="oi oi-plus" aria-hidden="true"></span>Counter
</NavLink>
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter/1" Match=@NavLinkMatch.All>
<span class="oi oi-plus" aria-hidden="true"></span>Counter/1
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter/2" Match=@NavLinkMatch.All>
<span class="oi oi-plus" aria-hidden="true"></span>Counter/2
</NavLink>
</li>
</ul>
</li>
还要编辑/wwwroot/site.css并添加以下内容,这样我们就可以很容易地看到哪些NavLink
元素被认为是“活动的”。
.nav-item a.active::after
{
content: " *";
margin-left: 1em;
}
这三个NavLink组件导航到/COUNTER
、/COUNTER/1
和/COUNTER/2
,如果我们运行应用程序并单击各种链接,我们将看到以下内容。
NavLinkMatch
NavLink
组件的Match
参数接受类型为NavLinkMatch
的值。这将告诉NavLink
组件如何将浏览器的URL与它呈现的<a>
元素的href
属性进行比较,以确定它们是否相同。
在前面的示例中,我们为每个NavLink
组件上的Match
参数指定了NavLinkMatch.All
。这意味着我们希望Blazor仅在其href
与浏览器的URL完全匹配时才将每个NavLink
视为活动的。如果我们现在更改链接到/COUNTER
的NavLink
,使其Match
参数为NavLinkMatch.Prefix
,我们将看到每当URL以/COUNTER
开头时,它都将被视为匹配,因此它还将匹配/COUNTER/1
和/COUNTER/2
。
要说明不同之处,请在/Shared/NavMenu.razor的代码部分中声明一个字段
NavLinkMatch MatchMode = NavLinkMatch.All;
查找<div class="@NavMenuCssClass"...
元素,并在<ul>
元素之前添加以下标记以将<select>
绑定到新字段。
<select @bind=MatchMode class="form-control">
<option value=@NavLinkMatch.All>All</option>
<option value=@NavLinkMatch.Prefix>Prefix</option>
</select>
最后,找到其href
链接到/counter
的NavLink
,并将其匹配参数更改为@MatchMode
。您的代码现在应该如下所示。
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">NavigationViaHtml</a>
<button class="navbar-toggler" @onclick=ToggleNavMenu>
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div class="@NavMenuCssClass" @onclick=ToggleNavMenu>
<select @bind=MatchMode class="form-control">
<option value=@NavLinkMatch.All>All</option>
<option value=@NavLinkMatch.Prefix>Prefix</option>
</select>
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="" Match=@NavLinkMatch.All>
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter" Match=@MatchMode>
<span class="oi oi-plus" aria-hidden="true"></span>Counter
</NavLink>
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter/1" Match=@NavLinkMatch.All>
<span class="oi oi-plus" aria-hidden="true"></span>Counter/1
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter/2" Match=@NavLinkMatch.All>
<span class="oi oi-plus" aria-hidden="true"></span>Counter/2
</NavLink>
</li>
</ul>
</li>
</ul>
</div>
@code {
NavLinkMatch MatchMode = NavLinkMatch.All;
bool collapseNavMenu = true;
string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
选择COUNTER/1
或COUNTER/2
链接,切换<select>
的值。
尽管浏览器URL保持不变,但我们可以看到第一个计数器NavLink根据其Match
参数的设置在活动/非活动之间切换。
Navigating our app via code
从Blazor访问浏览器导航是通过NavigationManager
服务提供的。这可以使用razor文件中的@Inject
或CS文件中的[Inject]
属性注入到Blazor组件中。NavigationManager
服务有两个特别感兴趣的成员:NavigateTo
和LocationChanged
。LocationChanged
事件将在检测导航事件中详细说明。
The NavigateTo method
NavigationManager.NavigateTo
方法使C#代码能够控制浏览器的URL。与截取的导航一样,浏览器实际上并不导航到新的URL。取而代之的是替换浏览器中的URL,并将先前的URL插入到浏览器的导航历史记录中,但不会向服务器请求新页面的内容。通过NavigateTo
进行的导航将触发LocationChanged
事件,为IsNavigationIntercepted
传递新URL和false
。
对于本例,我们将再次更改标准Blazor模板。我们将使用前面在路由参数和可选路由参数中学到的内容。
首先,删除Index.razor
和FetchData.razor
页面,并删除NavMenu.razor
文件中指向这些页面的链接。同样在NavMenu中,将链接的href
更改为counter为href=""
,因为我们将使其成为默认页面。
编辑Counter.razor并为其提供两条路由,"/"
和"/count/{CurrentCount:int}"
,还要确保它是从CounterBase
类派生出来的,这样我们就可以在浏览器的控制台窗口中看到导航日志—前面在OnLocationChanged
部分概述了CounterBase.cs文件。
@page "/"
@page "/counter/{CurrentCount:int}"
@inherits CounterBase
我们还需要更改currentCount
字段,使其成为具有getter和setter的属性,并将其修饰为[Parameter]
。请注意,它也已从CamelCase重命名为PascalCase。
[Parameter]
public int CurrentCount { get; set; }
我们现在有了一个计数器页面,可以简单地访问应用程序的主页,也可以通过指定/Counter/X来访问,其中X是整数值。NavigationManager
被注入到我们的CounterBase
类中,因此可以在我们的Counter.razor文件中进行访问。
@code {
[Parameter]
public int CurrentCount { get; set; }
bool forceLoad;
void AlterBy(int adjustment)
{
int newCount = CurrentCount + adjustment;
UriHelper.NavigateTo("/counter/" + newCount, forceLoad);
}
}
我们将从两个按钮调用AlterBy
方法,一个用于递增CurrentCount
,另一个用于递减CurrentCount
。用户还可以选择一个选项forceLoad
,它将在NavigateTo
调用中设置相关参数,以便我们可以看到差异。整个文件最终应该如下所示:
@page "/"
@page "/counter/{CurrentCount:int}"
@implements IDisposable
@inject NavigationManager NavigationManager
<h1>Counter value = @CurrentCount</h1>
<div class="form-check">
<input @bind=@forceLoad type="checkbox" class="form-check-input" id="ForceLoadCheckbox" />
<label class="form-check-label" for="ForceLoadCheckbox">Force page reload on navigate</label>
</div>
<div class="btn-group" role="group">
<button @onclick=@( () => AlterBy(-1) ) class="btn btn-primary">-</button>
<input value=@CurrentCount readonly class="form-control" />
<button @onclick=@( () => AlterBy(1) ) class="btn btn-primary">+</button>
</div>
<a class="btn btn-secondary" href="/Counter/0">Reset</a>
<p>
<em>Page redirects to ibm.com when count hits 10!</em>
</p>
@code {
[Parameter]
public int CurrentCount { get; set; }
bool forceLoad;
void AlterBy(int adjustment)
{
int newCount = CurrentCount + adjustment;
if (newCount >= 10)
NavigationManager.NavigateTo("https://ibm.com");
NavigationManager.NavigateTo("/counter/" + newCount, forceLoad);
}
protected override void OnInitialized()
{
// Subscribe to the event
NavigationManager.LocationChanged += LocationChanged;
base.OnInitialized();
}
private void LocationChanged(object sender, LocationChangedEventArgs e)
{
string navigationMethod = e.IsNavigationIntercepted ? "HTML" : "code";
System.Diagnostics.Debug.WriteLine($"Notified of navigation via {navigationMethod} to {e.Location}");
}
void IDisposable.Dispose()
{
// Unsubscribe from the event when our component is disposed
NavigationManager.LocationChanged -= LocationChanged;
}
}
单击-
或+
按钮将调用AlterBy
方法,该方法将指示NavigationManager
服务导航到/counter/X
,其中X是调整后的CurrentCount
的值-在浏览器控制台中产生以下输出:
WASM: Notified of navigation via code to http://localhost:6812/counter/1
WASM: Notified of navigation via code to http://localhost:6812/counter/2
WASM: Notified of navigation via code to http://localhost:6812/counter/3
WASM: Notified of navigation via code to http://localhost:6812/counter/4
单击Reset链接将导致截获的导航(即,不是用C#代码启动的),并导航到/counter/0
,重置CurrentCount
的值。
WASM: Notified of navigation via code to http://localhost:6812/counter/1
WASM: Notified of navigation via code to http://localhost:6812/counter/2
WASM: Notified of navigation via code to http://localhost:6812/counter/3
WASM: Notified of navigation via code to http://localhost:6812/counter/4
WASM: Notified of navigation via HTML to http://localhost:6812/Counter/0
ForceLoad
forceLoad参数指示Blazor绕过其自己的路由系统,让浏览器实际导航到新的URL。这将导致向服务器发出HTTP请求,以检索要显示的内容。
请注意,导航到 off-site URL不需要强制加载。对另一个域调用NavigateTo
将调用完整的浏览器导航。
使用本节中的GitHub示例。在浏览器的控制台窗口中查看IsNavigationIntercepted
在通过按钮和重置链接导航时的不同之处,并在浏览器的网络窗口中查看根据您是否处于以下状态,它的行为有何不同:
- Navigating with
forceLoad
set tofalse
. - Navigating with
forceLoad
set totrue
. - Navigating to an off-site URL.
要观察最后一个场景,您可能希望更新AdustBy
方法,以便在CurrentValue
传递特定值时进行异地导航。
void AlterBy(int adjustment)
{
int newCount = CurrentCount + adjustment;
if (newCount >= 10)
NavigationManager.NavigateTo("https://ibm.com");
NavigationManager.NavigateTo("/counter/" + newCount, forceLoad);
}
Detecting navigation events
从Blazor访问浏览器导航是通过NavigationManager
服务提供的。这可以使用razor文件中的@Inject
或CS文件中的[Inject]
属性注入到Blazor组件中。
The LocationChanged event
LocationChanged
是每当浏览器中的URL发生更改时触发的事件。它传递一个LocationChangedEventArgs
实例,该实例提供以下信息:
public readonly struct LocationChangedEventArgs
{
public string Location { get; }
public bool IsNavigationIntercepted { get; }
}
Location
属性是浏览器中显示的完整URL,包括协议、路径和任何查询字符串。IsNavigationIntercepted
指示导航是通过代码还是通过HTML导航启动的。
false
导航是由代码调用的NavigationManager.NavigateTo
启动的。
true
用户单击HTML导航元素(如a href
),Blazor拦截导航,而不是允许浏览器实际导航到新的URL,这将导致对服务器的请求。在其他情况下也是如此,比如如果页面上的某些JavaScript
导致导航(例如,在超时之后)。最终,任何不是通过NavigationManager.NavigateTo
发起的导航事件都将被视为拦截的导航,并且此值将为true
。
注意:当前没有办法拦截导航并阻止其继续进行。
Observing OnLocationChanged events
需要注意的是,NavigationManager
服务是一个长期存在的实例。因此,在服务的生命周期内,订阅其LocationChanged
事件的任何组件都将被强引用。因此,重要的是,我们的组件在被销毁时也要取消订阅此事件,否则它们将不会被垃圾收集。
目前,ComponentBase
类没有销毁时间的生命周期事件,但可以实现IDisposable
接口。
@implement IDisposable
@inject NavigationManager NavigationManager
protected override void OnInitialized()
{
// Subscribe to the event
NavigationManager.LocationChanged += LocationChanged;
base.OnInitialized();
}
void LocationChanged(object sender, LocationChangedEventArgs e)
{
string navigationMethod = e.IsNavigationIntercepted ? "HTML" : "code";
System.Diagnostics.Debug.WriteLine($"Notified of navigation via {navigationMethod} to {e.Location}");
}
void IDisposable.Dispose()
{
// Unsubscribe from the event when our component is disposed
NavigationManager.LocationChanged -= LocationChanged;
}