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框架,发现一个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" />
View Code

然后将<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>
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是页面中需要的实体定义和实体中枚举型数据字典项目的定义,在实际使用中不应该出现在这里。

本文地址:https://www.cnblogs.com/LiYunQi/p/13667065.html (码字不易,恳请保留)

 源码地址:https://gitee.com/LiYunQi1971/bootstrap-blazor.-table-demo

好了,关于使用BootstrapBlazor组件创建一个具有单表维护功能的表格页面的简单示例先介绍到这里。感谢您的阅读,欢迎大家留言交流。

原文地址:https://www.cnblogs.com/LiYunQi/p/13667065.html