《进击吧!Blazor!》是本人与张善友老师合作的Blazor零基础入门系列视频,此系列能让一个从未接触过Blazor的程序员掌握开发Blazor应用的能力。

视频地址:https://space.bilibili.com/483888821/channel/detail?cid=151273

演示代码:https://github.com/TimChen44/Blazor-ToDo

本系列文章是基于《进击吧!Blazor!》直播内容编写,升级.Net5,改进问题,讲解更全面。

作者:陈超超

Ant Design Blazor 项目贡献者,拥有十多年从业经验,长期基于.Net技术栈进行架构与开发产品的工作,现就职于正泰集团。

邮箱:timchen@live.com

欢迎各位读者有任何问题联系我,我们共同进步。

上一次课程我们完成了ToDo应用的界面制作,这次我们要将客户端的数据写入数据库,并从数据库中读物我们需要的数据。

数据交互过程

我们先看一下从客户端到数据库的流程

Blazor

Blazor客户端就是我们上节课做的ToDo程序。

HttpClient

HttpClient就是我们完成网络通讯用的组件,对于这类组件我们希望在一个应用中只构造一次,这样避免重复分配资源,因此我们在Program.cs中进行注册。

public class Program
{
public static async Task Main(string[] args)
{
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
}
}

BaseAddress为基地址,这样我们使用时,Url只需要传入相对地址即可,此处默认为当前主机的地址。

DefaultRequestHeaders 默认HTTP请求头参数

Timeout 连接超时参数

  • 依赖关系注入

上面通过服务注入的方式实现了HttpClient全局共享(单例),那么如何使用服务?这里我们就需要引入一下“依赖关系注入 (DI)”的概念。

DI是一种技术,基本原理是把有依赖关系的类放到容器中,解析出这些类的实例,就是依赖注入。应用可通过将内置服务注入组件来使用这些服务。 应用还可定义和注册自定义服务,并通过 DI 使其在整个应用中可用。

该技术在 Blazor 应用中常用于以下两个方面:

服务生存期决定了服务何时创建,何时销毁,有三种模式:

ScopedBlazor WebAssembly 应用当前没有 DI 范围的概念。 已注册 Scoped 的服务的行为与 Singleton 服务类似。 但是,Blazor Server 托管模型支持 Scoped 生存期。 在 Blazor Server 应用中,Scoped服务注册的范围为“连接”。 因此,即使当前意图是在浏览器中运行客户端,对于范围应限定为当前用户的服务来说,首选使用 Scoped 服务。

Singleton:DI 创建服务的单个实例。 需要 Singleton 服务的所有组件都会接收同一服务的实例。

Transient:每当组件从服务容器获取 Transient 服务的实例时,它都会接收该服务的新实例。

这里的 HttpClient 使用了 AddScoped 方法,那么就是当前范围内使用同一个实例,因为项目是Blazor WebAssembly模式,所以相当于单例服务。

ASP.Net Core

我用ASP.Net Core项目给Blazor应用提供WebAPI接口

官方文档:https://docs.microsoft.com/zh-cn/aspnet/core

项目结构如下

  • launchSettings.json

这里配置了我们调试的方式,端口等,相对于普通的Web项目多了inspectUri属性,具有以下作用:

  1. 使 IDE 能够检测到该应用为 Blazor WebAssembly 应用。
  2. 指示脚本调试基础结构通过 Blazor 的调试代理连接到浏览器。
  3. 已启动的浏览器 (browserInspectUri) 上 WebSocket 协议 (wsProtocol)、主机 (url.hostname)、端口 (url.port) 和检查器 URI 的占位符值由框架提供。
{
//省略其他配置
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
//省略其他配置
}
  • Controllers

控制器(Controller)放在这里,站点的路由表是通过遍历项目中带有ApiControllerAttribute(基类ControllerAttribute)的类,然后寻找里面的方法实现,他和Blazor的路由表创建方法上有点相似。

    [ApiController]
[Route("api/[controller]/[action]")]
public class TaskController : ControllerBase

Route定义了路由格式,上例中[controller]/[action]意为使用Controlleraction的名称作为路由地址,这样写可以省去每个action上标记路由名字的麻烦。

  • Pages

存放页面文件的位置,因为我们的项目页面全部使用Blazor构建,所以用不到此文件夹,因此这里就不做介绍了。

  • appsettings.json

站点的配置文件,我们的项目就用到了数据库链接字符串配置

  • Program.cs

应用的Main函数在这里,这里完成了Host的创建与启动

  • Startup.cs

启动类,项目启动时的服务注册,配置等工作都在此处完成

ConfigureServices使用此方法将服务添加到容器。

Configure使用此方法来配置HTTP请求管道。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//省略其他代码
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}

app.UseBlazorFrameworkFiles();配置应用程序提供Blazor WebAssembly框架文件,默认根路径为/,也可以自定义路径前缀

endpoints.MapControllers();添加控制器(Controller)的路由。

endpoints.MapFallbackToFile("index.html");添加默认路由地址是index.html

EF Code

所有的数据我们需要保存入数据库,这里我选择使用EF Core作为我们的数据访问技术

官方文档:https://docs.microsoft.com/zh-cn/ef/

EF Core部分特点

  • Entity Framework (EF) Core 是轻量化、可扩展、开源和跨平台版的数据访问技术。
  • EF Core可用作对象关系映射程序 (O/RM),能让我们用对象来处理数据库,使用Linq进行查询,这样我们就可以不用编写大量SQL代码了。
  • EF Core 支持多个数据库引擎,比如MySQL、SQLite等。

他支持采用Code Firs或者Database First两种模式

  • Code Firs用代码编写对象关系,然后通过它创建数据库。
  • Database First可以提供现有数据库,反向生成对象映射 。

Database

数据库我选择SQL Server,使用全套微软技术栈工具链使用体验比较好,当然我们也可以选择其他数据库。

SQL Server产品家族中有一个SQL Server LocalDB的东西,它是SQL Server的一个超级精简版本,安装包只有几十MB(安装好后200+MB),它包含了数据库的基础功能,但是不支持联网,只能本机连接,对于个人开发资源占用少,强烈推荐,VS安装Web开发组件会默认安装此数据库。

连接时服务器名称默认是(localdb)\MSSQLLocalDB,也可以使用C:\Program Files\Microsoft SQL Server\130\Tools\Binn\SqlLocalDB.exe进行配置数据库实例

我们可以使用VS的SQL Server对象资源管理器来查看我们的数据库,不过我这里强烈推荐使用SQL Server Management Studio (SSMS) 的“数据库关系图”功能来维护数据库,可视化编辑表,主外键关系等,保存即更新数据库,这对于数据库优先的模式下开发非常友好,效率极高。

下图是我们ToDo应用使用的表结构。

代码实战

上面介绍了数据交互的流程概念,接下来我们改造上回制作的ToDo项目。

引入和配置EF Code

我们先创建一个ToDo.Entity项目用于存储ORM映射以及EF的Context。

注意:目前VS 16.8.4版本创建类库会默认使用.net core 3.1,需要手动修改成.net 5

使用EF Core Power Tools工具创建代码

因为我们上面已经把数据库设计完成了,所以我们采用Database First模式创建EF相关的代码。

此处推荐一个从数据库到EF实体的代码生成扩展EF Core Power Tools

扩展下载地址:https://marketplace.visualstudio.com/items?itemName=ErikEJ.EFCorePowerTools

选择要连接的数据库。

选择要添加的数据库对象。

配置Context的名称和命名空间等,下图是我常用配置。

EF Core Power Tools生成的代码文件如下

appsettings.json中添加链接字符串

打开ToDo.Server\appsettings.json添加数据库连接字符串

  "ConnectionStrings": {
"DefaultConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=ToDo;Integrated Security=True"
},

使用EF Core Power Tools生成的TodoContext.cs文件中就有默认的连接字符串,开发时想偷懒可以直接从这里复制。

ConfigureServices中添加服务注册

打开ToDo.Server\Startup.cs,把TodoContext注册到DbContext中为,并设置连接字符串

services.AddDbContext<TodoContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});

有时候我们需要在输出EF执行的SQL语句,这便于我们调试以及优化数据库,下面的配置就把EF日志输出到控制台

/// <summary>
/// 输出日志
/// </summary>
public static readonly ILoggerFactory loggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); }); public void ConfigureServices(IServiceCollection services)
{
//省略其他代码
services.AddDbContext<TodoContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")).UseLoggerFactory(loggerFactory);
});
}

功能实现

首先创建ToDo.Server\Controllers\TaskController.cs文件用于编写WebAPI接口,代码如下:

namespace ToDo.Server.Controllers
{
[ApiController]
[Route("api/[controller]/[action]")]
public class TaskController : ControllerBase
{
TodoContext Context; public TaskController(TodoContext context)
{
Context = context;
}
}
}

通过依赖注入将TodoContext注入到当前类中。

1. 列出当天的所有代办工作

ToDo.Server

TaskController.cs中添加GetToDayTask方法用于返回当前待办数据。

[HttpGet]
public List<TaskDto> GetToDayTask()
{
var result = Context.Task.Where(x => x.PlanTime == DateTime.Now.Date);
return QueryToDto(result).ToList();
} [NonAction]
private IQueryable<TaskDto> QueryToDto(IQueryable<Entity.Task> query)
{
return query.Select(x => new TaskDto()
{
TaskId = x.TaskId,
Title = x.Title,
Description = x.Description,
PlanTime = x.PlanTime,
Deadline = x.Deadline,
IsImportant = x.IsImportant,
IsFinish = x.IsFinish,
});
}

ToDo.Client

添加Pages\ToDay.razor.cs类文件,VS会自动将ToDay.razorToDay.razor.cs折叠到一起。

在类定义中增加partial关键字,声明类为局部类,你可以理解成ToDay.razorToDay.razor.cs中的代码都属于同一个类,只是放在不同文件中,编译器编译时会将他们合并到一起后进行编译。

    public partial class ToDay

接着做一下代码迁移

  1. Pages\ToDay.razor文件@code{}中的代码剪切到ToDay.razor.cs
  2. Pages\ToDay.razor文件@inject代码采用[Inject] public MessageService MsgSrv { get; set; }这样的格式等价的迁移到ToDay.razor.cs

这样做我们可以实现界面代码与业务代码分开在不同的文件中,方便整理代码,提高代码可读性。

后续其他页面我默认完成了创建局部类的操作,不再赘述。

ToDay.razor.cs中添加HttpClient的依赖注入,用于向服务端发起Http请求

[Inject] public HttpClient Http { get; set; }

项目中其他类如果使用到HttpClient,我默认完成了依赖注入,不再赘述。

广告时间

BlazorAnt Design Blazor中有很多服务,我们经常在不同的地方需要注入,为了编码方便,我们提供了一个VS扩展快速插入常用服务代码段,安装地址:https://marketplace.visualstudio.com/items?itemName=TimChen44.AntDesignBlazorSnippets

修改OnInitializedAsync方法的代码

private List<TaskDto> taskDtos = new List<TaskDto>();
bool isLoading = true;
protected async override Task OnInitializedAsync()
{
isLoading = true;
taskDtos = await Http.GetFromJsonAsync<List<TaskDto>>("api/Task/GetToDayTask");
isLoading = false;
await base.OnInitializedAsync();
}

Http.GetFromJsonAsync<List<TaskDto>>使用HttpGet模式请求数据,这里使用await进行异步等待,充分利用await可以极大的简化代码量。

isLoading是载入状态,网络通讯必定有延迟,避免白屏,我们在载入前后分别改变载入状态,同时修改ToDay.razor代码添加Spin组件用于显示载入效果。

<PageHeader Title="@("我的一天")" Subtitle="@DateTime.Now.ToString("yyyy年MM月dd日")"></PageHeader>
<Spin Spinning="isLoading"><!--插入代码-->
@foreach (var item in taskDtos)
<!--省略其他代码-->
<Input @bind-Value="@newTask.Title" OnkeyUp="OnInsert" />
</div>
</Spin><!--插入代码-->

2. 添加代办

ToDo.Server

TaskController.cs中添加SaveTask方法用于保存新的待办内容

[HttpPost]
public Guid SaveTask(TaskDto dto)
{
Entity.Task entity;
if (dto.TaskId == Guid.Empty)
{
entity = new Entity.Task();
entity.TaskId = Guid.NewGuid();
Context.Add(entity);
}
else
{
entity = Context.Task.FirstOrDefault(x => x.TaskId == dto.TaskId);
}
entity.Title = dto.Title;
entity.Description = dto.Description;
entity.PlanTime = dto.PlanTime;
entity.Deadline = dto.Deadline;
entity.IsImportant = dto.IsImportant;
entity.IsFinish = dto.IsFinish;
Context.SaveChanges();
return entity.TaskId;
}

我通过判断dto.TaskId的值,直接将新增与更新写在一个接口中,这样可以复用代码。

此处可以使用AutoMapper库来简化赋值过程,这将在将来的章节中详细介绍

ToDo.Client

ToDay.razor.cs文件修改OnInsert方法相关的代码

TaskDto newTask = new TaskDto() { PlanTime = DateTime.Now.Date };
[Inject] public MessageService MsgSrv { get; set; }
bool isNewLoading = false;
async void OnInsert(KeyboardEventArgs e)
{
if (e.Code == "Enter")
{
if (string.IsNullOrWhiteSpace(newTask.Title))
{
MsgSrv.Error($"标题必须填写");
return;
}
isNewLoading = true;
var result = await Http.PostAsJsonAsync<TaskDto>($"api/Task/SaveTask", newTask);
if (result.IsSuccessStatusCode)
{
newTask.TaskId = await result.Content.ReadFromJsonAsync<Guid>();
taskDtos.Add(newTask);
newTask = new TaskDto() { PlanTime = DateTime.Now.Date };
}
else
{
MsgSrv.Error($"请求发生错误 {result.StatusCode}");
}
isNewLoading = false;
StateHasChanged();
}
}

ToDay.razor文件增加保存时等待组件

    <Spin Spinning="isNewLoading"><!--插入代码-->
<div class="task-input">
<DatePicker Picker="@DatePickerType.Date" @bind-Value="@newTask.PlanTime" />
<Input @bind-Value="@newTask.Title" OnkeyUp="OnInsert" />
</div>
</Spin><!--插入代码-->

通过Http.PostAsJsonAsync调用api/Task/SaveTasknewTask内容提交到后端并保存,返回的HttpResponseMessage包含了状态编码等,如果成功就在界面上显示新的待办,失败就提示错误

MessageService全局展示操作反馈信息。

组件文档地址:https://ant-design-blazor.github.io/zh-CN/components/message

3. 编辑待办

ToDo.Server

TaskController.cs中添加GetTaskDto方法用于获取待办信息

public TaskDto GetTaskDto(Guid taskId)
{
var result = Context.Task.Where(x => x.TaskId == taskId);
return QueryToDto(result).FirstOrDefault();
}

ToDo.Client

TaskInfo.razor文件中增加Spin@if代码

<Spin Spinning="isLoading">
@if (taskDto != null)<!--页面打开时taskDto并没有值,所以直接绑定到Form会发生异常,所以这里需要做一个不为空判断-->
{
<Form OnFinish="OnSave" Model="taskDto" LabelColSpan="8"><!--当用户点击submit按钮时会触发OnFinish事件,所以通常会在这里进行保存操作-->
<!--省略其他代码-->
<div>
<Button HtmlType="submit">保存</Button>
<Button OnClick="OnCancel">取消</Button>
</div>
</Form>
}
</Spin>

TaskInfo.razor.cs添加下面代码

public partial class TaskInfo : DrawerTemplate<TaskDto, TaskDto>
{
[Inject]
public HttpClient Http { get; set; } [Inject]
public MessageService MsgSvr { get; set; } TaskDto taskDto; bool isLoading = false; protected override async Task OnInitializedAsync()
{
//通过api/Task/GetTaskDto接口获得待办内容
taskDto = await Http.GetFromJsonAsync<TaskDto>($"api/Task/GetTaskDto?taskId={base.Options.TaskId}");
await base.OnInitializedAsync();
} async void OnSave()
{
var result = await Http.PostAsJsonAsync<TaskDto>($"api/Task/SaveTask", taskDto);
if (result.StatusCode == System.Net.HttpStatusCode.OK)
{
await base.CloseAsync(taskDto);//关闭抽屉,并返回当前待办数据
}
else
{
MsgSvr.Error($"请求发生错误 {result.StatusCode}");
}
} async void OnCancel()
{
await base.CloseAsync(null);//如果点击了取消,那么将null返回出去
}
}

ToDay.razor.cs中的OnCardClick方法更新

[Inject] public DrawerService DrawerSrv { get; set; }
async void OnCardClick(TaskDto task)
{
var result = await DrawerSrv.CreateDialogAsync<TaskInfo, TaskDto, TaskDto>(task, title: task.Title, width: 450);
if (result == null) return;
var index = taskDtos.FindIndex(x => x.TaskId == result.TaskId);
taskDtos[index] = result;
await InvokeAsync(StateHasChanged);
}

DrawerSrv.CreateDialogAsync相对于DrawerSrv.CreateAsync简化了调用方法,默认将抽屉的CloseAsync参数返回,这就简化了每次使用抽屉时需要注册CloseAsync事件的麻烦,也让代码更加清晰。

title: task.Title, width: 450使用可选参数简化对抽屉的参数配置。

DrawerService组件帮助文档:https://ant-design-blazor.github.io/zh-CN/components/drawer

4. 修改重要程度

ToDo.Server

TaskController.cs中添加SetImportant方法用于修改IsImportant字段的值

[HttpPost]
public void SetImportant(SetImportantReq req)
{
var entity = Context.Task.FirstOrDefault(x => x.TaskId == req.TaskId);
entity.IsImportant = req.IsImportant;
Context.SaveChanges();
}

ToDo.Shared

添加SetImportantReq类用于SetImportant接口请求参数

public class SetImportantReq
{
public Guid TaskId { get; set; }
public bool IsImportant { get; set; }
}

ToDo.Client

ToDay.razor.cs中的OnStar方法更新

private async void OnStar(TaskDto task)
{
var req = new SetImportantReq()//ToDo.Shared项目中的类可以前后端公用,这就是Blazor优势之一。
{
TaskId = task.TaskId,
IsImportant = !task.IsImportant,
}; var result = await Http.PostAsJsonAsync<SetImportantReq>("api/Task/SetImportant", req);
if (result.IsSuccessStatusCode)
{
task.IsImportant = req.IsImportant;//请求成功后需要修改本地重要状态
StateHasChanged();//状态改变,刷新页面的显示
}
}

5. 修改完成状态

ToDo.Server

TaskController.cs中添加SetFinish方法用于修改IsFinish字段的值

[HttpPost]
public void SetFinish(SetFinishReq req)
{
var entity = Context.Task.FirstOrDefault(x => x.TaskId == req.TaskId);
entity.IsFinish = req.IsFinish;
Context.SaveChanges();
}

ToDo.Shared

添加SetFinishReq类用于SetFinish接口请求参数

public class SetFinishReq
{
public Guid TaskId { get; set; } public bool IsFinish { get; set; }
}

ToDo.Client

ToDay.razor.cs中的OnFinish方法更新

private async void OnFinish(TaskDto task)
{
var req = new SetFinishReq()
{
TaskId = task.TaskId,
IsFinish = !task.IsFinish,
}; var result = await Http.PostAsJsonAsync<SetFinishReq>("api/Task/SetFinish", req);
if (result.IsSuccessStatusCode)
{
task.IsFinish = req.IsFinish;
StateHasChanged();
}
}

6. 删除代办

ToDo.Server

TaskController.cs中添加DelTask方法用于删除待办。

[HttpDelete]
public void DelTask(Guid taskId)
{
Context.Task.Remove(Context.Task.Find(taskId));
Context.SaveChanges();
}

ToDo.Client

ToDay.razor.cs中的OnFinish方法更新

[Inject] public ConfirmService ConfirmSrv { get; set; }
public async Task OnDel(TaskDto task)
{
if (await ConfirmSrv.Show($"是否删除任务 {task.Title}", "删除", ConfirmButtons.YesNo, ConfirmIcon.Info) == ConfirmResult.Yes)
{
taskDtos.Remove(task);
}
}

ConfirmService可以快捷地弹出一个内置的确认框,类似于 Windows MessageBox。

ConfirmService组件帮助文档:https://ant-design-blazor.github.io/zh-CN/components/modal

7. 查询代办

ToDo.Server

TaskController.cs中添加GetSearch方法用于修改SetFinish字段的值

[HttpPost]
public GetSearchRsp GetSearch(GetSearchReq req)
{
if (req.PageIndex == 0) req.PageIndex = 1;
var query = Context.Task.Where(x => x.Title.Contains(req.QueryTitle ?? "")); foreach (var sort in req.Sorts)
{
if (sort.SortOrder == "descend")
query = query.OrderBy(sort.SortField + " DESC");
else
query = query.OrderBy(sort.SortField);
} var result = new GetSearchRsp()
{
Data = QueryToDto(query.Skip(--req.PageIndex * req.PageSize).Take(req.PageSize)).ToList(),
Total = query.Count(),
}; return result;
}

if (req.PageIndex == 0) req.PageIndex = 1 吐槽开始:几乎所有的UI框架页码都是从0开始,但是AntDesign规范的页码是从1开始的,然而没有载入数据时又是返回0,所以要特别注意。

OrderBy使用了System.Linq.Dynamic.Core 扩展包,它提供了一些动态的Linq支持,比如此处排序我传入的参数不是一个表达式,而是一个字符串,这样可以让代码灵活性大增。

ToDo.Shared

public class GetSearchReq
{
public string QueryTitle { get; set; }
public int PageIndex { get; set; }
public int PageSize { get; set; }
public List<SortFieldName> Sorts { get; set; }
} public class SortFieldName
{
/// <summary>
/// 排序字段
/// </summary>
public string SortField { get; set; }
/// <summary>
/// 排序方向
/// </summary>
public string SortOrder { get; set; }
} public class GetSearchRsp
{
public List<TaskDto> Data { get; set; }
public int Total { get; set; }
}

ToDo.Client

TaskSearch.razor 文件的代码

@page "/search"
<PageHeader Title="@("全部待办事项")" Subtitle="@($"数量:{total}")"></PageHeader>
<Search @bind-Value="queryTitle" OnSearch="OnSearch"></Search>
<Table Loading="@isLoading" DataSource="@datas" PageSize="10" Total="@total" OnChange="OnChange" TItem="TaskDto">
<AntDesign.Column @bind-Field="@context.Title" Sortable>
@context.Title
@if (context.IsImportant)
{
<Tag Color="orange">重要</Tag>
}
</AntDesign.Column>
<AntDesign.Column @bind-Field="@context.Description" />
<AntDesign.Column @bind-Field="@context.PlanTime" Sortable />
<AntDesign.Column @bind-Field="@context.Deadline" />
<AntDesign.Column @bind-Field="@context.IsFinish">
@if (context.IsFinish)
{
<Icon Type="check" Theme="outline" />
}
</AntDesign.Column>
</Table>

TaskSearch.razor.cs 文件的代码

[Inject] public HttpClient Http { get; set; }
private bool isLoading = false;
List<TaskDto> datas = new List<TaskDto>();
private string queryTitle;
private int total = 0;
//点击查询按钮时检索数据
private async Task OnSearch()
{
await OnQuery(1, 10, new List<SortFieldName>());
}
//当前页码,排序发生改变时调用查询方法检索数据
private async Task OnChange(AntDesign.TableModels.QueryModel<TaskDto> queryModel)
{
await OnQuery(
queryModel.PageIndex,
queryModel.PageSize,
queryModel.SortModel.Where(x => string.IsNullOrEmpty(x.SortType.Name) == false).OrderBy(x => x.Priority)
.Select(x => new SortFieldName() { SortField = x.FieldName, SortOrder = x.SortType.Name }).ToList()
);
}
//检索数据
private async Task OnQuery(int pageIndex, int pageSize, List<SortFieldName> sort)
{
isLoading = true;
var req = new GetSearchReq()
{
QueryTitle = queryTitle,
PageIndex = pageIndex,
PageSize = pageSize,
Sorts = sort,
};
var httpRsp = await Http.PostAsJsonAsync<GetSearchReq>($"api/Task/GetSearch", req);
var result = await httpRsp.Content.ReadFromJsonAsync<GetSearchRsp>();
datas = result.Data;
total = result.Total;
isLoading = false;
}

Search带有查询按钮的文本框框

Search组件文档地址:https://ant-design-blazor.github.io/zh-CN/components/input

查看详细服务

我的一天全部页面上均存在打开待办详情的功能需求,这时我们就可以自己做一个服务将两边的功能合并到一起。

添加TaskDetailServices.cs文件,加入以下代码

namespace ToDo.Client
{
public class TaskDetailServices
{
public DrawerService DrawerSvr { get; set; } public TaskDetailServices(DrawerService drawerSvr)
{
DrawerSvr = drawerSvr;
} public async Task EditTask(TaskDto taskDto, List<TaskDto> datas)
{
var taskItem = await DrawerSvr.CreateDialogAsync<TaskInfo, TaskDto, TaskDto>(taskDto, title: taskDto.Title, width: 450);
if (taskItem == null) return;
var index = datas.FindIndex(x => x.TaskId == taskItem.TaskId);
datas[index] = taskItem;
}
}
}

TaskDetailServices(DrawerService drawerSvr) 只有razor文件可以使用[Inject]标记属性进行注入服务,普通得类需要在构造函数中定义才能注入服务。

Program.cs文件中注册TaskDetailServices

builder.Services.AddScoped<TaskDetailServices>();

TaskSearch.razor文件中添加详情按钮

    <AntDesign.Column TData="object">
<Button OnClick="x=>OnDetail(context)">详情</Button>
</AntDesign.Column>

TaskSearch.razor.cs 插入以下代码,我们注入自定义的服务,使用服务中的方法打开编辑界面。

[Inject] public TaskDetailServices TaskSrv { get; set; }

private async Task OnDetail(TaskDto taskDto)
{
await TaskSrv.EditTask(taskDto, datas);
}

次回预告

下一次我们要介绍Blazor的精髓,也是我个人认为Blazor框架体系中最优秀的特性——组件。我们通过几个小实例展示Blazor的组件开发方法,敬请期待

返回目录

《进击吧!Blazor!》第一章 4.数据交互的更多相关文章

  1. JSP/Servlet开发——第二章 JSP数据交互(一)

    1. JSP内置对象:JSP内置对象是 Web 容器创建的一组对象:   ●JSP常用的内置对象:out.request.application.session.response等: ●内置对象不需要 ...

  2. JSP/Servlet开发——第二章 JSP数据交互(二)

    1. JSP 内置对象 application: ●application 对象类似于系统的 "全局变量", 用于同一个应用内的所有用户之问的数据共享: ●application对 ...

  3. 第二章 jsp数据交互(一)

    JSP如何处理客户端的请求? 解析:通过jsp内置对象 表单数据被提交到了jsp页面! 什么是JSP内置对象(jsp核心)? Java 内置对象 Java  作用域 解析:jsp内置对象是web容器创 ...

  4. WEB的进击之路-第一章 HTML基本标签(1)

    一.HTML简介 超文本标记语言,标准通用标记语言下的一个应用. "超文本"就是指页面内可以包含图片.链接,甚至音乐.程序等非文字元素. 超文本标记语言的结构包括"头&q ...

  5. 第三章 jsp数据交互(二)

    Application:当前服务器(可以包含多个会话):当服务器启动后就会创建一个application对象,被所有用户共享page.request.session.application四个作用域对 ...

  6. SpringMVC框架笔记02_参数绑定返回值文件上传异常处理器JSON数据交互_拦截器

    目录 第1章 高级参数的绑定 1.1 参数的分类 1.2 数组类型的参数的绑定 1.3 集合类型的参数的绑定 第2章 @RequestMapping的用法 2.1 URL路径映射 2.2 请求方法限定 ...

  7. Hadoop专业解决方案-第1章 大数据和Hadoop生态圈

    一.前言: 非常感谢Hadoop专业解决方案群:313702010,兄弟们的大力支持,在此说一声辛苦了,经过两周的努力,已经有啦初步的成果,目前第1章 大数据和Hadoop生态圈小组已经翻译完成,在此 ...

  8. 《进击吧!Blazor!》第一章 1.初识 Blazor

    作者介绍 陈超超 Ant Design Blazor 项目贡献者 拥有十多年从业经验,长期基于.Net技术栈进行架构与开发产品的工作,Ant Design Blazor 项目贡献者,现就职于正泰集团 ...

  9. 《进击吧!Blazor!》系列入门教程 第一章 8.部署

    <进击吧!Blazor!>是本人与张善友老师合作的Blazor零基础入门教程视频,此教程能让一个从未接触过Blazor的程序员掌握开发Blazor应用的能力. 视频地址:https://s ...

随机推荐

  1. [ABP教程]第四章 集成测试

    Web应用程序开发教程 - 第三章: 集成测试 //[doc-params] { "UI": ["MVC","NG"], "DB& ...

  2. 洛谷P1055 字符串的处理-----ISBN

    题目描述 每一本正式出版的图书都有一个ISBN号码与之对应,ISBN码包括99位数字.11位识别码和33位分隔符,其规定格式如x-xxx-xxxxx-x,其中符号-就是分隔符(键盘上的减号),最后一位 ...

  3. PyCharm 2019、2020、2021专业版激活

    PyCharm下载地址:https://www.jetbrains.com/pycharm/download/ PyCharm社区版功能基本够用,但是作为傲娇的程序员,咱都是上来就专业版,然后各种破解 ...

  4. react项目中实现搜索关键字呈现高亮状态(一)

    最近有个需求,在一个react项目中,实现搜索关键字呈现高亮状态.这个在普通的html文件中还好操作些,在react项目中有点懵逼了,因为react项目中很少操作dom,有点无从下手.但最后还是实现了 ...

  5. 镜像批量迁移利器:image-transfer

    概述 用户业务在上云或者云迁移过程中,需要对镜像进行批量迁移.基于此背景,腾讯云容器专家团队开发了镜像批量迁移工具:image-transfer.该工具支持多种云厂商镜像仓库之间的批量迁移,同时支持腾 ...

  6. 【老孟Flutter】为什么 build 方法放在 State 中而不是在 StatefulWidget 中

    老孟导读:此篇文章是生命周期相关文章的番外篇,在查看源码的过程中发现了这一有趣的问题,欢迎大家一起探讨. Flutter 中Stateful 组件的生命周期:http://laomengit.com/ ...

  7. 如何利用Intellij Idea搭建python编译运行环境 (转)

    首先进入Intellij Idea的官方网站:点击打开链接 点击download,选择旗舰版进行下载.网上的破解教程很多,也可以注册一个学生账号拿到一年的免费试用权. 安装过程不再细说,第一次打开选择 ...

  8. kubernets之Ingress资源

    一  Ingress集中式的kubernets服务转发控制器 1.1  认识Ingress的工作原理 注意:图片来源于kubernets in action一书,如若觉得侵权,请第一时间联系博主进行删 ...

  9. 如何在K8s,Docker-Compose注入镜像Tag

    最近在做基于容器的CI/CD, 一个朴素的自动部署的思路是: 从Git Repo打出git tag,作为镜像Tag ssh远程登录到部署机器 向部署环境注入镜像Tag,拉取镜像,重新部署 下面分享我是 ...

  10. [APUE] 进程环境

    APUE 一书的第七章学习笔记. 进程终止 有 8 种方式可以使得进程终止,5 种为正常方式: Return from main Calling exit() Calling _exit or _Ex ...