ASP.NET Core 3.x 学习笔记(7)——Blazor
本系列学习笔记均来源于B站UP主”软件工艺师“的学习视频,学习连接如下:
https://www.bilibili.com/video/BV1c441167KQ
ASP.NET Core 3.x 学习笔记(7)——Blazor
编程模式对比
MVC
SPA(Single Page Application):这里的 Static Html、Js、Css 不一定总是来源于 Server API,也可能是服务器其它地方。这里主要体线的区别是 Browser 的 Javascript。
SPA(Blazor):Blazor 的 Browser 端使用 C#。
Blazor
- Blazor 是基于 Component(组件)的编程模式,即 Blazor 是写组件的。
- Blazor 的宿主模型分为:
- 客户端
- 服务器端
客户端宿主模型
如下图,mono 把 Server 端 C# 语言编写生成的 DLL,解析成了 Browser 中 浏览器可以理解的 WebAssembly,然后 Browser 就可以通过 Javascript 进行互操作。
优点:
- 没有 .NET 服务器端依赖项。应用下载到客户端之后完全正常运行。
- 完全利用客户端资源和功能。
- 工作从服务器卸载到客户端,即工作量从服务器转移到客户端进行。
- 不需要 ASP.NET Core web 服务器来托管应用程序,可能使用无服务器部署方案,例如,可通过 CDN 提供应用。
缺点:
- 应用程序限制为浏览器的功能
- 需要支持的客户端硬件和软件,例如 WebAssembly 支持
- 下载的文件大小较大,应用需要较长时间才能加载
- .NET 运行时和工具支持不太成熟。
Mono
- mono 是一个开源的 .NET Framework
- 它可以解释 IL(中间语言:intermediary language)
- C# 代码的 IL 是包含在 .NET 的 Assembly(程序集:DLL)里面;(然后发送给浏览器)
- 浏览器之所以可以执行 mono,是因为它接收到的 mono 版本是使用一种类似汇编(Assembly)的低级语言编写的。而浏览器可以理解这种语言,它就是 WebAssembly。
- 然后,mono 就会把 Assembly 里面的代码(包含着 Components)解析成为 WebAssembly。这样就可以在浏览器里面运行了。
服务器端宿主模型
服务器端(Server)对 Components 完成渲染,将渲染好的 Components 传递到浏览器(Browser)
浏览器(Browser)将前端互操作传递到服务器端(Server),服务器端对其重新进行渲染并把更新后的 UI 发送到浏览器(Browser)。
两者之间通过 SignalR 传输。
优点:
- 下载大小明显小于客户的应用,且应用加载速度更快。
- 应用充分利用服务器功能,包括使用任何与 .NET Core 兼容的 API。
- 服务器上的 .NET Core 用于运行应用程序,因此现有的 .NET Core 工具(如调试),可按预期方式工作。
- 支持廋客户端,例如服务器端应用程序适用于不支持 web assembly 的浏览器和资源限制的设备。因为所有的工作都是在服务器中处理的。
- 应用程序的 .NET/C# 代码库(包括应用程序的组件代码)不会提供给客户端。
缺点:
- 通常存在较高的延迟,每个用户交互都涉及网络跃点
- 无脱机支持。如果客户端连接失败,应用将停止工作。
- 对于包含多个用户的应用而言,可伸缩性非常困难,服务器必须管理多个客户端连接并处理客户端状态
- 为用户提供服务需要 ASP.NET Core 服务器。不可能的无服务器部署方案。例如,可通过 CDN 为应用提供服务。
从 Empty 模板开始建立一个 Blazor 项目
-
Models 和 Services 和之前的 MVC 项目没有区别,可以直接拿过来使用;静态文件也可以直接拿过来用。
-
在 Startup.cs 中添加 Blazor 项目需要的配置和需要使用到的中间件
using ASPNETCore_Learnting_Blazor.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace ASPNETCore_Learnting_Blazor { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); //Blazor 需要使用到 Razor 页面/组件 services.AddServerSideBlazor(); //添加服务器端的 Blazor //下面是注入项目中需要使用的实体类 services.AddSingleton<IClock, ChinaClock>(); services.AddSingleton<IDepartmentService, DepartmentService>(); services.AddSingleton<IEmployeeService, EmployeeService>(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); app.UseRouting app.UseEndpoints(endpoints => { endpoints.MapBlazorHub(); //类似于一个 特制版的 SingalR Hub endpoints.MapFallbackToPage("/_Host"); //设置默认启动页面,会找到当前项目路径下的“/Pages/_Host.cshtml”文件 }); } } }
-
建立默认页面”/Pages/_Host.cshtml“。这里通过路由设置具体的页面(Razor Components),这里的 App 进行路由设置。
@page "/" @namespace ASPNETCore_Learnting_Blazor.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Blazor</title> <base href="~/" /> <link rel="stylesheet" href="~/css/bootstrap.css" /> <link rel="stylesheet" href="~/css/site.css" /> </head> <body> <app> @* 这里通过路由设置具体的页面(Razor Components),这里的 App 进行路由处理 *@ @(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered)) </app> <script src="_framework/blazor.server.js"></script> </body> </html>
-
建立处理路由的文件,”/App.razor“。这里的 MainLayout 指的是模板页面,即布局页面。
<Router AppAssembly="@typeof(Program).Assembly"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry.</p> </LayoutView> </NotFound> </Router>
-
建立处理布局页面,”/Shared/MainLayout.razor“。每个具体页面的内容在下面页面代码的 @Body 处。
@inherits Microsoft.AspNetCore.Components.LayoutComponentBase <div class="container"> <div class="row"> <div class="col-md-2"> <img asp-append-version="true" alt="Logos" src="~/images/log.jpg" style="max-width:100%" /> </div> <div class="col-md-10"> <span class="h3">Blazor</span> </div> </div> <div class=" row"> <div class="col-md-10"> @Body </div> </div> </div>
-
添加公用的引用文件 ”/_importants.razor“。如下添加的引用不是一次性加入的,是在后续编写代码的过程中一步步补充的 。
@using System.Net.Http @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.JSInterop @using ASPNETCore_Learnting_Blazor.Models @using ASPNETCore_Learnting_Blazor.Services @using ASPNETCore_Learnting_Blazor.Shared @using ASPNETCore_Learnting_Blazor.Components
-
接下来实现具体的页面。
-
首先实现 "/Pages/Index.razor"页面。如下 @page 是设置页面路由;@inject 是注入相关实例。
如下代码中,在 @Code 代码块中可以编写处理相关业务逻辑的 C# 代码。
@page "/" @inject IDepartmentService departmentService @if (departments == null) { <p><em>加载中......</em></p> } else { <div class="row"> <div class="col-md-10 offset-md-2"> <table class="table"> <tr> <th>Name</th> <th>Location</th> <th>Employee</th> <th>操作</th> </tr> @foreach (var item in departments) { <DepartmentItem department="@item"></DepartmentItem> } </table> </div> </div> <div class="row"> <div class="col-md-4 offset-md-2"> <a href="/add-department">Add</a> </div> </div> } @code { IEnumerable<Department> departments; protected override async Task OnInitializedAsync() { departments = await departmentService.GetAll(); } }
-
如上代码,DepartmentItem 是表征每一行 Department 数据而建立的 Razor Compontents,在“/Components”文件夹下。
其中,@Department 是传递进来的实例,需要添加 [Parameter],在代码中即可获取相关参数。
<tr> <td>@Department.Name</td> <td>@Department.Location</td> @if (Department.EmployeeCount > 18) { <td><strong>@Department.EmployeeCount</strong></td> } else { <td>@Department.EmployeeCount</td> } <td> <a href="/employee/@Department.Id">Employees</a> </td> </tr> @code{ [Parameter] public Department Department { get; set; } }
-
添加增加部门的 AddDepartment.razor 页面,关联到上文中的 “/add-department” 路由。
如下表单代码中:
- EditForm InputText 等是内置的 Components
- OnValidSubmit="@HandleValidSubmit" 绑定表单提交时的业务逻辑处理函数
- DataAnnotationsValidator、ValidationSummary 等进行表单验证
- NavigationManager 进行路径跳转
- @bind-Value 进行参数值的双向绑定
@page "/add-department" @inject IDepartmentService departmentService @inject NavigationManager navigationManager @* EditForm InputText 等是内置的 Components *@ @*OnValidSubmit="@HandleValidSubmit" 表单提交*@ <EditForm Model="@department" OnValidSubmit="@HandleValidSubmit"> @* 表单验证 *@ <DataAnnotationsValidator /> <ValidationSummary /> <div class="row form-group"> <div class="col-md-2 offset-md-2"> <label for="name">Name</label> </div> <div class="col-md-2 offset-md-2"> @*@bind-Value 双向绑定*@ <InputText id="name" class="form-control" @bind-Value="department.Name" /> </div> </div> <div class="row form-group"> <div class="col-md-2 offset-md-2"> <label for="location">Location</label> </div> <div class="col-md-2 offset-md-2"> <InputText id="location" class="form-control" @bind-Value="department.Location" /> </div> </div> <div class="row form-group"> <div class="col-md-2 offset-md-2"> <label for="employeeCount">EmployeeCount</label> </div> <div class="col-md-2 offset-md-2"> <InputNumber id="employeeCount" class="form-control" @bind-Value="department.EmployeeCount" /> </div> </div> <div class="row"> <div class="col-md-2 offset-md-2"> <button type="submit" class="btn btn-primary">Add</button> </div> </div> </EditForm> @code { private Department department = new Department(); private async Task HandleValidSubmit() { await departmentService.Add(department); navigationManager.NavigateTo("/"); } }
-
添加 /Pages/Employee.razor、/Pages/AddEmployee.razor、/Components/EmployeeItem.razor 等页面,代码逻辑于前文大同小异。
若 razor 中 C#逻辑较为复杂,若C#与razor放在一起,则文件会较大,故单独放置C#逻辑代码
@page "/employee/{DepartmentId}" @inject IEmployeeService employeeService @if (Employees == null) { <p><em>Loading......</em></p> } else { <div class="row"> <div class="col-md-10 offset-md-2"> <table class="table"> <tr> <th>First Name</th> <th>Last Name</th> <th>Gender</th> <th>Is Fired</th> <th>Action</th> </tr> @foreach (var employee in Employees) { <EmployeeItem employee="@employee"></EmployeeItem> } </table> <a href="/add-employee/@DepartmentId">Add</a> </div> </div> } @code{ [Parameter] public string DepartmentId { get; set; } public IEnumerable<ASPNETCore_Learnting_Blazor.Models.Employee> Employees; //[Inject] //public IEmployeeService EmployeeService { get; set; } protected override async Task OnInitializedAsync() { Console.WriteLine(DepartmentId); Employees = await employeeService.GetByDepartmentId(int.Parse(DepartmentId)); Console.WriteLine(Employees.Count()); } }
AddEmployee.razor :
@page "/add-employee/{departmentId}" @inject IEmployeeService employeeService @inject NavigationManager navigationManager <EditForm Model="@employee" OnValidSubmit="@HandleValidSubmit"> <DataAnnotationsValidator /> <ValidationSummary /> <div class="row form-group"> <div class="col-md-2 offset-md-2"> <label for="firstName">First Name</label> </div> <div class="col-md-2 offset-md-2"> <InputText id="firstName" class="form-control" @bind-Value="employee.FirstName" /> </div> </div> <div class="row form-group"> <div class="col-md-2 offset-md-2"> <label for="lastName">Last Name</label> </div> <div class="col-md-2 offset-md-2"> <InputText id="lasName" class="form-control" @bind-Value="employee.LastName" /> </div> </div> <div class="row form-group"> <div class="col-md-2 offset-md-2"> <label for="gender">Gender</label> </div> <div class="col-md-2 offset-md-2"> <select id="gender" class="form-control" @onchange="OnGenderSelected"> <option selected hidden disabled>Please select gender</option> @foreach (var item in Enum.GetValues(typeof(Gender)).Cast<Gender>()) { <option value="@item">@item.ToString()</option> } </select> </div> </div> <div class="row"> <div class="col-md-2 offset-md-2"> <button type="submit" class="btn btn-primary">Add</button> </div> </div> </EditForm> @code { [Parameter] public string departmentId { get; set; } public ASPNETCore_Learnting_Blazor.Models.Employee employee = new ASPNETCore_Learnting_Blazor.Models.Employee(); private void OnGenderSelected(ChangeEventArgs e) { var gender = Enum.Parse(typeof(Gender), (string)e.Value); employee.Gender = (Gender)gender; } public async Task HandleValidSubmit() { employee.DepartmentId = int.Parse(departmentId); await employeeService.Add(employee); navigationManager.NavigateTo($"/employee/{departmentId}"); } }
EmployeeItem.razor
@inject IEmployeeService employeeService <tr> <td>@employee.FirstName</td> <td>@employee.LastName</td> <td>@employee.Gender</td> <td>@(employee.Fired ? "YES" : "")</td> <td> <a href="javascript:void(0)" @onclick="HandleFire"> Fire </a> </td> </tr> @code{ [Parameter] public Employee employee { get; set; } private async void HandleFire() { await employeeService.Fire(employee.Id); } }
-
-
完成新建后的项目结构和效果图如下: