《進擊吧!Blazor!》第一章 4.資料互動

MicrosoftReactor發表於2021-02-05

《進擊吧!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的元件開發方法,敬請期待

返回目錄

相關文章