Blazor

  • Blazor介绍

  • 可以在服务器上运行客户端逻辑,并且客户端UI事件使用Signalr发送回服务器的一个实时框架
  • Blazor官方介绍:ASP.NET Core Blazor | Microsoft Learn
    • Blazor Server在 ASP.NET Core 应用中支持在服务器上托管 Razor 组件。 可通过 SignalR 连接处理 UI 更新。SignalR又是基于websocket的,网络环境不好会断掉,长时间不操作也会断掉。
      • 网络的延迟会影响到Blazor的延迟问题
      • 场景:并发不高,用户访问不大的情况。
      • 适合调试,不需要下载整个运行时

Blazor Server 在服务器上运行 .NET 代码,并通过 SignalR 连接与客户端上的文档对象模型进行交互

    • Blazor WebAssembly,用于使用 .NET 生成交互式客户端 Web 应用。
      • 通过 WebAssembly(缩写为 ),可在 Web 浏览器内运行 .NET 代码。 WebAssembly 是针对快速下载和最大执行速度优化的压缩字节码格式。
      • 类似于纯前端 ,且安全性能高
      • 适合运行,会下载整个运行时
      • 优点:
        • 将应用程序加载到客户端设备后,UI交互反应快,因为应用程序的全部内容已下载到客户端设备或客户端
        • 由于应用程序在客户端下载的,因此应用程序可以离线工作(除了对服务器的API进行请求)
      • 缺点:
        • 初始加载需要耗费时间。必须将应用程序二进制文件下载到客户端。下载时间取决于应用程序二进制文件的大小。一旦下载完成,第二次以后应用程序可以更快的加载。
        • 界面切换的延时比较长,使用着就很难受。

Blazor WebAssembly 使用 WebAssembly 在浏览器中运行 .NET 代码。

    • 可以在网页->应用程序->Cache中看到要加载的二进制文件

  • WebAssembly vs Server
    • WebAssembly(WASM)是一种二进制指令格式,可以让代码在Web浏览器中以接近本地速度执行。它旨在通过允许开发人员使用除JavaScript之外的其他语言编写代码(例如C ++,Rust和Go),从而使复杂的Web应用程序运行得更快,更高效。简单说,Dll首次加载的时候,是通过服务器下载到浏览器中,然后全部在浏览器中运行。

    • Server是一种计算机程序,它提供服务并响应客户端的请求。服务器通常运行在远程计算机上,与客户端通过网络进行通信,可以提供各种服务,例如Web服务、文件共享、数据库服务等。

    • 因此,WebAssembly和服务器是不同类型的技术,它们服务于不同的目的。WebAssembly旨在提高Web应用程序的性能和效率,而服务器则旨在提供服务并响应客户端请求。在某些情况下,WebAssembly可以在服务器端使用,以提高服务器性能,但这不是其主要目的。

  • Blazor官方创建:Blazor 教程 - 生成首个应用 (microsoft.com)

  项目是在 Visual Studio 中创建和加载的。请使用解决方案资源管理器查看项目内容。

解决方案资源管理器包含为项目创建的文件夹和文件的列表

  已创建多个文件,以为你提供可运行的简单 Blazor 应用。

    • Program.cs 是启动服务器以及在其中配置应用服务和中间件的应用的入口点。
    • App.razor 为应用的根组件。
    • Pages 目录包含应用的一些示例网页。
    • BlazorApp.csproj 定义应用项目及其依赖项,且可以通过双击解决方案资源管理器中的 BlazorApp 项目节点进行查看。
    • Properties 目录中的 launchSettings.json 文件为本地开发环境定义不同的配置文件设置。创建项目时会自动分配端口号并将其保存在此文件上。
  • Blazor程序的运行 
    •  _Host.cshtml文件

    •  App.razor文件,这里主要是一个路由表的设置,并设置响应。

  • 细节
    • Titile占位符:_Layout.cshtml

    •  @RenderBody()

 @RenderBody() 是 Blazor WebAssembly 应用程序的核心功能之一。它指定一个布局页面,并将内容插入到该页面中。

在 Blazor 应用程序中,可以创建多个页面,并且每个页面都可以使用特定的布局。这些布局页面包含了页面中重复的HTML和结构,例如头部、导航菜单和页脚等,以减轻开发者的代码量和工作量。

要在布局页面中显示具体的内容,我们需要使用 @RenderBody()。它的作用就是定义主页面内容排版的区域,我们所编写的每个子页面在被呈现时,都会被加载到此处,并占据其所在位置。

 

路由

// 添加单个路由
<Router AppAssembly="@typeof(App).Assembly">
</Router>

// 在原来的路由基础上,添加多个路由
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new Assembly[]{typeof(App).Assembly}">
</Router>
    • 定义路由
      • 可以给一个razor定义多个路由
// 方式一:
@page "/blazor-route"

// 方式二:
@attribute [Route("/blazor-route")]
    • 路由报错
<NotFound>
    @*页面标题*@
    <PageTitle>Not found</PageTitle>
    @*布局绑定:布局页面*@
    <LayoutView Layout="@typeof(MainLayout)">
        <p role="alert">Sorry, there's nothing at this address.</p>
    </LayoutView>
</NotFound>
    • 路由参数
// 必选参数
@page "/{text}"
// 可选参数
@page "/{text?}"

// 定义参数
@code {
    [Parameter]
    public string? Text { get; set; }
}
    • 路由约束

// 必选类型
@page "/{text:int}"
// 可选类型
@page "/{text:int?}"

 

导航

  使用 NavigationManager 来来管理 URI 和导航。常用方法:

成员 描述
Uri 获取当前绝对URI
BaseUri 获取可在相对 URI 路径之前添加用于生成绝对 URI 的基 URI(带有尾部反斜杠)。 通常,BaseUri 对应于文档的 <base> 元素(<head> 内容的位置)上的 href 属性。
NavigateTo 导航到指定 URI。 如果 forceLoad 为 true,则:
  • 客户端路由会被绕过。
  • 无论 URI 是否通常由客户端路由器处理,浏览器都必须从服务器加载新页面。
如果 replace 为 true,则替换浏览器历史记录中的当前 URI,而不是将新的 URI 推送到历史记录堆栈中。
LocationChanged 导航位置更改时触发的事件。 有关详细信息,请参阅位置更改部分。
ToAbsoluteUri 将相对 URI 转换为绝对 URI。
ToBaseRelativePath 给定基 URI(例如,之前由 BaseUri 返回的 URI),将绝对 URI 转换为相对于基 URI 前缀的 URI。
RegisterLocationChangingHandler 注册一个处理程序来处理传入的导航事件。 调用 NavigateTo 始终调用处理程序。
GetUriWithQueryParameter 返回通过更新 NavigationManager.Uri 来构造的 URI(添加、更新或删除单个参数)。 有关详细信息,请参阅查询字符串部分。
  • 跳转
    • 通过修改URI的方式进行跳转
<button @oneclick=Goto></button>
@inject NavigationManager NavigationManager
@code{
    private void Goto()
    {
        // 参数二:默认false,不会刷新,只是显示UI
        //             true,会刷新UI,尽量少用,UI刷新多了,消耗性能
        NavigationManager.NavigateTo("/example/aidatd",false);
    }
}
  • 拼接参数
// 拼接参数
// /id?123
NavigationManager.GetUriWithQueryParameter("id","123");

  

配置

  官方介绍:ASP.NET Core Blazor 配置 | Microsoft Learn

 

依赖注入

  官方介绍:ASP.NET Core Blazor 依赖关系注入 | Microsoft Learn

  基于ASP.NET Core中的依赖注入实现的

服务 使用的生存期 描述
HttpClient 范围内

提供用于发送 HTTP 请求以及从 URI 标识的资源接收 HTTP 响应的方法。

Blazor WebAssembly 应用中 HttpClient 的实例由应用在 Program.cs 中注册,并使用浏览器在后台处理 HTTP 流量。

默认情况下,Blazor Server 应用不包含配置为服务的 HttpClient。 向 Blazor Server 应用提供 HttpClient。

HttpClient 注册为作用域服务,而不是单一实例。

IJSRuntime

Blazor WebAssembly (运行到浏览器):单例

Blazor Server (运行到服务器):作用域

Blazor 框架在应用的服务容器中注册 IJSRuntime。

 表示在其中调度 JavaScript 调用的 JavaScript 运行时实例。

试图在 Blazor Server 应用中将服务注入到单一实例服务时,请采用以下任一方法:

  • 将服务注册更改为限定为匹配 IJSRuntime 的注册,如果服务处理用户特定状态,那么这是合适的做法。
  • 将 IJSRuntime 作为方法调用的参数传递到单一实例服务的实现中,而不是将其注入到单一实例中。
NavigationManager Blazor WebAssembly(运行到浏览器) :单例

Blazor Server (运行到服务器):作用域

Blazor 框架在应用的服务容器中注册 NavigationManager。

 包含用于处理 URI 和导航状态的帮助程序。
  • 不能通过构造函数方式注入,只能通过@Inject注入对象
// 创建对象类
public class DemoService
{
    public string Demo { get; set; }
}

// 注入到服务
builder.Services.AddSingleton<DemoService>();

// 使用
@inject DemoService DemoService
@code{
    [Parameter]
    public string? PageRoute{ get; set; }
    private void Show()
    {
        PageRoute = DemoService.Demo;
        DemoService.Demo = "调试";
    }
}

  

异常处理

  官方介绍:处理 ASP.NET Core Blazor 应用中的错误 | Microsoft Learn

  要定义错误边界,请使用 ErrorBoundary 组件来包装现有内容。 

// ref:绑定组件
<ErrorBoundary @ref="errorBoundary">
    <ChildContent>
        <button @onclick=Show></button>
    </ChildContent>
    <ErrorContent>
        <p>Nothing to see here right now, Sorry!</p>
    </ErrorContent>
</ErrorBoundary>

@code{
    private ErrorBoundary? errorBoundary;
    private void Show()
    {
        errorBoundary.ErrorContent = builder => @<h1>异常页面</h1>;
    }
}

  

组件

  • 组件类
    • 组件的名称必须以大写字符开头
    • .razor文件都可以称之为组件。
    • 严格来说,继承自ComponentBase的都是组件
    • 如果在组件中加了路由,它就是一个页面。
  • 布局
    • 组件继承自 LayoutComponentBase。 LayoutComponentBase 为布局内呈现的内容定义 Body 属性(RenderFragment 类型)。
    • @Body 在布局标记中指定呈现内容的位置。
    • MainLayout组件:应用的默认组件
// 创建组件AppBar
@inject NavigationManager NavigationManager
<button onclick="Goto('/')">首页</button>
<button onclick="Goto('/function')">功能</button>
@code {
    private void Goto(string href)
    {
        NavigationManager.NavigateTo(href);
    }
}

// 创建页面home
@page "/home"
<h1>home</h1>

// 创建页面function
@page "/function"
<h1>function</h1>

// 在MainLayout使用组件
<div class="sidebar">
    <AppBar/>
</div>
  • 组件间传参
      1. 父、子组件间通信;
      2. 多级组件组件通信,例如祖、孙节点间通信;
      3. 非嵌套组件间通信。
    • 父与子的通讯
      • 父与子通信是最为简单的,直接通过数据绑定即可实现:
// 子组件
<div class="bg-white p-3" style="color: #000;">
    <h3>Son</h3>
    <p>parent: @Value</p>
</div>
@code {
    [Parameter]
    public string Value { get; set; }
}

// 父组件
<div class="bg-primary jumbotron  text-white">
    <h3>Parent</h3>
    <Son Value="I'm from Parent1"></Son>
</div>
    • 子与父的通讯
      • 子与父通信是通过回调事件实现的,通过将事件函数传递到子组件,子组件在数据发生变化时,调用该回调函数即可。
      • 为Son组件基础上添加一个事件OnValueChanged,并在数据Value发生变化时执行该事件,通知父组件新数据。
// 子组件
<div class="bg-white p-3" style="color: #000;">
    <h3>Son</h3>
    <button @onclick="ChangeValue">ChangeValue</button>
    <p>parent: @Value</p>
</div>
@code {
    [Parameter]
    public string Value { get; set; }

    [Parameter]
    public EventCallback<string> OnValueChanged { get; set; }

    private async Task ChangeValue()
    {
        string newValue = DateTime.Now.ToString("o");
        if (OnValueChanged.HasDelegate)
        {
            await OnValueChanged.InvokeAsync(newValue);
        }
    }
}

// 父组件
<div class="bg-primary jumbotron  text-white">
    <h3>Parent</h3>
    <p>@_value</p>
    <Son Value="@_value" OnValueChanged="@OnValueChanged"></Son>
</div>
@code{
    private string _value = "I'm from Parent2";

    private void OnValueChanged(string val)
    {
        _value = val;
    }
}
    • 多级组件通讯(组、孙组件通讯)
      • 正常是通过创建父节点中转实现祖-父-孙通信
      • 但是当跨越多个层级的时候就比较麻烦,好在Blazor提供了“Cascading values and parameters”,中文翻译为级联值和参数。
      • 级联值和参数是通过CascadingValue组件和CascadingParameter属性注解实现的。
      • 祖先组件使用 Blazor 框架的 CascadingValue 组件提供级联值,该组件包装组件层次结构的子树,并向其子树中的所有组件提供单个值。
      • 为了使用级联值,后代组件使用 [CascadingParameter] 特性来声明级联参数。 级联值按类型绑定到级联参数。
    • 祖与孙的通讯
      • CascadingParameter所声明的属性可以是private
      • CascadingValue和CascadingParameter可以不指定Name,这时将会通过类型进行匹配。
// 孙组件
<div class="bg-white p-3" style="color: #000;">
    <h3>Son</h3>
    <p>GrandValue: @GrandValue</p>
</div>

@code {
    /// <summary>
    /// Name参数必须与Name带有CascadingValue组件的属性匹配,如果我们没有注明Name,则会通过类型匹配一个最相似的属性
    /// </summary>
    [CascadingParameter(Name = "GrandValue")]
    string GrandValue { get; set; }
}

// 父组件(中间组件)
<div class="bg-primary jumbotron text-white">
    <h3>Parent</h3>
    <Son></Son>
</div>

// 祖组件
<h3>Grand</h3>
<p>GrandValue:@_grandValue</p>

<CascadingValue Value="@_grandValue" Name="GrandValue">
    <Parent/>
</CascadingValue>
@code {
    private string _grandValue = "GrandValue";
}
    • 祖与孙的通讯多个参数
      • 嵌套使用CascadingValue
      • CascadingValue组件运行嵌套使用,可以在祖组件中嵌套CascadingValue,而孙组件中则只需要将所有的来自祖组件的参数使用CascadingParameter进行声明即可。需要注意的是,如果指定Name,请确保每个Name都是唯一的。
// 孙组件
<div class="bg-white p-3" style="color: #000;">
    <h3>Son</h3>
    <p>GrandValue1: @GrandValue1</p>
    <p>GrandValue2: @GrandValue2</p>
</div>

@code {
    /// <summary>
    /// Name参数必须与Name带有CascadingValue组件的属性匹配,如果我们没有注明Name,则会通过类型匹配一个最相似的属性
    /// </summary>
    [CascadingParameter(Name = "GrandValue1")]
    string GrandValue1 { get; set; }
    [CascadingParameter(Name = "GrandValue2")]
    string GrandValue2 { get; set; }
}

// 父组件(中间组件)
<div class="bg-primary jumbotron text-white">
    <h3>Parent</h3>
    <Son></Son>
</div>

// 祖组件
<h3>Grand</h3>
<p>GrandValue1:@_grandValue1</p>
<p>GrandValue2:@_grandValue2</p>

<CascadingValue Value="@_grandValue1" Name="GrandValue1">
    <CascadingValue Value="@_grandValue2" Name="GrandValue2">
        <Parent/>
    </CascadingValue>
</CascadingValue>
@code {
    private string _grandValue1 = "GrandValue1";
    private string _grandValue2 = "GrandValue2";
}
      • 也可以使用Model类(不细讲)。CascadingValue可以是class,因此可以将所有的需要传递的参数使用一个class进行封装,然后传递到孙组件,孙组件使用同类型的class接收该参数即可。
    • 孙与祖通讯
      • 孙与祖通信与子与父通信一样,需要使用事件进行回调,这个回调方法也是一个参数,因此只需要将该回调也通过CascadingValue传递到孙组件中,当孙组件数据发生变化时调用该回调函数即可。
// 回调的封装
public class CascadingModel<T>
{
    public CascadingModel()
    {
    }

    public CascadingModel(T defaultValue)
    {
        _value = defaultValue;
    }
    public Action StateHasChanged;

    private T _value;
    public T Value
    {
        get => _value;
        set
        {
            _value = value;
            StateHasChanged?.Invoke();
        }
    }
}

// 孙组件
<div class="bg-white p-3" style="color: #000;">
    <h3>Son</h3>
    <p>GrandValueModel-GrandValue: @CascadingModel.Value</p>
    <button @onclick="ChangGrandValue">Chang GrandValue</button>
</div>

@code {
    [CascadingParameter(Name = "GrandValue")]
    CascadingModel<string> CascadingModel { get; set; }

    void ChangGrandValue()
    {
        CascadingModel.Value = "I'm Form self:"
            + DateTime.Now.ToString("HH:mm:ss");
    }
}

// 父组件
<div class="bg-primary jumbotron text-white">
    <h3>Parent</h3>
    <Son></Son>
</div>

// 祖组件
<h3>Grand</h3>
<p>GrandValue:@_cascadingModel.Value</p>

<CascadingValue Value="@_cascadingModel" Name="GrandValue">
    <Parent/>
</CascadingValue>

@code {
    private CascadingModel<string> _cascadingModel = new CascadingModel<string>("GrandValue");

    protected override void OnInitialized()
    {
        _cascadingModel.StateHasChanged += StateHasChanged;
        base.OnInitialized();
    }

    private void ChangeGrandValue()
    {
        _cascadingModel.Value = DateTime.Now.ToString("o");
    }
}
    • 非嵌套组件间通信
      • 非嵌套组件也就是说在渲染树中,任一组件无法向上或向下寻找到另外一个组件,例如兄弟组件、叔父组件等。
      • 最好的方式是使用服务注入。您可以创建一个服务,其中包含您要共享的状态或功能,并将其注入到需要访问该状态或功能的每个组件中。
// 公共服务类
public class MyService
{
    public event EventHandler<string> DataChanged;
    
    private string _data;
    
    public string Data 
    { 
        get => _data; 
        set
        {
            if (_data != value)
            {
                _data = value;
                DataChanged?.Invoke(this, value);
            }
        }
    }
}

// 在 Startup.cs 中将该服务注册为单例服务。
services.AddSingleton<MyService>();

// 在需要访问该服务的组件中注入该服务。
@inject MyService MyServiceInstance

// 在需要使用服务中的数据的组件中订阅 DataChanged 事件。
protected override void OnInitialized()
{
    MyServiceInstance.DataChanged += OnDataChanged;
}

private void OnDataChanged(object sender, string data)
{
    // 更新组件中的数据
    StateHasChanged();
}

public void Dispose()
{
    MyServiceInstance.DataChanged -= OnDataChanged;
}

// 在修改服务中的数据的组件中更新数据。
MyServiceInstance.Data = "New data";

  

数据绑定

官方介绍:ASP.NET Core Blazor 数据绑定 | Microsoft Learn

  • Blazor中Razor组件通过一个名为@bind的HTML元素属性提供数据绑定(双向绑定)功能,数据绑定的对象可以为字段、属性或表达式值。
  • @bind默认绑定的是元素的onchange事件,通过在组件中添加一个元素p可以看出效果,每当input离开focus或者回车时,p中的值才会更新
  • 相应input的oninput事件,@bind通过指定event参数来在指定绑定属性或字段响应的事件,
  • 如果要绑定字符串对象,需要在对象前@,其他对象可以不用加。
// 使用绑定
// 失去焦点时才触发
<div>
    <input @bind="value1"/>
</div>
// 使用onchange方法
// 失去焦点时才触发
<div>
    <input value="@value2" @onchange="(e)=>{value2=e.Value.ToString();}"/>
</div>
// 事件绑定
// 值变化就触发
<div>
    <input @bind="value3" @bind:event="oninput"/>
</div>
<div style="background-color: beige">
    <h2>@value1</h2>
    <h2>@value2</h2>
    <h3>@value3</h3>
</div>

@code{
    private string? value1;
    private string? value2;
    private string? value3;
}
  • 格式化字符串
    • 数据绑定通过 @bind:format="{FORMAT STRING}" 使用单个 DateTime 格式字符串,其中 {FORMAT STRING} 占位符是格式字符串。
<p>
    <code>yyyy-MM-dd</code> format:
    <input @bind="startDate" @bind:format="yyyy-MM-dd"/>
</p>

@code{
    private DateTime startDate = new DateTime(2020, 1, 1);
}
  • 组件间的事件绑定
    • 父组件到子组件
      • 通过使用@bind-{PROPERTY}的形式,可以将属性值从父组件向下绑定到子组件,其中PROPERTY必须为组件参数,
      • EventCallback<TValue> 必须以组件参数名称命名,并带有“Changed”后缀。
      • EventCallback.InvokeAsync 调用与提供的参数进行绑定相关联的委托,并为已更改的属性调度事件通知。
// 子组件
<input @bind="DateTime" @bind:format="yyyy-MM-dd HH:mm:ss" />
<p>@DateTime.ToString("o")</p>
[Parameter]
public DateTime DateTime { get; set; } = DateTime.Now;
[Parameter]
public EventCallback<DateTime> DateTimeChanged { get; set; }

// 父组件中使用
<Son @bind-DateTime="_dateTime" />
@code{
  private DateTime _dateTime;
}
    • 子组件到父组件
      • 子组件向父组件传递数据,则必须使用相应的回调事件。
// 子组件
<input @bind="DateTime" @bind:format="yyyy-MM-dd HH:mm:ss" @oninput="ChangeParentValue" />
<p>@DateTime.ToString("o")</p>
@code{
    [Parameter]
    public DateTime DateTime { get; set; } = DateTime.Now;
    /// <summary>
    /// 使用bind赋值,必须要有此事件:{property}Changed
    /// </summary>
    [Parameter]
    public EventCallback<DateTime> DateTimeChanged { get; set; }

    private async Task ChangeParentValue(ChangeEventArgs e)
    {
        var dateStr = e.Value.ToString();
        await DateTimeChanged.InvokeAsync(DateTime.Parse(dateStr));
    }
}

// 父组件
<div>
    <Son ChildEvents="(e)=>MyEvent(e)" />
    <p>@_Value</p>
</div>
@code{
    private string _Value;
    private void MyEvent(object Value)
    {
         Console.WriteLine($"Got the ChangeParentValue({Value})");
         _Value = Value.ToString();
    }
}  

  

组件生命周期

  官方介绍:ASP.NET Core Razor 组件生命周期 | Microsoft Learn

  • 生命周期是由于ComponentBase基类带来的
  • 分为三个阶段:初始化阶段、运行中阶段和销毁阶段,其相关方法有10个,包括设置参数前、初始化、设置参数之后、组件渲染后以及组件的销毁,但是这些方法有些是重复的,只不过是同步与异步的区别。

  • Blazor生命周期方法主要包括:
    • 分为两个阶段
      • 阶段一:只会执行一次

  Js的调用必须在OnParametersSet完成的Render下面

      • 阶段二:界面只要被再一次渲染,就会执行
1 SetParametersAsync 设置参数前(第一次触发) 传入参数ParameterView,这个类主要存储一个字典。用来保存键值对。用来存储标记了ParameterAttribute特性的属性和值
2 OnInitialized/OnInitializedAsync 初始化

OnInitializedAsync可能会导致页面两次刷新。OnInitialized不会。

异步的时候,代码块中获取数据,数据还没有获取到的时候,不能直接将对象赋值到界面中去

3 OnParametersSet/OnParametersSetAsync 设置参数后 

OnParametersSetAsync可能会导致两次触发

render-mode模式 = ServerPrerenderd ,会触发两次。

  • 应用场景:爬虫、互联网
  • 好处:程序一启动的时候就会渲染一次,第一次请求的时候,还会再渲染一次。

render-mode模式 = Server,只会触发一次

render-mode模式 = Static,就不会触发OnAfterRender/OnAfterRenderAsync

4 OnAfterRender/OnAfterRenderAsync 组件渲染呈现后  
5 ShouldRender 判断是否渲染组件  
6 Dispose 组件删除前  
7 StateHasChanged 通知组件渲染  
  • 输出显示

  • 在所有生命周期函数中,有以下需要注意的点:
  1. 前5种方法的声明都是virtual,除SetParametersAsync为public外,其他的都是protected。
  2. OnAfterRender/OnAfterRenderAsync方法有一个bool类型的形参firstRender,用于指示是否是第一次渲染(即组件初始化时的渲染)。
  3. 同步方法总是先于异步方法执行。
  4. Dispose函数需要通过使用@implements指令实现IDisposable接口来实现。
  5. StateHasChanged无法被重写,可以被显示调用,以便强制实现组件刷新(如果ShouldRender返回true,并且Blazor认为需要刷新);当组件状态更改时不必显示调用此函数,也可导致组件的重新渲染(如果ShouldRender返回true),因为其已经在ComponentBase内部的处理过程(第一次初始化设置参数时、设置参数后和DOM事件处理等)中被调用。
@code{
    // 1.设置参数
    public override Task SetParametersAsync(ParameterView parameters)
    {
        return base.SetParametersAsync(parameters);
    }
    // 2.初始化组件
    protected override void OnInitialized()
    {
        base.OnInitialized();
    }
    // 3.判断是否渲染
    protected override bool ShouldRender()
    {
        // true:渲染,false:不渲染
        return base.ShouldRender();
    }
    // 4.渲染后事件
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            // WebAssembly 可以在Initialized中使用Js互操,但是Server不行
            // Server 在Initialized的时候并没有在浏览器中,所以使用Js互操会报错
            // 可以在OnAfterRender判断 firstRender是否为第一次渲染,是就在里面写初始化逻辑
        }
        // 强制刷新
        // 同步
        StateHasChanged(); 
        // 异步
        InvokeAsync(StateHasChanged);
        base.OnAfterRender(firstRender);
    }
    // 5.销毁事件
    // 继承@implements IDisposable 得到组件销毁事件
    public void Dispose()
    {
    }
}
  • 渲染循序

  从上到下,从外到内(优先将父组件先渲染,后再渲染子组件)

// 由于先渲染父组件,所以Foos是空的。子组件再赋值,但是此时父组件已经渲染完成
<CascadingValue Value="Foos" IsFixed = "true">
    @*子组件,并且子组件会赋值Foos*@
    <Foo2Component></Foo2Component>
</CascadingValue>

@foreach(var i in Foos)
{
    <div>Comp @i</div>
}

  因此我们可以将父组件中的代码,再包一层空的子组件,使得其同一级别,从上到下渲染。

// 子组件RenderTemplate
@ChildContent
@code{
    [Parameter]
    public RenderFragment? ChildContent{ get; set; }
}

// 父组件重新使用
<CascadingValue Value="Foos" IsFixed = "true">
    @*子组件,并且子组件会赋值Foos*@
    <Foo2Component></Foo2Component>
</CascadingValue>

<RenderTemplate>
    // 现在大家都是子组件,渲染从上到下
    @foreach(var i in Foos)
    {
        <div>Comp @i</div>
    }
</RenderTemplate>

  

模板化组件

  官方介绍:ASP.NET Core Blazor 模板化组件 | Microsoft Learn

  • 模板化组件是接受一个或多个 UI 模板作为参数的组件,然后可将其用作组件呈现逻辑的一部分。 
  • 通过指定一个或多个 RenderFragment 或 RenderFragment<TValue> 类型的组件参数来定义模板化组件。
// 定义组件TemplatePage
<h1 style="background-color:antiquewhite">@Tab</h1>
<div style="background-color:antiquewhite">@Content</div>
<div>@Trailer</div>

@code{
    // 头部
    [Parameter]
    public RenderFragment Tab{ get; set; }
    // 内容
    [Parameter]
    public RenderFragment Content { get; set; }
    [Parameter]
    // 尾部
    public RenderFragment Trailer { get; set; }
}

// 使用组件
<TemplatePage>
    <Tab>
        这是标题内容
    </Tab>
    <Content>
        这是内容。。。。。
    </Content>
    <Trailer>
        这是尾部内容
    </Trailer>
</TemplatePage>
  • 在使用模板化组件时,可以使用与参数名称匹配的子元素来指定模板参数。
// 定义组件 TemplatePage
@typeparam TItem
@using System.Diagnostics.CodeAnalysis

<table class="table">
    <thead>
        <tr>@Tab</tr>
    </thead>
    <tbody>
        @foreach (var item in Items)
        {
            if (RowTemplate is not null)
            {
                <tr>@RowTemplate(item)</tr>
            }
        }
    </tbody>
</table>

@code{
    // 头部
    [Parameter]
    public RenderFragment Tab{ get; set; }
    // 行模板
    [Parameter]
    public RenderFragment<TItem> RowTemplate  { get; set; }
    // 内容
    [Parameter, AllowNull]
    public IReadOnlyList<TItem> Items  { get; set; }
}

// 使用,通过Context获取RenderFragment泛型对象TItem的实例,也就是这里的pet。也可以不写Context="",直接使用@context.就行
<TemplatePage Items="pets" Context="pet">
    <Tab>
        <th>ID</th>
        <th>Name</th>
    </Tab>
    <RowTemplate>
        <td>@pet.PetId</td>
        <td>@pet.Name</td>
    </RowTemplate>
</TemplatePage>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 4, Name = "Salem Saberhagen" },
        new Pet { PetId = 7, Name = "K-9" }
    };
    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
  • 模板化组件时,泛型套用模板化组件
// 定义组件 TemplateComponent
@if (Template != null)
{
    @Template(ChildContent)
}
else
{
    @ChildContent
}

@code {
    [parameter]
    public RenderFragment<RenderFragment>? Template { get; set; }

    [parameter]
    [NotNull]
    public RenderFragment? ChildContent { get; set; }
}

// 使用
<TemplateComponent>
    <Template>
        <div>Header</div>
        <div>@Context</div>
        <div>Footer</div>
    </Template>
    <ChildContent>
        <div>Body</div>
    </ChildContent>
</TemplateComponent>

  

 

 

CSS隔离

  官方介绍:ASP.NET Core Blazor CSS 隔离 | Microsoft Learn

  •  在相同文件夹中创建一个 .razor.css 文件,该文件与组件的 .razor 文件的名称相匹配。 

 

  •   <link> 元素将添加到从 Blazor 项目模板创建的应用
<link href="CssRazor.css" rel="stylesheet"/>
  • 使用
<Home></Home>

  

动态呈现组件

  • DynamicComponent 适用于呈现组件,而无需循环访问可能的类型或使用条件逻辑。
// 创建组件DataInfo
<h3>DataInfo</h3>

// 创建组件Home
<h3>Home</h3>

// 使用
@using CssRazor

<button @onclick="Home">Home</button>
<button @onclick="DataInfo">DataInfo</button>
<div>
    <DynamicComponent Type="ComponentType"></DynamicComponent>
</div>
@code{
    private Type ComponentType = typeof(Home);

    private void Home()
    {
        ComponentType = typeof(Home);
    }
    private void DataInfo()
    {
        ComponentType = typeof(DataInfo);
    }
}
  • 动态渲染并传递参数
// 创建组件DataInfo
<h3>DataInfo:@Title</h3>

@code {
    [Parameter]
    public string Title { get; set; }
}

// 创建组件Home
<h3>Home:@HomeData</h3>

@code {
    [Parameter]
    public string HomeData { get; set; }
}

// 使用
@using CssRazor

<button @onclick="Home">Home</button>
<button @onclick="DataInfo">DataInfo</button>
<div>
    <DynamicComponent Type="ComponentType" Parameters="Parameters"></DynamicComponent>
</div>
@code{
    private Type ComponentType = typeof(Home);
    public Dictionary<string,object> Parameters{ get; set; }

    private void Home()
    {
        Parameters = new Dictionary<string, object>();
        Parameters.Add("HomeData", "HomeData123");
        ComponentType = typeof(Home);
    }
    private void DataInfo()
    {
        Parameters = new Dictionary<string, object>();
        Parameters.Add("Title", "Title123");
        ComponentType = typeof(DataInfo);
    }
}

  

RenderFragment

  Blazor RenderFragment 是一种特殊的委托,用于将 HTML 和组件呈现到页面上。RenderFragment 可以看作是一个代表可呈现内容的函数,它在组件渲染时被调用,并将可呈现内容作为参数传递给该函数。在 Blazor 组件中,RenderFragment 通常用于将子组件或其他可呈现内容呈现到页面上。

  • RenderFragment 具有如下特点:
  1. RenderFragment 是一个委托类型,它接受一个参数,这个参数是一个 RenderTreeBuilder 对象。RenderTreeBuilder 是 Blazor 内部使用的一种用于构建渲染树的对象。
  2. 在 RenderFragment 中,可以使用 RenderTreeBuilder 对象来构建渲染树,以实现组件的呈现。
  3. RenderFragment 可以用于将组件作为子组件传递给其他组件。这是 Blazor 中组件间通信的一种方式,允许开发者在组件中嵌套其他组件,以实现更加复杂的界面。
  4. RenderFragment 还可以用于动态生成组件的呈现内容,例如根据数据源动态生成列表或表格等。
// 创建Home组件
@using Microsoft.AspNetCore.Components.RenderTree
@using Microsoft.AspNetCore.Components.Rendering
<h3>Home</h3>
@ChildContent

@code {
    [Parameter]
    public RenderFragment ChildContent{ get; set; }

    protected override void OnInitialized()
    {
        base.OnInitialized();

        var builder = new RenderTreeBuilder();
        builder.AddContent(0, this.ChildContent);
        var frame = builder.GetFrames().Array.FirstOrDefault(x => new[]
        {
            RenderTreeFrameType.Text,
            RenderTreeFrameType.Component
        }.Any(t => x.FrameType == t));
    }
}

// 使用Home组件
<Home>
    <Counter/>
</Home>

  

模式(render-mode)

  • 有两种渲染模式:
    • 服务器预渲染(ServerPrerendered):将组件呈现静态html,并包含blazor服务器端应用程序的标记。当用户代理启动时,它使用该标记来引导Blazor应用程序。
      • 在没渲染之前,localStorage和sessionStorage不可用
      • 在刷新页面的时候,自定义身份验证状态提供程序类的“GetAuthenticationStateAsync”中会收到错误。因为应用程序无法处理将用于访问会话IJSRuntime
    • 服务器(Server):为blazor服务器应用程序呈现标记。当用户代理启动时,它使用此标记来引导Blazor应用程序。
      • Server用来解禁ServerPrerenderd

 

Blazor Server 身份验证和授权

  官方说明:ASP.NET Core Blazor 身份验证和授权 | Microsoft Learn

  • 自定义CustomAuthenticationStateProvider
    • Blazor Server 应用内置的 AuthenticationStateProvider (提供有关当前用户的身份验证状态的信息。)服务可从 ASP.NET Core 的 HttpContext.User 获取身份验证状态数据。
    • 请勿在 Blazor Server 应用的 Razor 组件中直接或间接使用 IHttpContextAccessor/HttpContext。 
    • AuthenticationStateProvider 是 AuthorizeView 组件和 CascadingAuthenticationState 组件用于获取用户身份验证状态的基础服务。
// 创建验证对象
public class UserSession
{
    public string UserName { get; set;}
    public string Role { get; set;}
}

// 自定义身份认证
// 获取身份状态
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
    // Storage,用来村粗保护登陆用户的信息
    private readonly ProtectedSessionStorage _sessionStorage;
    private ClaimsPrincipal _principal = new ClaimsPrincipal(new ClaimsIdentity());

    public CustomAuthenticationStateProvider(ProtectedSessionStorage sessionStorage)
    {
        _sessionStorage = sessionStorage;
    }
    // 获取Authentication关联的List<ClaimsPrincipal>
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        try
        {
            // "UserSession": Storage的令牌的key
            var userSessionStorageResult = await _sessionStorage.GetAsync<UserSession>("UserSession");
            var userSession = userSessionStorageResult.Success ? userSessionStorageResult.Value : null;
            if (userSession == null)
            {
                return await Task.FromResult(new AuthenticationState(_principal));
            }
            var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
            {
                new Claim(ClaimTypes.Name, userSession.UserName),
                new Claim(ClaimTypes.Role, userSession.Role)
            }, "CustomAuth"));
            return await Task.FromResult(new AuthenticationState(claimsPrincipal));
        }
        catch
        {
            return await Task.FromResult(new AuthenticationState(_principal));
        }
    }
    // 更新UserSession到Storage中
    // 有值就更新,没值就清空
    public async Task UpdateAuthenticationState(UserSession userSession)
    {
        ClaimsPrincipal claimsPrincipal;

        if(userSession != null)
        {
            await _sessionStorage.SetAsync("UserSession", userSession);
            claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
            {
                new Claim(ClaimTypes.Name, userSession.UserName),
                new Claim(ClaimTypes.Role, userSession.Role)
            }));
        }
        else
        {
            await _sessionStorage.DeleteAsync("UserSession");
            claimsPrincipal = _principal;
        }
        // 将身份验证和声明一起传递到
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
    }
}
  • 注入Authentication服务
// 添加Storage服务
builder.Services.AddScoped<ProtectedSessionStorage>();
// 添加Authentication服务
builder.Services.AddAuthenticationCore();
// 添加自定义Authentication服务
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
  • 添加数据对象(源)以及注入数据服务
// 登陆实体对象
public class UserAccount
{
    public string UserName { get; set; }
    public string Password { get; set; }
    public string Role { get; set; }
}

// Dto
public class UserAccountDto
{
    public string UserName { get; set; }
    public string Password { get; set; }
}

// 服务(数据源)
public class UserAccountService
{
    private List<UserAccount> _users;

    public UserAccountService()
    {
        _users = new List<UserAccount>
        {
            new UserAccount{UserName="admin",Password="admin",Role="Administrator"},
            new UserAccount{UserName="user",Password="user",Role="user"},
        };
    }

    public UserAccount? GetByUserName(string userName)
    {
        return _users.FirstOrDefault(x=>x.UserName == userName);
    }
}

// 添加Server服务
builder.Services.AddSingleton<UserAccountService>();
  • 资源授权
    • 若要授权用户访问资源,请将请求的路由数据传递到 AuthorizeRouteView 的 Resource 参数。
    • 需将将App组将(App.razor)中请求的路由组件改为<AuthorizeRouteView>
    • 使用 App 组件中的 AuthorizeRouteView 和 CascadingAuthenticationState 组件来设置 Task<AuthenticationState> 级联参数。
    • <NotAuthorized>组件表示权限认证失败
    • <NotAuthorizing>组件表示权限认证中
// ---------- Index.razor ----------

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" >
                <NotAuthorized>
                    You are not authorized(This is a custom message)
                </NotAuthorized>
                <Authorizing>
                    You are getting authorized ( This is a custom message)
                </Authorizing>
            </AuthorizeRouteView>
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </Found>
        <NotFound>
            <PageTitle>Not found</PageTitle>
            <LayoutView Layout="@typeof(MainLayout)">
                <p role="alert">Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>
  • 获取权限并公开身份验证状态作为级联参数
    • 定义类型为 Task<AuthenticationState> 的级联参数来获取身份验证状态数据
    • 使用 App 组件中的 AuthorizeRouteView 和 CascadingAuthenticationState 组件来设置 Task<AuthenticationState> 级联参数。
    • AuthorizeView 组件公开了一个 AuthenticationState 类型的 context 变量(Razor 语法中的 @context),可以使用该变量来访问有关已登录用户的信息
    • <Authorized>组件表示权限认证成功。常用于异步身份验证期间显示
    • <NotAuthorized>组件表示权限认证失败
// ---------- Index.razor ----------
@page "/"

<AuthorizeView>
    <Authorized>
        <h1>Hello,@context.User.Identity.Name</h1>
    </Authorized>
    <NotAuthorized>
        <h1>Hello,Guest!</h1>
    </NotAuthorized>
</AuthorizeView>

<AuthorizeView>
    <Authorized>
        <br /><br />
        <button class="btn btn-outline-primary" @onclick="DisplayGreetingAlert">Display Greeting Alter</button>
    </Authorized>
</AuthorizeView>

@inject IJSRuntime js;
@code{
    [CascadingParameter]
    private Task<AuthenticationState> authenticationState{ get; set; }

    private async Task DisplayGreetingAlert()
    {
        var authState = await authenticationState;
        var message = $"Hello {authState.User.Identity.Name}";
        await js.InvokeVoidAsync("alter", message);
    }
}
  • 登出并撤销授权
// ---------- MainLayout.razor ----------
@inherits LayoutComponentBase

<PageTitle>BlazorServerAuthenticationAndAuthorization</PageTitle>

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">

            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
            <AuthorizeView>
                <Authorized>
                    <a @onclick="Logout" href="javascript:void(0)">Logout</a>
                </Authorized>
                <NotAuthorized>
                    <a href="/login">Login</a>
                </NotAuthorized>
            </AuthorizeView>
        </div>

        <article class="content px-4">
            @Body
        </article>
    </main>
</div>


@using BlazorServerAuthenticationAndAuthorization.Authentication;
@inject AuthenticationStateProvider authStateProvider;
@inject NavigationManager navManager
@code {
    private async Task Logout()
    {
        var customAuthStateProvider = (CustomAuthenticationStateProvider)authStateProvider;
        await customAuthStateProvider.UpdateAuthenticationState(null);
        navManager.NavigateTo("/", true);
    }
}
  • 基于角色和基于策略的授权
    • AuthorizeView 组件支持基于角色或基于策略的授权 。对于基于角色的授权,请使用 Roles 参数。常用于NavMenu上
// ---------- NavMenu.razor ----------
<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">BlazorServerAuthenticationAndAuthorization</a>
        <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
            <span class="navbar-toggler-icon"></span>
        </button>
    </div>
</div>

<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </div>

        <AuthorizeView Roles="Administrator,User">
            <Authorized>
                <div class="nav-item px-3">
                    <NavLink class="nav-link" href="counter">
                        <span class="oi oi-plus" aria-hidden="true"></span> Counter
                    </NavLink>
                </div>
            </Authorized>
        </AuthorizeView>
        <AuthorizeView Roles="Administrator">
            <Authorized>
                <div class="nav-item px-3">
                    <NavLink class="nav-link" href="fetchdata">
                        <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
                    </NavLink>
                </div>
            </Authorized>
        </AuthorizeView>
    </nav>
</div>

@code {
    private bool collapseNavMenu = true;

    private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}
  • [Authorize] 特性
    • [Authorize] 特性在 Razor 组件中可用:常用于界面上
    • 仅对通过 Blazor 路由器到达的 @page 组件使用 [Authorize]。 授权仅作为路由的一个方面执行,而不是作为页面中呈现的子组件来执行。
// ---------- FetchData.razor ----------
@page "/fetchdata"
@using BlazorServerAuthenticationAndAuthorization.Data
@inject WeatherForecastService ForecastService
@*允许管理员授权属性:锁整个界面*@
@attribute [Authorize(Roles = "Administrator")]

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now));
    }
}

  

 

posted @ 2023-04-10 10:35  怒弹你鸟  阅读(181)  评论(0编辑  收藏  举报