将动态路由、布局和RouteViews添加到Blazor应用程序组件

目录

概述

代码和示例

Blazor应用程序

应用组件

路由视图服务

路由视图管理器

动态布局

动态路由

自定义路由数据

更新RouteViewService

RouteNotFoundManager 组件

在没有路由的情况下切换RouteView

ViewData

RouteViewManager

我们已经看到了_renderComponentWithParameters。使用有效的_ViewData对象,它使用_ViewData。

示例页面

RouteViewer.razor

Form.Razor

总结


概述

AppBlazor UI根组件。本文着眼于它的工作原理并演示如何:

  1. 添加动态布局——在运行时更改默认布局
  2. 添加动态路由——在运行时添加和删除额外的路由
  3. 添加动态RouteViews——无需路由直接更改RouteView组件

 

代码和示例

这个项目的存储库在这里,它基于我的Blazor AllInOne模板

您可以在https://cec-blazor-database.azurewebsites.net/从突出显示的链接查看在我的Blazor.Database站点上运行的组件的演示。

Blazor应用程序

App通常在App.razor定义。在Web程序集和服务器上下文中使用相同的组件。

Web Assembly上下文中,SPA启动页面包含一个元素占位符,当ProgramWeb Assembly上下文中启动时,会替换该元素占位符。

....
<body>
    <div id="app">Loading...</div>
    ...
</body>

Program中定义替换的代码行是:

// Replace the app id element with the component App
builder.RootComponents.Add<App>("#app");

在服务器上下文中,App直接在Razor标记中声明为Razor组件。它由服务器预呈现,然后由浏览器中的Blazor服务器客户端更新。

...
<body>
    <component type="typeof(Blazor.App)" render-mode="ServerPrerendered" />
...
</body>

应用组件

App代码如下所示。它是一个标准的Razor组件,继承自ComponentBase

Router是本地根组件并设置AppAssembly为包含Program的程序集。 在初始化时,它会为Assembly搜索具有Route属性的所有类,并在NavigationManager服务上注册NavigationChanged事件。在导航事件中,它尝试将导航URL与路由匹配。如果找到,则渲染Found渲染片段,否则渲染NotFound

<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

Found内声明RouteViewRouteData设置为路由器的当前routeData对象并且DefaultLayout设置为应用程序布局TypeRouteView将一个RouteData.Type实例作为一个组件呈现在特定页面布局或默认布局中,并应用RouteData.RouteValues中的任何参数。

NotFound包含一个LayoutView组件,指定一个布局来呈现任何子内容。

路由视图服务

RouteViewService是新组件的状态管理服务。它在WASM和服务器服务中注册。服务器版本可以是SingletonScoped,具体取决于应用程序需求。您可以有两个单独的服务来分别管理应用程序和用户上下文。

public class RouteViewService 
{
  ....
}

在服务器,它加入到StartupConfigServices中。

services.AddSingleton<RouteViewService>();

Web Assembly上下文中,它被添加到Program中。

builder.Services.AddScoped<RouteViewService>();

路由视图管理器

RouteViewManager替换RouteView

它是实现RouteView的功能。它太大而无法完整显示,因此我们将分部分查看关键功能。

当路由事件发生时,RouteViewManager.RouteData更新并且Router重新渲染。RendererRouteViewManager上调用SetParametersAsync,传递更新的参数SetParametersAsync检查它是否具有有效的RouteData,设置_ViewDatanull并呈现组件。_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<CascadingValue<RouteViewManager>>(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<LayoutAttribute>()?.LayoutType
        ?? RouteViewService.Layout
        ?? DefaultLayout;

    builder.OpenComponent<LayoutView>(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<string, object> parameters = new Dictionary<string, object>();

    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布局是在编译时定义和修复的。@LayoutRazor的对话,当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<LayoutAttribute>()?.LayoutType
        ?? RouteViewService.Layout
        ?? DefaultLayout;

    builder.OpenComponent<LayoutView>(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<Type> 
    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属性:

  1. True 如果它拦截了DOM中的导航——锚点等。
  2. True如果UI组件调用其NavigateTo方法并设置ForceLoad
  3. 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<string, object> ComponentParameters
           { get; set; } = new SortedDictionary<string, object>();

    /// 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<string, object>();
            //  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<T>(Action<T> action) { matches.Add(typeof(T), 
                          (x) => action((T)x)); return this; }
        private Dictionary<Type, Action<object>> matches = 
                          new Dictionary<Type, Action<object>>();
        public void Switch(object x) { matches[x.GetType()](x); }
    }

更新RouteViewService

RouteViewService中更新后的部分如下所示。Routes保存自定义路由的列表——它故意打开以进行自定义。

public List<CustomRouteData> Routes { get; private set; } = new List<CustomRouteData>();

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 组件

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<RouteViewManager>(0);
        builder.AddAttribute(1, nameof(RouteViewManager.DefaultLayout), _pageLayoutType);
        builder.AddAttribute(1, nameof(RouteViewManager.RouteData), _routeData);
        builder.CloseComponent();
    }
    else
    {
        builder.OpenComponent<LayoutView>(0);
        builder.AddAttribute(1, nameof(LayoutView.Layout), _pageLayoutType);
        builder.AddAttribute(2, nameof(LayoutView.ChildContent), this.ChildContent);
        builder.CloseComponent();
    }
};

在没有路由的情况下切换RouteView

在没有路由的情况下切换RouteView有几个应用程序。这些是我用过的一些:

  1. 隐藏对页面的直接访问。它只能在应用程序内访问。
  2. 具有单个入口点的多部分表单/流程。保存的表单/流程的状态决定了加载哪个表单。
  3. 上下文相关的形式或信息。登录/注销/注册就是一个很好的例子。相同的URL,但根据上下文加载了不同的路由视图。

ViewData

相当于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<string, object> 
    ViewParameters { get; private set; } = new Dictionary<string, object>();

    /// Constructs an instance of <see cref="ViewData"/>.
    public ViewData(Type viewType, Dictionary<string, object> 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<DateTime, ViewData> ViewHistory { get; private set; } = 
                                      new SortedList<DateTime, ViewData>();

/// 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 <param name="view"> 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<string, object>()));

public async Task LoadViewAsync<TView>(Dictionary<string, object> data = null)
    => await this.LoadViewAsync(new ViewData(typeof(TView), data));

我们已经看到了_renderComponentWithParameters。使用有效的_ViewData对象,它使用_ViewData

private RenderFragment _renderComponentWithParameters => builder =>
{
    Type componentType = null;
    IReadOnlyDictionary<string, object> parameters = new Dictionary<string, object>();

    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

这表明:

  1. 向应用程序动态添加路由。选择要为其添加自定义路由的页面,添加路由名称,然后单击转到路由
  2. 加载一个RouteView没有导航。选择一个页面并点击Go To View。页面显示了,但URL没有改变!令人困惑,但它演示了原理。
  3. 更改默认布局。单击红色布局,布局将变为红色。基础的FetchData定义了特定的布局,因此它将使用原始布局。单击正常布局以更改回来。

Form.Razor

Blazr.Database.Web

这演示了一个多部分形式。有四种形式:

  1. Form.Razor 基本形式和第一形式
  2. Form2.Razor 第二种形式——从第一种形式继承
  3. Form3.Razor 第三种形式——从第一种形式继承
  4. Form4.Razor 结果表单——从第一个表单继承

表单链接到维护表单状态的WeathForecastService中的数据。尝试在中途离开表单然后返回。在维护SPA会话的同时保留State

总结

希望我已经演示了可用于将额外功能构建到核心Blazor框架中的原则。没有一个组件是成品。使用它们并根据需要开发它们。

https://www.codeproject.com/Articles/5299797/Adding-Dynamic-Routing-Layouts-and-RouteViews-to-t

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值