目录
1. 介绍
2. 软件环境
4. jqGrid和AJAX
5. GridSettings
8. 数据实体类和LINQ
介绍
“MVC网站教程”系列的目的是教你如何使用 ASP.NET MVC 创建一个基本的、可扩展的网站。
3) MVC网站教程(三):动态布局和站点管理(涉及技术:AJAX、jqGrid、Controller扩展、HTML Helpers等等)
4) MVC网站教程(四):MVC4网站中集成jqGrid表格插件(涉及技术:AJAX,JSON,jQuery,LINQ和序列化)
系列的第一篇文章“多语言网站框架”, 主要讲解如何去创建一个支持多语言的MVC网站,同时也讲解了用户认证和注册机制的实现。使用了微软的Entity Framework框架和LINQ查询技术。
系列的第二篇文章“异常管理”,提出了详细的异常管理规则并在ASP.NET MVC网站中实现异常管理,还提供一些通用的日志记录和异常管理的源代码。这些源代码不仅可以在任何ASP.NET网站中被重用(或经过比较小的改动适用),而且可以重用到任何.NET项目中。
系列的第三篇文章“动态布局和站点管理”,通过AJAX,jqGrid,自定义操作结果,控制器扩展,HTML帮助器和一些通用的C#源代码和JavaScript脚本实现了站点动态布局和站点管理,这些功能支持扩展和重用到其他项目中。
系列的第四篇(即本文),将详细介绍使用AJAX,JSON,jQuery,LINQ和序列化技术将jqGrid表格插件集成到MVC4网站中。
“MVC网站教程”系列的示例网站是采用增量式和迭代式软件过程开发的,这意味着系列中每一篇博文会在前一篇的解决方案中添加更多的功能,所以本文提供的示例下载只包含系列目前为止所介绍的功能。
本博文的第一部分介绍构件块的使用,第二部分介绍将jqGrid表格插件集成到MVC基础站点解决方案的步骤,使用到技术有AJAX,JSON,JavaScript和第一部分介绍的构件块。
软件环境
1. .NET 4.0 Framework
2. Visual Studio 2010 (or Express edition)
3. ASP.NET MVC 4.0
4. SQL Server 2008 R2 (or Express Edition version 10.50.2500.0)
在运行示例代码之前
在运行示例代码之前,你应该做下面事情:
1. 首先使用“管理员身份”运行CreateEventLogEntry控制台项目程序产生的exe,用来在事件日志中创建“MVC Basic”事件源。(EventLog在写日志时会创建指定名称的类别默认为“应用程序”的事件源。但是ASP.NET网站没有足够的权限来创建事件源,需要本地桌面应用程序)
2. 在你的SQL Server服务器中创建一个名为MvcBasicSite的数据库,然后用我提供的MvcBasicSiteDatabase.bak文件进行数据库还原。
3. 修改MVC应用程序示例的Web.config配置文件中的链接字符串。
示例帐号
1) 管理员帐户:Administrator 密码:tm77dac
2) 普通帐户: Ana 密码:ana
本博文示例下载:
1) MVC4网站中集成jqGrid表格插件-示例源代码.zip
2) MVC4网站中集成jqGrid表格插件-数据库.zip
jqGrid和AJAX
jqGrid是一个支持AJAX的开源的javascript控件,它提供在网页上显示和操作表格数据,并且支持通过AJAX回调函数动态的加载数据。
关于jqGrid控件的:
帮助文档详细请看:http://www.trirand.com
示例详细请看:http://www.trirand.com/blog/jqgrid/jqgrid.html
下面jqGrid插件包中重要的组件必须集成到网站应用程序中:
1) jquery.jqGrid.min.js:压缩格式的jqGrid表格插件脚本库。
2) jquery.jqGrid.src.js:jqGrid表格插件脚本库源码。
3) ui.jqgrid.css:jqGrid表格插件的css样式文件。
4) 一些命名为grid.local-XX.js特定语言文件。这里的XX是语言的两字符代码。
除了上面这些重要组件,我们还会在http://jqueryui.com/themeroller/下载一个或多个jQuery UI定制化主题。在当前解决方案中我使用的是Redmond(微软)定制版本的主题。
AJAX是异步的JavaScript和XML的缩写。AJAX不是单一的技术,而是一组相互关联用于创建异步web应用程序的开发技术。使用AJAX技术,web应用程序能异步发送数据和异步接收数据,异步方式不会影响当前页的显示和操作并且通过AJAX技术还能避免整个页面的刷新。
在下面图中,我使用UML图展示了“传统Web系统模型”和“AJAX Web系统模型”。
从“传统web系统模型”图中可知,每个用户界面端(eg:浏览器)的用户请求都会向web应用程序端(运行在web服务器)直接发送一个HTTP请求,在Web应用程序处理请求后响应一个HTML页面(HTTP+CSS)到浏览器,最后在浏览器中呈现。
MVC基础网站是基于AJAX的Web应用程序,当浏览器端发出一个用户请求,用户界面组件会触发一个JavaScript调用到AJAX Engine组件。这个引擎负责渲染用户界面和与服务端进行通信。AJAX技术允许用户与应用程序发生异步的交互---与服务器进行独立的通信。注意AJAX引擎给Web应用程序发送一个HTTP请求并且仅接收需要的数据(而不是整个HTML页面)。
GridSettings
这个主要类是用来存储jqGrid的设置信息并进行分页和排序。
如你在上面类图所见,这个类包含4个属性用于存储当前grid设置信息。
1) PageIndex:存储当前页索引(1表示第一页……);
2) PageSize:存储页尺寸(设置一个grid页面能加载的最大行数);
3) SortColumn:存储排序的列;
4) SortOrder:存储排序顺序(ASC或DESC);
这个类实现了Serializable接口,重写了ToString()方法将当前grid设置信息转化为string,并且提供了第二个构造函数用于从序列化数据创建一个新的GridSettings类实例。
下面源代码展示了本类的主要方法,用于使用当前grid设置信息以及从给定的数据源中加载数据。注意,这个数据源参数是使用LINQ创建的IQueryable查询。
public IQueryable<T> LoadGridData<T>(IQueryable<T> dataSource, out int count)
{
var query = dataSource;
//
// Sorting and Paging by using the current grid settings.
//
query = query.OrderBy<T>(this.SortColumn, this.SortOrder);
count = query.Count();
//
if (this.PageIndex < 1)
this.PageIndex = 1;
//
var data = query.Skip((this.PageIndex - 1) * this.PageSize).Take(this.PageSize);
return data;
}
从上面代码可知,LoadGridData是一个泛型方法,所以它能用于加载任何类型的数据并且通过out参数返回结果计数。首先使用grid设置信息将数据进行排序,然后计算数据源计数,最后仅从数据库中加载所需页面的数据并返回。
注意GridSettings类通过使用ModelBinder特性与GridModelBinder类进行关联。
GridModelBinder
ASP.NET MVC模型绑定通过引入一个抽象层简化了控制器操作,这个抽象层记录属性的隐射,与ASP.NET请求数据协作一起完成自动填充控制器操作参数以及类型转换。
GridModelBinder是我自定义的绑定模型,作用于GridSettings类,使用jqGrid参数来绑定其属性。
如上面类图所见,这个类只有一个命名为BindModel的方法,并且用于实现IModelBinder接口。
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
try
{
var request = controllerContext.HttpContext.Request;
return new GridSettings
{
PageIndex = int.Parse(request["page"] ?? "1"),
PageSize = int.Parse(request["rows"] ?? "20"),
SortColumn = request["sidx"] ?? "",
SortOrder = request["sord"] ?? "asc",
};
}
catch
{
//
// For unexpected errors use the default settings!
//
return null;
}
}
从上面源代码中可知,HTTP请求中的参数来自于jqGrid插件,通过JavaScript调用用于创建和初始化一个新的GridSettings对象,该对象用于做为控制器的模型。
LinqExtensions
这个类扩展了LINQ,用于简化排序过程。通过上述GridSettings类中的SortColumn和SortOrder属性。
如上面类图中可知,这个类只有一个静态的、命名为OrderBy的泛型方法。
public static IQueryable<T> OrderBy<T>(
this IQueryable<T> query, string sortColumn, string direction)
{
string methodName = string.Format("OrderBy{0}",
direction.ToLower() == "asc" ? "" : "descending");
ParameterExpression parameter = Expression.Parameter(query.ElementType, "p");
MemberExpression memberAccess = null;
//
foreach (var property in sortColumn.Split('.'))
{
memberAccess = MemberExpression.Property(memberAccess ?? (parameter as Expression), property);
}
//
LambdaExpression orderByLambda = Expression.Lambda(memberAccess, parameter);
MethodCallExpression result = Expression.Call(
typeof(Queryable),
methodName,
new[] { query.ElementType, memberAccess.Type },
query.Expression,
Expression.Quote(orderByLambda));
//
// For the sortColumn=='User.Username' and direction=="desc" ==>
// "OrderByDescending(p => p.User.Username)" expression
// will be appended to the input query!
//
return query.Provider.CreateQuery<T>(result);
}
上面代码,根据指定的排序列和排序方向创建了一个lambda表达式,然后将结果附加到指定的输入查询中。注意排序列可能是简单的像“ID”也可能是复杂的像“User.UserName(在数据实体之间使用导航属性)”
数据实体类和LINQ
用于本例的数据实体类命名为VisitorLog(置于Logic项目中),并且它包含了一系列方法用于从VisitorLogs数据库表中访问数据。
这个类的主要方法是用于读取访问日志数据并且最终使用jqGrid表格插件显示给用户。
public static IQueryable<VisitorLog> SearchLogByDates(MvcBasicSiteEntities dataContext, DateTime searchStartDate, DateTime searchEndDate)
{
if (searchStartDate.Date == DateTime.MinValue && searchEndDate.Date == DateTime.MinValue)
{
return dataContext.VisitorLogs.AsQueryable();
}
else if (searchStartDate.Date == DateTime.MinValue)
{
var searchResults = dataContext.VisitorLogs.Where(c =>
EntityFunctions.TruncateTime(c.StartDate) <= EntityFunctions.TruncateTime(searchEndDate));
//
return searchResults.AsQueryable();
}
else if (searchEndDate.Date == DateTime.MinValue)
{
var searchResults = dataContext.VisitorLogs.Where(c =>
EntityFunctions.TruncateTime(c.StartDate) >= EntityFunctions.TruncateTime(searchStartDate));
//
return searchResults.AsQueryable();
}
else
{
var searchResults = dataContext.VisitorLogs.Where(c =>
EntityFunctions.TruncateTime(c.StartDate) >= EntityFunctions.TruncateTime(searchStartDate) &&
EntityFunctions.TruncateTime(c.StartDate) <= EntityFunctions.TruncateTime(searchEndDate));
//
return searchResults.AsQueryable();
}
}
如上面源代码所示,这个方法根据给定的参数构建了4种可能的LINQ表达式:
1) 查询给定日期时间间隔内的所有实体,当日期参数都是不等于DateTime.MinValue的有效值时。
2) 查询所有实体,当日期参数都是无效值时。
3) 查询指定开始日期到今天内的所有实体,当只有开始日期是有效值时。
4) 查询截至到给定结束日期内的所有实体,当只有结束日期是有效值时。
在MVC中集成jqGrid表格插件
所有前文提到的构件块都是用于将jqGrid表格插件集成到MVC解决方案中的。为了查看集成后的结果,你必须打开管理区域的“Visitors”页面。(管理员帐号:Administrator,密码:tm77dac)
从上面截图中可知,这个grid表格拥有5个不同类型的列(字符串、日期时间、布尔值),并且所有列都支持排序。最后一列命名为“Actions”使用了我的ImageButton自定义HTML帮助器。当用户按下“Delete”图片按钮时,与之关联的访问日志条目将从数据库中删除。
在grid表格的底部有一个分页控制器,包括:当前页数,导航按钮,每一页显示的结果数。通过使用这些导航控件用户能浏览任意他想要的数据。
在grid表格的底部还有两个操作按钮:“Reload Grid”按钮(图片按钮)和“Delete All”按钮。
注意这个页面,通过使用国际化和本地化被实现为支持三种语言:English, Romanian (Română), German (Deutch),并且能扩展支持其他语言。同样这个页面的message和控制器(labels,buttons,tool tips等等)都支持多语言。
将jqGrid表格插件集成到MVC解决方案只需要下面四个步骤:
一、部分视图
在前面创建的构件块基础上,集成jqGrid表格插件的第一步是创建一个命名为_VisitorLogGrid.cshtml部分视图,这个视图是grid表格和分页控制的容器。
<table id="_visitorLogGrid" cellpadding="0" cellspacing="0">
</table>
<div id="_visitorLogPager" style="text-align: center;">
</div>
在上面HTML代码中定义的两个ID属性非常重要,因为它们将被用于下一步。
二、JavaScript
第二步是创建一个命名为VisitorLogGrid.js的脚本文件,并且放入Script文件夹中。
function showGrid() {
$('#_visitorLogGrid').jqGrid({
caption: paramFromView.Caption,
colNames: ['ID', paramFromView.VisitorName, paramFromView.StartDate,
paramFromView.EndDate, paramFromView.WasTimeOut, paramFromView.Actions],
colModel: [
{ name: 'ID', width: 1, hidden: true, key: true },
{ name: 'VisitorName', index: 'User.Username', width: 300 },
{ name: 'StartDate', index: 'StartDate', width: 150 },
{ name: 'EndDate', index: 'EndDate', width: 150 },
{ name: 'WasTimeOut', index: 'WasTimeOut', width: 120,
formatter: "checkbox", align: "center" },
{ name: 'Action', index: 'ID', width: 70, align: "center" }
],
hidegrid: false,
pager: jQuery('#_visitorLogPager'),
sortname: 'ID',
rowNum: paramFromView.PageSize,
rowList: [10, 20, 50, 100],
sortorder: "desc",
width: paramFromView.Width,
height: paramFromView.Height,
datatype: 'json',
caption: paramFromView.Caption,
viewrecords: true,
mtype: 'GET',
jsonReader: {
root: "rows",
page: "page",
total: "total",
records: "records",
repeatitems: false,
userdata: "userdata"
},
url: paramFromView.Url
}).navGrid('#_visitorLogPager', { view: false, del: false, add: false, edit: false, search: false },
{ width: 400 }, // default settings for edit
{}, // default settings for add
{}, // delete instead that del:false we need this
{}, // search options
{} /* view parameters*/
).navButtonAdd('#_visitorLogPager', {
caption: paramFromView.DeleteAllCaption, buttonimg: "", onClickButton: function () {
if (confirm(paramFromView.DeleteAllConfirmationMessage)) {
document.location = paramFromView.ClearGridUrl;
}
else {
$('#_visitorLogGrid').resetSelection();
}
}, position: "last"
});
}; $(document).ready(function () {
showGrid();
});
从上面JavaScript代码中可知,这里使用了document.ready事件用于显示表格当document加载完毕时。还有另一个主要的函数命名为showGrid,它定义了jqGrid表格的列、属性、分页和导航按钮。
注意在grid表格的标题、列、按钮和消息中使用的所有标签文本值都来自于资源文件(,以实现多语系),通过parameFormView对象进行传递。
这里有一些非常重要的细节,需要强调下:
1) _visitorLogGrid字符串是一个Id属性,在第一步的部分视图中定义并且用于创建grid表格。
2) _visitorLogPager字符串是一个Id属性,在第一步的部分视图中定义并且用于创建grid表格的分页控制和导航按钮(“Refresh”和“Delete All”)。
3) colNames属性用于定义grid表格中的所有列名(包括隐藏列)。
4) colModel属性用于定义列行为和与数据实体属性绑定的排序索引。
5) rowNum属性用于设置grid表格当前页尺寸(每页包含的行数),通过paramFromView对象的PageSize属性为其赋值。
6) width属性用于设置grid表格的宽度像素,通过paramFromView对象的Width属性为其赋值。
7) height属性用于设置grid表格的高度像素,通过paramFromView对象的Height属性为其赋值。
8) url设置一个URL链接,jqGrid通过AJAX技术使用该链接在服务器中获取数据,通过paramFromView对象的Url属性为其赋值。
9) Delete All按钮使用paramFromView对象的ClearGridUrl属性设置一个Url链接,用于执行“Delete All”操作。
10) JsonReader属性定义了从服务器中读取的JSON数据的键值对名字。
三、Razor视图
第三步是创建主页面,它将使用第二步中创建的JavaScript文件。这个页面是位于View\VisitorsLog文件夹下的Index.cshtml视图。
@using MvcBasicSite.Models.Grid;
@{
ViewBag.Title = Resources.Resource.VisitorLogIndexTitle;
Layout = "~/Views/Shared/_AdminLayout.cshtml";
//
int pageSize = 20;
if (Session["VisitorLogGridSettings"] != null)
{
//
// Get from cache the last page zise selected by the user.
//
GridSettings grid = new GridSettings((string)Session["VisitorLogGridSettings"]);
pageSize = grid.PageSize;
}
//
// Restore the last search params from cache.
//
string startDate = (Session["StartDate"] == null
? string.Empty
: ((DateTime)Session["StartDate"]).ToShortDateString());
string endDate = (Session["EndDate"] == null
? string.Empty
: ((DateTime)Session["EndDate"]).ToShortDateString());
}
<table>
<tr>
<td style="text-align: right; margin-top: 0px;">
@using (Ajax.BeginForm("Search", "VisitorLog",
new AjaxOptions
{
HttpMethod = "GET",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "jqGrid",
OnSuccess = "showGrid()"
}))
{
<table>
<tr>
<td>
<label>@Resources.Resource.VisitorLogIndexFrom</label>
<input type="text" id="from" name="from"
data-datepicker="true" value="@startDate" />
</td>
<td>
<label>@Resources.Resource.VisitorLogIndexTo</label>
<input type="text" id="to" name="to"
data-datepicker="true" value="@endDate" />
</td>
<td style="text-align: right; margin-top: 0px;">
<input type="submit" name="_search"
value="@Resources.Resource.VisitorLogApplyFilter"
class="searchButton" />
</td>
</tr>
</table>
}
</td>
</tr>
</table>
<div id="jqGrid">
@Html.Partial("_VisitorLogGrid")
</div>
<script type="text/javascript">
var paramFromView = {
DeleteAllCaption: '@Resources.Resource.VisitorLogDeleteAllCaption',
ClearGridUrl: '@Url.Content("~/VisitorLog/ClearGridData")',
DeleteAllConfirmationMessage: '@Resources.Resource.VisitorLogDeleteAllDataConfirmation',
Url: '@Url.Content("~/VisitorLog/GetData")',
Width: 790,
Height: 464,
Caption: '@Resources.Resource.VisitorLogIndexTitle',
VisitorName: '@Resources.Resource.VisitorLogIndexVisitorName',
StartDate: '@Resources.Resource.VisitorLogIndexStartDate',
EndDate: '@Resources.Resource.VisitorLogIndexEndDate',
WasTimeOut: '@Resources.Resource.VisitorLogIndexWasTimeOut',
Actions: '@Resources.Resource.VisitorLogIndexActions',
PageSize: @pageSize
}
</script>
@section scripts
{
@Content.Script("VisitorLogGrid.js", Url)
}
在上面razor视图的第一部分中,我设置了页面Title和Layout,并且从HTTP会话中获取最后一次设置的GridSettings对象和查询参数(开始日期和结束日期)用于当前页。
然后,使用一个table通过设置开始日期和结束日期过滤结果集。注意,这个AJAX表单使用VisitorLog控制器的Search操作仅仅获取需要的数据,在操作成功时调用javascript的showGrid函数,并且只有页面中包含grid表格的部分视图被更新。
在这一步中,呈现grid表格的部分视图和JavaScript脚本代码通过使用Razor块:@Html.Partial("_VisitorLogGrid") 和 @Content.Script("VisitorLogGrid.js", Url) 引入。
在视图的最后一部分,是一块内联的JavaScript脚本代码,它创建并且初始化一个paramFromView对象,该对象用于第二步中的javaScript脚本代码。通过这种方式,与最后一次GridSettings对象动态通信并且所有的标签值、按钮文本和消息信息都是从资源文件中读取(基于当前选择的语言)。注意下面两个URL链接设置,关联了对应的控制器操作。
1) VisitorLog/GetData:获取grid表格数据。
2) VisitorLog/ClearGridData:清除满足当前过滤设置的所有匹配数据。
四、控制器
现在来查看VisitorLogController控制器类,分析实现第三步中视图使用到的操作。
jqGrid表格使用AJAX加载数据所调用的主要方法如下:
public JsonResult GetData(GridSettings grid)
{
if (_fromIndex && Session["VisitorLogGridSettings"] != null)
{
//
// Get grid settings from cache!
//
grid = new GridSettings((str ing)Session["VisitorLogGridSettings"]);
}
//
_fromIndex = false; // Must be set on false here!
//
// Load the data from the database for the current grid settings.
//
DateTime searchStartDate = (Session["StartDate"] == null ?
DateTime.MinValue : (DateTime)Session["StartDate"]);
DateTime searchEndDate = (Session["EndDate"] == null ?
DateTime.MinValue : (DateTime)Session["EndDate"]);
int count;
var query = grid.LoadGridData<VisitorLog>(
VisitorLog.SearchLogByDates(_db, searchStartDate, searchEndDate), out count);
var data = query.ToArray();
//
// Convert the results in JSON jqGrid format.
//
string gridSettingsString = grid.ToString(); // Used to preserve grid settings!
Session["VisitorLogGridSettings"] = gridSettingsString;
gridSettingsString = null;
var result = new
{
total = (int)Math.Ceiling((double)count / grid.PageSize),
page = grid.PageIndex,
records = count,
rows = (from host in data
select new
{
ID = host.ID,
VisitorName = GetVisitorNameForBinding(host),
StartDate = host.StartDate.ToString(),
EndDate = host.EndDate.ToString(),
WasTimeOut = (host.WasTimeOut ?? false),
Action = string.Format("{0}",
RenderHelpers.ImageButton(
this,
Resources.Resource.DeleteTip,
"~/Content/Images/Delete.gif",
"Delete",
new { id = host.ID },
new { style = "border:0px;" }))
}).ToArray()
};
//
// Convert to JsonResult before to return.
//
return Json(result, JsonRequestBehavior.AllowGet);
}
在上面代码中,你能看到GridSettings对象,这个对象做为方法参数通过我们自定义的GridModelBinder机制使用页面上用户的设置进行赋值,用于分页和排序。然后这个参数对象或者是从缓存数据中创建的GridSettings对象(当从别的页面返回该页时,从缓存中取)用于根据当前过滤设置加载数据并且用于为当前页面创建行数据。最后结果数据被转化为JSON格式并返回。
JSON或JavaScript对象表示法,是设计为基于文本的数据交换开放标准协议。它来源于JavaScript脚本语言为了提供简单的数据结构和关联数组,但又是一个完全独立于语言的文本格式。
注意,上面代码中创建了一个命名为result的JSON对象,它拥有四个属性(total,page,records和rows)与在第二步的JavaScript代码中定义的命名相同。rows属性包含了用于grid表格当前页的行数据,并且JSON对象中的每一行所包含的属性必须在第二步中jqGrid表格的colModel属性中定义。
在下面截图中,你能看到Visitors(Index)页面,当用户指定开始日期和结束日期(通过data picker时间控件)触发控制器操作返回过滤后的结果集。
当用户按下“Apply Filter”按钮后,将会通过AJAX方式调用下面方法,根据用户指定的开始日期和结束日期查询访问日志。
public PartialViewResult Search()
{
string startDate = Request["from"];
string endDate = Request["to"];
//
// Cache the start and end dates into the session to be used by later one in the view.
//
Session["StartDate"] = (startDate.Length < 1 ? null :
(object)DateTime.Parse(startDate, Thread.CurrentThread.CurrentCulture));
Session["EndDate"] = (endDate.Length < 1 ? null :
(object)DateTime.Parse(endDate, Thread.CurrentThread.CurrentCulture));
//
return PartialView("_VisitorLogGrid");
}
上面代码,从request对象中获取参数,然后将其值缓存到session会话中待返回到视图中使用,并且最后将_VisitorLogGrid部分试图呈现给用户。
用户调用的第三个方法,根据当前过滤条件删除访问日志。
public ActionResult ClearGridData()
{
DateTime searchStartDate = (Session["StartDate"] == null ?
DateTime.MinValue : (DateTime)Session["StartDate"]);
DateTime searchEndDate = (Session["EndDate"] == null ?
DateTime.MinValue : (DateTime)Session["EndDate"]);
VisitorLog.DeleteLogByDates(_db, searchStartDate, searchEndDate);
//
return RedirectToAction("Index");
}
上面代码,使用当前session会话中缓存的开始日期和结束日期参数删除数据库中匹配返回数据,然后重定向到Index页面。
通过以上四步,即可将jqGrid插件引入到MVC项目中,赶紧试试吧……
终于把这个小系列翻译完了,觉得还不错的,多帮推荐推荐……
原文:http://www.codeproject.com/Articles/594150/MVC-Basic-Site-Step-jqGrid-In
作者:Raul Iloc