在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以上),請參考下圖所示:
從對話盒「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" > </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 意見:
張貼留言