首页 > 技术文章 > 反射实体自动生成EasyUi DataGrid模板 第二版--附项目源码

Bond 2014-03-10 09:16 原文

之前写过一篇文章,地址  http://www.cnblogs.com/Bond/p/3469798.html   大概说了下怎么通过反射来自动生成对应EasyUi datagrid的模板,然后贴了很多代码,看起来很乱,当时没用过easyui,没啥经验。 这次经过了项目的实际考验,我把它做了一些改动,在此分享下,并且附上源码,源码需要用vs2012打开,打开即可运行不要做任何设置。源码地址在 https://github.com/LittleBearBond/GenerateEasyUiDataGridTemplate 。都说现在程序员要玩GitHub,我一直都想用用,不过没时间,经过N久的加班,最近终于有时间学习了下,把源码也放到上面。

我为什么要去折腾写这个东西,之前博客也大概提了下,现在请看下图,如果我们用easyui的datagrid来展示这样的数据

页面必然对应这样一个datagrid模板

<table id="dt" class="easyui-datagrid" data-options="title:'商品列表'">
  <thead>
    <tr>       <th data-options="field:'Name',align:'center',formatter:format.formatVal"> 名称</th>
      <
th data-options="field:'Category',align:'center',formatter:format.formatVal"> 类别</th>
      <
th data-options="field:'Price',align:'center',sortable:true,formatter:format.formatVal"> 价格</th>
      <
th data-options="field:'CreateTime',align:'center',formatter:format.formatTime" sortable="true"> 创建时间</th>     </tr>
  </
thead>
</
table>

其实这个模板他要展示的数据对象必然对应一个后台的类,比如这个模板对应的后台类是Product

public class Product
    {
        /// <summary>
        /// 
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// 名称
        /// </summary>public string Name { get; set; }

        /// <summary>
        /// 
        /// </summary>public string Category { get; set; }

        /// <summary>
        /// 价格
        /// </summary>public decimal Price { get; set; }

        /// <summary>
        /// 创建时间
        /// </summary>public DateTime CreateTime { get; set; }
  }

而从后台返回的数据是这样的

通过以上观察我们会发现一些问题,datagrid要展示的数据后台必然是返回一个json对象,而单个对象里面的 Id 、CreateTime 、Name、 Price 其实是对应着Product实体对象的字段名,而datagrid模板里面要设置哪行显示哪个字段,比如Name 在datagrid模板里面对应着一行th,并设置Name这行的相关属性。

datagrid模板里面每个字段都是和后台的Product实体对象的字段名称一一对应的,而我们每次在设置模板的时候都要去拷贝字段名称,如果不小心弄错了数据就不会显示出来。如果我们能通过后台的实体对象Product来直接生成这个模板那就不会出错了。这个功能我已经做好了,用到的知识点很少,只是自定义属性加反射再加点字符串拼接就搞定啦。不过生成模板也是有很多问题需要考虑,大致有以下一些问题需要解决。

1:Product对象里面不是每个字段都要显示出来,比如Id不需要显示,我们希望能够动态设置需要显示的字段。

2:在设置单行th的时候fileid是Name,但是在thead表头上需要显示 “名称” 两个汉字而不是英文的Name

3:我们可以动态设置这个字段的data-options属性  以及这行的其他属性 如style  class之类的

4:显示有先后顺序,Name显示在前面还是Price显示在前面我们可以做动态设置

5:可以额外添加和Product无关的字段,然后显示到页面上

上面说在很抽象来看具体事例:

现在我的类是这样的,标记了display属性,和DataOptions自定属性

 [DataOptions(Options = "title:'商品列表'")]
    public class Product
    {
        /// <summary>
        /// 
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// 名称
        /// </summary>
        [Display(Name = "名称")]
        [DataOptions(Order = 1)]
        public string Name { get; set; }

        /// <summary>
        /// 
        /// </summary>
        [Display(Name = "类别")]
        [DataOptions(Order = 2)]
        public string Category { get; set; }

        /// <summary>
        /// 价格
        /// </summary>
        [Display(Name = "价格")]
        [DataOptions(Order = 3, Options = "sortable:true")]
        public decimal Price { get; set; }

        /// <summary>
        /// 创建时间
        /// </summary>
        [Display(Name = "创建时间")]
        [DataOptions(Order = 4, Property = "sortable=true")]
        public DateTime CreateTime { get; set; }

现在我的cshtml页面代码是这样的

@using GenerateDataGridDemo.EasyUi
@using GenerateDataGridDemo.Extends
@using GenerateDataGridDemo.Models
@{
    ViewBag.Title = "Test1";
    Layout = "~/Views/Shared/_EasyUiLayout.cshtml";
    var loadUrl = Url.Action("LoadTest1", "Home");
}
@section searchs
{
    <div>
        @EasyUiPageControls.SearchTimeInput()
        @EasyUiPageControls.SearchKeyWordInput("产品名称:", "KeyWord", "init=\"请输入产品名称\"")
        @EasyUiPageControls.SearchButton()
    </div>
}
@Html.CreateDataGridTemplate(typeof(Product))
@section scripts{
    <script type="text/javascript">
        $(function () {
            easyui.dg.LoadData('@loadUrl');
            WebJs.SearchCreateTime();
            $('#searchLoadList').click(function () {
                easyui.dg.Search('@loadUrl');
            });
        });
    </script>
}

在页面上得到的效果就是以下这样的,经过简单封装cshtml的页面代码基本上就只有几行,就搞定了数据显示、加载、搜索。

有时我们页面不会这么简单,我们的页面可能是这样的,前面有多复选框和后面的操作列。

此时我还是用的以前的Product,只是页面代拿略有改变,如下

@using GenerateDataGridDemo.EasyUi
@using GenerateDataGridDemo.Extends
@using GenerateDataGridDemo.Models
@{
    ViewBag.Title = "Test2";
    Layout = "~/Views/Shared/_EasyUiLayout.cshtml";
    var loadUrl = Url.Action("LoadTest1", "Home");
}
@section searchs
{
    <div>
        @EasyUiPageControls.SearchTimeInput()
        @EasyUiPageControls.SearchKeyWordInput("产品名称:", "KeyWord", "init=\"请输入产品名称\"")
        @EasyUiPageControls.SearchButton()
        <a class="easyui-linkbutton" id="GetAll">获取选择的ID</a>
    </div>
}
@{
    var end = EasyUiPageHtml.FormateOperate();
    var start = EasyUiPageHtml.FirstCheckBox();
}
@Html.CreateDataGridTemplate(typeof(Product), end, start)
@section scripts{
    <script type="text/javascript">
        function formatOperate(val, row) {
            return '<a href="###" onclick="Modify(' + row.Id + ',\'' + row.Name + '\')">编辑</a>  ';
        }
        function Modify(id, name) {
            WebJs.Dialog.Alert(utils.str.formatString('Id:{0},Name{1}', id, name));
        }
        $(function () {
            easyui.dg.LoadData('@loadUrl');
            WebJs.SearchCreateTime();
            $('#searchLoadList').click(function () {
                easyui.dg.Search('@loadUrl');
            });
            $('#GetAll').on('click', function () {
                var ids = easyui.dg.GetSelectionIds();
                if (utils.str.isNullOrWhiteSpace(ids)) {
                    WebJs.Dialog.Tip('请先选择!');
                    return;
                }
                WebJs.Dialog.Content(ids);
            });
        });
    </script>
}

大家会发现其实就多了end和start,其他其实是没变动的,只是曾加了一个GetALL按钮。

真正用于生成面datagrid模板的代码只有@Html.CreateDataGridTemplate(typeof(Product))这一行,然后加上Product上面标记的一些属性,就完成了datagrid模板的自动生成,具体实现上篇博客有说明,这次把代码抽取出来然后奉献给大家做个参考。

demo代码是这样的,一切从简,只有一个程序集,页面只有两个页面,但是功能还是比较齐全的。

 

点击左边导航,就会添加一个页面,如果已经存在就刷新存在的页面,页面有右键菜单。

扩展datagrid的view 在没有数据的时候显示提示信息

对Jq这个日期控件做了点改动,起始日期级联验证

页面搜索和加载数据做了相应封装,调用的时候只需一句话,在项目中做了很多公共方法的提取和封装,这里提出来的是部分,多了影响大家看这个代码

 支持排序,只有价格和创建时间支持排序,支持单个字段排序,多个字段也是可以的,后台ExtendClass.cs有相关代码只是我没具体做这个功能。

最后贴一下整个核心的代码,代码就一百多行,在项目源码中可以去看看,然后根据自己的需求去扩展和改进。

public class GenerateDataGrid
    {
        public static IList<PropertyInfo> GetAllPropertyInfoList(Type entity)
        {
            return entity.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
        }
public static string GetDataGridTemplate(Type entity, string appendEnd, string appendStart) { var sb = new StringBuilder(); //先获取类的Attribute var entityCustomAttr = entity.GetCustomAttributes(typeof(DataOptionsAttribute), false).FirstOrDefault() as DataOptionsAttribute; #region 对实体的Attibute属性进行处理 //是否显示没有 标记dataoptions的字段 var isShowNotAttr = false;//默认不显示 var options = string.Empty; var tableId = string.Empty; var tableProperty = string.Empty; //并没有处理可能发生的异常情况, 比如在Property 指定了id="xxx" 而又指定了id的值 if (entityCustomAttr != null) { isShowNotAttr = entityCustomAttr.IsShowNotAttr; options = string.IsNullOrWhiteSpace(entityCustomAttr.Options) ? string.Empty : entityCustomAttr.Options; //默认ID为dt , 假设在Property 中没有设置了Id,如果设置了这里没做处理 tableId = string.IsNullOrWhiteSpace(entityCustomAttr.Id) ? "dt" : entityCustomAttr.Id; tableProperty = string.IsNullOrWhiteSpace(entityCustomAttr.Property) ? string.Empty : entityCustomAttr.Property; } #endregion //获取所有的Property var properties = GetAllPropertyInfoList(entity); //如果设置有不显示没有dataoptions标记的,值取出标记有dataoptions的字段 if (!isShowNotAttr) { properties = properties.Where(n => n.CustomAttributes.Any(a => a.AttributeType == typeof(DataOptionsAttribute))).ToList(); } //没有打标记的也要取出来, 这里得到以字段name为key List<Attribute>为值的集合对象 Dictionary<string, List<Attribute>> colDicOpts = properties.ToDictionary( property => property.Name, property => { var list = new List<Attribute> { property.GetCustomAttributes(typeof (DataOptionsAttribute), false).FirstOrDefault() as DataOptionsAttribute, property.GetCustomAttributes(typeof (DisplayAttribute), false).FirstOrDefault() as DisplayAttribute }; return list; }); //在table上拼接 id data-options 和 Property sb.AppendLine(string.Format("<table id=\"{0}\" class=\"easyui-datagrid\" data-options=\"{1}\" {2} > <thead> <tr>", tableId, options, tableProperty)); //没有直接遍历加入数据 这里先取得所有数据,然后进行排序,得到th 列表 var listThs = (from pro in properties let custAttrs = colDicOpts.SingleOrDefault(n => n.Key == pro.Name) select AppenedTemplate(Template.DataGridTh, custAttrs, pro)).ToList(); //1、添加到开始部分的 add start if (!string.IsNullOrWhiteSpace(appendStart)) { sb.AppendLine(appendStart); } //2、添加中间部分,先排序,得到显示顺序 add center listThs = listThs.OrderBy(n => n.Key).Select(n => n.Value).ToList(); sb.AppendLine(string.Join("", listThs)); //3、追加后面的字符串 add end if (!string.IsNullOrWhiteSpace(appendEnd)) { sb.AppendLine(appendEnd); } sb.AppendLine(@"</tr></thead></table>"); return sb.ToString(); } //dynamic 可用 KeyValuePair private static dynamic AppenedTemplate(string template, KeyValuePair<string, List<Attribute>> attributes, PropertyInfo proinfo = null) { var displayName = attributes.Value.SingleOrDefault(n => n is DisplayAttribute) as DisplayAttribute; //设置字段显示的名称,直接设置 DisplayAttribute,这个大家肯定很熟悉的属性 var str = Template.RegV.Replace(template, displayName != null ? displayName.Name : attributes.Key); //设置显示的字段field ,即是当前th显示哪个字段,例如field:'Id' str = Template.RegF.Replace(str, attributes.Key); //从该字段的CustomAttributes中取得DataOptionsAttribute var dataOptions = attributes.Value.SingleOrDefault(n => n is DataOptionsAttribute) as DataOptionsAttribute; //设置Property, 如果property和data-options有设置相同的对象 这里没做异常处理 str = Template.RegP.Replace(str, dataOptions == null ? string.Empty : dataOptions.Property ?? ""); //没有设置排序的这里默认设置一个值 var order = dataOptions == null ? 100 : dataOptions.Order; //由于我自己的需要,我要对DateTime类型进行特殊处理 if (proinfo != null && proinfo.PropertyType == typeof(DateTime)) { //没有自定义属性的值 if (dataOptions == null) { //WebJs.Format.formatTime 自己的js时间格式化函数 这个一定程度上导致前后台耦合了 str = Template.RegD.Replace(str, "formatter:format.formatTime");//默认时间格式 } else { str = dataOptions.Options != null && dataOptions.Options.IndexOf("formatter", StringComparison.CurrentCultureIgnoreCase) >= 0 ? //已经设置formatter Template.RegD.Replace(str, dataOptions.Options) : //默认设置formatter Template.RegD.Replace(str, ((dataOptions.Options ?? "").TrimEnd(',') + ",formatter:format.formatTime").TrimStart(',')); } } else { //替换data-option 的值, 如果为空就直接替换为空 if (dataOptions == null) { str = Template.RegDi.Replace(str, string.Empty); } else { var opt = (dataOptions.Options ?? ""); //默认设置起格式化 var replaceStr = opt.IndexOf("formatter", StringComparison.CurrentCultureIgnoreCase) >= 0 ? opt : opt.TrimEnd(',') + ",formatter:format.formatVal"; str = Template.RegD.Replace(str, replaceStr.TrimStart(',')); } } return new { Value = str, Key = order }; } }

 

 PS:为了追逐爱情,准备离开成都,辞职北漂,忘大神收留。

 

推荐阅读