Blazor系统教程(.net8)

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

运算(表达式)

@* 我是注释 *@

html 复制代码
@* 代码区域 *@
@{
    var a = 1;
    var b = 2;
    var c = a + b;
}

@* 与字符串混用 *@
<h1>C的值:@(c)个单位</h1>
html 复制代码
@* 默认隐式调用为ToString *@
<p>@DateTime.Now</p>
<p>@DateTime.IsLeapYear(2016)</p>
html 复制代码
@* 显式表达式 *@
<p>上一周:@(DateTime.Now-TimeSpan.FromDays(7))</p>
html 复制代码
@* HTML自动转义 *@
<p>@("<span>Hello world</span>")</p>
<p>@((MarkupString)"<span>Hello world</span>")</p>
html 复制代码
@* 语句块 *@
@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中设置渲染模式

csharp 复制代码
...
//在此设置服务端渲染模式
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddInteractiveWebAssemblyComponents();
...
...
//设置服务端渲染模式
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(Client._Imports).Assembly);
  • App.razor为根网页,里面有<head>、<body>等信息,其中在body中指定了Routes
html 复制代码
<!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,及其他设置
xml 复制代码
<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的地方展示
html 复制代码
@inherits LayoutComponentBase
<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>
    <main>
        <article class="content px-4">
            @Body
        </article>
    </main>
</div>
  • NavMenu中设定了导航信息
html 复制代码
<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
html 复制代码
<h1>@Title</h1>

@code {
    [Parameter]public string? Title{ set; get; }
}
  1. 在父组件中使用自定义组件
html 复制代码
<CustomRazor Title="自定义名称"/>

渲染片段

默认(单渲染片段)
  1. 自定义组件CustomRazor,渲染片段必须是RenderFragment?类型,以ChildContent命名
html 复制代码
<h1>@Title</h1>

<p>渲染片段</p>
<p>@ChildContent</p>
@code {
    [Parameter]public string? Title{ set; get; }
    [Parameter] public RenderFragment? ChildContent { set; get; }
}
  1. 在父组件中使用自定义组件
xaml 复制代码
<CustomRazor>
    我是渲染片段
</CustomRazor>

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

xml 复制代码
<CustomRazor>
    <CustomRazor>
        渲染片段再次使用自定义组件
    </CustomRazor>
</CustomRazor>
多渲染片段
  1. 自定义组件CustomRazor
html 复制代码
<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. 使用多个渲染片段
xml 复制代码
<CustomRazor>
    <ChildContent>
        我是第一个渲染片段
    </ChildContent>
    <OtherChildContent>
        我是第二个渲染片段
    </OtherChildContent>
</CustomRazor>

6. 导航参数和查询参数

导航参数

html 复制代码
@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

查询参数

html 复制代码
@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. 定义子组件
html 复制代码
<h1>我是CustomRazor</h1>
<h1>@Title</h1>

@code {
    [CascadingParameter] string? Title{ set; get; }
}
  1. 使用子组件
xml 复制代码
<PageTitle>Home</PageTitle>

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

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

xml 复制代码
<CascadingValue Value="@("外层")">
    <CascadingValue Value="@("内层")">
        <CustomRazor />
    </CascadingValue>
</CascadingValue>

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

html 复制代码
<h1>我是CustomRazor</h1>
<h1>@Title1</h1>
<h1>@Title2</h1>

@code {
    [CascadingParameter(Name ="Title1")] string? Title1 { set; get; }
    [CascadingParameter(Name = "Title2")] string? Title2 { set; get; }
}
xml 复制代码
<PageTitle>Home</PageTitle>

<CascadingValue Name="Title1" Value="@("外层")">
    <CascadingValue Name="Title2" Value="@("内层")">
        <CustomRazor />
    </CascadingValue>
</CascadingValue>

8. 事件和事件参数

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

html 复制代码
<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类型的属性
html 复制代码
<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
html 复制代码
<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. 创建一个自定义布局
html 复制代码
@inherits LayoutComponentBase

<h3>EmptyLayout</h3>
<div>@Body</div>
  1. 使用该自定义布局
html 复制代码
@page "/event"
@layout Layout.EmptyLayout @* 只能使用一次 *@

<h3>Event</h3>
。。。

10. 单向绑定和双向绑定

使用@bind来进行绑定

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

html 复制代码
<p>
    <input @bind="InputValue" @bind:after="InputAfter"/>
</p>
<p>
    @message
</p>
@code{
    private string? InputValue{set;get;}
    string? message;
    
    void InputAfter()
    {
        message = "输入后得到";
    }
}

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

html 复制代码
<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方法中进行验证操作

下拉框的绑定

html 复制代码
<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. 在组件中绑定第一步属性值,及设置相应事件
html 复制代码
<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-
html 复制代码
<FormControl @bind-Text="@outerText">

</FormControl>
<p>
    @outerText
</p>
@code{
    string? outerText;
}

12. 组件的任意参数

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

html 复制代码
<input type="text"  class="form-control @(Class)" @attributes="@Attributes" />

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

使用组件

html 复制代码
<FormControl Attributes="@(new Dictionary<string, object>{
    ["Title"]="文本框",
    ["style"]="color:red;font-size:18px"
})">

</FormControl>

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

html 复制代码
<FormControl  title="文本框" style="color:red;font-size:18px">
</FormControl>

13. 表单和验证

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

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

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

错误信息的展示

html 复制代码
@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

html 复制代码
@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();

html 复制代码
@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

csharp 复制代码
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";
    }
}

只需设置

csharp 复制代码
public Form()
{
    Context = new EditContext(Model);
    Context.SetFieldCssClassProvider(new FormCssClassProvider());
}

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

15. 组件的生命周期

16. 泛型组件

基本使用

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

  1. 定义泛型组件
html 复制代码
<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. 可直接声明泛型类型,也可自动推断
html 复制代码
<Genaric TValue="int" Value="100" TText="string" Text=@outerText />
<Genaric  Value="100" Text=@outerText />

@code{
    string outerText="字符串";
}

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

  • 泛型组件Genaric.razor
csharp 复制代码
<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"
    };
}
  • 使用泛型组件
html 复制代码
<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. 模板化组件

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

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

  • 使用模版组件
html 复制代码
<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中增加相应中间件

csharp 复制代码
//在此设置服务端渲染模式
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddInteractiveWebAssemblyComponents();

//设置服务端渲染模式
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(Client._Imports).Assembly);

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

html 复制代码
<!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中设置错误提示

html 复制代码
<article class="content px-4">
    <ErrorBoundary>
        @Body
    </ErrorBoundary>
</article>

默认错误提示

可自定义错误样式

html 复制代码
<article class="content px-4">
    <ErrorBoundary>
        <ErrorContent>
            出现错误,@context.Message
        </ErrorContent>
        <ChildContent>
            @Body
        </ChildContent>          
    </ErrorBoundary>
</article>

21. 流式渲染

csharp 复制代码
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
html 复制代码
<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. 使用组件

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

html 复制代码
<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
html 复制代码
@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>

javascript 复制代码
//js中定义的函数
function hello() {
    alert('我是自定义hello函数');
}

在C#中调用

csharp 复制代码
async Task Alert()
{
    await JS.InvokeVoidAsync("hello", "参数");
}

JS调用C#中的函数

静态方法
  1. 用C#写静态方法
csharp 复制代码
public class Functions
{
    [JSInvokable]
    public static int Add()
    {
        return 1 + 5;
    }
    [JSInvokable]
    public static Task<int> AddAsync()
    {
        return Task.FromResult(1+10);
    }
}
  1. js调用写的静态方法
javascript 复制代码
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. 创建普通方发
csharp 复制代码
public class Functions
{
    [JSInvokable]
    public int Add()
    {
        return 1 + 5;
    }

    [JSInvokable]
    public Task<int> AddAsync()
    {
        return Task.FromResult(1 + 10);
    }
}
  1. 在razor页面中定义方法
html 复制代码
@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#中的方法
javascript 复制代码
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组件的功能

csharp 复制代码
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();
    }
}

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

html 复制代码
<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服务
csharp 复制代码
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。并利用上文中模版化组件进行展示
csharp 复制代码
@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版本设置为无托管代码
相关推荐
WineMonk7 小时前
.NET WPF CommunityToolkit.Mvvm框架
.net·wpf·mvvm
界面开发小八哥7 小时前
界面控件DevExpress WPF中文教程:Data Grid——卡片视图设置
.net·wpf·界面控件·devexpress·ui开发
九鼎科技-Leo18 小时前
了解 .NET 运行时与 .NET 框架:基础概念与相互关系
windows·c#·.net
九鼎科技-Leo21 小时前
什么是 ASP.NET Core?与 ASP.NET MVC 有什么区别?
windows·后端·c#·asp.net·mvc·.net
.net开发21 小时前
WPF怎么通过RestSharp向后端发请求
前端·c#·.net·wpf
九鼎科技-Leo1 天前
在 C# 中,ICollection 和 IList 接口有什么区别?
windows·c#·.net
时光追逐者1 天前
C#/.NET/.NET Core学习路线集合,学习不迷路!
开发语言·学习·c#·asp.net·.net·.netcore·微软技术
细心的莽夫1 天前
JavaWeb学习笔记
java·开发语言·笔记·学习·java-ee·web
Crazy Struggle1 天前
.NET 全功能流媒体管理控制接口平台
.net·开源项目·流媒体