之前写过一篇文章,地址 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="###" οnclick="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:为了追逐爱情,准备离开成都,辞职北漂,忘大神收留。