为 MVC 中的 Bootstrap 表格自动生成列





5.00/5 (2投票s)
使用 Bootstrap 表格,.Net Core 2.0 上实现最少代码!
引言
在这篇博文中,我将解释如何自动生成 Bootstrap 表格列。表格网格是 Web 应用程序的重要组成部分,但手动编写它们可能会枯燥且容易出错。在 MVC 应用程序中,控制器和带有表格网格的视图紧密耦合。控制器提供表格网格数据,因此必须能够从控制器中提取元数据。此元数据是列生成的源。列生成减少了视图中的代码行数,消除了重复的编码任务,从而实现了更快、更好的开发,并且更有趣。生成基于 Bootstrap table
手动编码
所需的网格编码
... $(document).ready(function () { var $table = $('#table'); // old style $table.bootstrapTable({ toolbar: '#toolbar', classes: 'table table-condensed table-hover table-striped', showRefresh: true, search: true, pagination: true, pageSize: 10, pageList: [10, 25, 50, 100, 250], url: window.location.pathname + '/load', sortName: 'Email', sortOrder: 'asc', sidePagination: 'server', cache: false, uniqueId: 'Id', columns: [ { title: "Edit", width: "40", halign: "center", align: "center", "formatter": "editFormatter" }, { field: "Id", title: "Id", sortable: true, visible: false }, { field: "UserName", title: "User", sortable: false, visible: true }, { field: "Email", title: "Email", sortable: true, visible: true }, { field: "EmailConfirmed", title: "Confirmed", sortable: true, visible: true, halign: "center", align: "center", formatter: "checkboxFormatter" }, { field: "PhoneNumber", title: "Phone", sortable: true, visible: true }, { field: "PhoneNumberConfirmed", title: "Confirmed", sortable: true, visible: true, halign: "center", align: "center", formatter: "checkboxFormatter" }, { field: "LockoutEnd", title: "Lockout End", sortable: true, visible: true }, { field: "LockoutEnabled", title: "Lockout Enabled", sortable: true, visible: true, halign: "center", align: "center", formatter: "checkboxFormatter" }, { field: "AccessFailedCount", title: "Access Failed Count", sortable: true, visible: true, align: "right" }, { title: "Del", width: "40", halign: "center", align: "center", formatter: "delFormatter" }] }); ...
手动设置网格需要大量工作,尤其是列。
自动编码
通过自动列生成,编码变得
$(document).ready(function () { var $table = $('#table'); // retrieve metadata from controller and apply results for initialization $.ajax({ url: window.location.pathname + '/metadata', success: function (settings) { // apply settings from controller $table.bootstrapTable({ sortName: settings.SortName, sortOrder: settings.SortOrder, sidePagination: settings.Pagination, cache: settings.UseCache, uniqueId: settings.Key, columns: settings.Columns, // init manual settings url: window.location.pathname + '/load', toolbar: '#toolbar', classes: 'table table-condensed table-hover table-striped', showRefresh: true, search: true, pagination: true, pageSize: 10, pageList: [10, 25, 50, 100, 250] }); }, }); ...
正如您所见,所需的编码现在已大大减少。
工作原理
文档加载后,会向控制器发出一个 Ajax 请求,URL 为 <controller>/MetaData。控制器收集元数据并将其发送回浏览器。元数据不仅包含列信息,还设置其他属性以正确配置网格。
- sortName 设置排序的列。
- sortOrder 设置排序方向(升序或降序)。
- sidePagination 设置分页位置,客户端还是服务器端。
- cache 设置是否缓存数据。
- uniqueId 指定唯一标识行的列。
- columns 如您所料,这是列定义。
URL 配置
控制器托管 MetaData 和 Load 方法。完整的 URL 是使用 window.location.pathname 参数创建的。请注意,这样控制器名称就不会硬编码,从而使代码可重用于其他视图而无需修改。
MetaData 结果
使用 Swagger 我们可以测试和检查 MetData 调用。运行解决方案,将 URL 改为 'https://:49842/swagger/' 并调用 MetaData API。
Controller MetaData 函数
控制器托管 MetaData 函数。BootStrapTableCreator 完成所有繁重的工作。在此示例中,BootStrapTableCreator 使用反射扫描 AccountListModel 类以查找属性。属性控制表格网格的行为。控制器还知道它是否具有 CRUD(创建、更新、删除)功能。如果适用且安全允许,您可以添加 CRUD 列。出于简单性,此处未编写安全部分。
HttpGet()] [Route("[controller]/[action]")] public IActionResult MetaData() { var tableCreator = new BootStrapTableCreator<AccountListModel>() { // add edit column AllowEdit = true, // add delete column AllowDelete = true }; return tableCreator.Serialize(); }
Serialize() 方法创建 JSON 结果,结果代码为 200 (OK)。
属性
BootStrapTableCreator 扫描多个属性
- Key 指定 uniqueId 字段。
- CrudTitle 指定 CRUD 对话框的标题字段,在此博文中未使用。
- HiddenInput 隐藏一列。
- DisableSorting(是的,你已经猜到了)禁用列的排序。
- OrderBy 设置字段 sortName 和 sortOrder
- Display 设置列标题。参数用法
- ShortName 用于列标题,如果未设置,则 Name 成为列标题。如果属性不存在,则属性名称成为列标题。
- AutoGenerateFilter = false 会跳过该列的过滤
属性示例
AccountListModel 类提供了一个如何使用这些属性的示例
public class AccountListModel { [Key] [HiddenInput(DisplayValue = false)] [Display(Name = "User Id", ShortName = "Id", AutoGenerateFilter = false)] public String Id { get; set; } [CrudTitle] [DisableSorting] [Display(Name = "User name", ShortName = "User")] public String UserName { get; set; } [CrudTitle] [OrderByAttributeAttribute(ListSortDirection.Ascending)] public String Email { get; set; } [Display(Name = "Email confirmed", ShortName = "Confirmed")] public Boolean EmailConfirmed { get; set; } [Display(Name = "Phone number", ShortName = "Phone")] public String PhoneNumber { get; set; } [Display(Name = "Phone number confirmed", ShortName = "Confirmed")] public Boolean PhoneNumberConfirmed { get; set; } [Display(Name = "Lockout ends at", ShortName = "Lockout end")] public DateTimeOffset? LockoutEnd { get; set; } [Display(Name = "Lockout enabled")] public Boolean LockoutEnabled { get; set; } public Int32 AccessFailedCount { get; set; } }
Key、CrudTitle 和 OrderBy 属性最多只能出现一次。如果使用多次,则只使用第一次出现。
BootStrapTableCreator
StrapTableCreator 根据找到的属性和内部规则创建元数据
- 布尔类型将渲染为只读复选框。
- 数字(Int、float、double、decimal)右对齐。
/// <summary> /// based on http://bootstrap-table.wenzhixin.net.cn/documentation/ /// </summary> public class BootStrapTableColumn { public String field { get; set; } public String title { get; set; } public Boolean? sortable { get; set; } public Boolean? visible { get; set; } public String width { get; set; } public String halign { get; set; } public String align { get; set; } public String formatter { get; set; } } public class BootStrapTableCreator<ListModel> where ListModel : class { private IQueryable<PropertyInfo> ListModelProperties { get; set; } public Boolean AllowEdit { get; set; } public Boolean AllowDelete { get; set; } public IList<BootStrapTableColumn> Columns { get; private set; } public BootStrapTableCreator() { ListModelProperties = typeof(ListModel).GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance).AsQueryable(); Columns = CreateColumns(); } private IList<BootStrapTableColumn> CreateColumns() { var result = new List<BootStrapTableColumn>(); foreach (var property in ListModelProperties) { var displayAttrib = property.GetCustomAttribute<DisplayAttribute>(); var hiddenAttrib = property.GetCustomAttribute<HiddenInputAttribute>(); var disableSortingAttrib = property.GetCustomAttribute<DisableSortingAttribute>(); if (displayAttrib == null) { displayAttrib = new DisplayAttribute() { Name = property.Name, ShortName = property.Name }; } var column = new BootStrapTableColumn() { field = property.Name, title = (displayAttrib.ShortName ?? displayAttrib.Name) ?? property.Name, sortable = disableSortingAttrib == null, visible = hiddenAttrib?.DisplayValue ?? true }; if (property.PropertyType.IsNumericType()) column.align = "right"; if (property.PropertyType == typeof(Boolean)) { column.formatter = "checkboxFormatter"; column.halign = "center"; column.align = "center"; } result.Add(column); } return result; } public ContentResult Serialize() { if (AllowEdit) { Columns.Insert(0, new BootStrapTableColumn() { title = "Edit", formatter = "editFormatter", halign = "center", align = "center", width = "40" }); } if (AllowDelete) { Columns.Add(new BootStrapTableColumn() { title = "Del", formatter = "delFormatter", halign = "center", align = "center", width = "40" }); } // Get column for title CRUD dialog var crudTitleProperty = ListModelProperties.FirstOrDefault(p => p.GetCustomAttribute<CrudTitleAttribute>() != null); // Only one field can be key, take the first one found var keyProperty = ListModelProperties.FirstOrDefault(p => p.GetCustomAttribute<KeyAttribute>() != null); // Only one field for sorting, take the first one found var sortProperty = ListModelProperties.FirstOrDefault(p => p.GetCustomAttribute<OrderByAttributeAttributeAttribute>() != null); // Get sortdirection var sortAttrib = sortProperty?.GetCustomAttribute<OrderByAttributeAttributeAttribute>(); var settings = new { CrudTitleFieldName = crudTitleProperty?.Name, Pagination = "server", UseCache = false, Key = keyProperty == null ? "" : keyProperty.Name, SortName = sortAttrib == null ? "" : sortProperty.Name, SortOrder = sortAttrib == null ? "" : (sortAttrib.Direction == ListSortDirection.Ascending ? "asc" : "desc"), Columns = Columns }; // NullValueHandling must be "ignore" to prevent errors with null value in the bootstrap table var content = JsonConvert.SerializeObject(settings, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.Indented }); return new ContentResult() { StatusCode = HttpStatusCode.OK.ToInt32(), Content = content, ContentType = "application/json" }; } }
Serialize() 方法封装了创建 Bootstrap table 所需的元数据的所有逻辑。Null 值在序列化过程中必须被忽略。Bootstrap table 在值为 null 时会崩溃。
自定义 MetaData
如果您有特定需求,可以在不更改 BootStrapTableCreator 的情况下自定义 Metadata 结果。
[HttpGet()] [Route("[controller]/[action]")] public IActionResult MetaData() { var tableCreator = new BootStrapTableCreator<AccountListModel>() { // add edit column AllowEdit = true, // add delete column AllowDelete = true }; // customize phone column // find column in collection var column = tableCreator.Columns.FirstOrDefault(c => c.field.EqualsEx(nameof(AccountListModel.PhoneNumber))); // set value(s) column.title = "Phone"; return tableCreator.Serialize(); }
Controller Load 函数
Controller Load 函数提供实际的网格数据,并且必须与 MetaData 匹配。在本博文示例中,数据源是 UserManager<ApplicationUser> 对象。它处理用户帐户,并在启动时使用 DI(依赖注入)进行配置。ApplicationUser 类有比我想展示的更多的字段,所以我创建了 AccountListModel 类,其中包含所有我想要的字段。字段选择是任意的,仅用作示例。AutoMapper 处理从 ApplicationUser 到 AccountListModel 的映射,并使用配置文件进行配置。
using AutoMapper; using Security.Models; namespace Models.Mappings { public class AccountMapping : Profile { public AccountMapping() { // Only 1 way mapping CreateMap<ApplicationUser, AccountListModel>(); } } }
在控制器构造函数注册后,映射即可使用。
public class UserController : Controller { private readonly UserManager<ApplicationUser> userManager; private readonly IMapper mapper; // Constructor public UserController(UserManager<ApplicationUser> userManager) { this.userManager = userManager; // Setup AutoMapper between ApplicationUser and AccountListModel var config = new AutoMapper.MapperConfiguration(cfg => { cfg.AddProfiles(typeof(AccountMapping).GetTypeInfo().Assembly); }); mapper = config.CreateMapper(); } [HttpGet()] [Route("[controller]/[action]")] public IActionResult Load(String sort, String order, Int32 offset, Int32 limit, String search) { // apply filtering paging and mapping on datasource var tableData = new BootStrapTableData<AccountListModel>(userManager.Users, sort, order, offset, limit, search, mapper); // send table data to client return tableData.Serialize(); } ...
Load(params ...) 接收来自表格网格的 Ajax 调用。参数包括排序、分页和用户输入的搜索文本。BootStrapTableData<T> 以 JSON 格式创建表格数据。控制器将此 JSON 数据发送回 Ajax 客户端。
BootStrapTableData 实现
BootStrapTableData 中的搜索方法很简单。除了以下字段之外的所有字段:
- 隐藏字段。
- Display.AutoGenerateFilter == false 的字段。
- 字节数组字段。
都会被转换为字符串值。当字符串值包含搜索文本时,将被视为匹配。搜索不区分大小写。搜索方法适用于小型到中型数据集。在大型数据集上,性能会下降,因为无法使用索引,您需要实现更智能的搜索模式。
namespace BootstrapTable.Wenzhixi { /// <summary> /// Filter,page and map items for http://bootstrap-table.wenzhixin.net.cn/ /// </summary> public class BootStrapTableData<ListModel> where ListModel : class { private IQueryable items; private String sortName; private String sortDirection; private Int32 skip; private Int32 take; private String search; private IMapper mapper; public Int32 MaxPageSize { get; set; } = 500; public BootStrapTableData(IQueryable items, String sort, String order, Int32 skip, Int32 take, String search, IMapper mapper) { this.items = items; this.sortName = sort; this.sortDirection = order; this.skip = skip; this.take = take; this.search = search; this.mapper = mapper; } /// <summary> /// Valid columns: /// - visible /// - AutoGenerateFilter!= false /// - No Byte array /// </summary> /// <param name="recordType"></param> /// <returns></returns> private IEnumerable<String> ValidSearchFields<T>() { var ListModelProperties = typeof(T).GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance).AsQueryable(); var nonAutoGenProps = ListModelProperties.Where(p => p.GetCustomAttribute<DisplayAttribute>() != null && p.GetCustomAttribute<DisplayAttribute>().GetAutoGenerateFilter().HasValue).ToList(); var hiddenInputProps = ListModelProperties.Where(p => p.GetCustomAttribute<HiddenInputAttribute>() != null && p.GetCustomAttribute<HiddenInputAttribute>().DisplayValue == false).ToList(); var byteArrayTypes = ListModelProperties.Where(p => p.PropertyType == typeof(Byte[])).ToList(); // Extract invalid types var validProperties = ListModelProperties.Except(nonAutoGenProps); validProperties = validProperties.Except(hiddenInputProps); validProperties = validProperties.Except(byteArrayTypes); var result = validProperties.Select(p => p.Name).ToList(); return result; } private IQueryable Search(IQueryable items, out Int32 count) { var itemType = items.ElementType; var propertyNames = itemType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(p => p.Name).ToList(); // Apply filtering to all visible column names if (!String.IsNullOrEmpty(search)) { var sb = new StringBuilder(); // create for valid search fields dynamic Linq expression foreach (String fieldName in ValidSearchFields<ListModel>()) sb.AppendFormat("({0} == null ? false : {0}.ToString().IndexOf(@0, @1) >=0) or {1}", fieldName, Environment.NewLine); String searchExpression = sb.ToString(); // remove last "or" occurrence searchExpression = searchExpression.Substring(0, searchExpression.LastIndexOf("or")); // Apply filtering items = items.Where(searchExpression, search, StringComparison.OrdinalIgnoreCase); } // apply count after filtering count = items.Count(); // Skip requires sorting, so make sure there is always sorting String sortExpression = ""; if (propertyNames.Any(c => c == sortName)) { sortExpression += String.Format("{0} {1}", sortName, sortDirection); items = items.OrderBy(sortExpression); } // save server and client resources if (take <= 0) take = MaxPageSize; items = items.Skip(skip).Take(take); return items; } public IActionResult Serialize() { // filter and map items var mappedItems = mapper.Map<IList<ListModel>>(Search(items, out var count)); var tableData = new { // Make sure paging and pagecount is in sync with filtered items total = count, rows = mappedItems }; // Prepare JSON content return new ContentResult() { StatusCode = HttpStatusCode.OK.ToInt32(), Content = JsonConvert.SerializeObject(tableData, new JsonSerializerSettings() { Formatting = Formatting.Indented, ContractResolver = new DefaultContractResolver() }), ContentType = "application/json" }; } } }
Serialize() 将所有内容封装起来,并返回适合填充表格网格的 JSON 数据。
结论
属性和反射为自动生成列扫清了道路。生成过程提供了修改列的钩子,而无需更改生成引擎。自动生成消除了手动定义列的枯燥任务,减少了代码行数,并加快了开发速度。