2024年5月28日 星期二

在Blazor使用Fluent UI元件-4

 作者:許薰尹  精誠資訊/恆逸教育訓練中心資深講師




在本站《在Blazor使用Fluent UI元件 - 2》與《在Blazor使用Fluent UI元件 -3》一文介紹如何在現有的Blazor專案中,手動加入Fluent UI元件的功能。在這一篇文章中,我們將介紹延續這系列文章的情境,介紹Fluent UI套件的「FluentDataGrid」元件,這次我們將透過Entity Framework Core來讀取Northwind資料庫的資料來顯示。


安裝Entity Framewrok Core套件


延續《在Blazor使用Fluent UI元件 -3》一文使用的專案來進行設計。首先需要在Blazor專案安裝「Entity Framewrok Core」相關套件,步驟如下,從「Solution Explorer」視窗 –> 專案名稱上方,按滑鼠右鍵,從快捷選單選擇「Manage NuGet Packages」項目,請參考下圖所示:





圖 1:使用NuGet套件管理員。
從對話盒「Browse」分頁上方文字方塊中,輸入查詢關鍵字「EntityFrameworkCore」,找到「Microsoft.EntityFrameworkCore.SqlServer」套件。點選「Install」按鈕進行安裝(版本為8.0.0以上),請參考下圖所示:






圖 2:安裝「Microsoft.EntityFrameworkCore.SqlServer」套件。


重複相同的步驟,安裝「Microsoft.EntityFrameworkCore.Tools」套件(版本為8.0.0以上),請參考下圖所示:






圖 3:安裝「Microsoft.EntityFrameworkCore.Tools」套件。




再重複相同的步驟,安裝「Microsoft.FluentUI.AspNetCore.Components.DataGrid.EntityFrameworkAdapter」套件,請參考下圖所示:




圖 4:安裝「Microsoft.FluentUI.AspNetCore.Components.DataGrid.EntityFrameworkAdapter」套件。



「Microsoft.FluentUI.AspNetCore.Components.DataGrid.EntityFrameworkAdapter」套件的作用是將 Entity Framework Core 的 DbContext 與 Fluent UI 的「FluentDataGrid」元件進行整合,以便更有效地處理資料庫的查詢操作。例如當你在 「FluentDataGrid」 中對資料進行排序、過濾或分頁時,「EntityFrameworkAdapter」可以將這些操作轉換為對應的 SQL 查詢,並直接在資料庫中執行,這樣可以大大提高資料處理的效率。EntityFrameworkAdapter 是特定於 Entity Framework Core 的,如果你使用的是其他的 ORM 開發框架,則可能需要使用其他的Adapter。

目前專案檔的內容參考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<Project Sdk="Microsoft.NET.Sdk.Web">
 
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
 
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.4" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.7.1" />
    <PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.DataGrid.EntityFrameworkAdapter" Version="4.7.0" />
    <PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Emoji" Version="4.6.0" />
    <PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.7.0" />
  </ItemGroup>
 
</Project>

 

建立Enity Framework Core實體模型(Entity Model)
我們以微軟的「Northwind」範例資料庫為例,若資料庫名稱為「.\sqlexpress」,且資料庫伺服器已經建立了微軟「Northwind」範例資料庫。我們可以使用Entity Framework Core 的「Scaffold-DbContext」命令,從現有的資料庫生成對應的模型(實體)類別和 DbContext 類別來進行資料存取。這種過程被稱為反向工程(Reverse Engineering)。

從Visual Studio 2022開發工具「Tools」- 「Nuget Package Manager」項目開啟選單,從選單選擇「Package Manager Console」選項,開啟「Package Manager Console」對話盒,直接輸入以下指令,這樣就可這以從現有的「Northwind」資料庫來建立模型:

 

1
Scaffold-DbContext "Server=.\sqlexpress;Database= Northwind;Trusted_Connection=True;TrustServerCertificate=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

 



「Scaffold-DbContext」之後是資料庫連接字串,指定了資料庫伺服器的位置(在這裡是本機的 SQL Express 伺服器),資料庫的名稱(在這裡是 Northwind),以及「Trusted_Connection=True」是指定使用Windows驗證連接到資料庫。連接字串之後的「Microsoft.EntityFrameworkCore.SqlServer」是資料庫提供者,這裡表示使用的是 SQL Server。「-OutputDir」是一個選項,用於指定生成的類別應該放在「Models」資料夾之中。這個命令的執行結果請參考下圖所示:






圖 5:反向工程(Reverse Engineering)。


在「Solution Explorer」視窗檢視Blazor專案,專案根目錄下會多出一個「Models」資料夾,其中包含許多實體類別程式碼,請參考下圖所示:




圖 6:實體類別程式碼。


下一步修改「appsettings.json」檔案,加入連接字串設定:

 

1
2
3
4
5
6
7
8
9
10
11
12
{
  "ConnectionStrings" : {
    "DefaultConnection" : "Server=.\\sqlexpress;Database=Northwind;Trusted_Connection=True;TrustServerCertificate=true;"
  },
  "Logging" : {
    "LogLevel" : {
      "Default" : "Information",
      "Microsoft.AspNetCore" : "Warning"
    }
  },
  "AllowedHosts" : "*"
}

 


接下來我們需要在「Program.cs」檔案中加入以下程式碼,叫用「IServiceCollection」的「AddDataGridEntityFrameworkAdapter」方法進行註冊。此外還需叫用「IServiceCollection」的「AddDbContext」方法,註冊「NorthwindContext」,參考以下程式碼:

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using FluentUIDataGrid.Components;
using FluentUIDataGrid.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.FluentUI.AspNetCore.Components;
 
var builder = WebApplication.CreateBuilder( args );
 
// Add services to the container.
builder.Services.AddRazorComponents( )
    .AddInteractiveServerComponents( );
 
builder.Services.AddFluentUIComponents( );
 
builder.Services.AddDataGridEntityFrameworkAdapter( );
 
builder.Services.AddDbContext<NorthwindContext>( options => options.UseSqlServer( builder.Configuration.GetConnectionString( "DefaultConnection" ) ) );
 
builder.Services.AddHttpClient( );
 
var app = builder.Build( );
 
// Configure the HTTP request pipeline.
if ( !app.Environment.IsDevelopment( ) ) {
  app.UseExceptionHandler( "/Error", createScopeForErrors: true );
  // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
  app.UseHsts( );
}
 
app.UseHttpsRedirection( );
 
app.UseStaticFiles( );
app.UseAntiforgery( );
 
app.MapRazorComponents<App>( )
    .AddInteractiveServerRenderMode( );
 
app.Run( );

 



使用資料庫資料


修改「MyDataGridComponent.razor」程式碼,在一開頭使用「@inject」指示詞在 Razor元件中插入一個「NorthwindContext」類型的實例,並將其賦值給「DbContext」變數,如下所示:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@using FluentUIDataGrid.Models
@inject NorthwindContext DbContext
 
<h3> MyDataGridComponent </h3>
 
<FluentDataGrid TGridItem = "Customer" Items = "Customers" GridTemplateColumns = "1fr 1fr 1fr 1fr 1fr 1fr">
  <PropertyColumn Property = "@(c => c.CompanyName)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.ContactName)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.ContactTitle)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.Address)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.City)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.Country)" Sortable = "true" />
</FluentDataGrid>
 
@code {
 
  public  IQueryable<Customer> Customers { get; set; }
  protected override async Task OnInitializedAsync() {
    Customers = DbContext.Customers.AsQueryable();
  }
}

 



在「@code」程式碼區段中定義了一個公開的「Customers」屬性,其型別為「IQueryable」。IQueryable 是一種特殊的集合類型,它可以用於表示對資料庫的查詢。「OnInitializedAsync」方法是元件生命周期方法,會在元件初始化自動呼叫。「Customers = DbContext.Customers.AsQueryable()」這行程式碼將「Customers」屬性設置為資料庫中所有「Customer」的查詢。這裡的「DbContext.Customers」型別為 DbSet,它代表資料庫中的「Customers」資料表。「AsQueryable」方法將 「DbSet」轉換為「IQueryable」,這樣我們就可以在上面進行查詢了。

最後我們在「FluentDataGrid」設定「Items」屬性,指明資料來自於「Customers」查詢。這個範例的執行結果請參考下圖所示:





圖 7:顯示資料庫資料。



使用Fluent UI DialogService


「DialogService」是一種Fluent UI提供的服務,可以用來顯示彈跳的對話盒。它可以插入到Blazor元件之中,並用於顯示不同類型的對話方塊。要在元件中使用「DialogService」,需要在DI容器中叫用「.AddFluentUIComponents()」方法進行註冊。



此外你還需要使用「FluentDialogProvider」來渲染對話盒。這個元件也需要加入到應用程式或網站的版面配置元件之中。一般版面配置頁的檔案通常會命名為「MainLayout.razor」,如下所示,我們在「main」的結尾標籤之上加入「FluentDialogProvider」:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@inherits LayoutComponentBase
 
<div class = "page">
    <div class = "sidebar">
        <NavMenu />
    </div>
 
    <main>
        <div class = "top-row px-4">
            <a href = "https://learn.microsoft.com/aspnet/core/" target = "_blank"> About </a>
        </div>
 
        <article class = "content px-4">
            @Body
        </article>
    <FluentDialogProvider />
    </main>
</div>
 
<div id = "blazor-error-ui">
    An unhandled error has occurred.
    <a href = "" class = "reload"> Reload </a>
    <a class = "dismiss"> x </a>
</div>

 


此外我們需要在路由設定「@rendermode = "@InteractiveServer"」,參考以下「App.razor」程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang = "en">
 
<head>
    <meta charset = "utf-8" />
    <meta name = "viewport" content = "width=device-width, initial-scale=1.0" />
    <base href = "/" />
    <link rel = "stylesheet" href = "bootstrap/bootstrap.min.css" />
    <link rel = "stylesheet" href = "app.css" />
    <link rel = "stylesheet" href = "FluentUIDataGrid.styles.css" />
    <link rel = "icon" type = "image/png" href = "favicon.png" />
    <HeadOutlet />
</head>
 
<body>
    <Routes @rendermode = "@InteractiveServer"/>
    <script src = "_framework/blazor.web.js"> </script>
</body>
 
</html>



「Routes」的「rendermode」屬性設定路由的渲染模式。「@rendermode = "@InteractiveServer"」告訴 Blazor 伺服端應用程式以交互伺服端模式來渲染路由。這意味著當客戶端訪問一個路由時,Blazor伺服端會生成一個可互動的 HTML 頁面,該頁面可以與服務器進行通信,以處理客戶端的互動。這與靜態渲染模式不同,靜態渲染模式只會生成一個靜態的 HTML 頁面,該頁面不能與伺服端進行通信。「FluentDialogProvider」需要將「@rendermode」設為「InteractiveServer」才能正常執行。


讓我們修改「MyDataGridComponent」元件程式如下:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@using FluentUIDataGrid.Models
@using Microsoft.EntityFrameworkCore
@inject NorthwindContext DbContext
@inject IDialogService DialogService
<h3> MyDataGridComponent </h3>
<FluentDataGrid  TGridItem = "Customer" Items = "Customers" GridTemplateColumns = "1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr">
  <PropertyColumn Property = "@(c => c.CustomerId)" Sortable = "true" />
  <PropertyColumn Property = "@(c  => c.CompanyName)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.ContactName)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.ContactTitle)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.Address)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.City)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.Country)" Sortable = "true" />
  <TemplateColumn Title = "Actions">
    <FluentButton IconStart = "@( new Icons.Regular.Size20.Delete() )" OnClick = "@( () => DeleteClicked(context.CustomerId) )">
      Delete
    </FluentButton>
  </TemplateColumn>
</FluentDataGrid>
 
 
@code {
  public async Task DeleteClicked( string id ) {
    var c = await DbContext.Customers.FirstOrDefaultAsync( c => c.CustomerId == id );
    DbContext.Customers.Remove( c );
    if ( c is not null ) {
      await DbContext.SaveChangesAsync( );
      await DialogService.ShowInfoAsync( "Deleted : " + id );
    }
    else {
      await DialogService.ShowInfoAsync( "Could not find this record : " + id );
    }
  }
 
  public IQueryable<Customer> Customers { get; set; } = null!;
  protected override async Task OnInitializedAsync()
  {
      await Task.Run(() =>
      {
          Customers = DbContext.Customers.AsQueryable();
      });
  }
}

重點說明如下,以下程式片段在「FluentDataGrid」中定義一個「TemplateColumn」,並將其標題設為「Actions」。在這個「TemplateColumn」中,每一個橫列都會有一個「Delete」按鈕。「FluentButton」定義了一個按鈕,按鈕的圖示設為一個刪除圖示,並且當按鈕被點擊時,會自動叫用「DeleteClicked」方法並傳入當前資料橫列的「CustomerId」值:
1
2
3
4
5
<TemplateColumn Title = "Actions">
    <FluentButton IconStart = "@( new Icons.Regular.Size20.Delete() )" OnClick = "@( ()=>DeleteClicked(context.CustomerId) )">
      Delete
    </FluentButton>
  </TemplateColumn>


以下程式片段,定義了一個名為「DeleteClicked」的非同步方法,該方法接受一個「id」參數,該參數表示要刪除的「Customer」的「CustomerId」。我們先利用「DbContext.Customers」的「FirstOrDefaultAsync」方法,從資料庫中查詢 「Customer」的「CustomerId」 與「id」參數值相符的第一個 「Customer」物件;接著利用「Remove」方法將此物件從「Customers」集合中移除。「Remove」方法並不會立即從資料庫中刪除「Customer」資料,而是將「Customer」標記為待刪除。直到叫用「SaveChangesAsync」這行程式碼才會將資料從資料庫真正的刪除;接著不管刪除是成功或失敗,我們都透過「DialogService」的「ShowInfoAsync」方法來顯示訊息。

1
2
3
4
5
6
7
8
9
10
11
public async Task DeleteClicked( string id ) {
    var c = await DbContext.Customers.FirstOrDefaultAsync( c => c.CustomerId == id );
    DbContext.Customers.Remove( c );
    if ( c is not null ) {
      await DbContext.SaveChangesAsync( );
      await DialogService.ShowInfoAsync( "Deleted : " + id );
    }
    else {
      await DialogService.ShowInfoAsync( "Could not find this record : " + id );
    }
  }


目前首頁的「Home.razor」組件程式如下:

1
2
3
4
5
@page "/"
@rendermode InteractiveServer
<PageTitle>Home</PageTitle>
 
<MyDataGridComponent ></MyDataGridComponent>


這個範例的執行結果請參考下圖所示:





圖 8:使用Dialog Service顯示對話盒。

使用FluentDialog元件


最後我們來談談如何使用FluentDialog元件設計確認刪除的對話盒,修改「MyDataGridComponent.razor」程式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@using FluentUIDataGrid.Models
@using Microsoft.EntityFrameworkCore
@inject NorthwindContext DbContext
@inject IDialogService DialogService
<h3> MyDataGridComponent </h3>
<FluentDataGrid TGridItem = "Customer" Items = "Customers" GridTemplateColumns = "1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr">
  <PropertyColumn Property = "@(c => c.CustomerId)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.CompanyName)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.ContactName)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.ContactTitle)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.Address)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.City)" Sortable = "true" />
  <PropertyColumn Property = "@(c => c.Country)" Sortable = "true" />
  <TemplateColumn Title = "Actions">
    <FluentButton IconStart = "@( new Icons.Regular.Size20.Delete() )" OnClick = "@( ()=> DeleteClicked( context ) )">
      Delete
    </FluentButton>
  </TemplateColumn>
</FluentDataGrid>
 
 
<FluentDialog Hidden = "ModalHidden" Modal = "true">
  <div class = "dialog-box">
    <h2 class = "dialog-title"> Are you sure you want to delete @CustomerToDelete?.CustomerId ? </h2>
    <div class = "dialog-buttons">
      <FluentButton IconStart = "@( new Icons.Regular.Size20.Check() )" @onclick = "OnOKClick">
        OK
      </FluentButton>
      <FluentButton IconStart = "@( new Icons.Regular.Size20.ArrowExit() )" @onclick = "OnCancelClick">
        Cancel
      </FluentButton>
    </div>
  </div>
</FluentDialog>
 
 
 
@code {
 
  public bool ModalHidden { get; set; } = true;
 
  public Customer CustomerToDelete { get; set; }
 
  public async Task DeleteClicked( Customer customer ) {
    ModalHidden = false;
    CustomerToDelete = customer;
  }
  private async Task OnOKClick() {
    ModalHidden = true;
    var c = await DbContext.Customers.FirstOrDefaultAsync( c => c.CustomerId == CustomerToDelete.CustomerId );
    DbContext.Customers.Remove( c );
    if ( c is not null ) {
      await DbContext.SaveChangesAsync( );
      await DialogService.ShowInfoAsync( "Deleted : " + c.CustomerId );
    }
    else {
      await DialogService.ShowInfoAsync( "Could not find this record : " + c.CustomerId );
    }
  }
 
  private void OnCancelClick() {
    ModalHidden = true;
  }
  public IQueryable<Customer> Customers { get; set; } = null!;
  protected override async Task OnInitializedAsync() {
 
    await Task.Run( () => {
      Customers = DbContext.Customers.AsQueryable( );
    } );
  }
}
 
<style>
  .dialog-box {
    background-color: #f2f2f2;
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 10px;
    margin-bottom: 10px;
  }
 
  .dialog-title {
    font-size: 18px;
    font-weight: bold;
    margin-bottom: 10px;
  }
 
  .dialog-buttons {
    margin-top: 10px;
  }
 
    .dialog-buttons button {
      margin-right: 10px;
    }
</style>


程式說明如下,下面這段程式碼,定義了一個「FluentDialog」,這是一個要求強制回應的對話盒,用於確認是否要刪除一個「Customer」。「Hidden」屬性決定了對話盒是否隱藏,它的值繫結到了「ModalHidden」屬性。「Modal」屬性設置為「true」,表示這是一個強制回應的對話盒。這個對話盒將詢問使用者是否確定要刪除「Customer」。如果使用者點擊「OK」按鈕,則叫用「OnOKClick」方法;如果使用者點擊「Cancel」按鈕,則叫用「OnCancelClick」方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
<FluentDialog Hidden = "ModalHidden" Modal = "true">
  <div class = "dialog-box">
    <h2 class = "dialog-title"> Are you sure you want to delete @CustomerToDelete?.CustomerId ? </h2>
    <div class = "dialog-buttons">
      <FluentButton IconStart = "@( new Icons.Regular.Size20.Check() )" @onclick = "OnOKClick">
        OK
      </FluentButton>
      <FluentButton IconStart = "@( new Icons.Regular.Size20.ArrowExit() )" @onclick = "OnCancelClick">
        Cancel
      </FluentButton>
    </div>
  </div>
</FluentDialog>


當使用者點選清單中的任一筆資料對應的刪除按鈕,將執行「DeleteClicked」方法,設定「ModalHidden」為「false」表示要顯示對話盒,並將要刪除的「Customer」放到「CustomerToDelete」屬性中:

1
2
3
4
public async Task DeleteClicked( Customer customer ) {
    ModalHidden = false;
    CustomerToDelete = customer;
  }


如果使用者點擊對話盒中的「OK」按鈕,則叫用「OnOKClick」方法,將「ModalHidden」為「true」表示要隱藏對話盒,然後真正進行資料刪除動作:


1
2
3
4
5
6
7
8
9
10
11
12
private async Task OnOKClick() {
    ModalHidden = true;
    var c = await DbContext.Customers.FirstOrDefaultAsync( c => c.CustomerId == CustomerToDelete.CustomerId );
    DbContext.Customers.Remove( c );
    if ( c is not null ) {
      await DbContext.SaveChangesAsync( );
      await DialogService.ShowInfoAsync( "Deleted : " + c.CustomerId );
    }
    else {
      await DialogService.ShowInfoAsync( "Could not find this record : " + c.CustomerId );
    }
  }


如果使用者點擊對話盒中的「Cancel」按鈕,則叫用「OnCancelClick」方法,將「ModalHidden」為「true」表示要隱藏對話盒:


1
2
3
private void OnCancelClick() {
    ModalHidden = true;
  }

最後我們利用CSS自訂對話盒的樣式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style>
  .dialog-box {
    background-color: #f2f2f2;
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 10px;
    margin-bottom: 10px;
  }
 
  .dialog-title {
    font-size: 18px;
    font-weight: bold;
    margin-bottom: 10px;
  }
 
  .dialog-buttons {
    margin-top: 10px;
  }
 
    .dialog-buttons button {
      margin-right: 10px;
    }
</style>

這個範例的執行結果請參考下圖所示:



圖 9:確認刪除。






0 意見:

張貼留言