1. 程式人生 > 實用技巧 >Blazor學習筆記01: 使用BootstrapBlazor元件 建立一個具有單表維護功能的表格頁面

Blazor學習筆記01: 使用BootstrapBlazor元件 建立一個具有單表維護功能的表格頁面

Blazor學習筆記01:

使用BootstrapBlazor元件

建立一個具有單表維護功能的表格頁面

一、提示和背景

本文適合零基礎的前端初學者閱讀。閱讀本文您將可以使用BootstrapBlazor元件建立一個具有單表維護功能的表格頁面(如下圖所示)。本人是一名年近半百(71年)的程式設計愛好者,90年代中期開始程式設計,當時用的開發語言叫Delphi,資料庫叫SQL Server6.5,當時開發的是C/S方式下的WinForm。到2006年左右B/S方式下的ASP開始流行,由於某種原因就沒有再跟進學習。一晃十多年過去了,最近聽說微軟推出了Blazor,可以用C#寫前端,因為興趣和愛好,就又入坑了。

在碼雲(gitee)搜尋了一下適合Blazor的UI框架,發現一個Stra較高的GVP開源專案BootstrapBlazor,就入手了,沒有與其他的UI元件進行過比較,因為都沒用過。一個多月下來,感覺還不錯,功能很多,也很漂亮。前端開發對我來說真的有點難,不知從何入手。專案的作者Argo比較忙,群裡的個別人對於我這樣的新手又有些不屑,不夠友好,走了很多彎路。(順便說一下,我目前做的專案後臺使用的ORM是FreeSql,在那個群裡的感受就完全不同。)所以才想寫這樣的一篇文章,以供像我一樣零基礎的菜鳥做入門參考。

《2020年 .NET ORM 完整比較、助力選擇》

https://www.cnblogs.com/kellynic/p/13664720.html

二、BootstrapBlazor元件的簡介和安裝

BootstrapBlazor 是一套 Bootstrap 風格的 Blazor UI 元件庫,可以認為是 Bootstrap 專案的 Blazor 版實現,目前有佈局、導航、表單、資料和訊息等五大類63個元件。Bootstrap Blazor UI 元件庫提供了從基本的 Button 元件到高階的網頁級 SmartPage 元件,優勢是使用元件無需編寫 Javascript,元件支援所有 html 特性,元件支援資料雙向繫結,元件支援自動客戶端驗證,元件支援組合。

演示網站:https://blazor.sdgxgz.com/components

碼雲地址:

https://gitee.com/LongbowEnterprise/BootstrapBlazor

GitHub地址:

https://github.com/ArgoZhang/BootstrapBlazor

元件支援Blazor的服務端模式(Server)和客戶端模式(WASM),安裝也很簡單,僅需元件引用、樣式表修改、新增名稱空間和註冊服務等幾個簡單步驟。下面,我們用示例逐一說明。

三、建立一個具有單表維護功能的表格頁面

首先是建立專案。啟動Visual Studio 2019,建立一個Blazor應用專案。選擇儲存位置,專案名稱我們改成BootstrapBlazor.TableDemo。選擇Blazor Server 應用,取消右側為HTTPS配置的複選框,然後單擊建立。專案建立好後,解決方案管理器如圖3,其中紅色箭頭標註是我們稍後要修改的位置或檔案。

其次是安裝元件。1、元件引用。在解決方案管理器中右鍵單擊剛建立的專案BootstrapBlazor.TableDemo,選擇“管理NuGet程式包”,在瀏覽介面中搜索BootstrapBlazor,安裝穩定版3.1.20。

2、樣式表修改。單擊Pages目錄下的“_Host.cshtml”檔案,在<head>中的所有其它樣式表之前新增如下內容。然後將<head>中原有的<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />這一句刪除或註釋掉。此語句與新新增的樣式會有衝突。在原有<script src="_framework/blazor.server.js"></script>語句之前前新增如下內容

    <link rel="stylesheet" href="_content/BootstrapBlazor/lib/bootstrap/css/bootstrap.min.css" />
    <link rel="stylesheet" href="_content/BootstrapBlazor/lib/font-awesome/css/font-awesome.min.css" />
    <link rel="stylesheet" href="_content/BootstrapBlazor/lib/chartjs/Chart.min.css" />
    <link rel="stylesheet" href="_content/BootstrapBlazor/lib/summernote/summernote-bs4.min.css">
    <link rel="stylesheet" href="_content/BootstrapBlazor/css/bootstrap.blazor.css" />
View Code

    <script src="_content/BootstrapBlazor/lib/jquery/jquery.min.js"></script>
    <script src="_content/BootstrapBlazor/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
    <script src="_content/BootstrapBlazor/lib/chartjs/Chart.bundle.min.js"></script>
    <script src="_content/BootstrapBlazor/lib/summernote/summernote-bs4.min.js"></script>
    <script src="_content/BootstrapBlazor/lib/summernote/summernote-zh-CN.min.js"></script>
    <script src="_content/BootstrapBlazor/lib/slimscroll/jquery.slimscroll.min.js"></script>
    <script src="_content/BootstrapBlazor/js/bootstrap.blazor.js"></script>
View Code

上述語句可以去演示網站或在我們的TableDemo原始碼中複製。

3、新增名稱空間。單擊“_Imports.razor”檔案,在檔案的末尾新增@using BootstrapBlazor.Components。

4、註冊服務。單擊“Startup.cs”檔案,找到public void ConfigureServices(IServiceCollection services)的方法,在其中加入services.AddBootstrapBlazor();

執行一下看是否報錯。怎麼,沒變化?別急。

再次是修改導航選單。單擊開啟Shared目錄下的“MainLayout.razor”檔案,刪除全部原有程式碼,新增如下程式碼。

 1 @inherits LayoutComponentBase
 2 
 3 <Layout SideWidth="0" IsPage="true" IsFullSide="true" IsFixedHeader="true" IsFixedFooter="false"
 4         ShowFooter="true" ShowGotoTop="true" ShowCollapseBar="true"
 5         OnCollapsed="@OnCollapsed" Menus="@GetIconSideMenuItems()">
 6     <Header>
 7         <span class="ml-3 flex-fill">Blazor學習筆記--年近半百的老李頭</span>
 8         <img src="../images/01.png" class="layout-avatar-right" />
 9         <span class="ml-3 d-none d-sm-block">登入</span>
10     </Header>
11     <Side>
12 
13         <div class="layout-banner">
14             <img class="layout-logo" src="../images/InLuck.png" />
15             <div class="layout-title">
16                 <span>演示系統</span>
17             </div>
18         </div>
19         <div class="layout-user">
20             <img class="layout-avatar" src="../images/01.png">
21             <div class="layout-title">
22                 <span>瀏覽者</span>
23             </div>
24             @*這是那跟線??*@
25             <div class="layout-user-state"></div>
26         </div>
27     </Side>
28     <Main>
29         <CascadingValue Value="this" IsFixed="true">
30             @Body
31         </CascadingValue>
32     </Main>
33     <Footer>
34         <div class="text-center flex-fill">
35             <a href="https://gitee.com/LongbowEnterprise/BootstrapAdmin" target="_blank">Bootstrap Admin</a>
36         </div>
37     </Footer>
38 </Layout>
39 
40 @code {
41 
42     /// <summary>
43     ///獲得/設定 是否收縮側邊欄
44     /// </summary>
45     public bool IsCollapsed { get; set; }
46 
47     /// <summary>
48     /// 獲得/設定 側邊欄是否佔滿整個左邊
49     /// </summary>
50     public bool IsFullSide { get; set; }
51     /// <summary>
52     /// 獲得/設定 是否固定 Footer 元件
53     /// </summary>
54     public bool IsFixedFooter { get; set; }
55 
56     private Task OnCollapsed(bool collapsed)
57     {
58         IsCollapsed = collapsed;
59         return Task.CompletedTask;
60 
61     }
62 
63     /// <summary>
64     /// 選單元件
65     /// </summary>
66     /// <returns></returns>
67     private IEnumerable<MenuItem> GetIconSideMenuItems()
68     {
69         var ret = new List<MenuItem>{
70                 new MenuItem() { Text = "首頁", Icon = "fa fa-fw fa-gears", Url = "/",IsActive = true,  },
71                 new MenuItem() { Text = "元件測試", Icon = "fa fa-fw fa-gears" },
72             };
73 
74         ret[1].AddItem(new MenuItem() { Text = "TableDemo", Icon = "fa fa-fw fa-tasks", Url = "/Pages/TableDemo" });
75 
76 
77         return ret;
78     }
79 
80 }
View Code

此時執行,圖示缺失,可以修改src="../images/01.png"指向正確的檔案或複製原始碼中的Images資料夾到你的wwwroot資料夾下。

最後是新增TableDemo元件。首先在解決方案管理器中右擊Pages資料夾,依次選擇新增àBlazor元件,在彈出的視窗中將新新增的元件命名為TableDemo.razor。如圖

刪除所有原有內容,新增如下程式碼。有很多錯誤提示先不要管。

 1 @page "/Pages/TableDemo"
 2 
 3 <Table TItem="BindItem"
 4        IsPagination="true" PageItemsSource="@PageItemsSource"
 5        IsStriped="true" IsBordered="true" IsMultipleSelect="true"
 6        ShowToolbar="true" ShowExtendButtons="true" ShowSkeleton="true"
 7        AddModalTitle="測試資料新增視窗" EditModalTitle="測試資料編輯視窗"
 8        OnQueryAsync="@OnEditQueryAsync"
 9        OnAddAsync="@OnAddAsync" OnSaveAsync="@OnSaveAsync" OnDeleteAsync="@OnDeleteAsync">
10     <TableColumns>
11         <TableColumn @bind-Field="@context.DateTime" Filterable="true" Sortable="true" />
12         <TableColumn @bind-Field="@context.Name" Filterable="true" Sortable="true" />
13         <TableColumn @bind-Field="@context.Address" Filterable="true" Sortable="true" />
14         <TableColumn @bind-Field="@context.Count" Editable="false" />
15         <TableColumn @bind-Field="@context.Education" Filterable="true" Sortable="true" />
16         <TableColumn @bind-Field="@context.Count" Editable="false" />
17         <TableColumn @bind-Field="@context.Complete">
18             <Template Context="v">
19                 <Switch IsDisabled="true" Value="v.Value" />
20             </Template>
21             <EditTemplate Context="v">
22                 <div class="form-group col-12 col-sm-6">
23                     <Switch @bind-Value="(v as BindItem)!.Complete" />
24                 </div>
25             </EditTemplate>
26         </TableColumn>
27     </TableColumns>
28 </Table>
View Code

然後再次右擊Pages資料夾,依次選擇新增à類,在彈出的視窗中將新新增的類命名為TableDemo.razor.cs。如圖

刪除所有原有內容,新增如下程式碼。

  1 using BootstrapBlazor.Components;
  2 using Microsoft.AspNetCore.Components;
  3 using System;
  4 using System.Collections.Generic;
  5 using System.ComponentModel;
  6 using System.ComponentModel.DataAnnotations;
  7 using System.Linq;
  8 using System.Collections.Concurrent;
  9 using System.Threading.Tasks;
 10 //using InLuckDSTS.Admin.Entity;
 11 //using InLuckDSTS.Admin.Service;
 12 
 13 namespace BootstrapBlazor.TableDemo.Pages
 14 {
 15     public partial class TableDemo
 16     {
 17 
 18         /// <summary>
 19         /// 設定翻頁元件的頁碼
 20         /// </summary>
 21         protected IEnumerable<int> PageItemsSource => new int[] { 5, 20, 30, 50, 100 };
 22 
 23         /// <summary>
 24         /// 搜尋模型
 25         /// </summary>
 26         protected BindItem SearchModel { get; set; } = new BindItem();
 27 
 28         protected List<BindItem> EditItems { get; set; } = GenerateItems();
 29 
 30         protected Task<QueryData<BindItem>> OnEditQueryAsync(QueryPageOptions options)
 31             => BindItemQueryAsync(EditItems, options);
 32 
 33         private static readonly ConcurrentDictionary<Type, Func<IEnumerable<BindItem>, string, SortOrder, IEnumerable<BindItem>>>
 34             SortLambdaCache = new ConcurrentDictionary<Type, Func<IEnumerable<BindItem>, string, SortOrder, IEnumerable<BindItem>>>();
 35 
 36 
 37         private static readonly Random random = new Random();
 38 
 39 
 40         protected static List<BindItem> GenerateItems() => new List<BindItem>(Enumerable.Range(1, 80).Select(i => new BindItem()
 41         {
 42             Id = i,
 43             Name = $"張三 {i:d4}",
 44             DateTime = DateTime.Now.AddDays(i - 1),
 45             Address = $"上海市普陀區金沙江路 {random.Next(1000, 2000)} 弄",
 46             Count = random.Next(1, 100),
 47             Complete = random.Next(1, 100) > 50
 48         }));
 49 
 50         /// <summary>
 51         /// 新增資料的方法
 52         /// </summary>
 53         /// <returns></returns>
 54         protected Task<BindItem> OnAddAsync()
 55         {
 56             //實際使用中,需要儲存到資料庫中
 57             return Task.FromResult(new BindItem() { DateTime = DateTime.Now });
 58         }
 59 
 60         private static readonly object _objectLock = new object();
 61         protected Task<bool> OnSaveAsync(BindItem item)
 62         {
 63             // 增加資料演示程式碼
 64             if (item.Id == 0)
 65             {
 66                 lock (_objectLock)
 67                 {
 68                     item.Id = EditItems.Max(i => i.Id) + 1;
 69                     EditItems.Add(item);
 70                 }
 71             }
 72             else
 73             {
 74                 var oldItem = EditItems.FirstOrDefault(i => i.Id == item.Id);
 75                 oldItem.Name = item.Name;
 76                 oldItem.Address = item.Address;
 77                 oldItem.DateTime = item.DateTime;
 78                 oldItem.Count = item.Count;
 79                 oldItem.Complete = item.Complete;
 80                 oldItem.Education = item.Education;
 81             }
 82             return Task.FromResult(true);
 83         }
 84 
 85         protected Task<bool> OnDeleteAsync(IEnumerable<BindItem> items)
 86         {
 87             items.ToList().ForEach(i => EditItems.Remove(i));
 88             return Task.FromResult(true);
 89         }
 90 
 91         protected Task<QueryData<BindItem>> BindItemQueryAsync(IEnumerable<BindItem> items, QueryPageOptions options)
 92         {
 93             //TODO: 此處程式碼後期精簡
 94             if (!string.IsNullOrEmpty(SearchModel.Name)) items = items.Where(item => item.Name?.Contains(SearchModel.Name, StringComparison.OrdinalIgnoreCase) ?? false);
 95             if (!string.IsNullOrEmpty(SearchModel.Address)) items = items.Where(item => item.Address?.Contains(SearchModel.Address, StringComparison.OrdinalIgnoreCase) ?? false);
 96             if (!string.IsNullOrEmpty(options.SearchText)) items = items.Where(item => (item.Name?.Contains(options.SearchText) ?? false)
 97                     || (item.Address?.Contains(options.SearchText) ?? false));
 98 
 99             // 過濾
100             var isFiltered = false;
101             if (options.Filters.Any())
102             {
103                 items = items.Where(options.Filters.GetFilterFunc<BindItem>());
104 
105                 // 通知內部已經過濾資料了
106                 isFiltered = true;
107             }
108 
109             // 排序
110             var isSorted = false;
111             if (!string.IsNullOrEmpty(options.SortName))
112             {
113                 // 外部未進行排序,內部自動進行排序處理
114                 var invoker = SortLambdaCache.GetOrAdd(typeof(BindItem), key => items.GetSortLambda().Compile());
115                 items = invoker(items, options.SortName, options.SortOrder);
116 
117                 // 通知內部已經過濾資料了
118                 isSorted = true;
119             }
120 
121             // 設定記錄總數
122             var total = items.Count();
123 
124             // 記憶體分頁
125             items = items.Skip((options.PageIndex - 1) * options.PageItems).Take(options.PageItems).ToList();
126 
127             return Task.FromResult(new QueryData<BindItem>()
128             {
129                 Items = items,
130                 TotalCount = total,
131                 IsSorted = isSorted,
132                 IsFiltered = isFiltered,
133                 IsSearch = !string.IsNullOrEmpty(SearchModel.Name) || !string.IsNullOrEmpty(SearchModel.Address)
134             });
135         }
136     }
137 
138     public class BindItem
139     {
140         /// <summary>
141         /// 
142         /// </summary>
143         [DisplayName("主鍵")]
144         public int Id { get; set; }
145 
146         /// <summary>
147         /// 
148         /// </summary>
149         [DisplayName("姓名")]
150         [Required(ErrorMessage = "姓名不能為空")]
151         public string? Name { get; set; }
152 
153         /// <summary>
154         /// 
155         /// </summary>
156         [DisplayName("日期")]
157         public DateTime? DateTime { get; set; }
158 
159         /// <summary>
160         /// 
161         /// </summary>
162         [DisplayName("地址")]
163         [Required(ErrorMessage = "地址不能為空")]
164         public string? Address { get; set; }
165 
166         /// <summary>
167         /// 
168         /// </summary>
169         [DisplayName("數量")]
170         public int Count { get; set; }
171 
172         /// <summary>
173         /// 
174         /// </summary>
175         [DisplayName("是/否")]
176         public bool Complete { get; set; }
177 
178         /// <summary>
179         /// 
180         /// </summary>
181         [Required(ErrorMessage = "請選擇學歷")]
182         [DisplayName("學歷")]
183         public EnumEducation? Education { get; set; }
184     }
185 
186     public enum EnumEducation
187     {
188         /// <summary>
189         /// 
190         /// </summary>
191         [Description("小學")]
192         Primary,
193 
194         /// <summary>
195         /// 
196         /// </summary>
197         [Description("中學")]
198         Middel
199     }
200 }
View Code

再執行一下看看如何?逐個點選新增、編輯和刪除等按鈕測試一下。

四、一些補充說明

如果正常的話,我們通過簡單的幾個步驟,就建立了一個漂亮的,具有單表維護功能的表格頁面。對我這樣的前端菜鳥來講,確實很是驚喜。但Blazor並非像我想象的,當年WinForm那樣,用C#語言寫前端。還是有大量的HTML和Javascript元素,看來還有很多路要走。把我目前能理解的跟大家分享一下,不一定完全正確,希望能和大家交流。

(一)關於TableDemo.razor元件。

1、我們在元件中添加了一個Table 並且綁定了一個實體"BindItem",由於這是前端測試,所以這個實體及資料是造出來的,在實際使用中,這個BindItem應該替換成你自己的實體,例如UserInfo,並且與後臺資料互動。

2、Table中有許多類似IsStriped="true" IsBordered="true" IsMultipleSelect="true"這樣的表格屬性,這是BootstrapBlazor元件定義好的,只要引用了BootstrapBlazor元件即可使用,Table還有許多屬性和方法具體可以檢視演示網站:https://blazor.sdgxgz.com/tables

3.頁面中綁定了查詢、新增、儲存和刪除等四個方法,在實際使用中,需要我們依據自身的業務邏輯去實現。這裡是難點。

4、頁面中表格的列綁定了實體BindItem中的欄位,實際使用中需要按照我們自己的實體修改。列定義中Filterable="true"是設定可否進行資料過濾,類似很實用的列屬性還有很多,可在演示網站中檢視。繫結的"@context.Complete"是bool型別。

(二)關於TableDemo.razor.cs類。

實際使用時,這個類中的所有BindItem實體,都需要根據實際情況替換成你自己的實體,例如UserInfo.

1、類中首先是定義幾個屬性和物件。其中PageItemsSource是翻頁元件的列舉器,SearchModel是BindItem實體模型,EditItems是BindItem實體列表,OnEditQueryAsync是非同步查詢資料的方法。

2、類中定義了幾個方法。其中GenerateItems是獲取資料到實體列表的方法,由於是前端演示,這裡的資料是造出來的,實際使用中需要到後臺讀取相應的資料。OnAddAsync、OnSaveAsync和OnDeleteAsync方法在實際使用中,都需要在此基礎上增加與後臺資料的互動,實現業務邏輯。

3、BindItemQueryAsync是資料查詢、排序和分頁的方法,需要的引數較多,比較難理解。感覺應該可以在後臺實現相關操作。希望有人能指點。

4、public class BindItem和public enum EnumEducation是頁面中需要的實體定義和實體中列舉型資料字典專案的定義,在實際使用中不應該出現在這裡。

原始碼地址:

好了,關於使用BootstrapBlazor元件建立一個具有單表維護功能的表格頁面的簡單示例先介紹到這裡。感謝您的閱讀,歡迎大家留言交流。