operamasks-ui2.0 +MVC4.0+EF5.0实战之四 部门管理功能及网格控件(datagrid)
前几篇侧重点还是在布局,下面,主角出场,网格控件的地位和意义已无需再说,内容也比较多,预计得分几篇才能说完,本文是一些基础的东西,但不乏需要注意的地方。
对于MIS系统来说,公司的组织架构是一个基础的功能(网站系统则没有所谓的部门及成员,而侧重于以个体为单位的会员),也即通常所说的部门。与前面说的菜单类似,通常也是采取自关联形成树形结构。为了方便维护,设计上采取左侧树,右侧网格的方式,先上效果图,以便有个直观的印象。
先说一下后台基本工作。
采用Code First模式,首先创建部门实体。
View Code using Model.Framework; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; using System.Web.Script.Serialization; using System.Web.Mvc; namespace Model.Sys { public class Department : BaseEntity { [DisplayName("内码")] public string ID { get; set; } [DisplayName("部门名称")] [StringLength(20)] [Required()] [Remote("CheckExistForName", "Department", AdditionalFields = "Name,ParentID,ID", ErrorMessage = "上级部门下已存在该名称的部门,请确认")] public string Name { get; set; } [DisplayName("电话")] public string Tel { get; set; } [DisplayName("传真")] public string Fax { get; set; } [DisplayName("地址")] public string Address { get; set; } [DisplayName("描述")] public string Describe { get; set; } [DisplayName("创建时间")] public Nullable<System.DateTime> CreateDate { get; set; } [DisplayName("使用标志")] [Required()] public string UseFlag { get; set; } [DisplayName("备注")] public string Remark { get; set; } [DisplayName("排序号")] public string SortNo { get; set; } public string ParentID { get; set; } [DisplayName("上级部门")] [ForeignKey("ParentID")] public Department ParentDept { get; set; } public string CreateUserID { get; set; } [DisplayName("创建人")] [ForeignKey("CreateUserID")] public User CreateUser { get; set; } public ICollection<Department> SonDepts { get; set; } public ICollection<User> Users { get; set; }
继承的BaseEntity是为了方便以后为所有实体加统一的方法预留的,目前为空,你可以无视。另外,用了一些数据声明和验证的东西,暂不做详细说明。这里有一点必须注意,去除virtual关键字,否则在执行Json序列化时,就会报检测到循环引用的错误。
然后在数据库里插入几条测试数据(以下是使用EntityFramework的迁移功能,在Configuration类的Seed方法里加入测试数据,关于迁移功能请参见我之前的一篇译稿前半部分 Asp.Net MVC4.0 官方教程 入门指南之八--为Movie模型和库表添加字段),当然你也可以在数据库里手工添加。
context.Department.AddOrUpdate( p => p.ID, //new Department { ID = "1", Name = "部门组织", ParentID = null, UseFlag = "4" }, new Department { ID = "2", Name = "软件公司", ParentID = null, UseFlag = "4" }, new Department { ID = "3", Name = "研发部", ParentID = "2", SortNo = "01", UseFlag = "4" }, new Department { ID = "4", Name = "产品部", ParentID = "2", SortNo = "02", UseFlag = "4" }, new Department { ID = "5", Name = "办公室", ParentID = "2", SortNo = "03", UseFlag = "4" }, new Department { ID = "6", Name = "信息中心", ParentID = "5", UseFlag = "4" }, new Department { ID = "11", Name = "部门11", ParentID = "4", SortNo = "11", UseFlag = "4" }, new Department { ID = "12", Name = "部门12", ParentID = "4", SortNo = "12", UseFlag = "4" }, new Department { ID = "13", Name = "部门13", ParentID = "4", SortNo = "13", UseFlag = "4" }, new Department { ID = "14", Name = "部门14", ParentID = "4", SortNo = "14", UseFlag = "4" }, new Department { ID = "15", Name = "部门15", ParentID = "4", SortNo = "15", UseFlag = "4" }, new Department { ID = "16", Name = "部门16", ParentID = "4", SortNo = "16", UseFlag = "4" }, new Department { ID = "17", Name = "部门17", ParentID = "4", SortNo = "17", UseFlag = "4" }, new Department { ID = "18", Name = "部门18", ParentID = "4", SortNo = "18", UseFlag = "4" }, new Department { ID = "19", Name = "部门19", ParentID = "4", SortNo = "19", UseFlag = "4" }, new Department { ID = "20", Name = "部门20", ParentID = "4", SortNo = "20", UseFlag = "4" }, new Department { ID = "21", Name = "部门21", ParentID = "4", SortNo = "21", UseFlag = "4" } );
以上是后台的基础性工作,关于前台调用的后台方法,跟前台一块描述,这样联系更紧密一些。
新建一个控制器,命名为DepartmentContorller,空模板空支架,也就是完全自己控制,不用mvc脚手架自动生成。然后在其Index方法里右键,选择生成视图,命名为ListPag,打开ListPage.cshtml
1.在head标签内部加入对om相关css样式表的引用
@Styles.Render("~/OperaMasksUI/css/default/om-default.css")
2.在</body>标签之前加入以下对js文件的引用
@Scripts.Render("~/OperaMasksUI/js/jquery163.min.js")
@Scripts.Render("~/OperaMasksUI/js/operamasks-ui200.min.js")
3.部门管理功能我们想实现左侧树右侧网格的效果,因此需要用到前面已经说过的布局控件,如下所示
<div id="page" > <div id="west-panel"> <ul id="tree"></ul> </div> <div id="center-panel"> <table id="datagrid" ></table> </div> </div>
对应的初始化js为
function LoadLayout() { $('#page').omBorderLayout({ panels: [ { id: "west-panel", title: "部门组织", region: "west", resizable: true, collapsible: true, width: 200 }, { id: "center-panel", region: "center", header: false } ], hideCollapsBtn: true, fit: true, spacing: 7 }); }
前面已经详细学习过布局控件的使用,在此就不再啰唆,仅将相关代码贴出来,以上效果就是仅使用左右布局,且左侧区域可折叠。
然后是左侧树,与前面菜单类似,同样只贴出代码。
树初始化js:
function LoadTree() { $("#tree").omTree({ simpleDataModel: true, dataSource: '@Url.Action("Tree")', onClick: TreeNodeClick }); }
后台获取数据Tree方法为:
//左侧部门树 public ActionResult Tree() { IQueryable<Department> all = DepartmentService.Query(); var nodes = new List<TreeNode>(); foreach (var item in all.ToList()) { TreeNode node = new TreeNode(); node.id = item.ID; node.pid = item.ParentID; node.text = item.Name; node.expanded = "true"; nodes.Add(node); } return Content(nodes.ToJsonString()); }
下面,网格控件登场。
初始化js:
var rootID = "2"; var defaultSort = { sortBy: 'Name', sortDir: 'asc' }; function LoadDataGrid() { $('#datagrid').omGrid({ method:'POST', title: '部门列表', extraData: $.extend({ id: rootID }, defaultSort), singleSelect: false, dataSource: '@Url.Action("DataGrid")', colModel: [ { header: '@Html.DisplayNameFor(model => model.Name)', name: 'Name', width: 200, align: 'center', sort: 'serverSide' }, { header: '@Html.DisplayNameFor(model => model.Tel)', name: 'Tel', width: 100, align: 'center'}, { header: '@Html.DisplayNameFor(model => model.Describe)', name: 'Describe', width: 'autoExpand', align: 'center' }, { header: '@Html.DisplayNameFor(model => model.SortNo)', name: 'SortNo', width: 40, align: 'center', sort: 'serverSide' }, { header: '操作', name: 'ID', width: 100, align: 'center', renderer:DatagridOpColumn }, ] }); }
前面有两个变量,一个是部门根目录id,另外一个是默认排序,这两部分,都通过初始化参数extraData属性传到后台,至于colModel则指明每一列,注意列显示名称没有写死,而是通过@Html.DisplayNameFor(model => model.Name)方式从模型里取的,这样一旦模型的DisplayName改了,所有的前台页面都会自动更新,要使用这种方式,需要在首行加上@model Model.Sys.Department。
dataSource指明取数据的后台方法,如下:
//右侧部门列表 [HttpPost] public ContentResult DataGrid(FormCollection form) { string id = form["id"]; //若为根目录,查询所有部门,否则附加查询限制条件 /***处理略,之后篇章里详述统一查询处理**/
//排序 Sort sort = new Sort(form["sortBy"], form["sortDir"]); //查询 IQueryable<Department> queryResult = DepartmentService.Query(model, sort); //分页 PageView pageView = new PageView(Int32.Parse(form["start"]), form["limit"]); //取得数据 var data = DataGrid<Department>.GetPageData(queryResult, pageView); //返回数据 return Content(data.ToJsonString()); }
辅助的各个类如下:
排序类Sort:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Common.Query { public class Sort { public string SortType { get; set; } public string Field { get; set; } public Sort(string field, string sortType = "asc") { Field = field; SortType = sortType; } public string Expression { get { return string.Format(" {0} {1} ", Field, SortType); } } } }
分页类PageView:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Common.Query { public class PageView { /// <summary> /// 页面索引 /// </summary> public int PageIndex { get; set; } /// <summary> /// 页面记录数 /// </summary> public int PageSize { get; set; } /// <summary> /// 记录起始数 /// </summary> public int RecordStart { get; set; } public PageView() { } public PageView(string pageIndex, string pageSize) { if (pageIndex != null ) { try { PageIndex = Int32.Parse(pageIndex); } catch { PageIndex =1; } } if (pageSize != null) { try { PageSize = Int32.Parse(pageSize); } catch { PageSize = 10; } } } public PageView(int recordStart, string pageSize) { if (pageSize != null) { try { PageSize = Int32.Parse(pageSize); } catch { PageSize = 10; } } PageIndex = (int)Math.Ceiling((double)recordStart / (double)PageSize); } } }
其实,我原来的分页类里只有当前页码PageIndex和页面记录数PageSize,om传给后台的limit是页面记录数,而start居然是起始记录数(不得不说,om设计人员的思维模式……),因此不得不修改这个类来适应,加入重载初始化函数以及通过RecordStart和PageSize来换算出PageIndex。
网格类DataGrid是为了前后台交换数据用的:
using Common.Query; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; namespace Model.Json { public class DataGridRow { public string id { get; set; } public List<string> cell { get; set; } } public class DataGrid<T>:Object { public int total { get; set; } public List<T> rows { get; set; } public static DataGrid<T> ConvertFromList(List<T> list) { DataGrid<T> data = new DataGrid<T>(); if (list != null) { data.total = list.Count; data.rows = list; } else { data.total = 0; } return data; } public static DataGrid<T> GetAllData(IQueryable<T> query) { DataGrid<T> data = new DataGrid<T>(); if (query != null) { data.total = query.Count(); data.rows = query.ToList(); } else { data.total = 0; } return data; } public static DataGrid<T> GetPageData(IQueryable<T> query,PageView pageView) { DataGrid<T> data = new DataGrid<T>(); if (query != null) { data.total = query.Count(); if (pageView != null) { if (pageView.PageSize > 0 && pageView.PageIndex >= 0) { query = query.Skip(pageView.PageIndex * pageView.PageSize).Take(pageView.PageSize); } } data.rows = query.ToList(); } else { data.total = 0; } return data; } } }
前台还有一个部门树点击的事件处理,就是就部门的id传给后侧的网格:
function TreeNodeClick(nodeData,event) { $('#datagrid').omGrid({ extraData: $.extend({ id: nodeData.id }, defaultSort) }); }
事实上,你看到的最终的结果,中间还是经历了一些曲折……
首先一个问题跟本节内容关系不大,但是本节中暴露出来了,就是实战三中,点击功能菜单后,在右侧业务区域中动态添加tab页,嵌入iframe,相关的js如下
function TreeNodeClick(node, event) { $("#tabs").omTabs('add', { title: node.text, content: '<iframe scrolling="yes" frameborder="0" src=' + node.url + ' style="width:100%;height:100%;"></iframe>', closable: true, tabId:node.id }); }
咋看上去是没问题,添加了tab页,当时运行也没发现问题,加了内容后就出问题了,高度!高度不能自动适配,只显示大概几百像素,没有填充整个tab页,即使设置了style="width:100%;height:100%;也没用,而在easyui中就没这问题,直接可以实现完美的完全填充效果,查找资料,反复试验,最终采取下面这种方式勉强达到效果:
content: '<iframe id="frame" onload="$(this).height($(this).contents().find(' + "tabs" + ').height()-55)" scrolling="yes" frameborder="0" src=' + node.url + ' style="width:100%;height:100%"></iframe>'
即使用js在iframe加载完成后,动态获取tab标签页的高度然后减去55px,设置为iframe的高度,至于为什么设置为55,一是tab标签头部自身25px,另外30是一些margin、border占用的,目测和试验55效果最好,未在多浏览器多显示器下测试,可能还有问题。若你有更好的解决方式,欢迎留言说明,先行谢过。
第二个问题是关于服务器端排序问题,datagrid的colModel属性,可以设置各列的排序方式,客户端、服务器端或者自定义js函数,如果是采用服务器端排序,即设置 sort: 'serverSide'。另外,我后台分页,对IQureyable对象使用Skip方法,该方法要求必须有orderBy子句,从业务角度考虑,通常也需要在datagrid首次加载的时候设置一个默认排序字段和排序方式(asc或desc)。查看了官方示例和说明,datagrid自身属性没有排序相关内容,而是在其基础了外挂了一个排序插件,点击列标题的时候会向后台发送sortBy和sortDir。初始化的时候,则没有提供对外设置关于排序的方法和属性,因此,只能放到extraData属性中。结果问题就来了,调试时候发现,服务器端排序不起作用,发现前台传给后台的排序参数,始终是初始化中设置的Name和asc,点击列头根本不起作用。无奈之下只能查看om源码,幸好源码是开放的且注释比较多,找到了omGrid的_populate方法,大概在11040行,发现了问题所在,合并参数的时候,用初始化的参数,把排序两字段覆盖了,源代码如下:
var param =$.extend(true,{},this._extraData,op.extraData,{ start : limit * (nowPage - 1), limit: limit, _time_stamp_ : new Date().getTime() });
问题是找到了,但是om没有提供任何关于排序的方法或属性,用于控制点击列头来排序这个过程,全部内置了。无奈之下,只有修改源代码,把排序两个字段传了过去
var param =$.extend(true,{},this._extraData,op.extraData,{ start : limit * (nowPage - 1), limit: limit, sortBy: this._extraData.sortBy, sortDir: this._extraData.sortDir, _time_stamp_ : new Date().getTime() });
小改动,改完后服务器端排序总算正常了,理论上对其他地方也没影响,应该不会因为改动带来新的问题。要设置初始加载后的默认排序字段,自身属性不提供,只能通过extraData这里加上,若自定义名字,不跟sortBy和sortDir重名,则后台方法就要分别处理,还要区分两种情况,若重名,则又会被初始参数覆盖,左右为难,这应该算一个BUG吧?
本篇到此为止,这就是网格控件,实现了取数、展现、排序和分页,下节介绍增、删、改、查。
最后,祝园子里各位新年快乐!