1.认识 Blazor 简单来讲,Blazor旨在使用C#来替代JavaScript的Web应用程序的UI框架。其主要优势有:
使用C#编写代码,这可提高应用开发和维护的效率利用现有的NET库生态系统 受益于NET的性能、可靠性和安全性与新式托管平台(如 Docker) 集成 以一组稳定、功能丰富且易用的通用语言、框架和工具为基础来进行生成 Blzaor具有3中托管类型:
Blazor Server Blazor Server 应用程序在服务器上运行,所有处理都在服务器上完成,UI/DOM 更改通过 SignalR 连接回传给客户端。
Blazor WebAssembly Blazor WebAssembly应用程序在浏览器中基于WebAssembly的.NET运行时运行客户端。Blazor应用程序及其依赖项和.NET运行时被下载到浏览器中。
Blazor Hybrid Blazor Hybrid 用于使用混合方法生成本机客户端应用。在 Blazor Hybrid 应用中,Razor 组件与任何其他 .NET 代码一起直接在本机应用中(而不在 WebAssembly 上)运行,并通过本地互操作通道基于 HTML 和 CSS 将 Web UI 呈现到嵌入式 Web View 控件。
2.Razor语法和指令 指令 路由定义,可以定义多个,但是不能重复,必须以/
开头 @page "/"
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@attribute [System.ComponentModel.DataAnnotations.Schema.Table("Table")]
@implements IDisposable
@implements IAsyncDisposable
@inherits ComponentBase
@inject IAsyncDisposable varName
@layout Layout.MainLayout
@namespace myNameSpace
@typeparam T1
@typeparam T2 where T2:class
@code{}
运算(表达式) @* 我是注释 *@
1 2 3 4 5 6 7 8 9 @* 代码区域 *@ @{ var a = 1; var b = 2; var c = a + b; } @* 与字符串混用 *@ <h1 > C的值:@(c)个单位</h1 >
1 2 3 @* 默认隐式调用为ToString *@ <p > @DateTime.Now</p > <p > @DateTime.IsLeapYear(2016)</p >
1 2 @* 显式表达式 *@ <p > 上一周:@(DateTime.Now-TimeSpan.FromDays(7))</p >
1 2 3 @* HTML自动转义 *@ <p > @("<span > Hello world</span > ")</p > <p > @((MarkupString)"<span > Hello world</span > ")</p >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @* 语句块 *@ @if (true) { <span > true</span > } else { <span > false</span > } @for(var i=0;i<10;i++) { <text > 这里是文本@(i)</text > } @try { throw new InvalidDataException("错误"); } catch (Exception e) { <p > @e.Message</p > } finally { <span > finally</span > }
3. Razo组件 以razor为后缀,且首字母大写,必须继承自IComponent
接口,默认继承ComponentBase
类。
4.项目结构和路由组件 如果选择Server模式则只有一个项目
如果选择其他模式,则会有两个项目BlazorApp+BlazorApp.Client
在Program.cs中设置渲染模式
1 2 3 4 5 6 7 8 9 10 11 12 ... builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); ... ... app.MapRazorComponents<App>() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof (Client._Imports).Assembly);
App.razor为根网页,里面有<head>、<body>
等信息,其中在body中指定了Routes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="utf-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <base href ="/" /> ... <HeadOutlet /> </head > <body > <Routes /> <script src ="_framework/blazor.web.js" > </script > </body > </html >
Routes
中指定了整体布局MainLayout
,及其他设置1 2 3 4 5 6 <Router AppAssembly ="typeof(Program).Assembly" > <Found Context ="routeData" > <RouteView RouteData ="routeData" DefaultLayout ="typeof(Layout.MainLayout)" /> <FocusOnNavigate RouteData ="routeData" Selector ="h1" /> </Found > </Router >
启动时Blazor会检查Assembly属性,扫描具有RouteAttribute
的组件,<Found>
标记指定在运行时处理路由的组件RouteView 组件。 此组件接收 RouteData 对象以及来自 URI 或查询字符串的任何参数。 然后,它呈现指定的组件及其布局。 可以使用 <Found>
标记来指定默认布局,当所选组件未通过 @layout
指令指定布局时,将使用该布局。使用 <NotFound>
标记指定在不存在匹配路由时返回给用户的内容
而在MainLayout
中,则指定了NavMenu
和@Body
,在NavMenu
中设置了导航,可以导航到定义的page,并在设置了@Body
的地方展示 1 2 3 4 5 6 7 8 9 10 11 @inherits LayoutComponentBase <div class ="page" > <div class ="sidebar" > <NavMenu /> </div > <main > <article class ="content px-4" > @Body </article > </main > </div >
1 2 3 4 5 6 7 8 9 10 11 <div class ="nav-item px-3" > <NavLink class ="nav-link" href ="" Match ="NavLinkMatch.All" > <span class ="bi bi-house-door-fill-nav-menu" aria-hidden ="true" > </span > Home </NavLink > </div > <div class ="nav-item px-3" > <NavLink class ="nav-link" href ="counter" > <span class ="bi bi-plus-square-fill-nav-menu" aria-hidden ="true" > </span > Counter </NavLink > </div >
NavLinkMatch.All:使用此值时,只有在链接的 href
与当前 URL 完全匹配时
NavLinkMatch.Prefix:使用此值时,当链接的 href
与当前 URL 的第一部分匹配就可以
NavLink会实现一个<a>
链接实现当跳转到指定路由时,增加active的class样式,也可以自己去设置active样式,也就是当匹配后你想设置的样式是什么,如<NavLink class="nav-link" href="" ActiveClass="myActive" Match="NavLinkMatch.All">
,这样我的样式则为myActive。
5. 组件参数 组件参数 组件可以具有参数,以供父组件控制,使用公共属性和[Parameter]特性来标记(组件参数)
自定义组件CustomRazor
1 2 3 4 5 <h1 > @Title</h1 > @code { [Parameter]public string? Title{ set; get; } }
在父组件中使用自定义组件 1 <CustomRazor Title ="自定义名称" />
渲染片段 默认(单渲染片段) 自定义组件CustomRazor
,渲染片段必须是RenderFragment?
类型,以ChildContent
命名 1 2 3 4 5 6 7 8 <h1 > @Title</h1 > <p > 渲染片段</p > <p > @ChildContent</p > @code { [Parameter]public string? Title{ set; get; } [Parameter] public RenderFragment? ChildContent { set; get; } }
在父组件中使用自定义组件 1 2 3 <CustomRazor> 我是渲染片段 </CustomRazor>
渲染片段RenderFragment可以呈现任何对象,不仅仅是字符串
1 2 3 4 5 <CustomRazor > <CustomRazor > 渲染片段再次使用自定义组件 </CustomRazor > </CustomRazor >
多渲染片段 自定义组件CustomRazor
1 2 3 4 5 6 7 8 9 10 <h1 > @Title</h1 > <p > 渲染片段</p > <p > @ChildContent</p > <p > @OtherChildContent</p > @code { [Parameter]public string? Title{ set; get; } [Parameter] public RenderFragment? ChildContent { set; get; } [Parameter] public RenderFragment? OtherChildContent { set; get; } }
使用多个渲染片段 1 2 3 4 5 6 7 8 <CustomRazor > <ChildContent > 我是第一个渲染片段 </ChildContent > <OtherChildContent > 我是第二个渲染片段 </OtherChildContent > </CustomRazor >
6. 导航参数和查询参数 导航参数 1 2 3 4 5 6 7 8 9 10 11 @page "/{id:int}/{name?}" <PageTitle > Home</PageTitle > <p > 导航参数是@(Id)</p > <p > 名称是@(Name)</p > @code{ [Parameter] public int Id { set; get; } [Parameter] public string? Name { set; get; } }
输入/100/tom
查询参数 1 2 3 4 5 6 7 8 9 10 @page "/" <PageTitle > Home</PageTitle > <p > 第@(Page)页,共@(Size)页</p > @code{ [Parameter][SupplyParameterFromQuery] public int? Page { set; get; } [Parameter][SupplyParameterFromQuery(Name ="count")] public int? Size { set; get; } }
地址栏输入?page=1&count=100
7. 级联参数 如果子组件中还有子组件,当子组件层次比较深时,可以使用级联参数
让参数沿着层次结构向下自动传递到下级组件,在父组件中使用<CascadingValue>
将子组件进行包裹,在该标记内呈现的任何组件都能够访问传递的相关参数。
定义子组件 1 2 3 4 5 6 <h1 > 我是CustomRazor</h1 > <h1 > @Title</h1 > @code { [CascadingParameter] string? Title{ set; get; } }
使用子组件 1 2 3 4 5 <PageTitle > Home</PageTitle > <CascadingValue Value ="@(" 标题 ")"> <CustomRazor /> </CascadingValue >
级联参数会自动匹配类型一样的值,比如上面级联参数的类型为string,如果具有多个级联参数,则会自动匹配最近的一个
1 2 3 4 5 <CascadingValue Value ="@(" 外层 ")"> <CascadingValue Value ="@(" 内层 ")"> <CustomRazor /> </CascadingValue > </CascadingValue >
如果想要有多个级联参数,可以指定名称
1 2 3 4 5 6 7 8 <h1 > 我是CustomRazor</h1 > <h1 > @Title1</h1 > <h1 > @Title2</h1 > @code { [CascadingParameter(Name ="Title1")] string? Title1 { set; get; } [CascadingParameter(Name = "Title2")] string? Title2 { set; get; } }
1 2 3 4 5 6 7 <PageTitle > Home</PageTitle > <CascadingValue Name ="Title1" Value ="@(" 外层 ")"> <CascadingValue Name ="Title2" Value ="@(" 内层 ")"> <CustomRazor /> </CascadingValue > </CascadingValue >
8. 事件和事件参数 事件是一个EventCallback
类型,切支持泛型参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <h3 > Event</h3 > <button style ="@style" @onmouseenter ="MouseOver" @onmouseleave ="MouseOut" > 按钮</button > @code { string style; void MouseOver() { style = "font-size:30px"; } void MouseOut() { style = String.Empty; } }
首先定义一个Collapse.Razor,在该Razor中定义EventCallback类型的属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <button class ="btn btn-primary" @onclick ="Toggle" > @ButtonText </button > <div class ="collapse @(Expand?" show ": "")"> @ChildContent </div > @code { [Parameter] public RenderFragment? ChildContent { get; set; } [Parameter] public EventCallback<bool > OnToggle { get; set; } string? ButtonText => Expand ? "折叠" : "展开"; bool Expand { get; set; } async Task Toggle() { Expand = !Expand; //触发传递进来的函数 await OnToggle.InvokeAsync(Expand); } }
使用定义的Razor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <h3 > Event</h3 > <Collapse OnToggle ="Toggle" > 要显示的内容 </Collapse > <h4 > @message</h4 > @code { string? message; void Toggle(bool expanded) { if (expanded) { message = "内容已经展开"; } else { message = ""; } } }
9. 模板页 模版页继承自LayoutComponentBase
,在LayoutComponentBase
中有一个属性名称为Body
的渲染片段,标识要显示的内容。在Router
组件中一般设定了默认模版页<RouteView DefaultLayout="typeof(Layout.MainLayout)" />
,也可以对不同的组件设置不同的模板页。
创建一个自定义布局 1 2 3 4 @inherits LayoutComponentBase <h3 > EmptyLayout</h3 > <div > @Body</div >
使用该自定义布局 1 2 3 4 5 @page "/event" @layout Layout.EmptyLayout @* 只能使用一次 *@ <h3 > Event</h3 > 。。。
10. 单向绑定和双向绑定 使用@bind
来进行绑定
1 2 3 4 5 6 7 8 9 10 <p > <input @bind ="InputValue" @bind:event ="oninput" /> @*默认是 onchange 标识失去焦点后更新*@ </p > <p > <code > InputeValue</code > :@InputValue </p > @code{ private string? InputValue{set;get;} }
可以使用@bing:format
来格式化字符串
使用@bind:after
,InputAfter在失去焦点触发,不支持任何参数,经常用于输入验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <p > <input @bind ="InputValue" @bind:after ="InputAfter" /> </p > <p > @message </p > @code{ private string? InputValue{set;get;} string? message; void InputAfter() { message = "输入后得到"; } }
@bind:after
的弊端是不能有参数,如果要含有参数则可以使用双向绑定
1 2 3 4 5 6 7 8 9 10 11 12 <p > <input @bind:get ="text" @bind:set ="OnInput" /> </p > @code { string? text; void OnInput(string value) { var newValue = value ?? string.Empty; text = newValue.Length > 4 ? "Long" : newValue; } }
上面都是绑定的字段,如果绑定的是属性则可以直接在属性的set和get方法中进行验证操作
下拉框的绑定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <p > <label > 选择一个品牌 <select @onchange ="SelectedCarsChanged" > <option value ="a" > A</option > <option value ="b" > B</option > <option value ="c" > C</option > <option value ="d" > D</option > </select > </label > </p > <p > 选择的车:@SelectedCar </p > @code{ public string?SelectedCar{get;set;} void SelectedCarsChanged(ChangeEventArgs e) { SelectedCar = e.Value?.ToString(); } }
11. 自定义组件实现双向绑定 bind
只适用于组件内部,自定义组件实现双向绑定需按如下步骤:
定义绑定属性值 [Parameter] public string? Text { set; get; }
定义一个EventCallback泛型类型的属性,名称必须为第一步定义的属性值+Changed [Parameter] public EventCallback<string> TextChanged{ set; get; }
在组件中绑定第一步属性值,及设置相应事件 1 2 3 4 5 6 7 8 9 10 11 12 <input type ="text" value ="@Text" @onchange ="OnChange" /> @code { [Parameter] public string? Text { set; get; } [Parameter] public EventCallback<string > TextChanged{ set; get; } Task OnChange(ChangeEventArgs e) { Text = e.Value?.ToString(); TextChanged.InvokeAsync(Text); return Task.CompletedTask; } }
使用定义的组件进行绑定时,使用@bing-
1 2 3 4 5 6 7 8 9 <FormControl @bind-Text ="@outerText" > </FormControl > <p > @outerText </p > @code{ string? outerText; }
12. 组件的任意参数 当组件需要定义多个标签属性时,可以在定义对应的组件参数,但这样过于麻烦。可以借助@attributes
来实现任意参数
1 2 3 4 5 <input type ="text" class ="form-control @(Class)" @attributes ="@Attributes" /> @code { [Parameter(CaptureUnmatchedValues =true)]public Dictionary<string,object>? Attributes{ get; set; } }
使用组件
1 2 3 4 5 6 <FormControl Attributes ="@(new Dictionary<string, object>{ [" Title "]="文本框" , ["style "]="color:red;font-size:18px" })"> </FormControl >
上面代码中使用了[Parameter(CaptureUnmatchedValues =true)]
,可以自动转换为键值对。下面的使用方式与上面的效果完全相同。
1 2 <FormControl title ="文本框" style ="color:red;font-size:18px" > </FormControl >
13. 表单和验证 在web中使用<form>
元素创建表单,将input
等放入其中实现表单功能,Blazor也支持这些,但提供了更多的组件
EditForm可以支持和对象直接关联,进行双向绑定,并提供更多功能
1 2 3 4 5 6 7 public class WeatherForecast { public DateTime Date { get ; set ; } public int TemperatureC { get ; set ; } public int TemperatureF => 32 + (int )(TemperatureC / 0.5556 ); public string Summary { get ; set ; } }
1 2 3 4 5 6 7 8 <EditForm Model =@currentForecast > <InputDate @bind-Value =currentForecast.Date > </InputDate > <InputNumber @bind-Value =currentForecast.TemperatureC > </InputNumber > <InputText @bind-Value =currentForecast.Summary > </InputText > </EditForm > @{ private WeatherForecast currentForecast; }
EditForm
具有三个在提交后运行的事件:
OnSubmit
:无论验证结果如何,只要用户提交表单,就会触发此事件。OnValidSubmit
:当用户提交表单并且他们的输入验证通过时,将触发此事件。OnInvalidSubmit
:当用户提交表单并且他们的输入验证失败时,将触发此事件。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <EditForm Model ="@p" onsubmit ="ValidateData" > <h3 > 名字:</h3 > <InputText @bind-Value ="p.Name" > </InputText > <h3 > 年龄:</h3 > <InputNumber @bind-Value ="p.Age" min ="0" max ="99" > </InputNumber > <Input type ="submit" value ="提交" /> <h3 > @message</h3 > </EditForm > @code { Person p = new(); string message; private async Task ValidateData(EditContext editContext) { var model =(Person)editContext.Model; if (model.Age>10) { message = "大于10岁"; } } class Person { public string Name{ set; get; } public int Age{ set; get; } } }
错误信息的展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @page "/form" @using System.ComponentModel.DataAnnotations <PageTitle > 表单验证</PageTitle > <h3 > 表单验证</h3 > <EditForm Model ="Model" OnValidSubmit ="SubmitValid" > <DataAnnotationsValidator /> @* 展示所有的错误信息 *@ <ValidationSummary /> <div class ="row mb-3" > <label class ="col-1 col-form-label" > 姓名:</label > <div class ="col-11" > <InputText @bind-Value ="Model.Name" class ="form-control" /> @* 展示单个验证信息 *@ <ValidationMessage For ="()=>Model.Name" /> </div > </div > <div class ="row mb-3" > <label class ="col-1 col-form-label" > 密码:</label > <div class ="col-11" > <InputText @bind-Value ="Model.Password" class ="form-control" type ="password" /> @* 展示单个验证信息 *@ <ValidationMessage For ="()=>Model.Password" /> </div > </div > <button type ="submit" > 提交</button > </EditForm > @code { class UserInfo { [Required(ErrorMessage = "名字不能为空")] public string? Name { get; set; } [Required(ErrorMessage = "密码不能为空")] public string? Password { get; set; } } UserInfo Model = new(); Task SubmitValid() { //数据库查询等操作 return Task.CompletedTask; } }
14. 表单验证的进阶 上面的案例中是在EditForm中进行提交并且验证,而有时提交是在外面。此时需要EditContext
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @page "/form" @using System.ComponentModel.DataAnnotations <PageTitle > 表单验证</PageTitle > <h3 > 表单验证</h3 > <button class ="btn btn-primary" @onclick =SubmitValid > 提交</button > @* 定义EditContext *@ <EditForm EditContext ="Context" > <DataAnnotationsValidator /> <ValidationSummary /> <div class ="row mb-3" > <label class ="col-1 col-form-label" > 姓名:</label > <div class ="col-11" > <InputText @bind-Value ="Model.Name" class ="form-control" /> <ValidationMessage For ="()=>Model.Name" /> </div > </div > <div class ="row mb-3" > <label class ="col-1 col-form-label" > 密码:</label > <div class ="col-11" > <InputText @bind-Value ="Model.Password" class ="form-control" type ="password" /> <ValidationMessage For ="()=>Model.Password" /> </div > </div > </EditForm > @code { class UserInfo { [Required(ErrorMessage = "名字不能为空")] public string? Name { get; set; } [Required(ErrorMessage = "密码不能为空")] public string? Password { get; set; } } UserInfo Model = new(); //定义EditContext属性 EditContext Context { get; set; } public Form() { Context = new EditContext(Model); } Task SubmitValid() { //查询验证是否通过 bool isValid = Context.Validate(); //数据库查询等操作 return Task.CompletedTask; } }
自定义错误消息 上面案例中在EditForm内部使用了<ValidationSummary/>和<ValidationMessage/>
来显示错误信息,这些组件必须放置在EditForm内部,如果在外部自定义错误信息则可以使用Context.GetValidationMessages();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 @page "/form" @using System.ComponentModel.DataAnnotations @using System.Reflection <PageTitle > 表单验证</PageTitle > <h3 > 表单验证</h3 > <button class ="btn btn-primary" @onclick =SubmitValid > 提交</button > @* 定义EditContext *@ <EditForm EditContext ="Context" > <DataAnnotationsValidator /> <div class ="row mb-3" > <label class ="col-1 col-form-label" > 姓名:</label > <div class ="col-11" > <InputText @bind-Value ="Model.Name" class ="form-control" /> </div > </div > <div class ="row mb-3" > <label class ="col-1 col-form-label" > 密码:</label > <div class ="col-11" > <InputText @bind-Value ="Model.Password" class ="form-control" type ="password" /> </div > </div > </EditForm > @if (Errors.Any()) { <div class ="alert alert-danger" > <ul > @foreach (var message in Errors) { <li > @message</li > } </ul > </div > } @GetValidation(nameof(Model.Name)); @code { class UserInfo { [Required(ErrorMessage = "名字不能为空")] public string? Name { get; set; } [Required(ErrorMessage = "密码不能为空")] public string? Password { get; set; } } UserInfo Model = new(); //定义EditContext属性 EditContext Context { get; set; } IEnumerable<string > Errors { get; set; } = []; public Form() { Context = new EditContext(Model); } Task SubmitValid() { //查询验证是否通过 bool isValid = Context.Validate(); if (!isValid) { Errors = Context.GetValidationMessages(); return Task.CompletedTask; } //数据库查询等操作 return Task.CompletedTask; } //获得单个属性验证消息 string? GetValidation(string name) { FieldIdentifier fieldIdentifier= Context.Field(name); if (!Context.IsValid(fieldIdentifier)) { var property = Model?.GetType()?.GetProperty(fieldIdentifier.FieldName); var requiredAtr = property?.GetCustomAttribute<RequiredAttribute > (); var value = property?.GetValue(Model); if (!requiredAtr.IsValid(value)) { return requiredAtr.ErrorMessage; } } return string.Empty; } }
上面案例中,如果出现错误则文本框边框会变为红色,这是因为当有错误时会添加invalid 的css类样式,如果想自定义样式,则可使用FormCssClassProvider
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class FormCssClassProvider : Microsoft.AspNetCore.Components.Forms.FieldCssClassProvider { public override string GetFieldCssClass (EditContext editContext, in FieldIdentifier fieldIdentifier ) { if (!editContext.IsModified()) { return string .Empty; } var valid = editContext.IsValid(fieldIdentifier); return valid ? "is-valid" : "is-invalid" ; } }
只需设置
1 2 3 4 5 public Form (){ Context = new EditContext(Model); Context.SetFieldCssClassProvider(new FormCssClassProvider()); }
此时,文本框中会加上对钩和感叹号
15. 组件的生命周期
16. 泛型组件 基本使用 泛型组件类似于C#中的泛型类,使用流程同样是先定义泛型参数,然后使用
定义泛型组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <h3 > 泛型组件</h3 > @typeparam TValue where TValue:struct @typeparam TText <p > 值是:@Value,类型是:@typeof(TValue) </p > <p > 值是:@Text,类型是:@typeof(TText) </p > @code { [Parameter] public TValue Value { set; get; } [Parameter] public TText Text { set; get; } }
可直接声明泛型类型,也可自动推断 1 2 3 4 5 6 <Genaric TValue ="int" Value ="100" TText ="string" Text =@outerText /> <Genaric Value ="100" Text =@outerText /> @code{ string outerText="字符串"; }
案例:根据绑定数据类型改变<input>
的type 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <h3>泛型组件</h3> @typeparam TValue <input type="@InputType" value ="@CurrentValue" @oninput="OnChange" /> @code { [Parameter ] public TValue? Value { set ; get ; } [Parameter ] public EventCallback<TValue?> ValueChanged { get ; set ; } string ? CurrentValue{ set ; get ; } Task OnChange (ChangeEventArgs e ) { var tmpValue = e.Value; if (tmpValue is null ) { return Task.CompletedTask; } var newValue = Convert.ChangeType(tmpValue, typeof (TValue)); Value = (TValue)newValue; ValueChanged.InvokeAsync(Value); CurrentValue = BindConverter.FormatValue(tmpValue)?.ToString(); return Task.CompletedTask; } string ? InputType => Value switch { double or float or int or decimal => "number" , DateOnly or DateTime or DateTimeOffset => "date" , _ => "text" }; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <ul > <li > 数字: <Genaric @bind-Value ="@num" /> </li > <li > 文本: <Genaric @bind-Value ="@text" /> </li > <li > 时间: <Genaric @bind-Value ="@time" /> </li > </ul > @code{ string text; float num; DateTime time = DateTime.Now; }
17. 模板化组件 模版化组件通常和泛型组件相结合,案例:需展示数据列表,展示的形式及数据需可自定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @typeparam TData @if (Datas is not null ) { <table class ="table" > <thead> <tr>@HeaderTemplage</tr> </thead> <tbody> @foreach (var item in Datas) { <tr> @RowTemplate?.Invoke(item) </tr> } </tbody> </table> } @code { [Parameter ] public IEnumerable<TData> Datas{ set ; get ; } [Parameter ] public RenderFragment<TData>? RowTemplate { set ; get ; } [Parameter ] public RenderFragment? HeaderTemplage { set ; get ; } }
上面代码中,Datas保存数据,但是TData类型不确定,在tbody中展示时,不确定里面有什么数据,所以需要用户显示方式。同样,表头thead同样也不确定需要展示哪些表头属性,需要用户来确定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <Genaric Datas ="@Users" > <HeaderTemplage > <th > Id</th > <th > 名称</th > </HeaderTemplage > <RowTemplate > <td > @context.Id</td > <td > @context.Name</td > </RowTemplate > </Genaric > @code{ class User { public int Id { get; set; } public string? Name { get; set; } } IEnumerable<User > Users => new List<User > { new(){ Id=1, Name="张三"}, new(){ Id=2, Name="李四"}, new(){ Id=3, Name="王五"}, new(){ Id=4, Name="赵六"} }; }
在RowTemplate
中的context
代表泛型类型,和this含义用法有些相同
18. 渲染模式 名称 描述 呈现位置 交互 静态 静态服务器端呈现(静态 SSR) 服务器 ❌否 交互式 Blazor Server 使用 Blazor Server 的交互式服务器端呈现(交互式 SSR)。 服务器 ✔️是 交互式 WebAssembly 使用 Blazor WebAssembly 的客户端呈现 (CSR)。 客户端 ✔️是 交互式自动 先使用 Blazor Server 然后使用 CSR 。 服务器,然后客户端
Blazor Server可兼容WebAssembly,反之不可以。也就是说在Server模式下的组件可放置WebAssembly组件,反之不行。
需要在Program中增加相应中间件
1 2 3 4 5 6 7 8 9 10 builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); app.MapRazorComponents<App>() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof (Client._Imports).Assembly);
如果在创建工程时设置了全局,则在App.razor中会自动设置渲染模式,渲染模式是向下传递的。也就是如果子组件没有设置渲染模式,则继承父组件的渲染模式。
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html > <html lang ="en" > <head > ... <HeadOutlet @rendermode ="InteractiveAuto" /> </head > <body > <Routes @rendermode ="InteractiveAuto" /> <script src ="_framework/blazor.web.js" > </script > </body > </html >
也可在组件中直接使用@rendermode InteractiveServer
来指定渲染模式,也可在外部使用<Genaric @rendermode="InteractiveWebAssembly">
进行指定,如果在外部使用则在内部不能指定渲染模式。
当组件中含有RenderFragment
参数,这种参数不可序列化,如果指定渲染模式时会报错,遇到这种问题需要在其外层包装一下就可以
19. CSS隔离和代码隔离 CSS隔离 一般在app.css中进行定义,但是不利于管理
可以定义一个组件名称+.css
的文件,如GenaricTable.razor.css
注意,在App.razor中一定要引用<link rel="stylesheet" href="工程名.styles.css" />
代码隔离 可以定义一个组件名称+.cs
的文件,如GenaricTable.razor.cs
,并将类设置为partial
20. 异常处理 当程序遇到未捕获的异常时,会在底部弹出如下提示。
可在MainLayout中设置错误提示
1 2 3 4 5 <article class ="content px-4" > <ErrorBoundary > @Body </ErrorBoundary > </article >
默认错误提示
可自定义错误样式
1 2 3 4 5 6 7 8 9 10 <article class ="content px-4" > <ErrorBoundary > <ErrorContent > 出现错误,@context.Message </ErrorContent > <ChildContent > @Body </ChildContent > </ErrorBoundary > </article >
21. 流式渲染 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Count:@count @code { int count = 0 ; async Task DoCount () { for (int i = 0 ; i < 10 ; i++) { await Task.Delay(1000 ); count++; StateHasChanged(); } } protected override async Task OnInitializedAsync () { await DoCount(); } }
流式渲染解决了这个问题
只需要加上@attribute [StreamRendering]
即可实现
22. 预呈现模式 预呈现是先呈现一部分尽快输出页面的HTML UI,让用户感觉提升了响应速度。
定义组件Perrender.razor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <div class ="card" > <div class ="card-body" > <h2 > 预呈现 :@Title</h2 > <hr /> <p > Hello world</p > <button class ="btn btn-success" > 提交</button > @if (_isComplete) { <h3 > 渲染完成</h3 > } </div > </div > @code { [Parameter] public string? Title{ set; get; } bool _isComplete; protected override async Task OnInitializedAsync() { await Task.Delay(2000); _isComplete = true; } }
使用组件 第一个关闭预呈现,第二个打开预呈现
1 2 3 4 <Perrender Title ="开启" @rendermode ="new InteractiveWebAssemblyRenderMode(false)" /> ------------------------------ <Perrender Title ="关闭" @rendermode ="new InteractiveWebAssemblyRenderMode(true)" /> ------------------------------
如果使用预呈现,在server模式中,需要注意状态保留问题。
23. C# 和 JS 的互操作 C#调用JS 直接写js <button onclick="javascript:alert('提示')">提示</button>
使用IJSRuntime 注入IJSRuntime
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @inject IJSRuntime JS <button @onclick ="Alert" > 提示</button > <button @onclick ="Propmt" > 弹出框</button > 输入的名称是 @Value @code { async Task Alert() { //带Void的表示无返回值 //第一个参数为js的函数名,后面的参数为可变参数列表 await JS.InvokeVoidAsync("hello", "参数"); } string? Value{ set; get; } async Task Propmt() { var value = await JS.InvokeAsync<string > ("prompt", "请输入名字"); Value = value; } }
调用自定义JS函数 在项目中wwwroot中增加js文件,并在APP.razor中引用该文件<script src="app.js"></script>
1 2 3 4 function hello ( ) { alert ('我是自定义hello函数' ); }
在C#中调用
1 2 3 4 5 async Task Alert (){ await JS.InvokeVoidAsync("hello" , "参数" ); }
JS调用C#中的函数 静态方法 用C#写静态方法 1 2 3 4 5 6 7 8 9 10 11 12 13 public class Functions { [JSInvokable ] public static int Add () { return 1 + 5 ; } [JSInvokable ] public static Task<int > AddAsync () { return Task.FromResult(1 +10 ); } }
js调用写的静态方法 1 2 3 4 5 6 7 8 9 10 function add ( ) { let result = DotNet .invokeMethod ('BlazorApp2.Client' , 'Add' ); console .log (result); } function addAsync ( ) { DotNet .invokeMethodAsync ('BlazorApp2.Client' , 'AddAsync' ).then (r =>console .log (r)); }
这种方式非常不推荐,如果有多个.net运行时,会导致错误
普通方法 创建普通方发 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Functions { [JSInvokable ] public int Add () { return 1 + 5 ; } [JSInvokable ] public Task<int > AddAsync () { return Task.FromResult(1 + 10 ); } }
在razor页面中定义方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @inject IJSRuntime JS <button @onclick ="Add" > 加</button > <button @onclick ="AddAsync" > 加-异步</button > @code { async Task Add() { //获取引用,并调用js中定义的方法,js中需要有引用参数 var dotReference = DotNetObjectReference.Create(new Functions()); await JS.InvokeVoidAsync("add", dotReference); } async Task AddAsync() { var dotReference = DotNetObjectReference.Create(new Functions()); await JS.InvokeVoidAsync("addAsync", dotReference); } }
js中调用C#中的方法 1 2 3 4 5 6 7 8 9 function add (p ) { let result = p.invokeMethod ('Add' ); console .log (result); } function addAsync (p ) { p.invokeMethodAsync ('AddAsync' ).then (r => console .log (r)); }
24. 渲染树 每个组件都是继承自ComponentBase类,完全可以自定义类来实现Razor组件的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Button : ComponentBase { [Parameter ] public RenderFragment? ChildContent { get ; set ; } [Parameter ]public bool Outline { get ; set ; } [Parameter ] public string ? Tag { get ; set ; } = "button" ; protected override void BuildRenderTree (RenderTreeBuilder builder ) { builder.OpenElement(0 , Tag); builder.AddAttribute(1 , "class" , $"btn btn-{(Outline?"outline-" :"" )} success" ); builder.AddAttribute(2 , "onclick" , EventCallback.Factory.Create(this , ()=>{ 。。。 })); builder.AddContent(10 , ChildContent); builder.CloseElement(); } }
实现上面的类后可以像使用组件一样来使用
1 2 3 <Button > 填充按钮</Button > <Button Outline > 边框按钮</Button > <Button Tag ="span" > Span 按钮</Button >
25. 与 WEB API 的交互 在用Auto模式或者WebAssembly模式时,往往需要获取远程数据,这时就涉及到与Web API的交互。
首先要确定Web API允许跨域 一定要在工程名.Client
项目下的Program.cs中注册HTTP服务 1 2 3 4 5 6 static async Task Main (string [] args ){ var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("http://localhost:5041/" ) }); await builder.Build().RunAsync(); }
WebAPI的返回形式一般是Json数据,首先声明对应Json数据的类,然后注入HttpClient。并利用上文中模版化组件进行展示 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @inject HttpClient client <button @onclick=GetDataAsync>获取远程数据</button> @if (Data is null ) { <div>数据加载中</div> } else { <Genaric Datas="Data" > <HeaderTemplage> <th>日期</th> <th>摄氏度</th> <th>华氏度</th> <th>说明</th> </HeaderTemplage> <RowTemplate> <td>@context.Date</td> <td>@context.TemperatureC</td> <td>@context.TemperatureF</td> <td>@context.Summary</td> </RowTemplate> </Genaric> } @code { public class WeatherForecast { public DateOnly Date { get ; set ; } public int TemperatureC { get ; set ; } public int TemperatureF { get ; set ; } public string ? Summary { get ; set ; } } IEnumerable<WeatherForecast>? Data{ set ; get ; } async Task GetDataAsync () { Data = await client.GetFromJsonAsync<IEnumerable<WeatherForecast>>("WeatherForecast" ); } }
26. 部署到 IIS .net Core运行时 下载Hosting Bundle
设置发布路径,Ip、端口号等 确认设置模块中是否含有AspNetCoreModuleV2
确认处理程序映射是否含有aspNetCore
设置应用程序池,将.net Clr中.net CLR版本设置为无托管代码