本文主要体验用jQuery Easyui的datagrid来实现Master-Detail主次表。谢谢Kevin的博文,助我打开了思路。
主表显示所有的Category,当点击主表的展开按钮,显示该Category下的所有Product。
涉及显示的2个Model
展开namespace DataGridInMVC2.Models { public class Category { public int ID { get; set; } public string Name { get; set; } public string Description { get; set; } } } namespace DataGridInMVC2.Models { public class Product { public int CategoryID { get; set; } public int ProductID { get; set; } public string ProductName { get; set; } public int QuantityPerUnit { get; set; } public decimal UnitPrice { get; set; } public int UnitsInStock { get; set; } public int UnitOnOrder { get; set; } } }
定义一个服务类和方法用来显示Category列表
展开using System; using System.Collections; using System.Linq; using DataGridInMVC2.Models; using System.Collections.Generic; namespace DataGridInMVC2.Helpers { public class Service { //获取所有Category public IEnumerable<Category> LoadPageCategories(CategoryParam param, out int total) { var categories = InitializeCategory(); //搜索逻辑 if (!string.IsNullOrEmpty(param.Name)) { categories = categories.Where(c => c.Name.Contains(param.Name)).ToList(); } total = categories.Count(); var result = categories.OrderBy(c => c.ID) .Skip(param.PageSize*(param.PageIndex - 1)) .Take(param.PageSize); return result; } //初始化Category public IEnumerable<Category> InitializeCategory() { var categories = new List<Category>(); for (int i = 0; i < 35; i++) { categories.Add(new Category() { ID = i + 1, Name = "CategoryName" + Convert.ToString(i+1), Description = "DescriptionDescriptionDescriptionDescriptionDescriptionDescription" + Convert.ToString(i + 1) }); } return categories; } } }
CategoryParam 延续了以前文章的思路,是对应View Model的封装类,继承于包含分页信息的基类。
展开namespace DataGridInMVC2.Models { public class PageParam { public int PageSize { get; set; } public int PageIndex { get; set; } } } using System; namespace DataGridInMVC2.Models { public class CategoryParam : PageParam { public string Name { get; set; } //对应视图搜索条件 } }
CategoryController
展开namespace DataGridInMVC2.Controllers { public class CategoryController : Controller { public ActionResult Index() { return View(); } public ActionResult GetData() { //接收datagrid传来的参数 int pageIndex = int.Parse(Request["page"]); int pageSize = int.Parse(Request["rows"]); //接收搜索参数 string name = Request["Name"]; //构建服务方法所需要的参数实例 var temp = new CategoryParam() { PageIndex = pageIndex, PageSize = pageSize, Name = name }; var service = new Service(); int totalNum = 0; var categories = service.LoadPageCategories(temp, out totalNum); var result = from category in categories select new {category.Name, category.ID, category.Description}; //total,rows是前台datagrid所需要的 var jsonResult = new { total = totalNum, rows = result }; //把json对象序列化成字符串 string str = JsonSerializeHelper.SerializeToJson(jsonResult); return Content(str); } } }
page和rows是前台视图datagrid传来的参数。
当我们把一个json对象往前台传的时候,需要序列化json对象。定义了一个序列化/反序列化json对象的静态类。
展开using System; using Newtonsoft.Json; namespace DataGridInMVC2.Helpers { public static class JsonSerializeHelper { /// <summary> /// 把object对象序列化成json字符串 /// </summary> /// <param name="obj">序列话的实例</param> /// <returns>序列化json字符串</returns> public static string SerializeToJson(object obj) { return JsonConvert.SerializeObject(obj); } /// <summary> /// 把json字符串反序列化成Object对象 /// </summary> /// <param name="json">json字符串</param> /// <returns>对象实例</returns> public static Object DeserializeFromJson(string json) { return JsonConvert.DeserializeObject(json); } /// <summary> /// 把json字符串反序列化成泛型T /// </summary> /// <typeparam name="T">泛型</typeparam> /// <param name="json">json字符串</param> /// <returns>泛型T</returns> public static T DeserializeFromJson<T>(string json) { return JsonConvert.DeserializeObject<T>(json); } } }
Category/Index视图
展开@{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml"; } <link href="~/Content/themes/default/easyui.css" rel="stylesheet" /> <link href="~/Content/themes/icon.css" rel="stylesheet" /> <table id="tt"></table> @section scripts { <script src="~/Scripts/jquery.easyui.min.js"></script> <script src="~/Scripts/easyui-lang-zh_CN.js"></script> <script type="text/javascript"> $(function() { initData(); }); function initData(params) { $('#tt').datagrid({ url: '@Url.Action("GetData","Category")', 600, height: 400, title: 'Category列表', iconCls: 'icon-save', fitColumns: true, rownumbers: true, //是否加行号 pagination: true, //是否显式分页 pageSize: 15, //页容量,必须和pageList对应起来,否则会报错 pageNumber: 2, //默认显示第几页 pageList: [15, 30, 45],//分页中下拉选项的数值 columns: [[ //book.ItemId, book.ProductId, book.ListPrice, book.UnitCost, book.Status, book.Attr1 { field: 'ID', title: '编号'}, { field: 'Name', title: '类别名称'}, { field: 'Description', title: '描述', 600 } ]], queryParams: params, //搜索json对象 }); } </script> }
这里的@section scripts对应/Shared/_Layout.cshtml中的@RenderSection("scripts", required: false)。
Master表有了,接下来就是Detail表。需要一个根据Category的ID来获取Product列表的服务类方法。
展开using System; using System.Collections; using System.Linq; using DataGridInMVC2.Models; using System.Collections.Generic; namespace DataGridInMVC2.Helpers { public class Service { //根据CategoryId获取Product集合 public IEnumerable<Product> LoadProductsByCategory(int categoryId) { var products = InitializeProducts(); var result = products.OrderBy( p => p.ProductID) .Where(p => p.CategoryID == categoryId); return result; } //初始化Product public IEnumerable<Product> InitializeProducts() { var products = new List<Product>(); var r = new Random(); for (int i = 0; i < 35; i++) { products.Add(new Product() { CategoryID = i + 1, ProductID = r.Next(10000), ProductName = "ProductName" + Convert.ToString(r.Next(10000)), QuantityPerUnit = i + 10, UnitPrice = (i + 5) * 10m, UnitsInStock = (i + 2) * 10 }); products.Add(new Product() { CategoryID = i + 1, ProductID = r.Next(10000), ProductName = "ProductName" + Convert.ToString(r.Next(10000)), QuantityPerUnit = i + 10, UnitPrice = (i + 5) * 10m, UnitsInStock = (i + 2) * 10 }); products.Add(new Product() { CategoryID = i + 1, ProductID = r.Next(10000), ProductName = "ProductName" + Convert.ToString(r.Next(10000)), QuantityPerUnit = i + 10, UnitPrice = (i + 5) * 10m, UnitsInStock = (i + 2) * 10 }); } return products; } } }
ProductController
展开using System.Web; using System.Web.Mvc; using DataGridInMVC2.Helpers; namespace DataGridInMVC2.Controllers { public class ProductController : Controller { public ActionResult Index() { return View(); } public ActionResult GetByCategory(int? categoryId = null) { if (!categoryId.HasValue) { return new EmptyResult(); } var service = new Service(); var products = service.LoadProductsByCategory((int)categoryId); return PartialView("_GetByCategory", products.ToList()); } } }
_GetByCategory.cshtml部分视图
展开@model IEnumerable<DataGridInMVC2.Models.Product> <style type="text/css"> .dv-table td { border: 0; vertical-align: middle; } </style> <table class="dv-table table table-striped"> <tr style="background-color:#f5f5dc;"> <th> @Html.DisplayNameFor(model => model.CategoryID) </th> <th> @Html.DisplayNameFor(model => model.ProductName) </th> <th> @Html.DisplayNameFor(model => model.QuantityPerUnit) </th> <th> @Html.DisplayNameFor(model => model.UnitPrice) </th> <th> @Html.DisplayNameFor(model => model.UnitsInStock) </th> <th> @Html.DisplayNameFor(model => model.UnitOnOrder) </th> </tr> @foreach (var item in Model) { <tr style="height:15px;line-height: 15px;"> <td> @Html.DisplayFor(modelItem => item.CategoryID) </td> <td> @Html.DisplayFor(modelItem => item.ProductName) </td> <td> @Html.DisplayFor(modelItem => item.QuantityPerUnit) </td> <td> @Html.DisplayFor(modelItem => item.UnitPrice) </td> <td> @Html.DisplayFor(modelItem => item.UnitsInStock) </td> <td> @Html.DisplayFor(modelItem => item.UnitOnOrder) </td> </tr> } </table>
Category/Index视图
展开@{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml"; } <link href="~/Content/themes/default/easyui.css" rel="stylesheet" /> <link href="~/Content/themes/icon.css" rel="stylesheet" /> <table id="tt"></table> @section scripts { <script src="~/Scripts/jquery.easyui.min.js"></script> <script src="~/Scripts/datagrid-detailview.js"></script> <script src="~/Scripts/easyui-lang-zh_CN.js"></script> <script type="text/javascript"> $(function() { initData(); }); function initData(params) { $('#tt').datagrid({ url: '@Url.Action("GetData","Category")', 600, height: 400, title: 'Category列表', iconCls: 'icon-save', fitColumns: true, rownumbers: true, //是否加行号 pagination: true, //是否显式分页 pageSize: 15, //页容量,必须和pageList对应起来,否则会报错 pageNumber: 2, //默认显示第几页 pageList: [15, 30, 45],//分页中下拉选项的数值 columns: [[ //book.ItemId, book.ProductId, book.ListPrice, book.UnitCost, book.Status, book.Attr1 { field: 'ID', title: '编号'}, { field: 'Name', title: '类别名称'}, { field: 'Description', title: '描述', 600 } ]], queryParams: params, //搜索json对象 view: detailview, detailFormatter: function(index, row) { return '<div id="ddv-' + index + '" style="padding:5px;"></div>'; }, onExpandRow: function(index, row) { $('#ddv-' + index).panel({ border: false, cache: false, href: '@Url.Action("GetByCategory", "Product", new { categoryId = "_id_" })' .replace("_id_", row.ID), onLoad: function () { $('#tt').datagrid('fixDetailRowHeight', index); } }); $('#tt').datagrid('fixDetailRowHeight', index); } }); } </script> }
使用了Easyui的panel插件显式Detail表内容。
使用了datagrid的一个扩展datagrid-detailview.js用来显式Detail表,如下:
展开var detailview = $.extend({}, $.fn.datagrid.defaults.view, { render: function (target, container, frozen) { var state = $.data(target, 'datagrid'); var opts = state.options; if (frozen) { if (!(opts.rownumbers || (opts.frozenColumns && opts.frozenColumns.length))) { return; } } var rows = state.data.rows; var fields = $(target).datagrid('getColumnFields', frozen); var table = []; table.push('<table class="datagrid-btable" cellspacing="0" cellpadding="0" border="0"><tbody>'); for (var i = 0; i < rows.length; i++) { // get the class and style attributes for this row var css = opts.rowStyler ? opts.rowStyler.call(target, i, rows[i]) : ''; var classValue = ''; var styleValue = ''; if (typeof css == 'string') { styleValue = css; } else if (css) { classValue = css['class'] || ''; styleValue = css['style'] || ''; } var cls = 'class="datagrid-row ' + (i % 2 && opts.striped ? 'datagrid-row-alt ' : ' ') + classValue + '"'; var style = styleValue ? 'style="' + styleValue + '"' : ''; var rowId = state.rowIdPrefix + '-' + (frozen ? 1 : 2) + '-' + i; table.push('<tr id="' + rowId + '" datagrid-row-index="' + i + '" ' + cls + ' ' + style + '>'); table.push(this.renderRow.call(this, target, fields, frozen, i, rows[i])); table.push('</tr>'); table.push('<tr style="display:none;">'); if (frozen) { table.push('<td colspan=' + (fields.length + 2) + ' style="border-right:0">'); } else { table.push('<td colspan=' + (fields.length) + '>'); } table.push('<div class="datagrid-row-detail">'); if (frozen) { table.push(' '); } else { table.push(opts.detailFormatter.call(target, i, rows[i])); } table.push('</div>'); table.push('</td>'); table.push('</tr>'); } table.push('</tbody></table>'); $(container).html(table.join('')); }, renderRow: function (target, fields, frozen, rowIndex, rowData) { var opts = $.data(target, 'datagrid').options; var cc = []; if (frozen && opts.rownumbers) { var rownumber = rowIndex + 1; if (opts.pagination) { rownumber += (opts.pageNumber - 1) * opts.pageSize; } cc.push('<td class="datagrid-td-rownumber"><div class="datagrid-cell-rownumber">' + rownumber + '</div></td>'); } for (var i = 0; i < fields.length; i++) { var field = fields[i]; var col = $(target).datagrid('getColumnOption', field); if (col) { var value = rowData[field]; // the field value var css = col.styler ? (col.styler(value, rowData, rowIndex) || '') : ''; var classValue = ''; var styleValue = ''; if (typeof css == 'string') { styleValue = css; } else if (cc) { classValue = css['class'] || ''; styleValue = css['style'] || ''; } var cls = classValue ? 'class="' + classValue + '"' : ''; var style = col.hidden ? 'style="display:none;' + styleValue + '"' : (styleValue ? 'style="' + styleValue + '"' : ''); cc.push('<td field="' + field + '" ' + cls + ' ' + style + '>'); if (col.checkbox) { style = ''; } else if (col.expander) { style = "text-align:center;height:16px;"; } else { style = styleValue; if (col.align) { style += ';text-align:' + col.align + ';' } if (!opts.nowrap) { style += ';white-space:normal;height:auto;'; } else if (opts.autoRowHeight) { style += ';height:auto;'; } } cc.push('<div style="' + style + '" '); if (col.checkbox) { cc.push('class="datagrid-cell-check '); } else { cc.push('class="datagrid-cell ' + col.cellClass); } cc.push('">'); if (col.checkbox) { cc.push('<input type="checkbox" name="' + field + '" value="' + (value != undefined ? value : '') + '">'); } else if (col.expander) { //cc.push('<div style="text-align:center;16px;height:16px;">'); cc.push('<span class="datagrid-row-expander datagrid-row-expand" style="display:inline-block;16px;height:16px;cursor:pointer;" />'); //cc.push('</div>'); } else if (col.formatter) { cc.push(col.formatter(value, rowData, rowIndex)); } else { cc.push(value); } cc.push('</div>'); cc.push('</td>'); } } return cc.join(''); }, insertRow: function (target, index, row) { var opts = $.data(target, 'datagrid').options; var dc = $.data(target, 'datagrid').dc; var panel = $(target).datagrid('getPanel'); var view1 = dc.view1; var view2 = dc.view2; var isAppend = false; var rowLength = $(target).datagrid('getRows').length; if (rowLength == 0) { $(target).datagrid('loadData', { total: 1, rows: [row] }); return; } if (index == undefined || index == null || index >= rowLength) { index = rowLength; isAppend = true; this.canUpdateDetail = false; } $.fn.datagrid.defaults.view.insertRow.call(this, target, index, row); _insert(true); _insert(false); this.canUpdateDetail = true; function _insert(frozen) { var v = frozen ? view1 : view2; var tr = v.find('tr[datagrid-row-index=' + index + ']'); if (isAppend) { var newDetail = tr.next().clone(); tr.insertAfter(tr.next()); } else { var newDetail = tr.next().next().clone(); } newDetail.insertAfter(tr); newDetail.hide(); if (!frozen) { newDetail.find('div.datagrid-row-detail').html(opts.detailFormatter.call(target, index, row)); } } }, deleteRow: function (target, index) { var opts = $.data(target, 'datagrid').options; var dc = $.data(target, 'datagrid').dc; var tr = opts.finder.getTr(target, index); tr.next().remove(); $.fn.datagrid.defaults.view.deleteRow.call(this, target, index); dc.body2.triggerHandler('scroll'); }, updateRow: function (target, rowIndex, row) { var dc = $.data(target, 'datagrid').dc; var opts = $.data(target, 'datagrid').options; var cls = $(target).datagrid('getExpander', rowIndex).attr('class'); $.fn.datagrid.defaults.view.updateRow.call(this, target, rowIndex, row); $(target).datagrid('getExpander', rowIndex).attr('class', cls); // update the detail content if (this.canUpdateDetail) { var row = $(target).datagrid('getRows')[rowIndex]; var detail = $(target).datagrid('getRowDetail', rowIndex); detail.html(opts.detailFormatter.call(target, rowIndex, row)); } }, bindEvents: function (target) { var state = $.data(target, 'datagrid'); var dc = state.dc; var opts = state.options; var body = dc.body1.add(dc.body2); var clickHandler = ($.data(body[0], 'events') || $._data(body[0], 'events')).click[0].handler; body.unbind('click').bind('click', function (e) { var tt = $(e.target); var tr = tt.closest('tr.datagrid-row'); if (!tr.length) { return } if (tt.hasClass('datagrid-row-expander')) { var rowIndex = parseInt(tr.attr('datagrid-row-index')); if (tt.hasClass('datagrid-row-expand')) { $(target).datagrid('expandRow', rowIndex); } else { $(target).datagrid('collapseRow', rowIndex); } $(target).datagrid('fixRowHeight'); } else { clickHandler(e); } e.stopPropagation(); }); }, onBeforeRender: function (target) { var state = $.data(target, 'datagrid'); var opts = state.options; var dc = state.dc; var t = $(target); var hasExpander = false; var fields = t.datagrid('getColumnFields', true).concat(t.datagrid('getColumnFields')); for (var i = 0; i < fields.length; i++) { var col = t.datagrid('getColumnOption', fields[i]); if (col.expander) { hasExpander = true; break; } } if (!hasExpander) { if (opts.frozenColumns && opts.frozenColumns.length) { opts.frozenColumns[0].splice(0, 0, { field: '_expander', expander: true, 24, resizable: false, fixed: true }); } else { opts.frozenColumns = [[{ field: '_expander', expander: true, 24, resizable: false, fixed: true }]]; } var t = dc.view1.children('div.datagrid-header').find('table'); var td = $('<td rowspan="' + opts.frozenColumns.length + '"><div class="datagrid-header-expander" style="24px;"></div></td>'); if ($('tr', t).length == 0) { td.wrap('<tr></tr>').parent().appendTo($('tbody', t)); } else if (opts.rownumbers) { td.insertAfter(t.find('td:has(div.datagrid-header-rownumber)')); } else { td.prependTo(t.find('tr:first')); } } var that = this; setTimeout(function () { that.bindEvents(target); }, 0); }, onAfterRender: function (target) { var that = this; var state = $.data(target, 'datagrid'); var dc = state.dc; var opts = state.options; var panel = $(target).datagrid('getPanel'); $.fn.datagrid.defaults.view.onAfterRender.call(this, target); if (!state.onResizeColumn) { state.onResizeColumn = opts.onResizeColumn; } if (!state.onResize) { state.onResize = opts.onResize; } function setBodyTableWidth() { var columnWidths = dc.view2.children('div.datagrid-header').find('table').width(); dc.body2.children('table').width(columnWidths); } opts.onResizeColumn = function (field, width) { setBodyTableWidth(); var rowCount = $(target).datagrid('getRows').length; for (var i = 0; i < rowCount; i++) { $(target).datagrid('fixDetailRowHeight', i); } // call the old event code state.onResizeColumn.call(target, field, width); }; opts.onResize = function (width, height) { setBodyTableWidth(); state.onResize.call(panel, width, height); }; this.canUpdateDetail = true; // define if to update the detail content when 'updateRow' method is called; dc.footer1.find('span.datagrid-row-expander').css('visibility', 'hidden'); $(target).datagrid('resize'); } }); $.extend($.fn.datagrid.methods, { fixDetailRowHeight: function (jq, index) { return jq.each(function () { var opts = $.data(this, 'datagrid').options; if (!(opts.rownumbers || (opts.frozenColumns && opts.frozenColumns.length))) { return; } var dc = $.data(this, 'datagrid').dc; var tr1 = opts.finder.getTr(this, index, 'body', 1).next(); var tr2 = opts.finder.getTr(this, index, 'body', 2).next(); // fix the detail row height if (tr2.is(':visible')) { tr1.css('height', ''); tr2.css('height', ''); var height = Math.max(tr1.height(), tr2.height()); tr1.css('height', height); tr2.css('height', height); } dc.body2.triggerHandler('scroll'); }); }, getExpander: function (jq, index) { // get row expander object var opts = $.data(jq[0], 'datagrid').options; return opts.finder.getTr(jq[0], index).find('span.datagrid-row-expander'); }, // get row detail container getRowDetail: function (jq, index) { var opts = $.data(jq[0], 'datagrid').options; var tr = opts.finder.getTr(jq[0], index, 'body', 2); return tr.next().find('div.datagrid-row-detail'); }, expandRow: function (jq, index) { return jq.each(function () { var opts = $(this).datagrid('options'); var dc = $.data(this, 'datagrid').dc; var expander = $(this).datagrid('getExpander', index); if (expander.hasClass('datagrid-row-expand')) { expander.removeClass('datagrid-row-expand').addClass('datagrid-row-collapse'); var tr1 = opts.finder.getTr(this, index, 'body', 1).next(); var tr2 = opts.finder.getTr(this, index, 'body', 2).next(); tr1.show(); tr2.show(); $(this).datagrid('fixDetailRowHeight', index); if (opts.onExpandRow) { var row = $(this).datagrid('getRows')[index]; opts.onExpandRow.call(this, index, row); } } }); }, collapseRow: function (jq, index) { return jq.each(function () { var opts = $(this).datagrid('options'); var dc = $.data(this, 'datagrid').dc; var expander = $(this).datagrid('getExpander', index); if (expander.hasClass('datagrid-row-collapse')) { expander.removeClass('datagrid-row-collapse').addClass('datagrid-row-expand'); var tr1 = opts.finder.getTr(this, index, 'body', 1).next(); var tr2 = opts.finder.getTr(this, index, 'body', 2).next(); tr1.hide(); tr2.hide(); dc.body2.triggerHandler('scroll'); if (opts.onCollapseRow) { var row = $(this).datagrid('getRows')[index]; opts.onCollapseRow.call(this, index, row); } } }); } });