笨小孩做开发

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理
Knockout (或者Knockout.js
,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的值,后者是一个布尔标签,控制只读或编辑模式。


  





<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>


  Order Entry显示模式


  当第一次选择一个Order编辑时,此时页面处于只读模式。要创建Knockout与HTML对象的自动绑定,我们必须创建一个JavaScript View
Model对象,与Knockout绑定,这样Knockout可以监听View Model对象属性的变化,并自动更新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);


 


  我们创建一个Edit Order点击事件函数,当用户点击Edit Order按钮,页面处于编辑模式。代码如下:





$("#btnEditOrder").click(function ()
{

  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函数。


  





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类以显示验证错误信息。CSS会循环遍历JSON返回的INPUT控件对象,验证其输入值是否合法如有错误则用红色标记高亮。代码如下:





for (var val in
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开始阶段做预渲染操作。下例中,拿到重新格式化的数据后,我们生成了一个订单明细列表。





@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);

  }

  }


 


  待数据完成格式化后,我们使用DIV标签加载编码后的JSON对象。稍后,JavaScript将访问该JSON对象,将数据绑定至knockout模版。


  



 



  我们创建一个Knockout模版,如下。Script标签的类型为text/html,包含各种内容与数据绑定标签。





<!--====== 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模版标签与一个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
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的编辑,更新,删除。





<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);

}
}


 


选择一个line item,点击铅笔编辑图标,EditLineItem函数会触发onclick事件,line item处于编辑模式。如下:


 





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模版与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标签。





<div
id
="dialog-modal" title="Product Inquiry">
<div id="ProductInquiryModalDiv"> </div>
</div>

 


       Modal 弹出窗口是一个具有dialog功能的JQuery插件。


 


 





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);

}



 


        Product Inquiry Search窗口 – UID生成机制


        Product Inquiry Search窗口本身是一个Partial View。由于该窗口与Order
Order页面加载的DOM一样,因此所有的HTML控件与动态创建的JavaScript函数及变量均要求名字独一无二。在渲染页面内容之前,该Partial
View实例化自定义的PageIDGeneration类,调用GenerateID方法,生成独一无二的控件ID,JavaScript函数名,以及变量名。PageIDGeneration类通过设置unique
Guid数目,保证生成ID的唯一性。
 





@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应用开发的日益成熟的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。

posted on 2013-07-12 15:21  笨小孩做开发  阅读(1182)  评论(1编辑  收藏  举报