微軟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的開發。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章