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 eventNavigationManager.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 disposedNavigationManager.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
forceLoadset tofalse. - Navigating with
forceLoadset 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 NavigationManagerprotected override void OnInitialized(){// Subscribe to the eventNavigationManager.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 disposedNavigationManager.LocationChanged -= LocationChanged;}
