,KnockoutJS)是一个开源的JavaScript库,网址为www.knockoutjs.com。Knockout语法简洁、可读性好,能轻松实现与DOM元素的关联。一旦数据模型的状态发生改变,则立即自动刷新UI。Knockout采用Model-View-View-Model
(MVVM)的设计模式来简化动态JavaScript UI。Knockout有效实现了JavaScript与UI
HTML呈现的分离。有了Knockout,在写JavaScript时,就不需要在页面中引用UI元素或DOM。
Knockout设计目标是把任何JavaScript对象当成View Model来使用。只要View
Model的属性具有可监听性,就可以使用Knockout将其与UI绑定。一旦属性值发生变化时,UI会被自动刷新。
Order Entry Header – 编辑模式与显示模式
Order Header页面的关键功能是,在不重复提交整个页面的前提下,自由切换编辑模式与显示模式。ASP.NET
post-back模式通常表现为:用户点击Edit按钮,post提交至服务器,返回后,整个页面被重新刷新。使用Knockout与MVVM数据绑定技术,则可以避免页面重新刷新。这里,我们需要做的仅仅是将Order
Header页面去绑定JavaScript创建的View Model。
数据绑定标签
为创建一个MVC View来回切换只读与编辑模式,我们为页面的每一个元素都创建单独的DIV与SPAN标签。一个(编辑模式)包含INPUT
HTML控件,另一个(只读)只显示文本。添加Knockout数据绑定标签可以灵活控制HTML元素何时被显示,何时被隐藏。下例中,ShipName
包含一个两个数据绑定标签,前者关联Ship Name的值,后者是一个布尔标签,控制只读或编辑模式。
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 >
Order Entry显示模式
当第一次选择一个Order编辑时,此时页面处于只读模式。要创建Knockout与HTML对象的自动绑定,我们必须创建一个JavaScript View
Model对象,与Knockout绑定,这样Knockout可以监听View Model对象属性的变化,并自动更新UI。
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);
我们创建一个Edit Order点击事件函数,当用户点击Edit Order按钮,页面处于编辑模式。代码如下:
{
viewModel.DisplayEditOrderButton( false );
viewModel.DisplayUpdateOrderButton( true );
viewModel.DisplayOrderDetailsButton( false );
viewModel.DisplayCancelChangesButton( true );
viewModel.EditFields( true );
viewModel.ReadOnlyMode( false );
});
上例中,我们使用Unobtrusive
JavaScript这种方式来触发Edit按钮点击事件,实现两种显示与编辑模式的切换。Knockout会监听View
Model,实现自动切换。Unobtrusive JavaScript是一项用于页面内容结构与页面呈现分离的新技术。
用户点击Update Oder 按钮,则调用UpdateOrder 函数。UpdateOrder 函数的功能是抓取View
Model的值,并创建一个表示物流信息的JavaScript对象。通过JQuery
AJAX调用,该对象将提交给UpdateOrderController函数。
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类以显示验证错误信息。CSS会循环遍历JSON返回的INPUT控件对象,验证其输入值是否合法如有错误则用红色标记高亮。代码如下:
result.ValidationErrors) {
var element = " # " +
val;
$(element).addClass( ' validation-error');
}
Oder Entry Details视图 – Knockout 模版
在完成Order Shipping Information的编辑之后,用户可查看订单详细列表,并可向订单中添加产品。下面的Order Details
View使用Knockout模版功能,实现了无需post–back的前提下,逐行编辑每一个line item。
Knockout 模版可轻松实现复杂的UI,例如不断重复与嵌套的Block。Knockout模版将模版渲染之结果填充至关联的DOM元素。
预渲染与格式化数据
通常情况下,数据在前后端的结构与模式所有不同,特别是对于日期,货币等字段,此时就免不了数据的重新格式化。在传统的ASP.NET
Web表单中,多数控件是通过预渲染或数据绑定事件,来实现数据到达给用户之前的重新格式化。在MVC中,我们可以抓取View Model数据,调用服务器端代码,实现在View开始阶段做预渲染操作。下例中,拿到重新格式化的数据后,我们生成了一个订单明细列表。
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);
}
}
待数据完成格式化后,我们使用DIV标签加载编码后的JSON对象。稍后,JavaScript将访问该JSON对象,将数据绑定至knockout模版。
id = " OrderDetailsData " style = " visibility: hidden; display: none " >
@Html.Raw(Json.Encode(orderDetails));
</ div >
我们创建一个Knockout模版,如下。Script标签的类型为text/html,包含各种内容与数据绑定标签。
======-->
< 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模版标签与一个foreach语句即可。
======-->
< 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
eval可编译并运行任何JavaScript程序,会导致安全性问题。因此,较安全的做法是使用JSON解析器。JSON解析器只识别JSON文本,而不会执行任何潜在风险的脚本。json.org网站中提供了许多JavaScript编写的JSON解析器。
使用JSON解析器,我们可以解析初始加载的订单明细数据,这些数据会与Knockout View Model实现绑定。当创建多个details line
items时,我们需要创建一个数组,供Knockout监听。
Knockout映射插件
上例中,我们采取的是自定义创建View
Model的方式。另一种方式是采用Knockout映射插件,选择合适的映射规则,直截了将JavaScript对象与View Model绑定。
编辑,更新与删除Template Items
完整的页面Knockout View Model包含有line item的编辑,更新,删除。
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);
}
}
选择一个line item,点击铅笔编辑图标,EditLineItem函数会触发onclick事件,line item处于编辑模式。如下:
(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模版与Knockout绑定技术,我们可以创建类似ASP.NET Web Forms
DataGrid控件的完整in-line编辑grid。
点击Add Line Item按钮,打开一个line
item,可将一个item添加至order中。
使用modal popup窗口,可搜索一个Product Item。在一个新的line item上点击Search按钮,弹出product
search 窗口。
The Modal Popup Product Search 窗口
Modal 弹出窗口是AJAX调用与Partial View的结合。AJAX 请求调用Product Inquiry
partial view,返回product search的内容,最后填充至DIV标签。
id = " dialog-modal " title = " Product Inquiry " >
< div id = " ProductInquiryModalDiv " > </ div >
</ div >
Modal 弹出窗口是一个具有dialog功能的JQuery插件。
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 );
}
Product Inquiry Search窗口 – UID生成机制
Product Inquiry Search窗口本身是一个Partial View。由于该窗口与Order
Order页面加载的DOM一样,因此所有的HTML控件与动态创建的JavaScript函数及变量均要求名字独一无二。在渲染页面内容之前,该Partial
View实例化自定义的PageIDGeneration类,调用GenerateID方法,生成独一无二的控件ID,JavaScript函数名,以及变量名。PageIDGeneration类通过设置unique
Guid数目,保证生成ID的唯一性。
@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应用开发的日益成熟的Web框架。MVC的架构思想是注重分离,对于具有Trial、Error、Discovery的Web应用开发而言,MVC的学习曲线就显得与众不同。MVC与我们过去一直使用的ASP.NET
Web Forms技术与Web Form post-back
model技术完全不同。在未来,MVC开发者需要更加注重新兴框架与开源库,增强型MVC的开发。
本文重点关注的是开源JavaScript库Knockout与JQuery,以及用于交换视图与控制器数据的JSON。建议MVC开发者也多多关注其它的开发工具与框架,特别是Backbone与JavaScriptMVC。作为比较,后续的文章将会在示例程序Northwind中引入Backbone与JavaScriptMVC。