目录
概述
代码和示例
Blazor应用程序
应用组件
路由视图服务
路由视图管理器
动态布局
动态路由
自定义路由数据
更新RouteViewService
RouteNotFoundManager 组件
在没有路由的情况下切换RouteView
ViewData
RouteViewManager
我们已经看到了_renderComponentWithParameters。使用有效的_ViewData对象,它使用_ViewData。
示例页面
RouteViewer.razor
Form.Razor
总结
概述App是Blazor UI根组件。本文着眼于它的工作原理并演示如何:
- 添加动态布局——在运行时更改默认布局
- 添加动态路由——在运行时添加和删除额外的路由
- 添加动态RouteViews——无需路由直接更改RouteView组件
代码和示例
这个项目的存储库在这里,它基于我的Blazor AllInOne模板。
您可以在https://cec-blazor-database.azurewebsites.net/从突出显示的链接查看在我的Blazor.Database站点上运行的组件的演示。
Blazor应用程序App通常在App.razor中定义。在Web程序集和服务器上下文中使用相同的组件。
在Web Assembly上下文中,SPA启动页面包含一个元素占位符,当Program在Web Assembly上下文中启动时,会替换该元素占位符。
....
Loading...
...
在Program中定义替换的代码行是:
// Replace the app id element with the component App
builder.RootComponents.Add("#app");
在服务器上下文中,App直接在Razor标记中声明为Razor组件。它由服务器预呈现,然后由浏览器中的Blazor服务器客户端更新。
...
...
App代码如下所示。它是一个标准的Razor组件,继承自ComponentBase。
Router是本地根组件并设置AppAssembly为包含Program的程序集。 在初始化时,它会为Assembly搜索具有Route属性的所有类,并在NavigationManager服务上注册NavigationChanged事件。在导航事件中,它尝试将导航URL与路由匹配。如果找到,则渲染Found渲染片段,否则渲染NotFound。
Sorry, there's nothing at this address.
Found内声明RouteView。RouteData设置为路由器的当前routeData对象并且DefaultLayout设置为应用程序布局Type。RouteView将一个RouteData.Type实例作为一个组件呈现在特定页面布局或默认布局中,并应用RouteData.RouteValues中的任何参数。
NotFound包含一个LayoutView组件,指定一个布局来呈现任何子内容。
路由视图服务RouteViewService是新组件的状态管理服务。它在WASM和服务器服务中注册。服务器版本可以是Singleton或Scoped,具体取决于应用程序需求。您可以有两个单独的服务来分别管理应用程序和用户上下文。
public class RouteViewService
{
....
}
在服务器,它加入到Startup的ConfigServices中。
services.AddSingleton();
在Web Assembly上下文中,它被添加到Program中。
builder.Services.AddScoped();
RouteViewManager替换RouteView。
它是实现RouteView的功能。它太大而无法完整显示,因此我们将分部分查看关键功能。
当路由事件发生时,RouteViewManager.RouteData更新并且Router重新渲染。Renderer在RouteViewManager上调用SetParametersAsync,传递更新的参数。SetParametersAsync检查它是否具有有效的RouteData,设置_ViewData为null并呈现组件。_ViewData设置为null以确保组件加载路由。在渲染过程中,有效ViewData对象优先于有效RouteData对象。
public await Task SetParametersAsync(ParameterView parameters)
{
// Sets the component parameters
parameters.SetParameterProperties(this);
// Check if we have either RouteData or ViewData
if (RouteData == null)
{
throw new InvalidOperationException($"The {nameof(RouteView)}
component requires a non-null value for the parameter {nameof(RouteData)}.");
}
// we've routed and need to clear the ViewData
this._ViewData = null;
// Render the component
await this.RenderAsync();
}
Render使用InvokeAsync确保render事件在正确的线程上下文中运行。_RenderEventQueued确保Renderer的队列中只有一个render事件。
public async Task RenderAsync() => await InvokeAsync(() =>
{
if (!this._RenderEventQueued)
{
this._RenderEventQueued = true;
_renderHandle.Render(_renderDelegate);
}
}
);
对于那些好奇的人,InvokeAsync看起来像这样:
protected Task InvokeAsync(Action workItem) => _renderHandle.Dispatcher.InvokeAsync(workItem);
RouteViewManager内容构建为一组组件,每个组件都定义在一个RenderFragment。
_renderDelegate定义本地根组件,级联自身并添加_layoutViewFragment片段作为它的ChildContent。
private RenderFragment _renderDelegate => builder =>
{
// We're being executed so no longer queued
_RenderEventQueued = false;
// Adds cascadingvalue for the ViewManager
builder.OpenComponent(0);
builder.AddAttribute(1, "Value", this);
// Get the layout render fragment
builder.AddAttribute(2, "ChildContent", this._layoutViewFragment);
builder.CloseComponent();
};
_layoutViewFragment选择布局,添加它并设置_renderComponentWithParameters为它的ChildContent。
private RenderFragment _layoutViewFragment => builder =>
{
Type _pageLayoutType =
RouteData?.PageType.GetCustomAttribute()?.LayoutType
?? RouteViewService.Layout
?? DefaultLayout;
builder.OpenComponent(0);
builder.AddAttribute(1, nameof(LayoutView.Layout), _pageLayoutType);
builder.AddAttribute(2, nameof(LayoutView.ChildContent), _renderComponentWithParameters);
builder.CloseComponent();
};
_renderComponentWithParameters选择要渲染的视图/路由组件并使用提供的参数添加它。有效视图优先于有效路由。
private RenderFragment _renderComponentWithParameters => builder =>
{
Type componentType = null;
IReadOnlyDictionary parameters = new Dictionary();
if (_ViewData != null)
{
componentType = _ViewData.ViewType;
parameters = _ViewData.ViewParameters;
}
else if (RouteData != null)
{
componentType = RouteData.PageType;
parameters = RouteData.RouteValues;
}
if (componentType != null)
{
builder.OpenComponent(0, componentType);
foreach (var kvp in parameters)
{
builder.AddAttribute(1, kvp.Key, kvp.Value);
}
builder.CloseComponent();
}
else
{
builder.OpenElement(0, "div");
builder.AddContent(1, "No Route or View Configured to Display");
builder.CloseElement();
}
};
开箱即用的Blazor布局是在编译时定义和修复的。@Layout是Razor的对话,当Razor被预编译为:
[Microsoft.AspNetCore.Components.LayoutAttribute(typeof(MainLayout))]
[Microsoft.AspNetCore.Components.RouteAttribute("/")]
[Microsoft.AspNetCore.Components.RouteAttribute("/index")]
public partial class Index : Microsoft.AspNetCore.Components.ComponentBase
....
要动态更改布局,我们使用RouteViewService来存储布局。它可以从任何注入服务的组件中设置。
public class RouteViewService
{
public Type Layout { get; set; }
....
}
RouteViewManager中的_layoutViewFragment选择布局——优先设置在默认布局之上的RouteViewService.Layout。
private RenderFragment _layoutViewFragment => builder =>
{
Type _pageLayoutType =
RouteData?.PageType.GetCustomAttribute()?.LayoutType
?? RouteViewService.Layout
?? DefaultLayout;
builder.OpenComponent(0);
builder.AddAttribute(1, nameof(LayoutView.Layout), _pageLayoutType);
builder.AddAttribute(2, nameof(LayoutView.ChildContent), _renderComponentWithParameters);
builder.CloseComponent();
};
演示页面中演示了布局的更改。
动态路由动态路由稍微复杂一些。Router是一个密封的盒子,所以要么拿走要么重写。除非你必须,不要重写它。我们不打算更改现有路由,只是添加和删除新的动态路由。
路由在编译时定义并在Router组件内部使用。
RouteView Razor Pages的标签如下:
@page "/"
@page "/index"
这是Razor对话,在预编译时会在C#类中转换为以下内容。
[Microsoft.AspNetCore.Components.RouteAttribute("/")]
[Microsoft.AspNetCore.Components.RouteAttribute("/index")]
public partial class Index : Microsoft.AspNetCore.Components.ComponentBase
.....
当Router初始化时,它搜索提供的所有程序集,并构建组件/路由对的路由字典。
您可以获得如下所示的路由属性组件列表:
static public IEnumerable
GetTypeListWithCustomAttribute(Assembly assembly, Type attribute)
=> assembly.GetTypes().Where(item =>
(item.GetCustomAttributes(attribute, true).Length > 0));
在初始化渲染时, Router会向NavigationManager.LocationChanged事件注册一个委托。此委托查找路由并触发Router。如果它找到了一条路由,它会渲染Found,其渲染我们的新RouteViewManager 。RouteViewManager构建布局并添加RouteData中定义的组件的新实例。
当它找不到路由时,会发生什么取决于事件LocationChangedEventArgs提供的IsNavigationIntercepted属性:
- True 如果它拦截了DOM中的导航——锚点等。
- True如果UI组件调用其NavigateTo方法并设置ForceLoad
- False如果UI组件调用其NavigateTo方法并设置ForceLoad
如果我们可以避免在Router中引起硬导航事件,我们可以在NotFound中添加一个组件来处理额外的动态路由。不太难,就是我们的代码!有一个增强的NavLink控件来帮助控制导航——稍后介绍。如果发生硬导航事件,路由仍然有效,但应用程序会重新加载。在测试期间应检测并修复任何恶意导航事件。
自定义路由数据CustomRouteData保存做出路由决策所需的信息。这个类看起来像这样,带有内嵌的详细解释。
public class CustomRouteData
{
/// The standard RouteData.
public RouteData RouteData { get; private set; }
/// The PageType to load on a match
public Type PageType { get; set; }
/// The Regex String to define the route
public string RouteMatch { get; set; }
/// Parameter values to add to the Route when created name/defaultvalue
public SortedDictionary ComponentParameters
{ get; set; } = new SortedDictionary();
/// Method to check if we have a route match
public bool IsMatch(string url)
{
// get the match
var match = Regex.Match(url, this.RouteMatch,RegexOptions.IgnoreCase);
if (match.Success)
{
// create new dictionary object to add to the RouteData
var dict = new Dictionary();
// check we have the same or fewer groups as parameters to map the to
if (match.Groups.Count >= ComponentParameters.Count)
{
var i = 1;
// iterate through the parameters and add the next match
foreach (var pars in ComponentParameters)
{
string matchValue = string.Empty;
if (i < match.Groups.Count)
matchValue = match.Groups[i].Value;
// Use a StypeSwitch object to do the Type Matching
// and create the dictionary pair
var ts = new TypeSwitch()
.Case((int x) =>
{
if (int.TryParse(matchValue, out int value))
dict.Add(pars.Key, value);
else
dict.Add(pars.Key, pars.Value);
})
.Case((float x) =>
{
if (float.TryParse(matchValue, out float value))
dict.Add(pars.Key, value);
else
dict.Add(pars.Key, pars.Value);
})
.Case((decimal x) =>
{
if (decimal.TryParse(matchValue, out decimal value))
dict.Add(pars.Key, value);
else
dict.Add(pars.Key, pars.Value);
})
.Case((string x) =>
{
dict.Add(pars.Key, matchValue);
});
ts.Switch(pars.Value);
i++;
}
}
// create a new RouteData object and assign it to the RouteData property.
this.RouteData = new RouteData(this.PageType, dict);
}
return match.Success;
}
/// Method to check if we have a route match and return the RouteData
public bool IsMatch(string url, out RouteData routeData)
{
routeData = this.RouteData;
return IsMatch(url);
}
}
对于那些感兴趣的人,TypeSwitch看起来像这:
/// =================================
/// Author: stackoverflow: cdiggins
/// ==================================
public class TypeSwitch
{
public TypeSwitch Case(Action action) { matches.Add(typeof(T),
(x) => action((T)x)); return this; }
private Dictionary matches =
new Dictionary();
public void Switch(object x) { matches[x.GetType()](x); }
}
RouteViewService中更新后的部分如下所示。Routes保存自定义路由的列表——它故意打开以进行自定义。
public List Routes { get; private set; } = new List();
public bool GetRouteMatch(string url, out RouteData routeData)
{
var route = Routes?.FirstOrDefault(item => item.IsMatch(url)) ?? null;
routeData = route?.RouteData ?? null;
return route != null;
}
RouteNotFoundManager是一个简单的RouteViewManager版本。
在组件加载时调用SetParametersAsync。它获取本地地址,在RouteViewService上调用GetRouteMatch,并呈现组件。如果没有布局,它只会呈现ChildContent。
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
// Get the route url
var url = $"/{NavManager.Uri.Replace(NavManager.BaseUri, "")}";
// check if we have a custom route and if so use it
if (RouteViewService.GetRouteMatch(url, out var routedata))
_routeData = routedata;
// if The layout is blank show the ChildContent without a layout
if (_pageLayoutType == null)
_renderHandle.Render(ChildContent);
// otherwise show the route or ChildContent inside the layout
else
_renderHandle.Render(_ViewFragment);
return Task.CompletedTask;
}
_ViewFragment要么呈现一个RouteViewManager, 如果找到自定义路由,则设置RouteData,要么呈现RouteNotFoundManager的内容。
/// Layouted Render Fragment
private RenderFragment _ViewFragment => builder =>
{
// check if we have a RouteData object and if so load the RouteViewManager,
// otherwise the ChildContent
if (_routeData != null)
{
builder.OpenComponent(0);
builder.AddAttribute(1, nameof(RouteViewManager.DefaultLayout), _pageLayoutType);
builder.AddAttribute(1, nameof(RouteViewManager.RouteData), _routeData);
builder.CloseComponent();
}
else
{
builder.OpenComponent(0);
builder.AddAttribute(1, nameof(LayoutView.Layout), _pageLayoutType);
builder.AddAttribute(2, nameof(LayoutView.ChildContent), this.ChildContent);
builder.CloseComponent();
}
};
在没有路由的情况下切换RouteView有几个应用程序。这些是我用过的一些:
- 隐藏对页面的直接访问。它只能在应用程序内访问。
- 具有单个入口点的多部分表单/流程。保存的表单/流程的状态决定了加载哪个表单。
- 上下文相关的形式或信息。登录/注销/注册就是一个很好的例子。相同的URL,但根据上下文加载了不同的路由视图。
相当于RouteData。
public class ViewData
{
/// Gets the type of the View.
public Type ViewType { get; set; }
/// Gets the type of the page matching the route.
public Type LayoutType { get; set; }
/// Parameter values to add to the Route when created
public Dictionary
ViewParameters { get; private set; } = new Dictionary();
/// Constructs an instance of .
public ViewData(Type viewType, Dictionary viewValues = null)
{
if (viewType == null) throw new ArgumentNullException(nameof(viewType));
this.ViewType = viewType;
if (viewValues != null) this.ViewParameters = viewValues;
}
}
所有功能都在RouteViewManager中。
RouteViewManager首先是一些属性和字段。
/// The size of the History list used for Views.
[Parameter] public int ViewHistorySize { get; set; } = 10;
/// Gets and sets the view data.
public ViewData ViewData
{
get => this._ViewData;
protected set
{
this.AddViewToHistory(this._ViewData);
this._ViewData = value;
}
}
/// Property that stores the View History. It's size is controlled by ViewHistorySize
public SortedList ViewHistory { get; private set; } =
new SortedList();
/// Gets the last view data.
public ViewData LastViewData
{
get
{
var newest = ViewHistory.Max(item => item.Key);
if (newest != default) return ViewHistory[newest];
else return null;
}
}
/// Method to check if is the current View
public bool IsCurrentView(Type view) => this.ViewData?.ViewType == view;
/// Boolean to check if we have a View set
public bool HasView => this._ViewData?.ViewType != null;
/// Internal ViewData used by the component
private ViewData _ViewData { get; set; }
接下来,一组LoadViewAsync方法提供了加载新视图的多种方式。main方法设置内部viewData字段并调用Render重新渲染组件。
// The main method
public await Task LoadViewAsync(ViewData viewData = null)
{
if (viewData != null) this.ViewData = viewData;
if (ViewData == null)
{
throw new InvalidOperationException($"The {nameof(RouteViewManager)}
component requires a non-null value for the parameter {nameof(ViewData)}.");
}
await this.RenderAsync();
}
public async Task LoadViewAsync(Type viewtype)
=> await this.LoadViewAsync(new ViewData(viewtype, new Dictionary()));
public async Task LoadViewAsync(Dictionary data = null)
=> await this.LoadViewAsync(new ViewData(typeof(TView), data));
private RenderFragment _renderComponentWithParameters => builder =>
{
Type componentType = null;
IReadOnlyDictionary parameters = new Dictionary();
if (_ViewData != null)
{
componentType = _ViewData.ViewType;
parameters = _ViewData.ViewParameters;
}
else if (RouteData != null)
{
componentType = RouteData.PageType;
parameters = RouteData.RouteValues;
}
if (componentType != null)
{
builder.OpenComponent(0, componentType);
foreach (var kvp in parameters)
{
builder.AddAttribute(1, kvp.Key, kvp.Value);
}
builder.CloseComponent();
}
else
{
builder.OpenElement(0, "div");
builder.AddContent(1, "No Route or View Configured to Display");
builder.CloseElement();
}
};
RouteNavLink是一种增强NavLink控制。该代码是带有少量添加代码的直接副本。它不会继承,因为NavLink是一个黑匣子。它确保导航是通过NavigationManager而不是Html锚链接,并提供对RouteView加载的直接访问。代码在Repo中——在这里复制太长了。
示例页面该应用程序具有RouteViews/Pages来演示新组件。您可以查看Repo中的源代码。您还可以查看演示站点上的页面。
RouteViewer.razor
Blazr.Database.Web
这表明:
- 向应用程序动态添加路由。选择要为其添加自定义路由的页面,添加路由名称,然后单击转到路由。
- 加载一个RouteView没有导航。选择一个页面并点击Go To View。页面显示了,但URL没有改变!令人困惑,但它演示了原理。
- 更改默认布局。单击红色布局,布局将变为红色。基础的FetchData定义了特定的布局,因此它将使用原始布局。单击“正常布局”以更改回来。
Blazr.Database.Web
这演示了一个多部分形式。有四种形式:
- Form.Razor 基本形式和第一形式
- Form2.Razor 第二种形式——从第一种形式继承
- Form3.Razor 第三种形式——从第一种形式继承
- Form4.Razor 结果表单——从第一个表单继承
表单链接到维护表单状态的WeathForecastService中的数据。尝试在中途离开表单然后返回。在维护SPA会话的同时保留State。
总结希望我已经演示了可用于将额外功能构建到核心Blazor框架中的原则。没有一个组件是成品。使用它们并根据需要开发它们。
https://www.codeproject.com/Articles/5299797/Adding-Dynamic-Routing-Layouts-and-RouteViews-to-t