前幾篇側重點還是在布局,下面,主角出場,網格控件的地位和意義已無需再說,內容也比較多,預計得分幾篇才能說完,本文是一些基礎的東西,但不乏需要注意的地方。
對於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吧?
本篇到此為止,這就是網格控件,實現了取數、展現、排序和分頁,下節介紹增、刪、改、查。
最后,祝園子里各位新年快樂!