Blazor學習筆記01:
使用BootstrapBlazor組件
創建一個具有單表維護功能的表格頁面
一、提示和背景
本文適合零基礎的前端初學者閱讀。閱讀本文您將可以使用BootstrapBlazor組件創建一個具有單表維護功能的表格頁面(如下圖所示)。本人是一名年近半百(71年)的編程愛好者,90年代中期開始編程,當時用的開發語言叫Delphi,數據庫叫SQL Server6.5,當時開發的是C/S方式下的WinForm。到2006年左右B/S方式下的ASP開始流行,由於某種原因就沒有再跟進學習。一晃十多年過去了,最近聽說微軟推出了Blazor,可以用C#寫前端,因為興趣和愛好,就又入坑了。
在碼雲(gitee)搜索了一下適合Blazor的UI框架,發現一個Star較高的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>中的所有其它樣式表之前添加如下內容。

<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" />
然后將<head>中原有的<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />這一句刪除或注釋掉。此語句與新添加的樣式會有沖突。在原有<script src="_framework/blazor.server.js"></script>語句之前前添加如下內容

<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>
上述語句可以去演示網站或在我們的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 }
此時運行,圖標缺失,可以修改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>
然后再次右擊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 }
再運行一下看看如何?逐個點擊新增、編輯和刪除等按鈕測試一下。
四、一些補充說明
如果正常的話,我們通過簡單的幾個步驟,就創建了一個漂亮的,具有單表維護功能的表格頁面。對我這樣的前端菜鳥來講,確實很是驚喜。但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是頁面中需要的實體定義和實體中枚舉型數據字典項目的定義,在實際使用中不應該出現在這里。
本文地址:https://www.cnblogs.com/LiYunQi/p/13667065.html (碼字不易,懇請保留)
源碼地址:https://gitee.com/LiYunQi1971/bootstrap-blazor.-table-demo
好了,關於使用BootstrapBlazor組件創建一個具有單表維護功能的表格頁面的簡單示例先介紹到這里。感謝您的閱讀,歡迎大家留言交流。