UpdatePanel for ASP.NET MVC
言歸正傳。先解釋一下標題,什麼是“UpdatePanel for ASP.NET MVC”呢?ASP.NET AJAX中的UpdatePanel相信大家都有所瞭解。可惜的是,ASP.NET MVC框架的誕生“毀滅”了大量基於PostBack的控制元件,首當其衝地可能就是UpdatePanel了。如果沒有PostBack,UpdatePanel就失去了全部作用,甚至不如一些繫結控制元件,至少它們還能夠用於展示。為UpdatePanel長吁短嘆之後,我們不禁又開始懷念UpdatePanel的優勢:“透明”。在UpdatePanel的幫助下,實現AJAX操作對於開發人員幾乎完全透明。我們要做的僅僅是將需要AJAX更新的內容用UpdatePanel包裝起來,一切都是那麼優雅。
我們能否在ASP.NET MVC中拯救UpdatePanel呢?也許是可以的吧,但這更像是一個“不可能完成的任務”。我不是傳說中的阿湯哥,因此重新為ASP.NET MVC量身定製一個AJAX解決方案似乎更為可行。雖然我們不會苛求一個新生事物從誕生開始就趨向完美,但即使只是一個原型,它也必須嚴格遵守的一些原則:
- 不得破壞MVC中的協議(協作,職責等等)
- 對開發人員儘可能地透明
Nikhil Kothari曾經提出了他在ASP.NET MVC框架下的AJAX解決方案。如果您還不瞭解他的做法,那麼我先在這裡進行一點概括。Nikhil擴充套件了Controller使之支援一種Ajax操作,於是我們在程式碼中就可以寫如下程式碼:
public class TaskListController : AjaxController {
...
public void CompleteTask(int taskID) {
if (String.IsNullOrEmpty(Request.Form["deleteTask"]) == false) {
InvokeAction("DeleteTask");
return;
}
Task task = _taskDB.GetTask(taskID);
if (task != null) {
_taskDB.CompleteTask(task);
}
if (IsAjaxRequest) {
if (task != null) {
RenderPartial("TaskView", task);
}
}
else {
RedirectToAction("List");
}
}
...
}
與AjaxController類似,Nikhil也為ViewPage和ViewControl提供了一些擴充套件方法,因此目前在View(List.aspx)中我們就能看到如下的程式碼:
<div id="taskList">
foreach (Task task in Tasks) { %>
<div>
this.RenderPartial("TaskView", task); %>
div>
} %>
div>
在View和Controller中都存在對於RenderPartiel方法的呼叫,它們的作用就是向客戶端輸出一個“Partial Template”生成的HTML程式碼。而在ASP.NET MVC的預設配置中,Partial Template即為User Control。而在TaskView這個Partial Template中可以看到一些輔助方法:
<div id="taskItem= Task.ID %>" class="taskPanel">
Ajax.Initialize(); %>
this.RenderBeginAjaxForm(
Url.Action("CompleteTask"),
new {
Update = "taskItem" + Task.ID,
UpdateType = "replace",
Completed = "endUpdateTask"}); %>
<input type="hidden" name="taskID" value="= Task.ID %>" />
<input type="submit" class="completeButton" name="completeTask" value="Done!" />
<input type="submit" class="deleteButton" name="deleteTask" value="Delete" />
<span>= Html.Encode(Task.Name) %>span>
this.RenderEndForm(); %>
Ajax.RenderScripts(); %>
div>
這些輔助方法的作用是生成一些觸發AJAX更新的標籤及指令碼,當使用者點選RenderBeginAjaxForm與RenderEndForm方法生成的tag之間的提交按鈕時,網頁將會向伺服器端發出一個AJAX請求,而伺服器端的Action並最終會通過RenderPartial方法輸出一個Partial Template生成的HTML。伺服器端最終輸出的HTML將會被替換或新增到頁面的某個元素內。這就形成了一個AJAX效果。這個解決方案從某些方面看上去很酷,尤其是生成的程式碼可以新增到某個元素中,而不單單是如同UpdatePanel的替換,例如Nikhil在他的例子中就使用了這個特性實現了一個新增功能。不過如果使用之前提出的原則來衡量的話,似乎這個解決方案並不十分理想。
原因很簡單,因為不夠透明。
也有評論認為,Controller中的邏輯不該根據一個請求AJAX與否而進行不同處理(Nikhil的解決方案使用RenderPartial來替代RenderView為AJAX操作進行輸出),因此這個解決方案破壞了MVC的職責。我不這麼認為,但是我希望能做到這一點,因為做到這一點即意味著絕對的透明。絕對透明則意味著Controller將一個應用程式是否AJAX的決定權完全交給了客戶端,這點非常理想,因為AJAX完全是一個表現層的概念。ASP.NET AJAX中的UpdatePanel在這方面的表現可圈可點(雖然還遠不夠完美),因此我最後決定也為ASP.NET MVC開發一款類似UpdatePanel的元件。值得慶幸的是,ASP.NET MVC預設使用WebForm頁面作為檢視模板,在這個強大的模型之下,構建出這樣一個AJAX解決方案(的原形)似乎並不十分困難。
我將這個控制元件命名為MvcAjaxPanel。MvcAjaxPanel與UpdatePanel最大的區別在於後者接收的是PostBack,而前者接收的只是普通的HTTP請求。Post“Back”意味著Post過後回到了原來的Page,而ASP.NET MVC的請求往往會被引導至不同的頁面。因此如何跨頁面進行內容更新是MvcAjaxPanel首要解決的問題。最終我選擇了為每個MvcAjaxPanel指定一個UpdateAreaID的做法。
<mvc:MvcAjaxPanel runat="server" ID="mvcAjaxPanel" UpdateAreaID="Header">
...
mvc:MvcAjaxPanel>
當頁面向伺服器端發出一個AJAX請求時將會附帶頁面中的UpdateAreaID資訊,而伺服器端的Action並不會意識到這一點,因此依舊按照尋常邏輯指定一個檢視模版並輸出HTML。不過,如果檢視模板中的MvcAjaxPanel發現這個請求實際上是一個符合約定的AJAX請求(請注意,只有View元件意識到這是個請求的性質),則會使用新的方法來替換標準的輸出。這時候模板就會根據客戶端傳遞過來的UpdateAreaID,尋找頁面上具有同樣屬性值的MvcAjaxPanel,有選擇性地輸出內容。在客戶端就會有對應的JavaScript程式碼接收伺服器端的資料,並且更新頁面中的相應區域。
很明顯,MvcAjaxPanel的工作原理與UpdatePanel有頗多相似之處,也做到了一定程度上的透明。而且與Nikhil的解決方案相比,一個非常重要的優勢就是可以一次更新頁面中的多個區域——其實這也就是UpdatePanel的特性之一。而且這種對Controller透明的做法又有一個天然的特點,那就是能夠輕鬆地在不支援AJAX的瀏覽器中使用傳統的方式切換頁面。
伺服器端的實現原理並不複雜,不過作為解決方案的另一個關鍵部分,如何在客戶端觸發一個AJAX提交也是一個值得思考的話題。UpdatePanel的方式可謂“全自動”:頁面載入時將會把伺服器端的Trigger資訊輸出至客戶端,然後在客戶端截獲form的提交事件,並通過UniqueID或DOM結構等方式來判斷這次提交是否該轉化為AJAX方式。不過在一個ASP.NET MVC頁面中幾乎不會出現產生PostBack的元素,相反會有大量的普通連結,它們才是AJAX更新的主要截獲目標。
為此我提供了一些JavaScript程式碼,截獲一個連結原本的目標地址並將其轉化為一個AJAX請求。我在這裡通過示例中的程式碼來展示這種使用方式(這個示例源於Brad Abrams提供的ASP.NET MVC示例,不過我捨棄了Northwind資料庫與Entity Framework,取而代之的是XML資料以及自定義的簡單Model。此外,我也將其移植到ASP.NET MVC框架的0416 Build中):
foreach (var category in this.ProductCategories)
{ %>
<li>
= Html.ActionLink<ProductsController>(
c => c.List(category, 1),
category,
new { nclick = "mvcAjax.get(this, event)" })%>
li>
} %>
這段程式碼來自分類列表頁。與AJAX改進之前的程式碼相比,唯一的區別就是額外指定了元素的onclick事件(加粗部分)。在onclick事件執行中,這個連結預設的跳轉行為將被取消,取而代之的是一個AJAX請求,請求的目標便是ProductsController中名為List的Action。
我們可以使用上面的方式應對普通連結,那麼又該如何將一個客戶端from的提交行為也變成AJAX操作呢?以下依舊是示例中的程式碼:
<form method="post"
action="= Url.Action("Update", new { id = this.Product.ProductID }) %>"
onsubmit="mvcAjax.submit(this, event);">
<table>
<tr>
<td>Name:td>
<td>= Html.TextBox("Name", this.Product.Name) %>td>
tr>
...
table>
<input type="submit" value="Save" />
form>
在截獲了form的submit事件之後,客戶端將會收集該form中的所有input、select等值,組成一個請求的body,並且以HTTP POST的方式發出一個AJAX請求。餘下的事情和之前就沒有什麼區別了。
與UpdatePanel相比,MvcAjaxPanel的客戶端截獲方式可謂“純手工”,但是我並不認為這會造成什麼問題。ASP.NET MVC強調的就是職責分離,而這種分離並不僅僅體現在程式碼上,也體現在開發人員的職責上。在開發ASP.NET MVC應用程式時,負責View的是前端開發工程師,對他們來說JavaScript與AJAX可謂是再熟悉不過的技術。在合時的地方手動編寫一些JavaScript呼叫反而會讓他們得到無比的自由性。例如在之前的程式碼示例中,呼叫mvcAjax.get或mvcAjax.submit方法時完全可以在前後自由地加入額外操作或者條件判斷。這就不會像使用UpdatePanel時,如果需要使用JavaScript提交一個AJAX更新,還需要藉助不登大雅之堂的trick。
也正因為如此,Nikhil提出的解決方案非常不錯,它能夠和前臺開發人員的自定義邏輯進行靈活地結合。此外,通過閱讀ASP.NET MVC框架0416 Build的程式碼,我發現在新版本的ASP.NET MVC中似乎將會內建這種AJAX解決方案了——不過這也的確符合微軟的一貫做法,不是嗎?:)
這個AJAX解決方案原型的使用方式和工作原理已經描述完了,如果您對其具體實現感興趣,或者想親自嘗試一下,可以下載文章末尾的附件。附件中的解決方案包含三個專案,MvcAjax為提供MvcAjaxPanel的專案,而MvcWebApp是一個普通的ASP.NET MVC示例程式,而MvcAjaxWebApp自然就是新增AJAX效果之後的結果了。在示例中,我還在Master Page中定義的選單(即頁面左側的選單)裡顯示了一塊當前時間,這是為了體現MvcAjaxPanel的“一次提交,多處更新”的特點。
不過需要強調的是,這僅僅是個原型。或者說這只是一種實現上嘗試,在很多細節方面並沒有作太多追求。如果要成為一個完善的AJAX解決方案,還需要作大量的改進。例如:
- 提供一些客戶端的hook供前臺開發人員使用(如提交前、接受後、或者處理一個提交還沒有返回,客戶端就發起另一個請求的情況等等)。
- 更強大的功能,更好的開發體驗(如客戶端觸發機制)
- 異常處理
- 支援指令碼
- 支援跳轉(Redirection)
- ...
此外,作為面向ASP.NET MVC特有的AJAX解決方案,也有一些額外的問題需要考慮。最典型的問題之一就是在使用ASP.NET MVC時很少使用模板控制元件,而更多的使用頁面中的迴圈,那麼如何讓MvcAjaxPanel在迴圈內容生效?我也產生過一些想法,但是如果要真正確定下來最終的實現方式,很多東西還需要進一步思考。如果您對於這個AJAX解決方案有什麼建議或其他任何想法,也請儘快告訴我。
最後再說一件有趣的事情:在我實現了這個原型之後的某一天,忽然意識到這個控制元件似乎不光可以為ASP.NET MVC使用,也能夠用於普通的WebForms應用程式。這真是一個令人意外的發現。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-434714/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 【 Jeffrey Zhao】UpdatePanel for ASP.NET MVCASP.NETMVC
- 【Jeffrey Zhao】不妨來做個嘗試:UpdatePanel for ASP.NET MVCASP.NETMVC
- Asp.net中UpdatePanel內FileUpload的正確使用方法ASP.NET
- ASP.NET MVC路由ASP.NETMVC路由
- ASP.NET MVC ModuleASP.NETMVC
- ASP.NET MVC TemplateASP.NETMVC
- ASP.NET MVC ErrorASP.NETMVCError
- ASP.NET MVC FilterASP.NETMVCFilter
- ASP.NET MVC與ASP.NET WebFormASP.NETMVCWebORM
- [ASP.NET MVC 小牛之路]01 - 理解MVC模式ASP.NETMVC模式
- Asp.Net MVC HttpPost用法ASP.NETMVCHTTP
- ASP.NET MVC 反射例子ASP.NETMVC反射
- Asp.Net MVC 快取ASP.NETMVC快取
- Asp.Net MVC 使用 AjaxASP.NETMVC
- ASP.NET MVC ValidationASP.NETMVC
- ASP.NET MVC: Membership, OAuthASP.NETMVCOAuth
- ASP.NET MVC系列:AreaASP.NETMVC
- ASP.NET MVC系列:ModelASP.NETMVC
- AJAX Panels with ASP.NET MVCASP.NETMVC
- ASP.NET MVC 之 AJAXASP.NETMVC
- ASP.NET MVC和AJAXASP.NETMVC
- ASP.NET MVC 介紹ASP.NETMVC
- ASP.NET MVC 4使用PagedList.Mvc分頁ASP.NETMVC
- 【輝郎】ASP.NET MVC深度接觸:ASP.NET MVC請求生命週期ASP.NETMVC
- ASP.Net MVC過濾器ASP.NETMVC過濾器
- asp.net core mvc 分頁ASP.NETMVC
- ASP.NET MVC – 安全簡介ASP.NETMVC
- ASP.NET MVC – 模型簡介ASP.NETMVC模型
- ASP.NET MVC 使用 Datatables (1)ASP.NETMVC
- ASP.NET MVC 使用 Datatables (2)ASP.NETMVC
- Asp.net mvc 知多少(二)ASP.NETMVC
- Asp.net MVC – ControllerASP.NETMVCController
- Asp.Net MVC 捆綁(Bundle)ASP.NETMVC
- ASP.NET MVC TagBuilder使用ASP.NETMVCUI
- 筆記:ASP.NET MVC安全筆記ASP.NETMVC
- ASP.NET 4.5 MVC4.0ASP.NETMVC
- ASP.NET MVC之初體驗ASP.NETMVC
- AJAX基於ASP.NET MVCASP.NETMVC