本文主要体验用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")',
width: 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: '描述', width: 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")',
width: 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: '描述', width: 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;width:16px;height:16px;">');
cc.push('<span class="datagrid-row-expander datagrid-row-expand" style="display:inline-block;width: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, width: 24, resizable: false, fixed: true });
} else {
opts.frozenColumns = [[{ field: '_expander', expander: true, width: 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="width: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);
}
}
});
}
});
最终效果: