微軟MVC框架實戰:開源的JS庫Knockout

【IT168 技術】 Knowckout – 當MVC遭遇MVVMjavascript

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

Knockout設計目標是把任何JavaScript對象當成View Model來使用。只要View Model的屬性具備可監聽性,就可使用Knockout將其與UI綁定。一旦屬性值發生變化時,UI會被自動刷新。java

Order Entry Header – 編輯模式與顯示模式web

Order Header頁面的關鍵功能是,在不重複提交整個頁面的前提下,自由切換編輯模式與顯示模式。ASP.NET post-back模式一般表現爲:用戶點擊Edit按鈕,post提交至服務器,返回後,整個頁面被從新刷新。使用Knockout與MVVM數據綁定技術,則能夠避免頁面從新刷新。這裏,咱們須要作的僅僅是將Order Header頁面去綁定JavaScript建立的View Model。json

數據綁定標籤後端

爲建立一個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模版。

<div id="OrderDetailsData" style="visibility: hidden; display: none">
@Html.Raw(Json.Encode(orderDetails));
</div>

咱們建立一個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。

相關文章
相關標籤/搜索