【IT168 技术】奥巴马说过,“如果我们总是试图等待他人来改变命运,那么这种改变是永远不会发生的,因为命运掌握在自己手中。”作为读者,无论您身怀何种政治信仰,改变无处不在。它不仅体现在政治,经济全球化,气候变暖,甚至是软件开发都产生了一系列深刻的变化。
对于从事软件开发的您来说,改变不是一件坏事,反而是一件好事。毕竟,正是软件的日益变化才使得钞票流入了口袋,弄得你忙得不可开交,不是吗?当然,作为代价,作为工程师的你,必须能时刻适应这种变化的节奏,这不是件容易的事啊。
本文的出发点,是带您熟悉并掌握一种新型的微软MVC框架。这种MVC框架融合了两大JavaScrip库JQuery与Knockout,以及一个标准数据交换格式JSON等多项技术。为便于您的理解,我们给出了一个经典的Web App样例的代码。
关于示例程序
本文的示例WebApp采用的是微软MVC ASP.NET框架的最新版本——MVC4。WebApp的名称为Order Entry,关联的数据库是著名的Northwind SQL-Server(创建数据库的脚本可在本文的代码区下载)。
程序架构
示例Web App的架构设计目标是:实现Model与MVC (Model-View-Controller)其余部分的分离。Web App只包含View与Controller两个组件,另一个组件Model则被移至应用服务(application service)层。应用服务层的底层实现技术之一是基于Windows Communication Foundation (WCF)框架的Web Service。这种设计模式特别适用于“不同前后端应用共享同一业务组件”的SOA架构。
“点击按钮”的背后玄机
您可能有所不知,简单的鼠标点击操作,背后其实暗藏玄机。事实上,示例Web App的大部分按钮点击操作均会触发若干的JQuery AJAX调用,并返回若干JSON对象。这些JSON对象包含:一或多个MVC Partial视图,一个View Model对象。
轻量级Controller
示例Web App的Controller均是轻量级的,除了在View与前面提及的应用服务层做转发操作或Middleman,什么都不做。换句话说,Controller要做的仅仅是,接收来自View的用户表单数据,然后与某个View Model绑定。紧接着,Controller将View Model对象传递到应用服务层,并调用独立于MVC之外的业务组件,完成操作。待调用完成之后,Controller要做的即是,将View Model对象返回给View。
轻量级Controller带来了程序的易于测试,重用性好,关注分离等诸多好处。
面对MVC的诱惑,还不赶紧创建自己的DataGrid控件?
作为本文的示例Web App,Northwind Order Entry的第一步是从一个data grid中选取一个customer。MVC兼容不同的data grid控件,例如Telerik, JQuery,及其它各种开源库。这些并不重要,重要的MVC框架内置一个称为Razor的强大View引擎,可以灵活自如地开发各种不同的网页。Razor的特点是:实用、精准、轻量级。ASP.NET MVC工程通过Razor创建View,具有关注分离,易于测试,基于模式开发等一系列优点。
说了这么多,您会不会按捺不住,跃跃欲试呢?来,弄上几个Data Grid吧。基本上,要创建一个data grid,只需要渲染一个HTML表格,注入一些JavaScript以及一些隐藏的HTML控件,就搞定啦。示例Web App使用的是一个用户自定义的Data Grid,支持数据的分页,排序,筛选。
Customer Inquiry View – 分页,排序,选择
加载Customer Inquiry View时,调用一个JavaScript函数CustomerInquiry 。CustomerInquiry 利用JQuery AJAX函数最终调用另一个MVC Controller函数。MVC Controller函数会返回一个Partial 视图,并将Data Grid控件渲染到页面。
function CustomerInquiryRequest() {
this.CurrentPageNumber;
this.PageSize;
this.CustomerID;
this.CompanyName;
this.ContactName;
this.SortDirection;
this.SortExpression;
};
function CustomerInquiry(currentPageNumber, sortExpression, sortDirection) {
var url = "/Orders/CustomerInquiry";
var customerInquiryRequest = new CustomerInquiryRequest();
customerInquiryRequest.CustomerID = $("#CustomerID").val();
customerInquiryRequest.CompanyName = $("#CompanyName").val();
customerInquiryRequest.ContactName = $("#ContactName").val();
customerInquiryRequest.CurrentPageNumber = currentPageNumber;
customerInquiryRequest.SortDirection = sortDirection;
customerInquiryRequest.SortExpression = sortExpression;
customerInquiryRequest.PageSize = 15;
$.post(url, customerInquiryRequest, function (data, textStatus) {
CustomerInquiryComplete(data);
});
};
function CustomerInquiryComplete(result) {
if (result.ReturnStatus == true) {
$("#CustomerResults").html(result.CustomerInquiryView);
$("#MessageBox").html("");
}
else {
$("#MessageBox").html(result.MessageBoxView);
}
}
</script>
function CustomerInquiryRequest() {
this.CurrentPageNumber;
this.PageSize;
this.CustomerID;
this.CompanyName;
this.ContactName;
this.SortDirection;
this.SortExpression;
};
function CustomerInquiry(currentPageNumber, sortExpression, sortDirection) {
var url = "/Orders/CustomerInquiry";
var customerInquiryRequest = new CustomerInquiryRequest();
customerInquiryRequest.CustomerID = $("#CustomerID").val();
customerInquiryRequest.CompanyName = $("#CompanyName").val();
customerInquiryRequest.ContactName = $("#ContactName").val();
customerInquiryRequest.CurrentPageNumber = currentPageNumber;
customerInquiryRequest.SortDirection = sortDirection;
customerInquiryRequest.SortExpression = sortExpression;
customerInquiryRequest.PageSize = 15;
$.post(url, customerInquiryRequest, function (data, textStatus) {
CustomerInquiryComplete(data);
});
};
function CustomerInquiryComplete(result) {
if (result.ReturnStatus == true) {
$("#CustomerResults").html(result.CustomerInquiryView);
$("#MessageBox").html("");
}
else {
$("#MessageBox").html(result.MessageBoxView);
}
}
Customer Inquiry Controller函数
Controller函数签名多种多样,其中之一是单独定义每一个函数参数。MVC框架按名称自动填充参数值。下面的Controller函数定义则采取了完全不同的一种方法,即采用FormCollection 数组解析法。早在经典的ASP时代, Form Collection数组就已经成为了HTTP表单数据解析技术的基础。我们之所以采用Form Collection数组这种方法,是因为即便表单数据与Controller函数签名不完全匹配,MVC也总能解决问题。
/// Customer Inquiry
/// </summary>
/// <param name="postedFormData"></param>
/// <returns></returns>
public ActionResult CustomerInquiry(FormCollection postedFormData)
{
CustomerApplicationService customerApplicationService = new CustomerApplicationService();
CustomerViewModel customerViewModel = new CustomerViewModel();
customerViewModel.PageSize = Convert.ToInt32(postedFormData["PageSize"]);
customerViewModel.SortExpression = Convert.ToString(postedFormData["SortExpression"]);
customerViewModel.SortDirection = Convert.ToString(postedFormData["SortDirection"]);
customerViewModel.CurrentPageNumber = Convert.ToInt32(postedFormData["PageNumber"]);
customerViewModel.Customer.CustomerID = Convert.ToString(postedFormData["CustomerID"]);
customerViewModel.Customer.CompanyName = Convert.ToString(postedFormData["CompanyName"])
customerViewModel.Customer.ContactName = Convert.ToString(postedFormData["ContactName"]);
customerViewModel = customerApplicationService.CustomerInquiry(customerViewModel);
return Json(new
{
ReturnStatus = customerViewModel.ReturnStatus,
ViewModel = customerViewModel,
MessageBoxView = RenderPartialView(this,"_MessageBox", customerViewModel),
CustomerInquiryView = RenderPartialView(this, "CustomerInquiryGrid", customerViewModel)
});
}
View Model
Controller函数Customer Inquiry 调用应用服务层,返回一个表示customer data的Customer View Model。MVC Model指的是一系列表示后端数据的类,而MVC View通常需要收集不同的后端数据,此时就有了View Model类的概念,方便View 获取所有Model信息。从这个角度将,View Model是一个front-end类,连接前端的UI与后端数据通信。
/// Customer View Model
/// </summary>
public class CustomerViewModel : ViewInformation
{
public List<Customer> Customers;
public Customer Customer;
public int TotalCustomers { get; set; }
public CustomerViewModel()
{
Customer = new Customer();
Customers = new List<Customer>();
ReturnMessage = new List<String>();
ValidationErrors = new Hashtable();
TotalCustomers = 0;
}
}
/// <summary>
/// Order View Model
/// </summary>
public class OrderViewModel : ViewInformation
{
public Orders Order;
public OrderDetails OrderDetail;
public List<Orders> Orders;
public List<OrdersCustomer> OrderCustomer;
public List<OrderDetailsProducts> OrderDetailsProducts;
public OrderDetailsProducts OrderLineItem;
public List<OrderDetails> OrderDetails;
public List<Shippers> Shippers;
public Customer Customer;
public int TotalOrders { get; set; }
public OrderViewModel()
{
Customer = new Customer();
Order = new Orders();
OrderDetail = new OrderDetails();
Orders = new List<Orders>();
OrderDetails = new List<OrderDetails>();
OrderCustomer = new List<OrdersCustomer>();
Shippers = new List<Shippers>();
OrderDetailsProducts = new List<OrderDetailsProducts>();
OrderLineItem = new OrderDetailsProducts();
ReturnMessage = new List<String>();
ValidationErrors = new Hashtable();
TotalOrders = 0;
}
}
SQL-Server的数据分页
从后端SQL的角度,要从数据路SQL-Server返回一页数据,可使用ROW_NUMBER OVER语法,配合Record的开始与结束游标。相比于因应用程序调用而返回大量的Recordset,SQL的效率要高许多。
SELECT (ROW_NUMBER() OVER (ORDER BY CompanyName ASC)) as record_number,
CustomerID, CompanyName, ContactName, ContactTitle, City, Region
FROM Customers ) Rows where record_number between 16 and 30
强大的JSON与Partial View
JSON (JavaScript Object Notation)是一个轻量级的数据交互格式。如上所述,Customer Inquiry Conroller函数调用应用服务层,以View Model的形式获取数据。
Conroller函数返回给客户端页面的是一个JSON对象。此JSON对象既包含如上所述的View Model,也包含Partial View对应的Data Grid控件经过渲染产生的HTML。MVC的真正威力之处在于,能渲染Partial View的局部HTML片段。
Partial View渲染小帮手
ASP.NET MVC框架包含各种不同的Helper函数,可轻松实现在视图中渲染HTML,例如创建按钮,文本框,超链接,表单。Helper函数的使用方式有两种,一是扩展已有的MVC Helper方法,二是也可以根据业务需要,自己定制。
这里,我们创建一个RenderPartialView Helper函数,调用Partial View并将调用结果以字符串的形式输出。之后,该字符串会被封装成一个JSON对象,供AJAX调用。该Helper函数调用Razor View Engine,在服务器端运行Partial View。当我们需要将HTML返回给AJAX调用时,这一点显得尤为适用。
string viewName, object model)
{
if (string.IsNullOrEmpty(viewName))
return null;
controller.ViewData.Model = model;
using (var sw = new StringWriter())
{
ViewEngineResult viewResult =
ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName);
var viewContext = new ViewContext(controller.ControllerContext,
viewResult.View, controller.ViewData, controller.TempData, sw);
viewResult.View.Render(viewContext, sw);
return sw.GetStringBuilder().ToString();
}
}
Partial View “Customer Inquiry Grid”
View与Partial View内部均含有服务器/客户端代码。两者在外观上与使用感觉上,都有经典ASP功能的味道。下面的Customer Inquiry Grid Partial View仅包含服务器端代码,目的是渲染一个用户自定义的Data Grid。
@using NorthwindWebApplication.Helpers;
@{
NorthwindDataGrid pagedDataGrid = new NorthwindDataGrid("CustomerInquirGrid");
pagedDataGrid.Title = "Customers";
pagedDataGrid.TotalPages = Model.TotalPages;
pagedDataGrid.TotalRecords = Model.TotalCustomers;
pagedDataGrid.CurrentPageNumber = Model.CurrentPageNumber;
pagedDataGrid.SortDirection = Model.SortDirection;
pagedDataGrid.SortExpression = Model.SortExpression;
pagedDataGrid.RowSelectionFunction = "CustomerSelected";
pagedDataGrid.AjaxFunction = "CustomerInquiry";
pagedDataGrid.AddColumn("CustomerID", "Customer ID", "20%", "left");
pagedDataGrid.AddColumn("CompanyName", "Company Name", "40%", "left");
pagedDataGrid.AddColumn("ContactName", "Contact Name", "20%", "left");
pagedDataGrid.AddColumn("City", "City", "20%", "left");
foreach (var item in Model.Customers)
{
pagedDataGrid.AddRow();
pagedDataGrid.PopulateRow("CustomerID", item.CustomerID , true);
pagedDataGrid.PopulateRow("CompanyName", item.CompanyName, false);
pagedDataGrid.PopulateRow("ContactName", item.ContactName, false);
pagedDataGrid.PopulateRow("City", item.City, false);
pagedDataGrid.InsertRow();
}
}
@Html.RenderNorthwindDataGrid(pagedDataGrid)
RenderNorthwindDataGrid 函数调用一个MVC HtmlHelper对象,返回一个MvcHtmlString。Data Grid的渲染就变得与其它HTML控件一样。
NorthwindWebControls.NorthwindDataGrid dataGrid)
{
string control = dataGrid.CreateControl();
return MvcHtmlString.Create(control);
}
下面Partial View的名称为MessageBox ,包含Razor View Engine语法的客户端/服务器端代码。这个MessageBox 会贯穿整个示例Web App,为客户端渲染状态与错误信息。
@{
ViewInformation viewInformation = new NorthwindViewModel.ViewInformation();
viewInformation.ReturnMessage = Model.ReturnMessage;
viewInformation.ReturnStatus = Model.ReturnStatus;
if (viewInformation.ReturnMessage.Count() > 0)
{
<div style="padding: 10px 10px 10px 0px; width:90%">
@if (viewInformation.ReturnStatus == true)
{
<div style="background-color: Scrollbar;
border: solid 1px black; color: black; padding: 15px 15px 15px 15px">
@foreach (var message in viewInformation.ReturnMessage)
{
<text>@Html.Raw(message)</text>
<br />
}
</div>
}
else
{
// ====== an error has occurred - Display the message box in red ======
<div style="background-color: #f4eded; border:
solid 1px #d19090; color: #762933; padding: 15px 15px 15px 15px">
@foreach (var message in viewInformation.ReturnMessage)
{
<text>@Html.Raw(message)</text>
<br />
}
</div>
}
</div>
}
}
渲染Customer Inquiry DataGrid
CustomerInquiry Controller函数调用返回一个JSON对象,客户端JavaScript函数CustomerInquiryComplete函数调用并解析该JSON对象,得到返回状态值,使用JQuery将返回的Data Grid更新DIV标签。
如果服务器发生错误,页面会渲染前面提到的Partial View —— Message Box。这个过程实际上是一个AJAX调用,利用MVC的灵活控制性,渲染局部页面内容。
{
if (result.ReturnStatus == true)
{
$("#CustomerResults").html(result.CustomerInquiryView);
$("#MessageBox").html("");
}
else
{
$("#MessageBox").html(result.MessageBoxView);
}
}
选择一个Customer
当选择一个下订单的客户(Customer ID域位于Customer Inquiry grid)时,调用JavaScript函数theCustomerSelected,将customer ID传到表单对象,然后使用POST方法提交至服务器。之所以采用POST而非GET方法,是处于安全考虑。
function CustomerSelected(customerID) {
$("#OrderEntry #CustomerID").val(customerID);
$("#OrderEntry").submit();
}
</script>
<form id="OrderEntry" method="post" action="/Orders/OrderEntry">
<input id="CustomerID" name="CustomerID" type="hidden" />
</form>
Order Entry Header View
选定customer后,OrderEntryHeader View被渲染,让用户填入订单的物流信息。OrderEntryHeader View的功能由Knockout控制。