Blazor系统教程

本文涉及的产品
对象存储 OSS,20GB 3个月
对象存储 OSS,恶意文件检测 1000次 1年
对象存储 OSS,内容安全 1000次 1年
简介: 基于.net8的Blazor系统教程

Blazor系统教程

1.认识 Blazor

简单来讲,Blazor旨在使用C#来替代JavaScript的Web应用程序的UI框架。其主要优势有:

  1. 使用C#编写代码,这可提高应用开发和维护的效率利用现有的NET库生态系统
  2. 受益于NET的性能、可靠性和安全性与新式托管平台(如 Docker) 集成
  3. 以一组稳定、功能丰富且易用的通用语言、框架和工具为基础来进行生成

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{}

运算(表达式)

@* 我是注释 *@

@* 代码区域 *@

@{

   var a = 1;

   var b = 2;

   var c = a + b;

}

 

@* 与字符串混用 *@

<h1>C的值:@(c)个单位</h1>

@* 默认隐式调用为ToString *@

<p>@DateTime.Now</p>

<p>@DateTime.IsLeapYear(2016)</p>

@* 显式表达式 *@

<p>上一周:@(DateTime.Now-TimeSpan.FromDays(7))</p>

@* HTML自动转义 *@

<p>@("<span>Hello world</span>")</p>

<p>@((MarkupString)"<span>Hello world</span>")</p>

@* 语句块 *@

@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中设置渲染模式

...

//在此设置服务端渲染模式

builder.Services.AddRazorComponents()

   .AddInteractiveServerComponents()

   .AddInteractiveWebAssemblyComponents();

...

...

//设置服务端渲染模式

app.MapRazorComponents<App>()

   .AddInteractiveServerRenderMode()

   .AddInteractiveWebAssemblyRenderMode()

   .AddAdditionalAssemblies(typeof(Client._Imports).Assembly);

  • App.razor为根网页,里面有<head>、<body>等信息,其中在body中指定了Routes

<!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,及其他设置

<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的地方展示

@inherits LayoutComponentBase

<div class="page">

   <div class="sidebar">

       <NavMenu />

   </div>

   <main>

       <article class="content px-4">

           @Body

       </article>

   </main>

</div>

  • NavMenu中设定了导航信息

<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]特性来标记(组件参数)

  1. 自定义组件CustomRazor

<h1>@Title</h1>


@code {

   [Parameter]public string? Title{ set; get; }

}

  1. 在父组件中使用自定义组件

<CustomRazor Title="自定义名称"/>

渲染片段

默认(单渲染片段)

  1. 自定义组件CustomRazor,渲染片段必须是RenderFragment?类型,以ChildContent命名

<h1>@Title</h1>


<p>渲染片段</p>

<p>@ChildContent</p>

@code {

   [Parameter]public string? Title{ set; get; }

   [Parameter] public RenderFragment? ChildContent { set; get; }

}

  1. 在父组件中使用自定义组件

<CustomRazor>

   我是渲染片段

</CustomRazor>

渲染片段RenderFragment可以呈现任何对象,不仅仅是字符串

<CustomRazor>

   <CustomRazor>

       渲染片段再次使用自定义组件

   </CustomRazor>

</CustomRazor>

多渲染片段

  1. 自定义组件CustomRazor

<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. 使用多个渲染片段

<CustomRazor>

   <ChildContent>

       我是第一个渲染片段

   </ChildContent>

   <OtherChildContent>

       我是第二个渲染片段

   </OtherChildContent>

</CustomRazor>

6. 导航参数和查询参数

导航参数

@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

查询参数

@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. 定义子组件

<h1>我是CustomRazor</h1>

<h1>@Title</h1>


@code {

   [CascadingParameter] string? Title{ set; get; }

}

  1. 使用子组件

<PageTitle>Home</PageTitle>


<CascadingValue Value="@("标题")">

       <CustomRazor />

</CascadingValue>

级联参数会自动匹配类型一样的值,比如上面级联参数的类型为string,如果具有多个级联参数,则会自动匹配最近的一个

<CascadingValue Value="@("外层")">

   <CascadingValue Value="@("内层")">

       <CustomRazor />

   </CascadingValue>

</CascadingValue>

如果想要有多个级联参数,可以指定名称

<h1>我是CustomRazor</h1>

<h1>@Title1</h1>

<h1>@Title2</h1>


@code {

   [CascadingParameter(Name ="Title1")] string? Title1 { set; get; }

   [CascadingParameter(Name = "Title2")] string? Title2 { set; get; }

}

<PageTitle>Home</PageTitle>


<CascadingValue Name="Title1" Value="@("外层")">

   <CascadingValue Name="Title2" Value="@("内层")">

       <CustomRazor />

   </CascadingValue>

</CascadingValue>

8. 事件和事件参数

事件是一个EventCallback类型,切支持泛型参数

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

   }

}

  • 自定义事件
  1. 首先定义一个Collapse.Razor,在该Razor中定义EventCallback类型的属性

<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);

   }

}

  1. 使用定义的Razor

<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. 创建一个自定义布局

@inherits LayoutComponentBase


<h3>EmptyLayout</h3>

<div>@Body</div>

  1. 使用该自定义布局

@page "/event"

@layout Layout.EmptyLayout @* 只能使用一次 *@


<h3>Event</h3>

。。。

10. 单向绑定和双向绑定

使用@bind来进行绑定

<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在失去焦点触发,不支持任何参数,经常用于输入验证

<p>

   <input @bind="InputValue" @bind:after="InputAfter"/>

</p>

<p>

   @message

</p>

@code{

   private string? InputValue{set;get;}

   string? message;

   

   void InputAfter()

   {

       message = "输入后得到";

   }

}

@bind:after的弊端是不能有参数,如果要含有参数则可以使用双向绑定

<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方法中进行验证操作

下拉框的绑定

<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只适用于组件内部,自定义组件实现双向绑定需按如下步骤:

  1. 定义绑定属性值

[Parameter] public string? Text { set; get; }

  1. 定义一个EventCallback泛型类型的属性,名称必须为第一步定义的属性值+Changed

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

  1. 在组件中绑定第一步属性值,及设置相应事件

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

   }

}

  1. 使用定义的组件进行绑定时,使用@bing-

<FormControl @bind-Text="@outerText">


</FormControl>

<p>

   @outerText

</p>

@code{

   string? outerText;

}

12. 组件的任意参数

当组件需要定义多个标签属性时,可以在定义对应的组件参数,但这样过于麻烦。可以借助@attributes来实现任意参数

<input type="text"  class="form-control @(Class)" @attributes="@Attributes" />


@code {

   [Parameter(CaptureUnmatchedValues =true)]public Dictionary<string,object>? Attributes{ get; set; }

}

使用组件

<FormControl Attributes="@(new Dictionary<string, object>{

   ["Title"]="文本框",

   ["style"]="color:red;font-size:18px"

})">


</FormControl>

上面代码中使用了[Parameter(CaptureUnmatchedValues =true)],可以自动转换为键值对。下面的使用方式与上面的效果完全相同。

<FormControl  title="文本框" style="color:red;font-size:18px">

</FormControl>

13. 表单和验证

在web中使用<form>元素创建表单,将input等放入其中实现表单功能,Blazor也支持这些,但提供了更多的组件

EditForm可以支持和对象直接关联,进行双向绑定,并提供更多功能

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

}

<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:当用户提交表单并且他们的输入验证失败时,将触发此事件。

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

   }

}

错误信息的展示

@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外面提交验证

上面的案例中是在EditForm中进行提交并且验证,而有时提交是在外面。此时需要EditContext

@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();

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

   }

}

FormCssClassProvider

上面案例中,如果出现错误则文本框边框会变为红色,这是因为当有错误时会添加invalid 的css类样式,如果想自定义样式,则可使用FormCssClassProvider

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";

   }

}

只需设置

public Form()

{

   Context = new EditContext(Model);

   Context.SetFieldCssClassProvider(new FormCssClassProvider());

}

此时,文本框中会加上对钩和感叹号

15. 组件的生命周期

16. 泛型组件

基本使用

泛型组件类似于C#中的泛型类,使用流程同样是先定义泛型参数,然后使用

  1. 定义泛型组件

<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. 可直接声明泛型类型,也可自动推断

<Genaric TValue="int" Value="100" TText="string" Text=@outerText />

<Genaric  Value="100" Text=@outerText />


@code{

   string outerText="字符串";

}

案例:根据绑定数据类型改变<input>的type

  • 泛型组件Genaric.razor

<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"

   };

}

  • 使用泛型组件

<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. 模板化组件

模版化组件通常和泛型组件相结合,案例:需展示数据列表,展示的形式及数据需可自定义。

@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同样也不确定需要展示哪些表头属性,需要用户来确定

  • 使用模版组件

<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中增加相应中间件

//在此设置服务端渲染模式

builder.Services.AddRazorComponents()

   .AddInteractiveServerComponents()

   .AddInteractiveWebAssemblyComponents();


//设置服务端渲染模式

app.MapRazorComponents<App>()

   .AddInteractiveServerRenderMode()

   .AddInteractiveWebAssemblyRenderMode()

   .AddAdditionalAssemblies(typeof(Client._Imports).Assembly);

如果在创建工程时设置了全局,则在App.razor中会自动设置渲染模式,渲染模式是向下传递的。也就是如果子组件没有设置渲染模式,则继承父组件的渲染模式。

<!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中设置错误提示

<article class="content px-4">

   <ErrorBoundary>

       @Body

   </ErrorBoundary>

</article>

默认错误提示

可自定义错误样式

<article class="content px-4">

   <ErrorBoundary>

       <ErrorContent>

           出现错误,@context.Message

       </ErrorContent>

       <ChildContent>

           @Body

       </ChildContent>          

   </ErrorBoundary>

</article>

21. 流式渲染

Count:@count


@code {

   int count = 0;


   async Task DoCount()

   {

       for (int i = 0; i < 10; i++)

       {

           await Task.Delay(1000);

           count++;

           StateHasChanged();

       }

   }

   // 页面会一直卡着,直到运行完DoCount

   protected override async Task OnInitializedAsync()

   {

       await DoCount();

   }

}

流式渲染解决了这个问题

只需要加上@attribute [StreamRendering]即可实现

22. 预呈现模式

预呈现是先呈现一部分尽快输出页面的HTML UI,让用户感觉提升了响应速度。

  1. 定义组件Perrender.razor

<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. 使用组件

第一个关闭预呈现,第二个打开预呈现

<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

  1. 注入IJSRuntime

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

//js中定义的函数

function hello() {

   alert('我是自定义hello函数');

}

在C#中调用

async Task Alert()

{

   await JS.InvokeVoidAsync("hello", "参数");

}

JS调用C#中的函数

静态方法

  1. 用C#写静态方法

public class Functions

{

   [JSInvokable]

   public static int Add()

   {

       return 1 + 5;

   }

   [JSInvokable]

   public static Task<int> AddAsync()

   {

       return Task.FromResult(1+10);

   }

}

  1. js调用写的静态方法

function add() {

   //参数1:C#函数所在的程序集 参数2:函数名,参数3:可变参数列表

   //DotNet是固定的

   let result = DotNet.invokeMethod('BlazorApp2.Client', 'Add');

   console.log(result);

}


function addAsync() {

   DotNet.invokeMethodAsync('BlazorApp2.Client', 'AddAsync').then(r=>console.log(r));

}

这种方式非常不推荐,如果有多个.net运行时,会导致错误

普通方法

  1. 创建普通方发

public class Functions

{

   [JSInvokable]

   public int Add()

   {

       return 1 + 5;

   }


   [JSInvokable]

   public Task<int> AddAsync()

   {

       return Task.FromResult(1 + 10);

   }

}

  1. 在razor页面中定义方法

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

   }

}

  1. js中调用C#中的方法

function add(p) {

   //不需要传递程序集,p为dot引用,里面有相关函数信息

   let result = p.invokeMethod('Add');

   console.log(result);

}


function addAsync(p) {

   p.invokeMethodAsync('AddAsync').then(r => console.log(r));

}

24. 渲染树

每个组件都是继承自ComponentBase类,完全可以自定义类来实现Razor组件的功能

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)

   {

       //<button></button>

       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();

   }

}

实现上面的类后可以像使用组件一样来使用

<Button>填充按钮</Button>

<Button Outline>边框按钮</Button>

<Button Tag="span">Span 按钮</Button>

25. 与 WEB API 的交互

在用Auto模式或者WebAssembly模式时,往往需要获取远程数据,这时就涉及到与Web API的交互。

  1. 首先要确定Web API允许跨域
  2. 一定要在工程名.Client项目下的Program.cs中注册HTTP服务

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();

}

  1. WebAPI的返回形式一般是Json数据,首先声明对应Json数据的类,然后注入HttpClient。并利用上文中模版化组件进行展示

@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

  1. .net Core运行时下载Hosting Bundle
  2. 设置发布路径,Ip、端口号等
  3. 确认设置模块中是否含有AspNetCoreModuleV2
  4. 确认处理程序映射是否含有aspNetCore
  5. 设置应用程序池,将.net Clr中.net CLR版本设置为无托管代码
相关文章
|
2天前
|
存储 前端开发 JavaScript
强烈推荐一个Python库!制作Web Gui也太简单了!
强烈推荐一个Python库!制作Web Gui也太简单了!
|
10月前
|
C#
如何使用MASA.Blazor
如何使用MASA.Blazor
83 0
|
10月前
|
C# 计算机视觉 开发者
在Winform中一分钟入门使用好看性能还好的Blazor Hybrid
在Winform中一分钟入门使用好看性能还好的Blazor Hybrid
143 0
在Winform中一分钟入门使用好看性能还好的Blazor Hybrid
|
开发框架 JavaScript 前端开发
|
前端开发 安全 JavaScript
【前端之旅】Web基础与开发工具
【前端之旅】Web基础与开发工具
【前端之旅】Web基础与开发工具
|
JavaScript 小程序 容器
使用mpvue开发小程序教程(六)
在上一章节中,我们列举了在Vue中能用但在mpvue中不能用或需要特别注意的特性,在实际开发前了解一下还是很有必要的,可以避免浪费找错误的时间。 如果你使用过原生的小程序框架,你一定经历过或思考过怎么解决以下的问题: 怎么存放可全局访问的变量? 页面...
1441 0
|
JavaScript 索引 小程序
使用mpvue开发小程序教程(五)
在上一章节中,我们了解了组件的三个基本特性以及组件的基本使用方法。在实际的小程序开发中,我们应该以组件的思维去设计每个小程序的功能页面,对其进行合理的组件拆分,让每个部分都保持功能简洁、条理清楚、各司其职,这样会让代码变得更易理解和维护,间接的也提升了代码的健壮性,降低出现Bug的几率,即使出现Bug,也会更容易进行定位和调试。
2171 0
|
JavaScript 索引 容器
使用mpvue开发小程序教程(四)
在上一章节中,我们将vue-cli命令行工具生成的代码骨架中的src目录清理了一遍,然后从头开始配置和编写了一个可以运行的小程序页面,算是正真走上了使用mpvue开发小程序的第一步。
1491 0
|
JavaScript 小程序 容器
使用mpvue开发小程序教程(三)
【注意事项】由于mpvue也在不断的开发演进,大家在不同时间段使用的时候,可能会遇到和文中的做法不一样的地方。请关注文章的评论区中大家的讨论,寻找解决方案,或者及时查阅官方文档,避免陷入版本更新的坑里哦。
1167 0
|
JavaScript API
使用mpvue开发小程序教程(二)
在上一篇文章中,我们介绍了使用mpvue开发小程序所需要的一些开发环境的搭建,并创建了第一个mpvue小程序代码骨架并将其运行起来。在本文中,我们来研究熟悉一下mpvue项目的主要目录和文件结构。
1622 0