使用 jQuery、JSON、Knockout 和 C# 进行 MVC 技术






4.94/5 (138投票s)
开发一个带有 MVC 的订单录入应用程序。
引言
“改变不会到来,如果我们等待别人,或者等待另一个时间。我们就是我们一直在等待的人。我们就是我们所寻求的改变。” - 巴拉克·奥巴马。无论您的政治信仰如何,改变无处不在。政治的改变、全球经济的改变、气候的改变。软件开发也是如此。如果您是一名软件开发人员,改变是好事。改变能给我们带来收入,并让我们保持忙碌。挑战在于跟上软件开发技术变革的步伐。本文重点介绍 Microsoft 的 MVC Web 开发框架,该框架结合了使用最新的 JavaScript 库 jQuery 和 Knockout 的开发技术,同时结合 JSON 对象表示法进行数据交换。
示例应用
本文的示例 Web 应用程序使用 Microsoft 的 MVC ASP.NET 框架的最新版本,即 MVC 4。本文将介绍一个使用举世闻名的 Northwind SQL Server 数据库的示例订单录入应用程序(创建数据库的脚本包含在本文的下载源代码中)。
架构目标
此示例应用程序的架构目标是将模型与 MVC(模型-视图-控制器)Web 应用程序的其他部分分离。此示例 MVC Web 应用程序仅包含视图和控制器。控制器和视图将通过视图模型类(稍后讨论)进行通信。模型将位于应用程序服务层之后。应用程序服务层可以选择使用 Web 服务技术(如 Windows Communication Foundation (WCF))来实现。如果您希望设计一种服务导向架构 (SOA),并且可能需要多个不同的前端应用程序访问相同的业务组件,这将非常有用。
按钮点击
此应用程序中的大多数按钮点击都将执行 jQuery AJAX 调用,返回包含一个或多个 MVC 部分视图和视图模型对象的 JSON 对象。
精简的控制器
此应用程序的控制器将是精简的。精简意味着控制器类将仅仅是视图和应用程序服务层之间的传递者或中间人。控制器将仅接收来自视图的已发布表单数据,并将其绑定到视图模型。
控制器将视图模型传递到应用程序服务层,并执行将驻留在 MVC 项目外部的业务组件。然后,控制器将简单地将视图模型返回到视图。精简的控制器可以提高应用程序的可测试性、可重用性,并促进关注点分离。
MVC 的诱惑 – 自己构建数据网格控件
Northwind 订单录入应用程序的第一步是从数据网格中选择一个客户。市面上有各种有用的 MVC 数据网格控件,包括 Telerik、jQuery 以及其他开源库的数据网格。但是 MVC 框架的强大之处在于,它可以通过其强大的视图引擎让您完全控制网页上渲染的内容。
MVC 自带一个名为 Razor 的内置视图引擎。Razor 视图引擎是一种精确、有用、轻量级的语言,使您能够在 ASP.NET 中为 MVC 项目创建视图,同时保持关注点分离、可测试性和基于模式的开发。
随着您对 MVC 及其控制力的熟悉程度不断提高,您会产生自己构建一些控件的冲动。基本上,数据网格只需要渲染一个 HTML 表,并注入一些带有隐藏 HTML 控件的 JavaScript。本示例应用程序中的数据网格是一个自定义构建的数据网格,支持分页和排序以及数据选择。
客户查询视图 – 分页、排序和选择
当上面的客户查询视图加载时,页面内的 JavaScript 函数 `CustomerInquiry` 被调用,该函数执行一个 jQuery AJAX 调用到 MVC 控制器方法,该方法返回一个部分视图,该视图将数据网格渲染回页面。
<script language="javascript" type="text/javascript">
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>
客户查询控制器方法
在编码控制器方法的签名时,您有多种选择。您可以单独定义每个参数,并让 MVC 自动根据显式名称填充每个参数的值。下面的控制器方法采用了不同的方法。在下面的示例中,我选择使用一个可以解析的 `FormCollection` 数组。自经典 ASP 时代以来,FormCollection 一直是所有 HTTP 表单发布的基石。使用 FormCollection 数组将允许 MVC 始终找到您的方法,即使您的已发布表单数据与控制器方法签名不匹配。
/// <summary>
/// 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)
});
}
视图模型
客户查询控制器方法调用应用程序服务,该服务返回一个包含客户数据的客户视图模型。在 MVC 中,模型被定义为一组定义后端数据的类。MVC 中的视图经常需要来自各种后端数据的信息。为了收集所有这些信息,需要一个单独的视图模型类。视图模型类是一个可以与用户界面通信的前端类。
/// <summary>
/// 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 代码中,通过使用 `ROW_NUMBER OVER` 语法并指定开始和结束记录号,可以实现从 SQL Server 返回一页数据。这比将大型记录集返回到应用程序层更有效。
SELECT * FROM (
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 和部分视图的力量
JSON(JavaScript 对象表示法)是一种轻量级的数据交换格式。客户查询控制器方法从应用程序服务层以视图模型的形式获取数据后,控制器方法将返回到客户端网页的 JSON 对象,该对象包含视图模型数据和来自部分视图的数据网格的已渲染 HTML。MVC 的真正强大之处在于能够渲染部分视图中包含的小块 HTML。
渲染部分视图辅助方法
ASP.NET MVC 框架包含多个辅助方法,可方便地在视图中渲染 HTML,用于创建按钮、文本框、链接和表单等。您可以扩展 MVC 中内置的现有辅助函数,也可以为各种需求创建自己的辅助函数。
下面的自定义 `RenderPartialView` 辅助函数执行部分视图,并将部分视图的输出作为字符串返回。生成的字符串可以打包在 JSON 对象中,然后返回给 AJAX 调用。此辅助函数调用 Razor 视图引擎在服务器端执行部分视图。当您想将 HTML 返回给 AJAX 调用时,这非常有用。
public static string RenderPartialView(this Controller controller,
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();
}
}
客户查询网格部分视图
视图和部分视图都可以在其中包含服务器端和客户端代码。视图和部分视图具有旧的经典 ASP 功能的外观和感觉。下面的客户查询网格部分视图仅包含服务器端代码,该代码构建并渲染我的自定义自制数据网格。
@model NorthwindViewModel.CustomerViewModel
@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`,以便网格可以像任何其他 HTML 控件一样渲染。
public static MvcHtmlString RenderNorthwindDataGrid(this HtmlHelper html,
NorthwindWebControls.NorthwindDataGrid dataGrid)
{
string control = dataGrid.CreateControl();
return MvcHtmlString.Create(control);
}
下面的 `MessageBox` 部分视图包含服务器端和客户端代码,使用 Razor 视图引擎语法。此 `MessageBox` 部分视图将在整个示例应用程序中用于将状态和错误信息渲染回客户端。
@model NorthwindViewModel.ViewInformation
@{
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>
}
}
客户查询数据网格渲染
当 `CustomerInquiry` 控制器方法完成时,客户端 JavaScript 函数 `CustomerInquiryComplete` 将执行,解析控制器返回的 JSON 对象,检查返回状态,并使用 jQuery 更新带有返回数据网格的 `DIV` 标签。
如果服务器上发生错误,则会在页面上渲染消息框部分视图。此功能是执行 AJAX 调用和渲染部分页面内容以及在 MVC 中拥有完全控制权的本质。
function CustomerInquiryComplete(result)
{
if (result.ReturnStatus == true)
{
$("#CustomerResults").html(result.CustomerInquiryView);
$("#MessageBox").html("");
}
else
{
$("#MessageBox").html(result.MessageBoxView);
}
}
选择客户
在选择要订购的客户(客户查询网格上的客户 ID 字段)时,将执行 `CustomerSelected` JavaScript 函数,将选定的客户 ID 传递到表单对象,然后使用表单 POST 方法提交到服务器。将 POST 方法而不是 GET 方法用于所有控制器调用,将允许您保护您的控制器方法免遭直接访问。
<script language="javascript" type="text/javascript">
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>
订单录入头部视图
选择客户后,将渲染 `OrderEntryHeader` 视图,并允许您输入订单的发货信息。`OrderEntryHeader` 视图使用 Knockout 来控制页面功能。
Knockout – MVC 与 MVVM 相遇
Knockout(也称为 _Knockout.js_ 和 KnockoutJS)是一个开源 JavaScript 库,可在 www.knockoutjs.com 上找到,它允许您使用简洁、可读的语法轻松地将 DOM 元素与模型数据关联起来,并在数据模型的状态发生变化时自动刷新您的 UI。Knockout 遵循模型-视图-视图模型 (MVVM) 设计模式,以简化动态 JavaScript 用户界面。最终的结果是分离了 JavaScript 和 UI HTML 表示之间的关注点。使用 Knockout,您可以编写不直接引用 Web 页面中的 UI 元素和文档对象模型 (DOM) 的 JavaScript。
Knockout 设计用于允许您使用任意 JavaScript 对象作为视图模型。只要您的视图模型的属性是可观察的,您就可以使用 Knockout 将它们绑定到您的 UI,并且当可观察属性发生变化时,UI 将自动更新。
订单录入头部 - 编辑模式和显示模式
订单头部页面实现的关键功能是能够在不重新发布整个页面的情况下将页面置于编辑模式或显示模式。在 ASP.NET 的发布回模型中,当用户按下编辑按钮时,页面通常会发布回服务器,并且页面会被完全重新显示。借助 Knockout 及其 MVVM 数据绑定功能,这不再需要。您所要做的就是绑定到通过 JavaScript 创建的视图模型。
数据绑定标签
要创建可以在显示模式和编辑模式之间切换的 MVC 视图,您可以为页面上的每个元素创建单独的 `DIV` 和 `SPAN` 标签,一个用于包含 `INPUT` HTML 控件,另一个用于仅显示文本。向对象添加 Knockout `data-bind` 标签可以轻松控制何时自动显示或隐藏元素给用户。在下面的示例中,`ShipName` 包含一个数据绑定标签用于船名值,以及一个布尔数据绑定标签用于确定元素是在仅显示模式还是编辑模式。
<div style="float:left; width:150px; height:25px; text-align:right;"
class="field-label">Ship To Name:
</div>
<div style="float:left; width:300px; height:25px;">
<span data-bind="visible:EditFields">
@Html.TextBox("ShipName", @Model.Order.ShipName, new Dictionary<string, object> {
{ "data-bind", "value: ShipName" }, { "style", "width:300px" } })
</span>
<span data-bind="visible: ReadOnlyMode, text: OriginalShipName"></span>
</div>
订单录入显示模式
当最初选择订单进行编辑时,页面处于仅显示模式。要设置 Knockout 以自动绑定到您的 HTML 对象,您必须在 JavaScript 中设置一个视图模型对象,该对象创建可观察的绑定供 Knockout 侦听并自动更新 UI。
// Overall viewmodel for this screen, along with initial state
var viewModel = {
EditFields: ko.observable(false),
ReadOnlyMode: ko.observable(false),
DisplayCreateOrderButton: ko.observable(false),
DisplayEditOrderButton: ko.observable(false),
DisplayUpdateOrderButton: ko.observable(false),
DisplayOrderDetailsButton: ko.observable(false),
DisplayCancelChangesButton: ko.observable(true),
SelectedShipVia: ko.observable($("#OriginalShipVia").val()),
Shippers: ko.observableArray(shippers),
OrderID: ko.observable($("#OrderID").val()),
ShipperName: ko.observable($("#ShipperName").val()),
CustomerID: ko.observable($("#CustomerID").val()),
OriginalShipName: ko.observable($("#OriginalShipName").val()),
OriginalShipAddress: ko.observable($("#OriginalShipAddress").val()),
OriginalShipCity: ko.observable($("#OriginalShipCity").val()),
OriginalShipRegion: ko.observable($("#OriginalShipRegion").val()),
OriginalShipPostalCode: ko.observable($("#OriginalShipPostalCode").val()),
OriginalShipCountry: ko.observable($("#OriginalShipCountry").val()),
OriginalRequiredDate: ko.observable($("#OriginalRequiredDate").val()),
OriginalShipVia: ko.observable($("#OriginalShipVia").val()),
ShipName: ko.observable($("#OriginalShipName").val()),
ShipAddress: ko.observable($("#OriginalShipAddress").val()),
ShipCity: ko.observable($("#OriginalShipCity").val()),
ShipRegion: ko.observable($("#OriginalShipRegion").val()),
ShipPostalCode: ko.observable($("#OriginalShipPostalCode").val()),
ShipCountry: ko.observable($("#OriginalShipCountry").val()),
RequiredDate: ko.observable($("#OriginalRequiredDate").val()),
MessageBox: ko.observable("")
}
ko.applyBindings(viewModel);
当用户按下“编辑订单”按钮时,我们可以通过创建一个用于“编辑订单”点击事件的函数来将页面置于编辑模式,如下所示:
$("#btnEditOrder").click(function () {
viewModel.DisplayEditOrderButton(false);
viewModel.DisplayUpdateOrderButton(true);
viewModel.DisplayOrderDetailsButton(false);
viewModel.DisplayCancelChangesButton(true);
viewModel.EditFields(true);
viewModel.ReadOnlyMode(false);
});
上面的示例使用**非侵入式 JavaScript** 来设置编辑按钮点击事件,以更改 Knockout 侦听并自动更改页面模式的视图模型的布尔设置。非侵入式 JavaScript 是一种分离 JavaScript 与网页结构/内容和表示的新兴技术。
按下“更新订单”按钮将执行 `UpdateOrder` 函数。`UpdateOrder` 函数将简单地获取视图模型的值,创建一个 shipping information JavaScript 对象,该对象将通过 jQuery AJAX 调用提交到 `UpdateOrder` 控制器方法。
function UpdateOrder() {
var shippingInformation = new ShippingInformation();
shippingInformation.OrderID = viewModel.OrderID();
shippingInformation.CustomerID = viewModel.CustomerID();
shippingInformation.ShipName = viewModel.ShipName();
shippingInformation.ShipAddress = viewModel.ShipAddress();
shippingInformation.ShipCity = viewModel.ShipCity();
shippingInformation.ShipRegion = viewModel.ShipRegion();
shippingInformation.ShipPostalCode = viewModel.ShipPostalCode();
shippingInformation.ShipCountry = viewModel.ShipCountry();
shippingInformation.RequiredDate = viewModel.RequiredDate();
shippingInformation.Shipper = viewModel.SelectedShipVia();
var url = "/Orders/UpdateOrder";
$(':input').removeClass('validation-error');
$.post(url, shippingInformation, function (data, textStatus) {
UpdateOrderComplete(data);
});
}
function UpdateOrderComplete(result) {
if (result.ReturnStatus == true) {
viewModel.MessageBox(result.MessageBoxView);
viewModel.OrderID(result.ViewModel.Order.OrderID);
viewModel.ShipperName(result.ViewModel.Order.ShipperName);
viewModel.DisplayEditOrderButton(true);
viewModel.DisplayUpdateOrderButton(false);
viewModel.DisplayOrderDetailsButton(true);
viewModel.DisplayCancelChangesButton(false);
viewModel.DisplayCreateOrderButton(false);
viewModel.EditFields(false);
viewModel.ReadOnlyMode(true);
viewModel.OriginalShipName(result.ViewModel.Order.ShipName);
viewModel.OriginalShipAddress(result.ViewModel.Order.ShipAddress);
viewModel.OriginalShipCity(result.ViewModel.Order.ShipCity);
viewModel.OriginalShipRegion(result.ViewModel.Order.ShipRegion);
viewModel.OriginalShipPostalCode(result.ViewModel.Order.ShipPostalCode);
viewModel.OriginalShipCountry(result.ViewModel.Order.ShipCountry);
viewModel.OriginalRequiredDate(result.ViewModel.Order.RequiredDateFormatted);
viewModel.OriginalShipVia(viewModel.SelectedShipVia());
}
else
{
viewModel.MessageBox(result.MessageBoxView);
}
for (var val in result.ValidationErrors) {
var element = "#" + val;
$(element).addClass('validation-error');
}
}
验证错误
此外,您可以使用 CSS 类来显示验证错误,该类可以通过循环遍历 JSON 请求中返回的对象集合(包含与您的 `INPUT` 控件匹配的名称)来突出显示哪些元素存在错误,如下所示:
for (var val in result.ValidationErrors) {
var element = "#" + val;
$(element).addClass('validation-error');
}
订单录入详细信息视图 – Knockout 模板
在编辑订单发货信息后,用户现在可以进入订单详细信息并为订单添加产品。下面的订单详细信息视图使用 Knockout 模板功能,允许内联编辑行项目,而无需发布回。
Knockout 模板是一种简单便捷的方式,可以构建复杂的 UI 结构——具有重复或嵌套块——作为您的视图模型数据的函数。模板绑定通过渲染模板的结果来填充关联的 DOM 元素。
预渲染和格式化数据
在处理后端数据结构和模型中的数据时,大多数情况下都需要在将数据呈现给用户之前对其进行重新格式化(日期和货币字段等)。在传统的 ASP.NET Web Forms 中,大多数控件都实现了预渲染或数据绑定事件,允许您在数据呈现给用户之前对其进行重新格式化。在 MVC 中,您可以获取视图模型数据,并在视图的开头使用服务器端代码执行预渲染任务。在下面的示例中,正在创建一个包含重新格式化数据的订单详细信息列表。
@model NorthwindViewModel.OrderViewModel
@{
ViewBag.Title = "Order Entry Detail";
ArrayList orderDetails = new ArrayList();
foreach (var item in Model.OrderDetailsProducts)
{
var orderDetail = new
{
ProductID = item.OrderDetails.ProductIDFormatted,
ProductName = item.Products.ProductName,
Quantity = item.OrderDetails.Quantity,
UnitPrice = item.OrderDetails.UnitPriceFormatted,
QuantityPerUnit = item.Products.QuantityPerUnit,
Discount = item.OrderDetails.DiscountFormatted
};
orderDetails.Add(orderDetail);
}
}
一旦您的数据被重新格式化,您就可以使用编码后的 JSON 对象加载视图中的 `DIV` 标签,该对象将由 JavaScript 访问以绑定数据到 knockout 模板。
<div id="OrderDetailsData" style="visibility: hidden; display: none">
@Html.Raw(Json.Encode(orderDetails));
</div>
您可以通过在类型为 _text/html_ 的脚本标签内包含您的内容和数据绑定标签来设置 Knockout 模板。
<!--====== Template ======-->
<script type="text/html" id="OrderDetailTemplate">
<tr data-bind="style: { background: viewModel.SetBackgroundColor($data) }">
<td style="height:25px"><div data-bind="text:ProductID"></div></td>
<td><div data-bind="text: ProductName"></div></td>
<td>
<div data-bind="text: Quantity, visible:DisplayMode "></div>
<div data-bind="visible: EditMode" >
<input type="text" data-bind="value: Quantity" style="width: 50px" />
</div>
</td>
<td><div data-bind="text:UnitPrice"></div></td>
<td><div data-bind="text: QuantityPerUnit"></div></td>
<td><div data-bind="text: Discount, visible:DisplayMode "></div>
<div data-bind="visible: EditMode" >
<input type="text" data-bind="value:Discount" style="width:50px" />
</div>
</td>
<td>
<div data-bind="visible:DisplayDeleteEditButtons">
<div style="width:25px;float:left"><img alt="delete" data-bind="click:function()
{ viewModel.DeleteLineItem($data) }"
title="Delete item" src="@Url.Content("~/Content/Images/icon-delete.gif")"/>
</div>
<div style="width:25px;float:left"><img alt="edit" data-bind="click:function()
{ viewModel.EditLineItem($data) }" title="Edit item"
src="@Url.Content("~/Content/Images/icon-pencil.gif")"/>
</div>
</div>
<div data-bind="visible:DisplayCancelSaveButtons">
<div style="width:25px;float:left"><img alt="save" data-bind="click: function()
{viewModel.UpdateLineItem($data) }" title="Save item"
src="@Url.Content("~/Content/Images/icon-floppy.gif")"/>
</div>
<div style="width:25px;float:left"><img alt="cancel edit"
data-bind="click:function() { viewModel.CancelLineItem($data) }"
title="Cancel Edit" src="@Url.Content("~/Content/Images/icon-pencil-x.gif")"/>
</div>
</div>
</td>
</tr>
</script>
将 Knockout 模板链接到您的 HTML 只需使用 data-bind template 标签并使用 `foreach` 语句。
<!--====== Container ======-->
<table border="0" cellpadding="0" cellspacing="0" style="width:100%">
<tr class="DataGridHeader">
<td style="width:10%; height:25px">Product ID</td>
<td style="width:30%">Product Description</td>
<td style="width:10%">Quantity</td>
<td style="width:10%">Unit Price</td>
<td style="width:15%">UOM</td>
<td style="width:10%">Discount</td>
<td style="width:15%">Edit Options</td>
</tr>
<tbody data-bind='template: {name: "OrderDetailTemplate", foreach:LineItems}'> </tbody>
</table>
JavaScript `eval` 函数可以用来解析 JSON 对象。然而,它可以编译和执行任何 JavaScript 程序,因此可能存在安全问题。使用 JSON 解析器要安全得多。JSON 解析器只识别 JSON 文本,拒绝所有可能被视为不安全或恶意的脚本。JavaScript 中有许多 JSON 解析器可在 json.org 上找到。
您可以使用解析您初始加载的订单详细信息的 JSON 解析器,以便它可以与 Knockout 视图模型进行数据绑定。Knockout 要求您在创建详细行项目数组时创建一个可观察数组。
<script language="javascript" type="text/javascript">
initialLineItems = jsonParse($("#OrderDetailsData").text());
var viewModel = {
LineItems: ko.observableArray()
}
ko.applyBindings(viewModel);
for (i = 0; i < initialLineItems.length; i++) {
var newLineItem = CreateLineItem(initialLineItems[i]);
viewModel.LineItems.push(newLineItem);
}
var lineItemDisplay = function () {
this.ProductID;
this.ProductName;
this.Quantity;
this.UnitPrice;
this.QuantityPerUnit;
this.Discount;
this.OriginalQuantity;
this.OriginalDiscount;
this.EditMode;
this.DisplayMode;
this.DisplayDeleteEditButtons;
this.DisplayCancelSaveButtons;
};
function CreateLineItem(LineItem) {
var lineItem = new lineItemDisplay();
lineItem.ProductID = ko.observable(LineItem.ProductID);
lineItem.ProductName = ko.observable(LineItem.ProductName);
lineItem.Quantity = ko.observable(LineItem.Quantity);
lineItem.OriginalQuantity = ko.observable(LineItem.Quantity);
lineItem.OriginalDiscount = ko.observable(LineItem.Discount);
lineItem.UnitPrice = ko.observable(LineItem.UnitPrice);
lineItem.QuantityPerUnit = ko.observable(LineItem.QuantityPerUnit);
lineItem.Discount = ko.observable(LineItem.Discount);
lineItem.BackgroundColor = ko.observable(LineItem.BackgroundColor);
lineItem.EditMode = ko.observable(false);
lineItem.DisplayMode = ko.observable(true);
lineItem.DisplayDeleteEditButtons = ko.observable(true);
lineItem.DisplayCancelSaveButtons = ko.observable(false);
return lineItem;
}
</script>
Knockout 映射插件
在上面的示例中,我手动编写了自己的 JavaScript 代码来构建视图模型。或者,您可以使用 Knockout 的映射插件,它提供了一种简单的方法将 JavaScript 对象映射到具有适当可观察对象的视图模型。
编辑、更新和删除模板项
此页面的完整 Knockout 视图模型包括用于编辑、更新和删除行项目的函数。
<script language="javascript" type="text/javascript">
var viewModel = {
LineItems: ko.observableArray(),
MessageBox: ko.observable(),
AddNewLineItem: ko.observable(false),
SetBackgroundColor: function (currentLineItemData) {
var rowIndex = this.LineItems.indexOf(currentLineItemData);
var colorCode = rowIndex % 2 == 0 ? "White" : "WhiteSmoke";
return colorCode;
},
EditLineItem: function (currentLineItemData) {
var currentLineItem = this.LineItems.indexOf(currentLineItemData);
this.LineItems()[currentLineItem].DisplayMode(false);
this.LineItems()[currentLineItem].EditMode(true);
this.LineItems()[currentLineItem].DisplayDeleteEditButtons(false);
this.LineItems()[currentLineItem].DisplayCancelSaveButtons(true);
},
DeleteLineItem: function (currentLineItemData) {
var currentLineItem = this.LineItems.indexOf(currentLineItemData);
var productName = this.LineItems()[currentLineItem].ProductName();
var productID = this.LineItems()[currentLineItem].ProductID();
ConfirmDeleteLineItem(productID, productName, currentLineItem);
},
DeleteLineItemConfirmed: function (currentLineItem) {
var row = this.LineItems()[currentLineItem];
this.LineItems.remove(row);
},
CancelLineItem: function (currentLineItemData) {
currentLineItem = this.LineItems.indexOf(currentLineItemData);
this.LineItems()[currentLineItem].DisplayMode(true);
this.LineItems()[currentLineItem].EditMode(false);
this.LineItems()[currentLineItem].DisplayDeleteEditButtons(true);
this.LineItems()[currentLineItem].DisplayCancelSaveButtons(false);
this.LineItems()[currentLineItem].Quantity(this.LineItems()
[currentLineItem].OriginalQuantity());
this.LineItems()[currentLineItem].Discount(this.LineItems()
[currentLineItem].OriginalDiscount());
},
UpdateLineItem: function (currentLineItemData) {
currentLineItem = this.LineItems.indexOf(currentLineItemData);
var lineItem = this.LineItems()[currentLineItem];
UpdateOrderDetail(lineItem, currentLineItem);
},
UpdateOrderDetailComplete: function (currentLineItem, discount) {
this.LineItems()[currentLineItem].DisplayMode(true);
this.LineItems()[currentLineItem].EditMode(false);
this.LineItems()[currentLineItem].DisplayDeleteEditButtons(true);
this.LineItems()[currentLineItem].DisplayCancelSaveButtons(false);
this.LineItems()[currentLineItem].OriginalQuantity(this.LineItems()
[currentLineItem].Quantity());
this.LineItems()[currentLineItem].OriginalDiscount(discount);
this.LineItems()[currentLineItem].Discount(discount);
}
}
按下行项目上的铅笔编辑图标会将行项目置于编辑模式,当 `EditLineItem` 函数在 `onclick` 事件上执行时,如下所示:
EditLineItem: function (currentLineItemData) {
var currentLineItem = this.LineItems.indexOf(currentLineItemData);
this.LineItems()[currentLineItem].DisplayMode(false);
this.LineItems()[currentLineItem].EditMode(true);
this.LineItems()[currentLineItem].DisplayDeleteEditButtons(false);
this.LineItems()[currentLineItem].DisplayCancelSaveButtons(true);
},
借助 Knockout 模板及其数据绑定技术,您可以创建一个类似于 ASP.NET Web Forms `DataGrid` 控件的完全内联可编辑网格。
按下“添加行项目”按钮会打开一个行项目,允许将一个项目添加到订单中。
通过模态弹出窗口可以完成产品项的搜索。按下新行项目上的搜索按钮将显示产品搜索窗口。
模态弹出产品搜索窗口
模态弹出窗口是 AJAX 调用和部分视图的组合。AJAX 请求调用产品查询部分视图并返回产品搜索内容。然后将内容填充到 `DIV` 标签中。
<div id="dialog-modal" title="Product Inquiry">
<div id="ProductInquiryModalDiv"> </div>
</div>
模态弹出窗口是一个 jQuery 插件,通过调用 jQuery `dialog` 函数显示。
function ShowProductInquiryModal() {
var url = "/Products/BeginProductInquiry";
$.post(url, null, function (html, textStatus) {
ShowProductInquiryModalComplete(html);
});
}
function ShowProductInquiryModalComplete(productInquiryHtml) {
$("#ProductInquiryModalDiv").html(productInquiryHtml);
$("#dialog-modal").dialog({
height: 500,
width: 900,
modal: true
});
//
// execute Product Inquiry query after the initial page content has been loaded
//
setTimeout("ProductInquiryInitializeGrid()", 1000);
}
产品查询搜索窗口 - 唯一 ID 生成
产品查询搜索窗口是一个部分视图。由于此窗口将加载到订单详细信息页面中的同一浏览器 DOM 中,因此所有 HTML 控件以及动态创建的 JavaScript 函数和变量都需要具有唯一的名称。此部分视图实例化一个自制的 `PageIDGeneration` 类,并调用 `GenerateID` 方法为每个 HTML 控件生成唯一的控件 ID,并生成唯一的 JavaScript 函数名和变量,然后再渲染内容。基本上,`PageIDGeneration` 类通过设置唯一的 GUID 数字来生成唯一的 ID。使用 GUID 数字可以保证唯一性。
@model NorthwindViewModel.ProductViewModel
@using NorthwindWebApplication.Helpers;
@{
NorthwindWebControls.PageIDGeneration webControls =
new NorthwindWebControls.PageIDGeneration();
string txtProductID = webControls.GenerateID("ProductID");
string txtProductDescription = webControls.GenerateID("ProductName");
string btnSearch = webControls.GenerateID("BtnSearch");
string btnReset = webControls.GenerateID("BtnReset");
string messageBox = webControls.GenerateID("MessageBox");
string productResults = webControls.GenerateID("ProductResults");
}
<div class="SearchBar">
<div style="float:left; width:200px">
Product ID
</div>
<div style="float:left; width:200px">
Product Description
</div>
<div style="clear:both;"></div>
<div style="float:left; width:200px">
<input id="@txtProductID" type="text" value="" style = "width:150px" />
</div>
<div style="float:left; width:200px ">
<input id="@txtProductDescription" type="text" value="" style = "width:150px" />
</div>
<input id="@btnSearch" type="button" value="Search" />
<input id="@btnReset" type="button" value="Reset"/>
</div>
<div style="clear:both;"></div>
<div id="@productResults"></div>
<div id="@messageBox"></div>
@Html.RenderJavascript(webControls.RenderJavascriptVariables("ProductInquiry_"))
<script language="javascript" type="text/javascript">
$(ProductInquiry_BtnSearch).click(function() {
ProductInquiryInitializeGrid();
});
$(ProductInquiry_BtnReset).click(function() {
$(ProductInquiry_ProductID).val("");
$(ProductInquiry_ProductName).val("");
ProductInquiryInitializeGrid();
});
function ProductInquiryRequest() {
this.CurrentPageNumber;
this.PageSize;
this.ProductID;
this.ProductName;
this.SortDirection;
this.SortExpression;
this.PageID;
};
function ProductInquiry(currentPageNumber, sortExpression, sortDirection) {
var url = "/Products/ProductInquiry";
var productInquiryRequest = new ProductInquiryRequest();
productInquiryRequest.ProductID = $(ProductInquiry_ProductID).val();
productInquiryRequest.ProductName = $(ProductInquiry_ProductName).val();
productInquiryRequest.CurrentPageNumber = currentPageNumber;
productInquiryRequest.SortDirection = sortDirection;
productInquiryRequest.SortExpression = sortExpression;
productInquiryRequest.PageSize = 10;
productInquiryRequest.PageID = $(ProductInquiry_PageID).val();
$.post(url, productInquiryRequest, function (data, textStatus) {
ProductInquiryComplete(data);
});
};
function ProductInquiryComplete(result) {
if (result.ReturnStatus == true) {
$(ProductInquiry_ProductResults).html("");
$(ProductInquiry_ProductResults).html(result.ProductInquiryView);
$(ProductInquiry_MessageBox).html("");
}
else {
$(ProductInquiry_MessageBox).html(result.MessageBoxView);
}
}
function ProductInquiryInitializeGrid() {
ProductInquiry(1, "ProductName", "ASC");
}
function ProductSelected(productID) {
GetProductInformation(productID);
}
</script>
结论
ASP.NET MVC 是一个不断发展的框架,用于构建可扩展的、基于标准的 Web 应用程序。由于其关注点分离的架构,MVC 有一个学习曲线,需要一种不同的方式来思考 Web 应用程序的开发,其中包含一些试错和一些探索。它与我们过去所有使用 ASP.NET Web Forms(Web 开发的 COBOL)和 Web Form 发布回模型开发 Web 应用程序的方式不同。展望未来,MVC 开发人员需要密切关注其他新兴框架和开源库,这些库可以补充和增强 MVC 开发。本文重点介绍了开源 JavaScript 库 Knockout 和 jQuery 以及用于在视图和控制器之间交换数据的 JSON。还应考虑其他新兴的开发人员工具和框架,特别是 Backbone 和 JavaScriptMVC。为了进行比较,或许本 Northwind 示例应用程序的后续文章将包含 Backbone 和/或 JavaScriptMVC。