UpdatePanel for ASP.NET MVC

iDotNetSpace發表於2008-08-26
其實這是一篇遲釋出近2個月的文章。事實上在ASP.NET MVC Preview 2釋出之前我就已經將這篇文章的所有內容準備完畢了。當時想,就等Preview 2釋出吧,而真一旦Preview 2釋出之後卻又懶得進行移植——移植了之後卻又懶得寫文章。這一拖就是近2個月,毫無長進。可能工作等其他事情的確多了些,但是捫心自問,也並沒有忙到不可開交。時間往往都是在點點滴滴間浪費的。唉,可能是自視太高,越來越不願意寫一些普普通通的介紹性文章,導致可寫的東西大大減少。不過話說回來,其實打算寫的,甚至多次說過要寫得東西也並不少,為什麼就就是沒有動筆呢?其實還是一個“懶”字——當年的勤奮勁兒到哪裡去了呢?

  言歸正傳。先解釋一下標題,什麼是“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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章