微软MVC框架实战:开源的JS库Knockout

http://tech.it168.com/a2012/0210/1310/000001310252_all.shtml

微软MVC框架实战:开源的JS库Knockout

2012年02月11日00:00  it168网站原创 作者:DoubleLife 编辑: 皮丽华 评论: 1

        【IT168 技术】  Knowckout – 当MVC遭遇MVVM

  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< stringobject> {
" 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,  nullfunction (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的开发。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值