65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2017 年 9 月 12 日

CPOL

4分钟阅读

viewsIcon

14463

downloadIcon

310

使用 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 配置

控制器托管 MetaDataLoad 方法。完整的 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 设置字段 sortNamesortOrder
  • 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; }
  }

KeyCrudTitleOrderBy 属性最多只能出现一次。如果使用多次,则只使用第一次出现。

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 数据。

结论

属性和反射为自动生成列扫清了道路。生成过程提供了修改列的钩子,而无需更改生成引擎。自动生成消除了手动定义列的枯燥任务,减少了代码行数,并加快了开发速度。

深入阅读

Bootstrap table

AutoMapper

© . All rights reserved.