Skip to content

Blazor

基础

路由

## 8.0 开始,可能不一样了,看下具体手册 

<!-- 1. 扫描并注册所有的 Page 组件, 使用 AdditionalAssemblies 加载其它程序集中的组件   -->
<Router AppAssembly="@typeof(App).Assembly" 
            AdditionalAssemblies="new[] { typeof(Component1).Assembly }">> 
    <Found Context="routeData">
        <!-- 2. 如果有对应的路由, 使用 RouteView 加载页面,并指定一个模板 -->
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        <!-- 3. 设置页面导航后的焦点 -->
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <!-- 3. 如果没有对应的路由,使用 LayoutView 加载一个页面,并指定一个模板 -->
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(MainLayout)">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>


// 页面中使用 @page "/BlazorRoute" 指定路由,可以多次指定

页面参数

@page "/xpage/{text?}"

<!-- 1. 使用text 值  -->
<h3>Xpage @Text</h3>

@code {

    //2. 定义一个参数从 routeData 中接收参数 
    [Parameter]
    public string? Text { get; set; }

    // 3. 页面初始化时,如果没有指定值,则默认设置一个 
    protected override Task OnInitializedAsync()
    {
        this.Text = this.Text ?? @"fantastic"; // 页面初始化时, 指定默认参数值 
        return base.OnInitializedAsync();
    }

    // 3. 参数绑定函数,可能在页面 /xpage/xxxxx 导航至不同参数时,OnInitializedAsync 只会调用一次,OnParametersSetAsync
    protected override Task OnParametersSetAsync()
    {
        this.Text = this.Text ?? @"fantastic"; // 页面初始化时, 指定默认参数值 
        return base.OnParametersSetAsync();
    }

}
参数约束
@page "/user/{Id:int}/{Option:bool?}"


> bool  {active:bool}   true, FALSE
> datetime  {dob:datetime}  2016-12-31, 2016-12-31 7:32pm   
> decimal   {price:decimal} 49.99, -1,000.01
> double    {weight:double} 1.234, -1,001.01e8
> float {weight:float}  1.234, -1,001.01e8
> guid  {id:guid}   CD2C1638-1638-72D5-1638-DEADBEEF1638, {CD2C1638-1638-72D5-1638-DEADBEEF1638}
> int   {id:int}    123456789, -123456789
> long  {ticks:long}    123456789, -123456789

路由中带 .

默认带 . 号的认为是一个文件,需要特别设置,请见手册 ASP.NET Core Blazor 路由和导航 | Microsoft Docs

所有路由匹配
使用 {*pageRoute} , 比如 

@page "/catch-all/{*pageRoute}" // 所有 /catch-all/xxx/xxx 

@code {
    [Parameter]
    public string PageRoute { get; set; }
}
导航管理
@inject NavigationManager NavigationManager // 注入导航管理器

    // 使用
    NavigationManager.LocationChanged += HandleLocationChanged;
    NavigationManager.NavigateTo("counter");
    NavigationManager.Uri
    NavigationManager.BaseUri

    // 获取导航参数 
    var query = new Uri(NavigationManager.Uri).Query;



页面导航中 ...
@using Microsoft.AspNetCore.Components.Routing

<!-- 在页面导航中显示  -->
<Navigating>
    <p>Loading the requested page&hellip;</p>
</Navigating>

在 Rout 中管理导航事件


<Router AppAssembly="@typeof(App).Assembly" OnNavigateAsync="OnNavigateAsync">
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext context)
    {
        if (context.Path == "/about") 
        {
            var stats = new Stats = { Page = "/about" };
            await WebRequestMethods.Http.PostAsJsonAsync("api/visited", stats, 
                context.CancellationToken);

            context.CancellationToken.ThrowIfCancellationRequested();

        }
    }
}

NavLink 和 NavMenu 可以使用 Match 属性根据当前链接设置不同的 active css 类

配置

配置路径 `wwwroot/appsettings.json`.wwwroot/appsettings.{ENVIRONMENT}.json

该配置对用户可见, 如果指定其它配置文件,需要在 Program.cs 中 调用 httpclinet 动态载入 ( ASP.NET Core Blazor 配置 | Microsoft Docs

可以合适 MemoryConfigurationSource 在 Program.cs 中配置内存参数


@page "/configuration-example"
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration // 注入 IConfiguration 使用

<h1 style="font-size:@Configuration["h1FontSize"]">
    Configuration example
</h1>

身份验证

{
  "Local": {
    "Authority": "{AUTHORITY}",
    "ClientId": "{CLIENT ID}"
  }
}

Program.cs 中 加载身份验证库 https://docs.microsoft.com/zh-cn/aspnet/core/blazor/security/webassembly/standalone-with-authentication-library?view=aspnetcore-5.0&tabs=visual-studio

builder.Services.AddOidcAuthentication(options => builder.Configuration.Bind("Local", options.ProviderOptions));

日志配置

// add  Microsoft.Extensions.Logging.Configuration

// wwwroot/appsettings.json:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

// Program.cs
using Microsoft.Extensions.Logging;
builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));

// 页面中
@inject ILogger<Xpage> logger

注入

默认注入服务

HttpClient IJSRuntime NavigationManager

注入

var builder = WebAssemblyHostBuilder.CreateDefault(args);
# 注入
# 范围 Scoped(同 Singleton ), Singleton 唯一实例, Transient 每次生成新实例
builder.Services.AddSingleton<IExampleDependency, ExampleDependency>();
await builder.Build().RunAsync();

# 使用
var host = builder.Build();

var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeWeatherAsync();


# 组件中使用 1
@inject IDataAccess DataRepository # 使用注入语法

# 组件中使用 2, 通常在基类中这么使用,并且子类不再需要 @inject
[Inject]
protected IDataAccess DataRepository { get; set; }

# 更多注入对象生存期管理,见,我也没看明白
https://docs.microsoft.com/zh-cn/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-5.0

启动

## .net 8 中可能不一样了,要注意一下
# 可以设置手动启用,并在启动过程中调用函数控制加载的程序集, 
# 1. 设置 autostart = false
# 2. 运行 Blazor.start
# 3. 定义 loadBootResource 函数定制加载过程,https://docs.microsoft.com/zh-cn/aspnet/core/blazor/fundamentals/startup?view=aspnetcore-5.0 
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
<script>
    document.addEventListener("DOMContentLoaded", function() {
        // 加入自定义函数
        Blazor.start.then(function () { console.log("启动"); }).catch(function (err) { console.log("失败" + err); });
    });
</script>

环境

设置

# 1. js 代码加载时设置, 见《启动》部分
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
<script>
  if (window.location.hostname.includes("localhost")) {
    Blazor.start({
      environment: "Staging"
    });
  } else {
    Blazor.start({
      environment: "Production"
    });
  }
</script>

# 2. 发布的 web.config 中定义, https://docs.microsoft.com/zh-cn/aspnet/core/blazor/fundamentals/environments?view=aspnetcore-5.0
<add name="Blazor-Environment" value="Staging" />

读取

# program.cs 中
builder.HostEnvironment.Environment == "Custom"、
builder.HostEnvironment.IsStaging()
builder.HostEnvironment.IsEnvironment("Custom")

# 组件中
## 注入
@inject IWebAssemblyHostEnvironment HostEnvironment 

## 读取
<p>Environment: @HostEnvironment.Environment</p>

日志

# 注入
@inject ILogger<Counter> logger
@inject ILoggerFactory LoggerFactory

# 使用
logger.LogWarning("Someone has clicked me!");
var logger = LoggerFactory.CreateLogger<Counter>();
logger.LogWarning("Someone has clicked me!");

自定义日志输出格式: ASP.NET Core Blazor 日志记录 | Microsoft Docs

错误处理

默认处理方式

# 默认在 wwwroot/index.html 定义如下代码处理错误信息
## 会在界面上显示一个错误提示,开发模式下跳转至控制台,发布模式下提示进行刷新 
<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

通用组件处理方式

写一个通用的组件包在所有组件的最外层,并在组件中定义一个通用的异常处理函数。在页面出错时调用该函数 。操作如下:

写一个 Error.arzor 的共享页面, 然后在 App.arzor 中将 Error 包在 Router 外面。这样所有的组件都可以调用 Error 中的处理函数了

Error.arzor
<!-- 全局错误处理组件 -->

@using Microsoft.Extensions.Logging
@inject ILogger<Error> Logger

<!-- 这里相当于 DataContext 将处理组件自身也传了进去  -->    
<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {

    // 内部呈现组件
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    // 这里全局进行错误处理
    public void ProcessError(Exception exception)
    {
        // 进行错误记录, todo: 也可以使用 api 记录到服务器数据库中
        Logger.LogError("Error:ProcessError - Type: {Type} Message: {Message}", 
            exception.GetType(), exception.Message);

        // 处理完错误后, 刷新一下页面数据 
        this.StateHasChanged();
    }
}
App.arzor
<Error>
    <Router AppAssembly="@typeof(App).Assembly"
            OnNavigateAsync="OnNavigateAsync">
       <!-- 这里为其它参数 -->
    </Router>
</Error>
调用页面
@code {

    [CascadingParameter]
    public Error Error { get; set; }

    private void IncrementCount()
    {
        try
        {
            throw new NotImplementedException("我是一个错误");
        }
        catch (Exception exception)
        {
            Error.ProcessError(exception);
        }
    }
}

BlazorSignalR

ASP.NET Core SignalR 概述 | Microsoft Docs

静态文件

发布的静态资源默认位于根目录下,可以通过修改 .csproj 文件中的 StaticWebAssetBasePath 值进行更新

<PropertyGroup>
  <StaticWebAssetBasePath>app1</StaticWebAssetBasePath>
</PropertyGroup>

基路径

# index.html 中设置
<base href="/CoolApp/">

# program.cs 中设置
app.UsePathBase("/CoolApp");

# 启动参数中设定
dotnet run --pathbase=/CoolApp

组件

  • 必须以大写字母开头

  • 路由时自动过滤 - 字符,比如 @page "/product-detail" 路由至 ProductDetail 组件

  • 组件全路径为 项目名.路径名.组件名

  • 如果代码不使用 @code 嵌入在组件中,可以使用同名 partial class 部分类

  • 在 html 中嵌入 @ 表达式时推荐使用 引号,而且页面表达式是同步的,不能使用 @await

  • @code 异步函数不能直接返回 void 需要 返回 task

  • 子组件的 Parameter 参数使用 get;set 而且不要带有处理逻辑,因为可能会引用循环引用之类的操作 。 要转换参数在 OnParametersSetAsyncOnInitialized 中进行

  • 子组件中的 Parameter 特别注意,有时需要使用一个私有变量保存状,因为父组件调用 StateHasChanged() 时可能会重新子组件的状态

  • 页面中默认使用同步方式调用函数。

  • 组件中调用 await this.InvokeAsync(() => { }); 调度回组件线程上下文

ASP.NET Core Razor 组件 | Microsoft Docs 路由参数这一节

样例

  1. 指定路径

  2. 指定基类

  3. 引用子组件

  4. @ 引用变量

  5. @code 引用代码

  6. 定义变量

  7. 输出子内容 RenderFragment

  8. @attributes 使用展开参数

  9. CaptureUnmatchedValues 捕获所有未定义参数

  10. @ref 引用组件 , 自动生成字段变量, 生成的变量请在 OnAfterRenderAsync 调用后使用

  11. @attribute [Authorize] 给组件设置属性

  12. 使用 MarkupString 显示原始 HTML ,避免使用

  13. 使用 RenderFragment 显示片段

  14. @preservewhitespace true 不忽略代码中出现的空格, 也可以加在 中 _Imports.razor 作为全局使用

@page "/markup" <!-- 1. 指定路径  -->
@inherits BlazorRocksBase <!-- 2. 指定基类 -->
@attribute [Authorize] <!-- 11. 给组件设置属性 -->
@preservewhitespace true <!-- 14  不忽略代码中出现的空格 -->



    <!-- 3. 引用 Heading.razor, 并传入组件属性值 (Title, Body) -->
    <!-- 10. @ref 引用组件,自动生成字段变量 -->
    <Heading 
            Title="test title" 
            Body="@(new PanelBody() { Text = "Set by parent.", Style = "italic" })"
            @ref="heading">
    <p>我是子组件</p>
    </Heading>

<!-- 4. 使用 @ 引用变量 -->
<h1 style="font-style:@headingFontStyle">@headingText</h1>

<!-- 12. 显示原始 html 或 csv  -->
@((MarkupString)myMarkup)

<!-- 13. 显示片断 -->
@timeTemplate
@petTemplate(new Pet { Name = "Nutty Rex" })

<!-- 5. 定义代码 -->
@code { 
    <!-- 6. 变量默认为 private,可以不用指定 -->
    private string headingFontStyle = "italic"; 

    <!-- 10 @ref 自动生成的字段变量 -->
    private Heading heading ;

    <!-- 12. 显示原始 html 或 csv  -->
    private string myMarkup =
        "<p class=\"text-danger\">This is a dangerous <em>markup string</em>.</p>";

    <!-- 13. 显示片断 -->
    private RenderFragment timeTemplate = @<p>The time is @DateTime.Now.</p>;
    private RenderFragment<Pet> petTemplate = (pet) => @<p>Pet: @pet.Name</p>;
} 


<!-- 3. 定义子组件 -->
==========   Heading.razor 子组件 ======================
    <!-- 4. @ 输出 title 变量 -->
    <div class="card-header font-weight-bold">@Title</div>

    <!-- 4. @ 输出 body 对象变量 -->
    <div class="card-body" style="font-style:@Body.Style">
        @Body.Text
    </div>

    <!-- 7. 输出子内容 -->
    <div class="card-body">@ChildContent</div>

    <!-- 8. 参数展开, 如果对一个元素的多个属性赋值,那么可以定义一个字典指定 @attributes 属性  -->
    <input @attributes="InputAttributes" />



</div>

@code {
    <!-- 定义一个子组件变量 -->
    [Parameter]
    public string Title { get; set; } = "Set By Child";

    <!-- 定义一个对象为子组件变量 -->
    [Parameter]
    public PanelBody Body { get; set; } =
        new()
        {
            Text = "Set by child.",
            Style = "normal"
        };

    <!-- 7. RenderFragment 表示支持子内容, 变量名 ChildContent 约定值  -->
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    <!-- 8. 展开属性,这里 key 为 属性 name, value 为属性 value  -->
    private Dictionary<string, object> InputAttributes { get; set; } =
        new()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };

    <!-- 9. 作为子组件时,父组件传入的参数值,如果没有在子组件中定义参数值 ,可以使用 CaptureUnmatchedValues 都捕获住 -->    
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}

使用 @key 保存页面缓存

数据刷新时,页面也会跟着刷新 。但是一些列表类组件没有必要进行刷新。可以使用 @key 指定列表项与某个对象绑定,只有对象变化时该项才会跟着变化

@key 在同 个父节点下相互影响 ,。值可以使用对象或唯一值 (int, string, Guid)。@key 不要冲突。会有发生成本

<Details @key="person" Data="@person.Data" />

<!-- 如果 person 更新, 则 li 及后代都被放弃并重新生成 --->
<li @key="person">
    <input value="@person.Data" />
</li>


<!--- !!!! 避免这么使用,@key 之间可能会没有关联  ---->
@foreach (var person in people)
{
    <div>
        <Details @key="person" Data="@person.Data" />
    </div>
}

泛型

泛型子组件

@typeparam TExample // 指定泛型参数

@if (ExampleList is not null)
{
    <ul>
        @foreach (var item in ExampleList)
        {
            <li>@item</li>
        }
    </ul>
}

@code {
    [Parameter]
    public IEnumerable<TExample> ExampleList{ get; set; }
}

调用泛型组件

@page "/generic-type-example-1"

<h1>Generic Type Example 1</h1>

<ListGenericTypeItems1 ExampleList="@(new List<string> { "Item 1", "Item 2" })" TExample="string" />

<ListGenericTypeItems1 ExampleList="@(new List<int> { 1, 2, 3 })"  TExample="int" />

布局组件

继承自 LayoutComponentBase, 使用 [RenderFragment ] @Body 属性设置子组件

可嵌套使用

@inherits LayoutComponentBase  <!-- 继承自 LayoutComponentBase -->

<!-- 渲染 Body 组件 -->
   <div class="content px-4">
            @Body
        </div>

使用

在页面中引用
@page "/episodes"
@layout DoctorWhoLayout <!-- 指定使用的布局组件 -->

<!-- 以下部分作为 body 渲染 -->

<h2>Episodes</h2>

<ul>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vfknq">
            <em>The Ribos Operation</em>
        </a>
    </li>
</ul>
在 _Imports.razor 中引用

设置在每个文件夹中的 _Imports.razor 中自动作用于该文件夹中 请不要在根 _Imports.razor 中引用 @layout 会引发无限循环

@layout DoctorWhoLayout 
在 Router 中引用
// App.razor
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <p>Sorry, there's nothing at this address.</p>
    </NotFound>
</Router>


级联值和参数

父页面向子页面传参, 感觉像是 wpf 中的 dataContext ,

父页面

@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">

    <div class="main">
        <!-- 定义 CascadingValue 的值  -->
        <CascadingValue Value="@theme">
            <div class="content px-4">
                @Body
            </div>
        </CascadingValue>
    </div>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}


子页面

@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<p>
    <!-- 应用接收到的参数  -->
    <button class="btn @ThemeInfo.ButtonClass" @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    <!-- 定义  CascadingParameter 属性接收参数  -->
    [CascadingParameter]
    protected ThemeInfo ThemeInfo { get; set; }
}


传入多个值

如果要传入多个值,可以对每个值命名

<!-- 上级传传 -->
<CascadingValue Value="@parentCascadeParameter1" Name="CascadeParam1">
    <CascadingValue Value="@ParentCascadeParameter2" Name="CascadeParam2">
        ...
    </CascadingValue>
</CascadingValue>

<!-- 子级接收 -->
@code {
    [CascadingParameter(Name = "CascadeParam1")]
    protected CascadingType ChildCascadeParameter1 { get; set; }

    [CascadingParameter(Name = "CascadeParam2")]
    protected CascadingType ChildCascadeParameter2 { get; set; }
}

可以跨级传递

参数值可以跨级传递

一个例子

这是一个多页 tab 切换的例子, 分为三个部分

TabItem.razor tab 中的某一页, 供 TabSet 引用

TabSet.razor tab 控件,包含所 TabItem

TabPage.razor TabSet 调用例子

关掉是了一些是否为 null 是否为自身的判断

TabItem.razor
<!-- 1. 显示一个 li 用来在 TabSet 中作为列表头 -->
<!-- 2. ChildContent 接收页面中设置的 body 内容, 但是不显示,以后会在 TabSet 中显示,见 TabSet.razor 中的 @ActiveTab?.ChildContent 部分  -->
<!-- 3. ContainerTabSet 接收 TabSet 传入的参数(本例中会传入 TabSet 本身) -->
<!-- 4. 点击 li>a 时调用 TabSet 函数将自身传入以切换至自己代表的 tab  -->
<li>
    <a @onclick="ActivateTab" class="nav-link @TitleCssClass" role="button">
        @Title
    </a>
</li>

@code {
    // 上层传入的 dataContext
    [CascadingParameter] public TabSet ContainerTabSet { get; set; }
    // 上层传入的标题值
    [Parameter] public string Title { get; set; }
    // 上层传入默认 Body 的值 
    [Parameter] public RenderFragment ChildContent { get; set; }
    // 自动设置是否选中
    private string TitleCssClass =>  ContainerTabSet.ActiveTab == this ? "active" : null;
    // 进行初始化
    protected override void OnInitialized() { ContainerTabSet.AddTab(this); }
    // 设置自身为激活控件
    private void ActivateTab() { ContainerTabSet.SetActiveTab(this);    }
}
TabSet.razor
<!-- 1. 在 ul 中显示 @childcontent, 在页面中这里会被调用为 TabItem 列表,而 TabItem 会显示 li>a  -->
<!-- 2. 设置一个 CascadingValue 将自身传入,本例中将自身传至 TabItem   -->
<!-- 3. 设置一个 SetActiveTab 函数供 TabItem 调用  -->
<!-- 4. 在 SetActiveTab 函数中设置当前选择的 TabItem 并在一个 div 中显示 -->


@using BlazorWebApp.Models

<CascadingValue Value="this">
    <ul class="nav nav-tabs">
        @ChildContent
    </ul>
</CascadingValue>

// 这里显示激活 tab 的 body 部分
<div class="nav-tabs-body p-4">
    @ActiveTab?.ChildContent
</div>

@code {
    // 默认传入的 body
    [Parameter] public RenderFragment ChildContent { get; set; }
    // 设置为当前激活的 tab 
    public TabItem ActiveTab { get; private set; }
    // 添加 tab
    public void AddTab(TabItem tab) { SetActiveTab(tab); }
    // 设置某个 tab 为激活 tab
    public void SetActiveTab(TabItem tab) {
        ActiveTab = tab;
        StateHasChanged();
    }
}
TabPage.razor
<!-- 引用 TabSet, 并在其中设置多个 TabItem -->
@page "/tabpage"

<TabSet>
    <TabItem Title="First tab">
        <h4>Greetings from the first tab!</h4>
        <label>
            <input type="checkbox" @bind="showThirdTab"/>
            Toggle third tab
        </label>
    </TabItem>

    <TabItem Title="Second tab">
        <h4>Hello from the second tab!</h4>
    </TabItem>

    @if (showThirdTab)
    {
        <TabItem Title="Third tab">
            <h4>Welcome to the disappearing third tab!</h4>
            <p>Toggle this tab from the first tab.</p>
        </TabItem>
    }
</TabSet>

@code {
    private bool showThirdTab;
}

数据绑定

bind 与 bind-value 区别

通常,@bind 将表达式的当前值与 的 value 特性关联,并使用注册的处理程序处理更改(可能是 onchange, 可使用 @bind:event="{EVENT}" 改变 ), 可以会自动进行格式化

我的体会,@bind 是 @bind-value 的简写,两者应该是一样的。可能 @bind 是 @bind-value + @bind-value:event="onchange" , @bind-value (语法为 @bind-{PROPERTY}), 就是将值与 value 属性进行绑定。chatGPT 说 @bind-value 是单向绑定


bind 自动与 input value 绑定, 并自动更新  <input @bind="InputValue"/>

onchange 事件中手动更新 <input value="@InputValue" @onchange="@((ChangeEventArgs __e) => InputValue = __e.Value.ToString())"/>     

# 指定值更新时机
oninput 事件时更新 <input @bind="InputValue" @bind:event="oninput"/>

仅显示 <code>InputValue</code>: @InputValue

绑定格式化 <input @bind="startDate" @bind:format="yyyy-MM-dd" />

@code {
    // 可以绑定属性也可以绑定字段
    private string InputValue { get; set; }
}

# @bind:after, 值更新后引发事件。预览功能,以后可能会变更
<input @bind="searchText" @bind:after="PerformSearch" />
private async Task PerformSearch() {}

#@bind:get:指定要绑定的值。@bind:set:指定值更改时的回调。预览功能,以后可能会变更
<input type="text" @bind:get="text" @bind:set="SetAsync" />
private string text = "";
private Task SetAsync(string value) {}

select 的绑定

<p><code>selectedValue</code>: @SelectedValue</p>
<select @bind="SelectedValue">
    @foreach (var idx in Enumerable.Range(0, 10))
    {
        <option value="@idx">@idx</option>
    }

</select>

定义组件绑定值

定义一个组件,比如名为 PasswordEntry

  1. 定义一个 Password 属性,
  2. 定义 Password 对应的事件 EventCallback PasswordChanged , 并根据需要进行触发 PasswordChanged.InvokeAsync
  3. 父组件中调用并使用 @bind-Password="父组件中变量" 进行绑定
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged"
                       required
                       type="@(showPassword ? "text" : "password")"
                       value="@password" />
            </label>
            <span class="text-danger">@validationMessage</span>
        </p>
        <button class="btn btn-primary" @onclick="ToggleShowPassword">
            Show password
        </button>
    </div>
</div>

@code {
    private bool showPassword;
    private string password;
    private string validationMessage;

    // 1. 定义变量
    [Parameter]
    public string Password { get; set; }

    // 2. 定义与变量对应的事件 
    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e.Value.ToString();

        if (password.Contains(' '))
        {
            validationMessage = "Spaces not allowed!";

            return Task.CompletedTask;
        }
        else
        {
            validationMessage = string.Empty;

            // 3. 根据需要触发事件 
            return PasswordChanged.InvokeAsync(password);
        }
    }

    private void ToggleShowPassword()
    {
        showPassword = !showPassword;
    }
}
使用
@page "/password-binding"

<h1>Password Binding</h1>

<PasswordEntry @bind-Password="password" />

<p>
    <code>password</code>: @password
</p>

@code {
    // 但是我测试时,初始化值 Not set 无效
    private string password = "Not set";
}

事件处理

// 1. 定义事件处理函数
@onevent 定义事件处理函数, event 事件列表 https://developer.mozilla.org/zh-CN/docs/Web/Events
<button @onclick="UpdateHeading" />

// 2. 事件处理函数可以同步也可以异步 
private void UpdateHeading(args)
private async Task UpdateHeading(args)

// 3. 事件参数
https://docs.microsoft.com/zh-cn/aspnet/core/blazor/components/event-handling?view=aspnetcore-5.0
https://github.com/dotnet/aspnetcore/tree/main/src/Components/Web/src/Web

// 4. 使用 Lambda 处理事件 
<button @onclick="@(e => heading = "New heading!!!")" />

// 5. 子组件中定义事件 
public EventCallback<MouseEventArgs> OnClickCallback { get; set; } // 子组件中定义
<Child Title="Panel Title from Parent" OnClickCallback="@ShowMessage" />  // 父组件中调用 

// 6. 阻止默认操作, 也可以写成 @onkeydown:preventDefault=true
<input value="@count" @onkeydown="KeyHandler" @onkeydown:preventDefault />

// 7. 阻止事件在 blazor 中传播(不阻止在 js 中传播)
<div @onclick:stopPropagation="stopPropagation" />

// 8. 调用焦点
<input @ref="exampleInput" /> // 1. 引用组件变量
await exampleInput.FocusAsync(); // 2. 调用组件 FocusAsync 事件 

生命周期

https://docs.microsoft.com/zh-cn/aspnet/core/blazor/components/lifecycle?view=aspnetcore-5.0

生命周期

start=>start: 启动
inject=>operation: 属性注入
initParams=>operation: 初始化参数  SetParametersAsync, 参数 ParameterView 中带有所有值,可以自定义初始化值, 调用 ParameterView.TryGetValue 获取原始参数
init=>operation: 初始化 OnInitialized{Async}
init2=>operation: 设置参数  OnParametersSet{Async}, 可能在这里处理一些复杂参数
render=>operation: 呈现组件 Render, OnAfterRender{Async}, firstRender 参数表示是否第一次呈现, JS.InvokeVoidAsync 可在此安全调用  
end=>end: 完成
start->inject->initParams->init->init2->render->end

Render 过程

  1. 第一次呈现并且 ShouldRender 为 false, 或是调用 StateHasChanged ( EventCallback 事件会自动调用 StateHasChanged )

  2. 生成呈现树并呈现组件

  3. 等待DOM 更新

  4. 调用 OnAfterRender{Async},页面呈现时无法调用 js , 建议放在 OnAfterRender 中进行调用

释放组件

IDisposable 和 IAsyncDisposable , 只要重载一个,如果两者都有,则只会调用 IAsyncDisposable

##### 匿名 Lambda 方法 释放

匿名 Lambda 正常会自动释放 ,建议在一个函数中 new 出对象,并调用 Lambda ,这样会安全的释放 lambda , 比如

protected override void OnInitialized()
{
    editContext = new(starship);
    editContext.OnFieldChanged += (s, e) => HandleFieldChanged((editContext)s, e);
}
长时间运行函数释放

一些长时间运行函数建议使用计时器在超时后取消, 比如

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

虚拟化

// 1. 将原来使用 foreach 循环生成数据项改为使用 Virtualize 组件,指定 Items 或是使用 ItemsProvider提供数据源
// 1.1 使用 ItemSize 指定每一项的高度(像素), 
// 1.2 OverscanCount 指定显示区前与后预留的项数 
// 1.3 tabindes 提供键盘滚支持,  访值为 html 属性
<Virtualize Context="employee" ItemsProvider="@LoadEmployees" ItemSize="25" OverscanCount="4" tabindes="-1"> 
    <ItemContent @key=employee.id> // 2. 使用@key 将组件与employee 绑定
        <p>
            // 3. 输出组件 
            @employee.FirstName @employee.LastName has the 
            job title of @employee.JobTitle.
        </p>
    </ItemContent>
    <Placeholder> // 4. 使用 Placeholder 提供数据显示前的点位符
            Loading&hellip;
    </Placeholder>
</Virtualize>

// 5. 调用 RefreshDataAsync 更新数据源,但不会自动更新显示内容,可以调用  StateHasChanged  进行更新 
await virtualizeComponent?.RefreshDataAsync();
StateHasChanged();

渲染

应该是重载 ShouldRender 根据需要返回 true|false 决定要不要重新渲染

从父组件应用一组已更新的参数之后。
为级联参数应用已更新的值之后。
通知事件并调用其自己的某个事件处理程序之后。
在调用其自己的 StateHasChanged 方法后(请参阅 ASP.NET Core Razor 组件生命周期)。 有关在父组件中调用 StateHasChanged 时如何防止覆盖子组件参数的指导,请参阅 ASP.NET Core Razor 组件。

// 重载 ShouldRender 返回 true , 同步进行重新呈现
protected override bool ShouldRender()
{
    return shouldRender;
}


否则请调用  StateHasChanged 请求更新界面,一般不建议频繁调用。因为调用成本比较高

Task 调用过程中根据需要调用 StateHasChanged 更新界面,默认情况 task 完成后进行刷新界面 

模板化组件

将 ui 模板( RenderFragment 或 RenderFragment )作为参数的组件

组件

@typeparam TItem // 1. 指定使用的模板参数 

<table class="table">
    <thead>
      @{ /* 3.1.2 显示传入的显示参数组件 */ }  
        <tr>@TableHeader</tr>  
    </thead>
    <tbody>
        @foreach (var item in Items)
        {
            // 3.2.2 显示传入的显示参数组件
            <tr>@RowTemplate(item)</tr> 
        }
    </tbody>
</table>

@code {

    // 3.1.1 传入 UI 显示参数
    [Parameter]
    public RenderFragment TableHeader { get; set; }

    // 3.2.1 传入 UI 显示参数
    [Parameter]
    public RenderFragment<TItem> RowTemplate { get; set; }

    // 2. 传入待显示的数据
    [Parameter]
    public IReadOnlyList<TItem> Items { get; set; }
}

调用

@page "/pets1"

<h1>Pets</h1>

// TItem 指定类型, 可选 
// Context 指定上下文, 可选 
<TableTemplate Items="pets" Context="pet"  TItem="Pet">
    <TableHeader>
        <th>ID</th>
        <th>Name</th>
    </TableHeader>
    <RowTemplate>
        <td>@pet.PetId</td>
        <td>@pet.Name</td>
    </RowTemplate>
</TableTemplate>

@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; }
    }
}

CSS 隔离

在 .razor 同目录下创建一个同名 .razor.css 文件。该 css 文件作为同名组件的专用 css 样式

比如 Example.razor.css 专为 Example.razor 使用 。 标签中会加入人一个 b-10个随机字符,比如 <h1 b-3xxtam6d07> , css 自动生成为 h1[b-3xxtam6d07] { color: brown; }

子组件中应用样式

https://docs.microsoft.com/zh-cn/aspnet/core/blazor/components/css-isolation?view=aspnetcore-5.0

// 使用 ::deep 表示该样式应用于子组件, 但是子组件必须与标签在同一个父组件中, 比如 <div> <h1>test</h1> <child /> </div> , 这样 child 中的 h1 将会被应用样式

::deep h1 {  
    color: red;
}

可以自定义属性编号以共用 css 样式

创建 Razor 组件

https://docs.microsoft.com/zh-cn/aspnet/core/blazor/components/class-libraries?view=aspnetcore-5.0&tabs=visual-studio

内置 库

全球化支持

https://docs.microsoft.com/zh-cn/aspnet/core/blazor/globalization-localization?view=aspnetcore-5.0&pivots=server

窗口验证

https://docs.microsoft.com/zh-cn/aspnet/core/blazor/forms-validation?view=aspnetcore-5.0#built-in-form-components

定义验证实体

[Required]
[StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")]
[Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")]
[Range(typeof(bool), "true", "true", ErrorMessage = "This form disallows unapproved ships.")]

/// <summary>
/// 登录属性
/// </summary>
public class LoginUserModel
{
    [Required]
    [StringLength(10, ErrorMessage = "Name is too long.")]
    public string Name { get; set; }

    [Required]
    [StringLength(10, ErrorMessage = "Pass is too long.")]
    public string Pass { get; set; }
}

将登录用户实体绑定至 EditForm

可以将实体绑定至 Model, 程序自动生成对应的 EditContext 上下文至事件参数中

也可以创建绑定实例的 EditContext , 绑定至 EditForm 的 EditContext 中

绑定 model


// OnValidSubmit, OnInvalidSubmit 事件参数中自动生成 EditContext 
<EditForm OnValidSubmit="@HandleValidSubmitAsync" OnInvalidSubmit="HandleInvalidSubmitAsync" Model="@loginUserModel">
</EditForm>

@code {
  private  LoginUserModel loginUserModel = new LoginUserModel();    
}   

绑定 EditContext

EditContext 比 model 中提供了更多的验证控件

<EditForm OnValidSubmit="@HandleValidSubmitAsync" OnInvalidSubmit="HandleInvalidSubmitAsync" EditContext="@editContext">
</EditForm>

@code {
    EditContext editContext;

    protected override async Task OnInitializedAsync()
    {
        editContext = new EditContext(new LoginUserModel());
        await base.OnInitializedAsync();
    }
}  

绑定事件

  • OnValidSubmit 验证成功时
  • OnInvalidSubmit 验证失败时
  • OnSubmit 验证成功或制作时, 以上会传入 EditContext 实例,调用 ctx.Validate() 检测是否有问题
<EditForm OnValidSubmit="@HandleValidSubmitAsync" OnInvalidSubmit="HandleInvalidSubmitAsync" OnSubmit="@HandleSubmitAsync" >
</EditForm>

验证消息显示

# 重定义属性名
# ParsingErrorMessage  重定义错误信息, {0} 为占位符
# DisplayName 定义错误时属性名称 , 也可以使用属性的 DisplayName attr 值 
<InputDate @bind-Value="starship.ProductionDate" DisplayName=" 产品生产日期"  ParsingErrorMessage="The {0} field has an incorrect date value." />

自定义验证流程

@implements IDisposable

<EditForm OnSubmit="@HandleSubmitAsync" EditContext="@editContext">
    <!-- 加入使用属性验证能力 -->
    <DataAnnotationsValidator/>

    <!-- ValidationSummary 使用 class validation-message  -->
    <!-- 显示验证的错误信息 -->
    <ValidationSummary/>
    <!-- 显示特定内容的错误信息 -->
    <ValidationSummary Model="@starship" />
    <!-- 仅显示某一个属性的错误信息 -->
    <ValidationMessage For="() => loginUserModel.Pass"/>

    <InputText id="name" @bind-Value="loginUserModel.Name"></InputText>

    <InputText type="password" @bind-Value="loginUserModel.Pass" DisplayName="用户密码"></InputText>

    <button type="submit">Submit</button>

</EditForm>

@code {

    LoginUserModel loginUserModel => editContext.Model as LoginUserModel ?? new LoginUserModel();

    EditContext editContext;

    // 1. 定义 ValidationMessageStore 保存验证到的消息
    private ValidationMessageStore messageStore;

    protected override async Task OnInitializedAsync()
    {
        editContext = new EditContext(new LoginUserModel());
        // 2. 将 ValidationMessageStore 与绑定的上下文进行绑定
        messageStore = new ValidationMessageStore(editContext);
        // 3. 订阅验证请求事件
        editContext.OnValidationRequested += EditContextOnOnValidationRequested;

        await base.OnInitializedAsync();
    }

    private void EditContextOnOnValidationRequested(object? sender, ValidationRequestedEventArgs e)
    {
        // 4. 在验证请求时进行验证处理
        messageStore.Clear();
        if (string.IsNullOrWhiteSpace(loginUserModel.Pass))
        {
            // 5. 将指定属性的验证结果保存起来
            messageStore.Add(() => loginUserModel.Pass, "请输入密码");
        }
    }


    private async Task HandleSubmitAsync(EditContext ctx)
    {
        // 6. 检查验证结果 
        this.Logger.LogInformation($@"当前验证结果 {ctx.Validate()}");
    }


    public void Dispose()
    {
        editContext.OnValidationRequested -= EditContextOnOnValidationRequested;
    }

一个验证常用样例


<EditForm EditContext="@editContext" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary style="@displaySummary" />
    <button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>

@code {
    private string displaySummary = "display:none";
    private bool formInvalid = false;
    private EditContext editContext;

    protected override void OnInitialized()
    {
        editContext = new(starship);
        <!-- 1. 订阅验证属性变更时 -->
        editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
    {
        <!-- 2. 验证属性变更时设置提交按钮的可用性 -->
        formInvalid = !editContext.Validate();
        StateHasChanged();
    }

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");
        <!-- 3. 通过验证时显示或是隐藏错误提示 -->
        displaySummary = "display:block";
    }

    public void Dispose()
    {
        editContext.OnFieldChanged -= HandleFieldChanged;
    }
}
自定义验证属性
using System;
using System.ComponentModel.DataAnnotations;

public class CustomValidator : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, 
        ValidationContext validationContext)
    {
        return new ValidationResult("Validation message to user.",
            new[] { validationContext.MemberName }); // 注意这里要传入验证对应的属性名
    }
}

自定义验证css 值

<!-- 1. 自定义一个 FieldCssClassProvider 子类 -->
public class CustomFieldClassProvider : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext, 
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

        // fieldIdentifier.FieldName == "Name" 验证的属性名       

        <!-- 2. 根据结果返回 css 名 -->
        return isValid ? "validField" : "invalidField";
    }
}

<!-- 3. 在 EditContext 中加入  FieldCssClassProvider 子类  -->
EditContext editContext = new(exampleModel);
editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());   

自定义验证组件还没有看完

    public class CustomValidation : ComponentBase // 1. 自定义基类
    {
        private ValidationMessageStore messageStore; // 2. 定义一个 ValidationMessageStore 处理错误信息

        [CascadingParameter]
        private EditContext CurrentEditContext { get; set; }

        protected override void OnInitialized()
        {
            if (CurrentEditContext == null)
            {
                throw new InvalidOperationException(
                    $"{nameof(CustomValidation)} requires a cascading " +
                    $"parameter of type {nameof(EditContext)}. " +
                    $"For example, you can use {nameof(CustomValidation)} " +
                    $"inside an {nameof(EditForm)}.");
            }

            messageStore = new(CurrentEditContext); // 3. 将 ValidationMessageStore 绑定一个  EditContext

            // 4. 在数据变量时清空相应的错误信息
            CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear();
            CurrentEditContext.OnFieldChanged += (s, e) => messageStore.Clear(e.FieldIdentifier);
        }

        // 供子类调用添加错误相关信息
        public void DisplayErrors(Dictionary<string, List<string>> errors)
        {
            foreach (var err in errors)
            {
                messageStore.Add(CurrentEditContext.Field(err.Key), err.Value);
            }

            CurrentEditContext.NotifyValidationStateChanged();
        }

        public void ClearErrors()
        {
            messageStore.Clear();
            CurrentEditContext.NotifyValidationStateChanged();
        }
    }

调用

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <CustomValidation @ref="customValidation" /> <!-- 定义错误处理组件 -->
    <ValidationSummary /> <!-- 错误信息显示组件 -->
</EditForm>

@code {
    private CustomValidation customValidation;
    private Starship starship = new() { ProductionDate = DateTime.UtcNow };

    private void HandleValidSubmit()
    {
        // 进行错误信息验证流程
        customValidation.ClearErrors();

        // 将错误信息保存于字典并调用 DisplayErrors 
        var errors = new Dictionary<string, List<string>>();

        if (starship.Classification == "Defense" &&
                string.IsNullOrEmpty(starship.Description))
        {
            errors.Add(nameof(starship.Description),
                new() { "For a 'Defense' ship classification, " +
                "'Description' is required." });
        }

        if (errors.Any())
        {
            customValidation.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("HandleValidSubmit called: Processing the form");
            // Process the valid form
        }
    }
}

第三方组件

FluentValidation

1. nuget 
nuget FluentValidation
nuget Accelist.FluentValidation.Blazor
nuget FluentValidation.DependencyInjectionExtensions // 如果不使用注入系统的话这个不用加入


2. 创建验证过程 
继承 AbstractValidator 及验证的 model
public class LoginUserModelValidator : AbstractValidator<LoginUserModel>
{
    public LoginUserModelValidator()
    {
        RuleFor(it => it.Name).NotEmpty().MaximumLength(12).MinimumLength(3).WithMessage("用户名称错误");
        RuleFor(it => it.Pass).NotEmpty().MaximumLength(8).MinimumLength(6).WithMessage("用户密码错误");
    }
}

3. 注入系统
builder.Services.AddValidatorsFromAssemblyContaining<Program>(); // 自动扫描程序集
builder.Services.AddScoped<LoginUserModelValidator>(); // 或是指定验证过程 

4. 使用
@inject LoginUserModelValidator modelValidator // 使用注入

<EditForm OnSubmit="@HandleSubmitAsync" EditContext="@editContext">

    <!-- 使用 FluentValidator 代替 DataAnnotationsValidator  -->
    <!-- 如果不使用注入的 LoginUserModelValidator, 直接 new 一个。如果验证器需要一些第三方对象,建议使用注入 -->
    @*<FluentValidator Validator="@(new LoginUserModelValidator())"></FluentValidator>*@
    <FluentValidator Validator="@modelValidator"></FluentValidator>

    <!-- 显示汇总的验证信息,也可以不使用 -->
    <ValidationSummary/>

    <InputText id="name" @bind-Value="loginUserModel.Name"></InputText>
    <ValidationMessage For="() => loginUserModel.Name"></ValidationMessage>

    <InputText id="password" type="password" @bind-Value="loginUserModel.Pass" DisplayName="用户密码"></InputText>
    <ValidationMessage For="() => loginUserModel.Pass"></ValidationMessage>

    <button type="submit">Submit</button>

</EditForm>

窗口控件

InputFile

<img id="showImageHere" />
<InputFile OnChange="ResizeAndDisplayImageUsingStreaming" />

private async Task ResizeAndDisplayImageUsingStreaming(InputFileChangeEventArgs e)
{
    var imageFile = args.File;
    var resizedImage = await imageFile.RequestImageFileAsync("image/jpg", 250, 250);
    var jsImageStream = resizedImage.OpenReadStream();
    var dotnetImageStream = new DotNetStreamReference(jsImageStream); // 转为 js 能接受的流
    // 调用 js 设置 img 的内容, 这里加载了一个 js 函数 setImage  , 这里说明  https://docs.microsoft.com/zh-cn/aspnet/core/blazor/images?view=aspnetcore-6.0#stream-images   
    await JS.InvokeVoidAsync("setImage", "showImageHere", dotnetImageStream); 
}

InputSelect

<!-- 为了测试,只绑定到自己 -->
<EditForm Model="this">

    <InputSelect @bind-Value="SelectedClassification">
        <option value="">Select classification ...</option>
        @foreach (var it in Classifications)
        {
            <option value="@it">@it</option>
        }
    </InputSelect>

</EditForm>

<p>Selected Classification</p>
<p>@( string.Join(", ", this.SelectedClassification) )</p>


@code {

    protected override async Task OnInitializedAsync()
    {
        Classifications = Enum.GetValues<Classification>();
        await base.OnInitializedAsync();
    }


    Classification[] SelectedClassification { get; set; } = Array.Empty<Classification>();
    Classification[] Classifications { get; set; }

    enum Classification
    {
        Exploration,
        Diplomacy,
        Defense
    }

}

InputText

自定义一个扩展 InputText 使用 oninput 事件进行验证的控件。(默认使用 change)

<!-- 文件名 CustomInputText.razor -->
@inherits InputText

<!-- 使用 @bind + @bind:event -->
<input @attributes="AdditionalAttributes" 
       class="@CssClass" 
       @bind="CurrentValueAsString" 
       @bind:event="oninput" />



<!-- 调用 -->
<CustomInputText @bind-Value="exampleModel.Name" />

InputRadioGroup

<!-- 可能使用 name 进行分组, 这段代码我没有测试 -->

<InputRadioGroup Name="engine" @bind-Value="starship.Engine">
        <InputRadioGroup Name="color" @bind-Value="starship.Color">
            <InputRadio Name="engine" Value="@Engine.Ion" />
            Engine: Ion<br>
            <InputRadio Name="color" Value="@Color.ImperialRed" />
            Color: Imperial Red<br><br>
            <InputRadio Name="engine" Value="@Engine.Plasma" />
            Engine: Plasma<br>
            <InputRadio Name="color" Value="@Color.SpacecruiserGreen" />
            Color: Spacecruiser Green<br><br>
            <InputRadio Name="engine" Value="@Engine.Fusion" />
            Engine: Fusion<br>
            <InputRadio Name="color" Value="@Color.StarshipBlue" />
            Color: Starship Blue<br><br>
            <InputRadio Name="engine" Value="@Engine.Warp" />
            Engine: Warp<br>
            <InputRadio Name="color" Value="@Color.VoyagerOrange" />
            Color: Voyager Orange
        </InputRadioGroup>
    </InputRadioGroup>

文件上传

使用 InputFile (<input type="file" />) 进行文件处理。文件不能超过 2G

文件读取


<h3>UploadFile</h3>

<!-- 为了测试,只绑定到自己 -->
<EditForm Model="this">

    @*处理文件*@
    <InputFile OnChange="LoadFileAsync" multiple ></InputFile>

    @if (isLoading)
    {
        <p>处理中 ...</p>
    }
    else
    {
        <ul>
            @foreach (var file in files)
            {
                <li>
                    <ul>
                        <li>Name: @file.Name</li>
                        <li>Last modified: @file.LastModified.ToString()</li>
                        <li>Size (bytes): @file.Size</li>
                        <li>Content type: @file.ContentType</li>
                    </ul>
                </li>
            }
        </ul>
    }

</EditForm>


@code {

    bool isLoading;

    // 文件列表 
    readonly List<IBrowserFile> files = new();

    // 处理文件
    private async Task LoadFileAsync(InputFileChangeEventArgs arg)
    {
        try
        {
            isLoading = true;
            files.Clear();

            await Task.Delay(TimeSpan.FromSeconds(5));

            foreach (var file in arg.GetMultipleFiles(12))
            {
                files.Add(file);
            }
        }
        finally
        {
            isLoading = false;
        }
    }

}

文件上传

@inject HttpClient Http

// 上传文件
    private async Task UploadFilesAsync(List<IBrowserFile> browserFiles)
    {
        using var dataContent = new MultipartFormDataContent();

        foreach (var file in  browserFiles)
        {
            // 构建上传内容 
            HttpContent content = new StreamContent(file.OpenReadStream(1024)); // 读取时批定文件大小 
            content.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType);

            dataContent.Add(content, "files", file.Name); // 使用 files 自动生成服务端 api 变量
        }

        // 上传文件
        var response = await Http.PostAsync("/Filesave", dataContent);

        // 读取返回内容 UploadResult 为上传 api 返回内容 
        var newUploadResults = await response.Content.ReadFromJsonAsync<IList<UploadResult>>();
    }

    // 服务端的 api 函数
    async Task<ActionResult<IList<UploadResult>>> PostFile([FromForm] IEnumerable<IFormFile> files)
    {
        foreach (var file in files)
        {
            file.Name;
            file.Length ;
            await using FileStream fs = new(path, FileMode.Create);
            await file.CopyToAsync(fs); // 复制到本地文件系统
        }
    }

JS 互操作

调用 JS 代码

<!-- 注册 js convertArray 函数 (注入在 wwwroot/index.html) -->
<script>
  window.convertArray = (win1251Array) => {
    var win1251decoder = new TextDecoder('windows-1251');
    var bytes = new Uint8Array(win1251Array);
    var decodedArray = win1251decoder.decode(bytes);
    console.log(decodedArray);
    return decodedArray;
  };
</script>

<!-- 注入 js -->
@inject IJSRuntime JS

<!-- 调用 convertArray, 出错时引发  JSException  异常 -->
MarkupString text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));

调用 DOM 组件

// 定义 js 函数,可以见下面的隔离导入, 也可以定义为 window.blazorfuns.setElementText1, 调用时使用 blazorfuns.setElementText1
window.setElementText1 = (element, text) => element.innerText = text;

// html 组件建立关联
<div @ref="divElement">Text during render</div>
private ElementReference divElement;

// 调用 
await JS.InvokeVoidAsync("setElementText1", divElement, "Text after render");

同步执行 js

@inject IJSRuntime JS

 // 强制转为 IJSInProcessRuntime 调用同步函数 
 var jsInProcess = (IJSInProcessRuntime)JS;
 var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
}

// 同步导入 js 脚本 
private IJSInProcessObjectReference module;
module = await JS.InvokeAsync<IJSInProcessObjectReference>("import", "./scripts.js");

加载 js 脚本

隔离导入 js 脚本方法
// 1. 创建 js 脚本, 比如 wwwroot/scripts.js:
export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}

// 2. 载入 js 脚本 
IJSObjectReference module = await JS.InvokeAsync<IJSObjectReference>("import", "./scripts.js");

// 3. 调用函数
await module.InvokeAsync<string>("showPrompt", message);

// 4. 释放 
@implements IAsyncDisposable
async ValueTask IAsyncDisposable.DisposeAsync()
{
        if (module is not null)
        {
            await module.DisposeAsync();
        }
}

不封闭直接调用

JS 调用时会进行 js 序列化,如果量很大时性能会有影响, 可以进行不序列化调用

// 1. 定义 js 函数, 返回 string 必须调用 BINDING.js_string_to_mono_string 进行返回
<script>
  window.returnObjectReference = () => {
    return {
      unmarshalledFunctionReturnBoolean: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return name === "Brigadier Alistair Gordon Lethbridge-Stewart" &&
            year === 1968;
      },
      unmarshalledFunctionReturnString: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return BINDING.js_string_to_mono_string(`Hello, ${name} (${year})!`);
      }
    };
  }
</script>

// 2. 调用 
// 2.1 返回 bool 
var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

var jsUnmarshalledReference = unmarshalledRuntime.InvokeUnmarshalled<IJSUnmarshalledObjectReference>("returnObjectReference");
callResultForBoolean = jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, bool>("unmarshalledFunctionReturnBoolean", GetStruct());

// 2.2 返回 string
var jsUnmarshalledReference = unmarshalledRuntime.InvokeUnmarshalled<IJSUnmarshalledObjectReference>("returnObjectReference");
callResultForString = jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, string>("unmarshalledFunctionReturnString", GetStruct());

// 3 还有一些调用, 请看手册 

调用 .NET

// 1. 定义 .net 函数, 函数名 DifferentMethodName, 不定义的话为函数名
[JSInvokable("DifferentMethodName")] 
public static Task<int[]> ReturnArrayAsync()
    {
        return Task.FromResult(new int[] { 1, 2, 3 });
    }

// 2. js 中调用 
<script>
  window.returnArrayAsync = () => {
    DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync') // 同步版本 invokeMethod
      .then(data => {
        console.log(data);
      });
    };
</script>

传入 .net 对象

// 1. 定义 js 函数
<script>
  window.sayHello1 = (dotNetHelper) => {
    return dotNetHelper.invokeMethodAsync('GetHelloMessage');
  };
</script>

// 2. 调用 
DotNetObjectReference<CallDotNetExample2> objRef = DotNetObjectReference.Create(this); // 2.1 创建待传入 js 的对象
result = await JS.InvokeAsync<string>("sayHello1", objRef); // 2.2 调用 js 并传入.net 对象
objRef?.Dispose(); // 2.3 释放对象, 释放可以放在 js 中调用 


API 调用

// nuget Microsoft.Extensions.Http

// Program.cs
builder.Services.AddScoped(sp => 
    new HttpClient
    {
        BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
    });

注入调用

@inject HttpClient Http
var jsonDatas = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
OutputJsonDatas(jsonDatas?.Cast<object>() ?? Array.Empty<object>());

自定义类型

// Program.cs 中注入一个命名的 httpClient 并进行初始化
builder.Services.AddHttpClient("gorestApi", client => client.BaseAddress = new Uri("https://gorest.co.in/public/v2/"));

// 注入 IHttpClientFactory 返回注入的命名 httpClient 
@inject IHttpClientFactory HttpClientFactory

using var httpClient = HttpClientFactory.CreateClient("gorestApi");
var message = await httpClient.GetAsync("users");
var jsonDatas = await message.Content.ReadFromJsonAsync<UserData[]>();

this.OutputJsonDatas(jsonDatas?.Cast<object>() ?? Array.Empty<object>());

其它域名调用

因为跨域了,一个解决文案是调用本域 API , 由本域服务包装一下。还有一个是做一个 API 转发服务

要跨域了, https://www.cnblogs.com/ittranslator/p/making-http-requests-in-blazor-webassembly-apps.html#%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA-aspnet-core-web-api

跨域解决方案,做数据转发

[【Blazor】解决Blazor WebAssembly跨域访问后台服务问题_catshitone的技术博客_51CTO博客

验证服务

https://docs.microsoft.com/zh-cn/aspnet/core/blazor/call-web-api?view=aspnetcore-5.0&pivots=webassembly

SERVER JWT 例子

nuget Microsoft.AspNetCore.Authentication.JwtBearer

// 配置 jwt 认证
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(option =>
{
    option.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["jwt:Issuer"], // Issuer 保存在配置中
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["jwt:Key"])), // key 保存在配置中
        ClockSkew = TimeSpan.Zero
    };
});

// 启动认证
app.UseAuthentication();
app.UseRouting(); // <- 认证的两个函数必须在 UseRouting 之间
app.UseAuthorization();

    // 生成 jwt 验证信息
    private UserToken BuildToken(UserInfo userInfo)
    {
        //記在jwt payload中的聲明,可依專案需求自訂Claim
        var claims = new List<Claim>
        {
            new(ClaimTypes.Name, userInfo.Email),
            new(ClaimTypes.Role, "admin")
        };

        //取得對稱式加密 JWT Signature 的金鑰, 从配置中读出加密 key
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.configuration["jwt:Key"]));
        var credential = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        //設定token有效期限
        var expireTime = DateTime.Now.AddMinutes(30);

        //產生token
        var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
        var jwtSecurityToken = new JwtSecurityToken( null, null, claims, expires: expireTime, signingCredentials: credential );
        var jwtToken = jwtSecurityTokenHandler.WriteToken(jwtSecurityToken);

        //建立UserToken物件後回傳client
        var userToken = new UserToken
        {
            StatusCode = HttpStatusCode.OK,
            Token = jwtToken,
            ExpireTime = expireTime
        };

        return userToken;
    }

CLIENT JWT 例子

nuget Microsoft.AspNetCore.Components.Authorization

nuge Blazored.LocalStorage 要保存 token

  1. 实现一个 public class JwtAuthenticationStateProvider : AuthenticationStateProvider , 里面实现授权相关代码。具体代码见后

  2. 注入授权相关类

csharp builder.Services.AddAuthorizationCore(); // 启用授权 builder.Services.AddScoped<AuthenticationStateProvider, JwtAuthenticationStateProvider>(); // 注入授权组件, 本想用 AddSingleton 但是 LocalStorage 不支持, builder.Services.AddScoped<AuthService>(); // 这是一个辅助类,帮助使用 httpclient 调用登录 api 并调用 JwtAuthenticationStateProvider 管理授权 builder.Services.AddBlazoredLocalStorage(); // 注册一个本地存储,存储 jwt 数据

  1. 使用 AuthenticationStateProvider 调用认证服务

一般不建议使用,因为不支持自动更新

``` c# @inject AuthenticationStateProvider AuthenticationStateProvider // 注入

// 进行调用 var state = await AuthenticationStateProvider.GetAuthenticationStateAsync(); if (state.User.Identity.IsAuthenticated) { Logger.LogInformation($@"用户 {state.User.Identity.Name} 已经登录"); } ```

  1. 使用参数注入

``` csharp // 1. 需要修改 App.razor // 这里加一段 // 用这个代替 RouteView

可选的认证未通过的显示内容

可选的认证过程中的内容

Sorry, there's nothing at this address.

// 2. 使用参数注入的方式 [CascadingParameter] private Task authenticationStateTask { get; set; }

// 2. 调用 private async Task OnTestClickAsync(MouseEventArgs arg) { var state1 = await authenticationStateTask; if (state1.User.Identity.IsAuthenticated) { Logger.LogInformation($@"用户 {state1?.User?.Identity?.Name} 已经登录"); } } ```

  1. 使用 html 代码

```react // 页面使用属性控制整个页面访问 @attribute [Authorize]

// 页面中使用 AuthorizeView 控制局部显示

@context.User.Identity.Name 已登录

未登录

登录中

```

  1. JwtAuthenticationStateProvider 代码,作为授权管理必须实现 AuthenticationStateProvider

```csharp // 验证处理器, 里面放具体的实现 。这里实现 JWT public class JwtAuthenticationStateProvider : AuthenticationStateProvider { // 本地存储 key private const string LocalStorageKey = @"authToken";

   // 默认权限 
   private static readonly AuthenticationState Anonymous = new(new ClaimsPrincipal(new ClaimsIdentity()));

   // 本地数据存储 
   private readonly ILocalStorageService localStorageService;

   // 当前权限, 登录或登出时更新该值  
   private AuthenticationState currentAuthenticationState = Anonymous;

   // 类初始化
   public JwtAuthenticationStateProvider(ILocalStorageService localStorageService)
   {
       this.localStorageService = localStorageService;

       // 3. 初始化时从本地载入 token
       Task.Run(async () => await this.LoadFromLocalStoreAsycn());
   }

   // 从本地载入 token
   private async Task LoadFromLocalStoreAsycn()
   {
       var token = await this.GetJwtTokenAsync();

       if (!string.IsNullOrWhiteSpace(token))
       {
           await this.NotifyUserAuthenticationAsync(token);
       }
   }

   // 重载函数, 返回当前正在使用的授权
   public override Task<AuthenticationState> GetAuthenticationStateAsync()
   {
       var state = this.currentAuthenticationState;
       return Task.FromResult(state);
   }

   // 当登录后调用, 解析出授权信息,并通知 token 变更,
   public async Task NotifyUserAuthenticationAsync(string token)
   {
       // 解析出授权信息
       var claims = ParseClaimsFromJwt(token);
       var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt"));

       // 通知授权及 token 变更
       await this.UpdateCurrentAuthenticationStateAsync(new AuthenticationState(authenticatedUser), token);
   }

   // 通知授权及 token 变更  
   private async Task UpdateCurrentAuthenticationStateAsync(AuthenticationState state, string token)
   {
       var authState = Task.FromResult(state);
       this.NotifyAuthenticationStateChanged(authState); // 发送通知
       Interlocked.Exchange(ref this.currentAuthenticationState, state); // 因为认证经常用到,设置当前授权至内存

       await this.localStorageService.SetItemAsync(LocalStorageKey, token); // 将 token 保存至本地
   }

   // 返回当前使用 jwt token, 供 httpclient 鉴权使用
   public async Task<string> GetJwtTokenAsync()
   {
       var token = await this.localStorageService.GetItemAsync<string>(LocalStorageKey);
       return token;
   }

   // 解析出 jwt 值,jwt 分三段,中间那段为授权信息
   private static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
   {
       var payload = jwt.Split('.')[1];

       var jsonBytes = ParseBase64WithoutPadding(payload);

       var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
       var claims = keyValuePairs?.Select(it => new Claim(it.Key, $@"{it.Value}")).ToArray() ?? Array.Empty<Claim>();

       return claims;
   }

   // jwt 用的 base64 编码与标准 base64 标记有点不一样,这里进行一些调整
   private static byte[] ParseBase64WithoutPadding(string base64)
   {
       switch (base64.Length % 4)
       {
           case 2:
               base64 += "==";
               break;
           case 3:
               base64 += "=";
               break;
       }

       return Convert.FromBase64String(base64);
   }

   // 2. 进行登出操作
   public async Task NotifyUserLogOutAsync()
   {
       await this.UpdateCurrentAuthenticationStateAsync(Anonymous, string.Empty);
   }

} ```

  1. AuthService 登录相关辅助服务,在 httpclient 中整合入授权

```csharp // 注入 IHttpClientFactory builder.Services.AddHttpClient(string.Empty, client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) );

// 一个登录相关的辅助服务 public class AuthService { private readonly JwtAuthenticationStateProvider authenticationStateProvider; private readonly IHttpClientFactory httpFactory;

   public AuthService(IHttpClientFactory httpFactory, AuthenticationStateProvider authenticationStateProvider)
   {
       this.httpFactory = httpFactory;
       this.authenticationStateProvider = (JwtAuthenticationStateProvider)authenticationStateProvider;
   }

   // 返回一个带鉴权信息的 httpclient, 这里调用了 JwtAuthenticationStateProvider 函数
   public async Task<HttpClient> CreateHttpClientAsync()
   {
       // 返回存储的 token
       var jwtToken = await this.authenticationStateProvider.GetJwtTokenAsync();

       var httpClient = this.httpFactory.CreateClient();
       if (!string.IsNullOrWhiteSpace(jwtToken))
       {
           // 调用 httpclient 的授权信息
           httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(@"bearer", jwtToken);
       }

       return httpClient;
   }

   // 登录 
   public async Task LoginAsync(UserInfo userInfo)
   {
       // 创建 httpclient
       using var httpClient = this.httpFactory.CreateClient();

       var json = JsonSerializer.Serialize(userInfo);
       HttpContent httpContent = new StringContent(json, Encoding.UTF8, "application/json");

       // 进行登录 
       var response = await httpClient.PostAsync("/LoginAuth/Login", httpContent);

       // 如果登录成功,通知 authenticationStateProvider
       if (response.IsSuccessStatusCode)
       {
           var resContent = await response.Content.ReadAsStringAsync();
           var options = new JsonSerializerOptions
           {
               PropertyNameCaseInsensitive = true // Deserialize 默认区分大小写
           };

           var userToken = JsonSerializer.Deserialize<UserToken>(resContent, options);
           var tokenValue = userToken?.Token ?? string.Empty;

           await this.authenticationStateProvider.NotifyUserAuthenticationAsync(tokenValue);
       }
   }

   // 登出
   public async Task LogoutAsync()
   {
       // 简单删除 token
       await this.authenticationStateProvider.NotifyUserLogOutAsync();
   }

} ```

  1. 使用规则或是策略进行控制

```react @attribute [Authorize(Roles = "admin", Policy = "admins")]

You can only see this if you satisfy the "content-editor" policy.

```

  1. 根据路由信息控制授权

```react // 1. 将路由信息传入 Resource

// 2. 定义规则 options.AddPolicy("EditUser", policy => policy.RequireAssertion(context => { if (context.Resource is RouteData rd) { var routeValue = rd.RouteValues.TryGetValue("id", out var value); var id = Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty;

           if (!string.IsNullOrEmpty(id))
           {
               // 如果路由规则中以 EMP 开头,返回 true, 授权成功
               return id.StartsWith("EMP", StringComparison.InvariantCulture);
           }
       }

       return false;
   })

);

// 更多策略说明 https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/policies?view=aspnetcore-5.0 ```

调试

https://docs.microsoft.com/zh-cn/aspnet/core/blazor/debug?view=aspnetcore-5.0&tabs=visual-studio

延迟加载程序集

https://docs.microsoft.com/zh-cn/aspnet/core/blazor/webassembly-lazy-load-assemblies?view=aspnetcore-5.0

性能

晚点看 https://docs.microsoft.com/zh-cn/aspnet/core/blazor/performance?view=aspnetcore-5.0

进行测试

使用 bUnit https://docs.microsoft.com/zh-cn/aspnet/core/blazor/test?view=aspnetcore-5.0

将应用转为 PWA

可脱机使用,似乎于本机应用

https://docs.microsoft.com/zh-cn/aspnet/core/blazor/progressive-web-app?view=aspnetcore-5.0&tabs=visual-studio

部署

有空看 https://docs.microsoft.com/zh-cn/aspnet/core/blazor/host-and-deploy/?view=aspnetcore-5.0&tabs=visual-studio

一些教程

https://dotnet9.com/cat/dotnet-web-blazor

一些教程

[今晚,我想來點Blazor 系列] https://ithelp.ithome.com.tw/articles/10251013

自定验证流程: Custom Authentication in Blazor WebAssembly - Detailed (codewithmukesh.com)

30個你必須記住的CSS選擇器 (tutsplus.com)

twitchax/AspNetCore.Proxy:ASP.NET 核心代理变得容易。 (github.com)

twitchax/AspNetCore.Proxy: ASP.NET Core Proxies made easy. (github.com)

仿真API接口 [GraphQL and REST API for Testing and Prototyping | GO REST](https://gorest.co.in/)

仿真API接口 Fake REST API - OnlineTool