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…</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 组件
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
而且不要带有处理逻辑,因为可能会引用循环引用之类的操作 。 要转换参数在OnParametersSetAsync
或OnInitialized
中进行 -
子组件中的
Parameter
特别注意,有时需要使用一个私有变量保存状,因为父组件调用StateHasChanged()
时可能会重新子组件的状态 -
页面中默认使用同步方式调用函数。
-
组件中调用
await this.InvokeAsync(() => { });
调度回组件线程上下文
ASP.NET Core Razor 组件 | Microsoft Docs 路由参数这一节
样例
-
指定路径
-
指定基类
-
引用子组件
-
@ 引用变量
-
@code 引用代码
-
定义变量
-
输出子内容 RenderFragment
-
@attributes 使用展开参数
-
CaptureUnmatchedValues 捕获所有未定义参数
-
@ref 引用组件 , 自动生成字段变量, 生成的变量请在 OnAfterRenderAsync 调用后使用
-
@attribute [Authorize] 给组件设置属性
-
使用 MarkupString 显示原始 HTML ,避免使用
-
使用 RenderFragment 显示片段
-
@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
- 定义一个 Password 属性,
- 定义 Password 对应的事件 EventCallback PasswordChanged , 并根据需要进行触发 PasswordChanged.InvokeAsync
- 父组件中调用并使用 @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 过程
-
第一次呈现并且 ShouldRender 为 false, 或是调用 StateHasChanged ( EventCallback 事件会自动调用 StateHasChanged )
-
生成呈现树并呈现组件
-
等待DOM 更新
-
调用 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…
</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
内置 库
App
Authentication
AuthorizeView
CascadingValue
InputCheckbox
InputDate
InputFile
InputNumber
InputRadio
InputRadioGroup
InputSelect
InputText
InputTextArea
LayoutView
MainLayout
NavLink
NavMenu
Router
RouteView
Virtualize
全球化支持
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
-
实现一个
public class JwtAuthenticationStateProvider : AuthenticationStateProvider
, 里面实现授权相关代码。具体代码见后 -
注入授权相关类
csharp
builder.Services.AddAuthorizationCore(); // 启用授权
builder.Services.AddScoped<AuthenticationStateProvider, JwtAuthenticationStateProvider>(); // 注入授权组件, 本想用 AddSingleton 但是 LocalStorage 不支持,
builder.Services.AddScoped<AuthService>(); // 这是一个辅助类,帮助使用 httpclient 调用登录 api 并调用 JwtAuthenticationStateProvider 管理授权
builder.Services.AddBlazoredLocalStorage(); // 注册一个本地存储,存储 jwt 数据
- 使用 AuthenticationStateProvider 调用认证服务
一般不建议使用,因为不支持自动更新
``` c# @inject AuthenticationStateProvider AuthenticationStateProvider // 注入
// 进行调用 var state = await AuthenticationStateProvider.GetAuthenticationStateAsync(); if (state.User.Identity.IsAuthenticated) { Logger.LogInformation($@"用户 {state.User.Identity.Name} 已经登录"); } ```
- 使用参数注入
``` csharp
// 1. 需要修改 App.razor
可选的认证未通过的显示内容 可选的认证过程中的内容 Sorry, there's nothing at this address.
// 2. 使用参数注入的方式
[CascadingParameter]
private Task
// 2. 调用 private async Task OnTestClickAsync(MouseEventArgs arg) { var state1 = await authenticationStateTask; if (state1.User.Identity.IsAuthenticated) { Logger.LogInformation($@"用户 {state1?.User?.Identity?.Name} 已经登录"); } } ```
- 使用 html 代码
```react // 页面使用属性控制整个页面访问 @attribute [Authorize]
// 页面中使用 AuthorizeView 控制局部显示
@context.User.Identity.Name 已登录 未登录 登录中
```
- 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);
}
} ```
- 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();
}
} ```
- 使用规则或是策略进行控制
```react @attribute [Authorize(Roles = "admin", Policy = "admins")]
You can only see this if you satisfy the "content-editor" policy.
- 根据路由信息控制授权
```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