Get the sample code for this article.
目錄將遠程任務形式化 取消任務的簡便方法 深入瞭解 abortPostBack 方法 設計不間斷任務 用戶端代碼 事務 產生進度條 |
在上個月,我構建了一個架構,用以從用戶端監視正在執行的伺服器端任務。使用此架構(以後稱為進度監視器架構,或 PMF),您可以為 Web 使用者提供關於伺服器上啟動並執行操作的進度資訊,此類資訊通常需要大量的自訂代碼才可獲得。使用 PMF,您可以使伺服器端任務註冊其操作的目前狀態(表示為百分比形式,或是對剩餘時間的估計),還可以使用戶端服務通過 Ping 伺服器來讀取此狀態資訊。將狀態資訊下載到用戶端(帶外執行)後,更新使用者介面將變得非常輕鬆。
關於上個月的專欄的部分早期反饋中指出了兩處可能的改進。第一處指是否能夠將 PMF 用於停止正在執行的伺服器任務,第二處指尋找一種更好的方法來產生進度條標記。
將遠程任務形式化
遠程任務是在伺服器上執行的用於響應用戶端事件的一段代碼。ASP.NET AJAX 用戶端頁面觸發遠程任務的方法有以下三種:使得回傳由 UpdatePanel 控制項管理,在通過本地 Web 服務公開的應用程式後端直接調用一種方法,使用頁面方法。很快會有第四種方法:一種 Windows Communication Foundation (WCF) 服務。
一旦觸發了伺服器上的某項任務,用戶端將不再控制該任務。僅當由任務產生的響應已下載到用戶端並經過解析後,用戶端頁面才能夠重新控制操作。使用 PMF,您可以動態地讀取任務狀態,但不存在將資料動態傳送給伺服器任務的機制。
取消任務的簡便方法
使用 ASP.NET AJAX 取消遠程服務非常簡單,但是存在以下兩個限制。首先,該任務必須已通過 UpdatePanel 啟動。其次,伺服器上不需要任何額外工作來補償任務的突然中斷。圖 1 顯示了基於 UpdatePanel 的頁面樣本的原始碼,在該頁面中,會彈出帶有“取消”按鈕的進度模板,如圖 2 所示。單擊該按鈕可以取消操作。是否單擊?
圖 2 帶有取消按鈕的進度模板 (單擊該映像獲得較小視圖)
圖 2 帶有取消按鈕的進度模板 (單擊該映像獲得較大視圖)
1 中的 abortTask 函數所示,進度模板包含一個幫定到 JavaScript 代碼的用戶端按鈕。此函數的首要任務是檢索頁面要求管理器。在 Microsoft AJAX 用戶端庫中,PageRequestManager 對象是部分呈現的神經中樞。進行頁面初始化時,頁面要求管理器會為表單的提交事件註冊一個處理常式。這樣,每次回傳頁面時,都會調用要求管理器。此時,要求管理器會根據瀏覽器中所示產生請求主體的副本,並通過當前的 HTTP 執行器(預設指的是常見的 XMLHttpRequest 對象)運行該副本。
頁面要求管理器設定部分呈現的事件模型,並跟蹤正在執行的操作。如果存在任何擱置動作,則 Boolean 屬性 isInAsyncPostBack 將返回 true。
當使用者單擊圖 1 中所示的“取消”按鈕時,頁面要求管理器將通過其 abortPostBack 方法中止當前請求。頁面要求管理器是一個獨立對象,即所有調用都只能傳遞給一個執行個體。此情形的原因與部分呈現機制緊密相關。部分呈現由發送頁面請求組成,包括在伺服器上的整個常規處理過程(呈現階段除外)。此外,這意味著檢視狀態將被發送,並用於重新建立伺服器控制項的上次已知正常狀態。回傳和狀態變更事件是定期觸發的,檢視狀態即根據這些操作進行更新。然後,更新的檢視狀態會與進行了部分修改的標記一起發送回來。
由於檢視狀態的關係,需要對來自同一頁面的兩個非同步回傳調用進行序列化,並且每次只允許運行一個調用。由於這一原因,頁面要求管理器上的 abortPostBack 方法不必指出要停止哪一請求 — 因為至多有一個掛起的請求。
深入瞭解 abortPostBack 方法
讓我們簡要瞭解一下 PageRequestManager 類上 abortPostBack 方法的原始碼:
function Sys$WebForms$PageRequestManager$abortPostBack(){if (!this._processingRequest && this._request){this._request.get_executor().abort();this._request = null;}}
如果存在掛起的請求,則管理器將指示中止請求的執行器。執行器是從 Sys.Net.WebRequestExecutor 繼承的一個 JavaScript 類,負責發送請求和接收響應。
在 Microsoft AJAX 用戶端庫中,只有一個執行器類(Sys.Net.XMLHttpExecutor 類),它使用 XMLHttpRequest 對象執行請求。簡要說來,當上述代碼調用中止方法時,主要是告知 XMLHttpRequest 對象要中止。從另一個角度來講,它僅指示執行器用來接收響應資料的通訊端必須關閉。
現在,假設遠程任務在伺服器上執行破壞性操作。例如,假設為使用者提供了一次機會,使其能夠通過單擊一個按鈕來刪除資料庫表中的少量記錄。通過上述過程嘗試取消操作實際上不會停止伺服器操作。它所能實現的所有功能就是關閉用來接收確認訊息的通訊端。PageRequestManager 對象上的 abortPostBack 方法僅僅是一個用戶端方法,對伺服器中啟動並執行操作不會起到任何作用。
設計不間斷任務
要使中止請求對伺服器操作有效,任務必須是不間斷的。換句話說,任務必須定期檢查是否存在來自用戶端的指示任務退出的說明。PMF 的雙向版本將為您提供協助。
當我首次實現 PMF 時,架構的用戶端和伺服器元素共用一個通用資料容器,伺服器使用該容器寫入關於其進度的資料,用戶端使用該容器讀取此資料,以更新使用者介面。要使得伺服器代碼接收並處理動態用戶端反饋(如單擊“取消”按鈕),需要用到一些增強功能。
目前,進程伺服器 API 基於以下約定:
public interface IProgressMonitor{void SetStatus(int taskID, object message);string GetStatus(int taskID);bool ShouldTerminate(int taskID);void RequestTermination(int taskID);}
我已經添加了兩個新方法:ShouldTerminate 和 RequestTermination。前者返回一個 Boolean 值,表明是否應終止正在執行的任務。RequestTermination 方法為希望結束任務的用戶端指示 API 中的進入點。調用此方法時,它會在資料容器(ASP.NET 緩衝)中建立一個與任務相關的入口,ShouldTerminate 會檢查此入口以確定是否請求了中斷。
上文中定義的 IProgressMonitor 介面指示伺服器上某個應用程式的預期行為。您可以在可能使用不同資料容器的各種類中實現該介面。我使用名為 InMemoryProgressMonitor 的 ASP.NET 緩衝建立了一個樣本類(請參見圖 3)。圖 4 提供了每個方法的簡要說明。正如我在上個月介紹的那樣,遠程任務重複調用 SetStatus,以跟蹤其當前執行狀態並標記其進度。要支援動態中斷,相同的任務將定期調用 ShouldTerminate,以便在用戶端請求退出時獲得通知。圖 5 顯示了可監視的不間斷任務的典型結構。
Figure 5 中顯示的方法用於協調組成遠程任務的各個步驟。該任務可以是應用程式的中介層的一部分,可以作為工作流程實現。它在各步驟間必須是相互關聯的,以便用戶端插入到其中讀取狀態和請求終止。
用戶端代碼
用於觸發遠程任務的用戶端 JavaScript 代碼原樣保留了上個月中的大部分內容。您可以使用頁面或 Web 服務方法( 5 中的 ExecuteTask 方法)啟動任務,或在 UpdatePanel 地區中運行伺服器代碼:
<asp:UpdatePanel runat=”server” ID=”UpdatePanel1”><ContentTemplate><asp:Button runat=”server” ID=”Button1” Text=”Start Task ...”OnClick=”Button1_Click” /><hr /><asp:Label runat=”server” ID=”Label1” /><br /></ContentTemplate></asp:UpdatePanel>
在 Button1_Click 事件處理常式中,您定義了遠程任務,並使其調用進度監視器對象以及 SetStatus 和 ShouldTerminate 方法。要突然終止一個遠程任務,需要在進度模板中添加一個“取消”按鈕,它可以是 UpdateProgress 控制項,也可以是使用者定義的一個 <div> 塊。但此時,“取消”按鈕的單擊處理常式不指向頁面要求管理器中的 abortPostBack 方法,而是指向用戶端進度 API 中您自己的中止方法:
<script type=”text/javascript”>var progressManager = null;var taskID = null;function pageLoad() {progressManager = new Samples.PMF2.Progress();}function abortTask() {progressManager.abortTask(taskID);}...</script>
讓我們來看一下經過修改的用戶端進度 API。此 API 在 progress.js 檔案中進行編碼,因此必須連結到計劃使用不間斷或可監視任務的每個 ASP.NET AJAX 頁面:
<asp:ScriptManager ID=”ScriptManager1” runat=”server”EnablePageMethods=”true”><Scripts><asp:ScriptReference path=”random.js” /><asp:ScriptReference path=”progress.js” /></Scripts></asp:ScriptManager>
random.js 檔案與 progress.js 相關,定義了一種可產生隨機數量的任務的方法。要從用戶端跟蹤遠程任務的狀態,需要定期輪詢伺服器。要停止正在執行的任務,或者更確切地說,要發出一個請求以停止任務,需要調用一個伺服器方法,該方法是由進度監視器伺服器 API 作為應用程式後端的一部分發布的:
// Cancel the operationfunction Samples$PMF2$Progress$abortTask() {PageMethods.TerminateTask(_taskID, null, null, null);}
我選擇使用頁面方法發布此用戶端可調用函數。圖 6 提供了整個解決方案的架構視圖。
圖 6 雙向進度監視器架構 (單擊該映像獲得較小視圖)
圖 6 雙向進度監視器架構 (單擊該映像獲得較大視圖)
使用者單擊“取消”按鈕時,會觸發一個帶外調用以執行 TerminateTask 方法,此方法是作為頁面的後續代碼類上的頁面方法定義的。TerminateTask 方法在內部資料存放區(ASP.NET 緩衝)中建立一個與任務相關的入口。此入口是按帶有“Quit”尾碼的任務 ID 命名的。設計為不間斷的任務在執行過程中的各個階段檢查此入口。如果找到了該入口,則伺服器任務中止(請參見圖 7)。
圖 7 使用者單擊“取消”按鈕,結束伺服器任務 (單擊該映像獲得較小視圖)
圖 7 使用者單擊“取消”按鈕,結束伺服器任務 (單擊該映像獲得較大視圖)
通過此方式實現的任務取消將更有效。如果在 UpdatePanel 重新整理過程中僅中止用戶端回傳,所導致的全部結果將是關閉用於接收響應的用戶端通訊端。對伺服器上啟動並執行代碼不會產生任何影響。也不存在以編程方式停止對 Web 服務或頁面方法的遠程調用的內建方法。在這種情形下,JavaScript 代理類完全隱藏了正被用於推送調用的請求對象。雖然請求對象及其執行器具有中止方法,但在服務方法調用的上下文中找不到對它的引用。
最後,如果您需要允許遠程任務控制,進度列指示器模式是唯一可行的方法。您設定並行通道來監視狀態,並向正在啟動並執行任務傳遞更多資訊(如退出命令)。這種相同的體繫結構允許用戶端動態更改參數或請求其他動作。雙向進度監視器架構是雙工通道,伺服器任務及其 JavaScript 用戶端可使用該通道交換訊息形式的資料。
事務
至此,我已建立了一個架構用以監視和停止 ASP.NET AJAX 任務。關鍵需要注意的是,該架構只是通知任務使用者請求其終止。如果設計正確,任務會立刻停止並返回。但對於已完成的工作會如何處理呢?
一般情況下,當任務突然中斷時,應撤消它所做的所有更改並返回。但進度監視器架構無法實現此功能。不過,如果您將遠程任務封裝在事務中,即可在該任務中斷後立即復原它(假設已完成的任務僅涉及事務性資源,如 SQL Server 資料庫)。另一種選擇是使用工作流程。在該情形下,您將任務封裝在 TransactionScope 活動中,使用 Code 活動設定目前狀態並檢查是否有終止請求。如果任務必須終止,會引發異常並自動導致交易回復(請參見 2007 年 6 月的“領先技術”專欄,擷取有關 Windows Workflow Foundation 中事務性任務的更多資訊)。
遺憾的是,並非所有操作都可輕鬆地自動復原。一般情況下,您可以實現 TransactionScope 塊內部的任務,並安全有效地使用用於實現 ITransaction 介面的所有對象。如果您這樣做,則所有對象都將相應地復原或提交。其中每個對象都瞭解如何撤消其更改。
底線是從用戶端監視遠程任務的進度,此操作相對簡單,不會產生嚴重的負面影響。PMF 在其上增加了一些好的抽象,並提供了一些現成的編程工具。使任務不間斷會引發一些其他問題,當任務具有固有的事務語義時尤其如此。編寫代碼來只通知任務使用者請求其終止是遊戲中最簡單的一部分。真正複雜的部分在任務實現及其補償策略中。
產生進度條
在本文即將結束時,讓我們來瞭解一下如何使用 JavaScript 輕鬆產生進度條標記,並使其更易於維護。圖 7 中顯示的進度條是通過構建 HTML 表產生的,如下所示:
<table width=”100%”><tr><td>69% done</td></tr><tr><td bgcolor=”blue” width=”69%”> </td><td width=”31%”></td></tr></table>
此表包含兩行:附帶文本和儀錶。儀錶使用兩儲存格的行來呈現,其中的儲存格已給定背景色和成比例的寬度。
仔細查看上述標記,您至少能夠識別三個參數:面向使用者的訊息,要顯示的值,以及要對“已完成”和“未完成”地區使用的顏色。這樣就不再產生字串形式的標記,建立 JavaScript 類豈不更簡潔?圖 8 中的代碼顯示了 Samples.GaugeBar 類的主要方法。
該方法使用文本和百分比,返回包含兩行的 HTML 表。頂行僅顯示文本;底行分為兩個儲存格,分別帶有不同的顏色。
標記字串是使用 JavaScript 版本的 Microsoft .NET Framework StringBuilder 對象構建的。JavaScript StringBuilder 對象是在系統命名空間中定義的,其編程介面類似於其 .NET Framework 介面。向 StringBuilder 的內部緩衝區發送文本,然後使用 toString 方法輸出文本。
Samples.GaugeBar 類具有一個 generateMarkup 方法,以及“已完成”和“未完成”地區的背景色、附帶文本的前景色彩等屬性。由於效能方面的原因,此類作為單例來使用。這個類不是很大,但每次需要更新進度條時,仍然不必為其建立新執行個體。因此,您定義了該類的一個靜態執行個體,並添加了一些靜態方法和屬性:
Samples.GaugeBar.registerClass(‘Samples.GaugeBar’);Samples.GaugeBar._staticInstance = new Samples.GaugeBar();Samples.GaugeBar.generateMarkup = function(text, perc) {return Samples.GaugeBar._staticInstance.generateMarkup(text, perc);}
您有很多方法來自訂規杆。要更改顏色,請執行以下操作:
Samples.GaugeBar.set_DoneBackColor(“#ff00ee”);Samples.GaugeBar.set_TodoBackColor(“#ffccee”);
同樣,可以通過為表的“已完成”儲存格定義開始邊框樣式,添加美觀的 3D 效果,如下所示:
if (this._effect3D)builder.append(“ style=’border:outset white 2px;’”);
通過建立一個類來公開功能可大大提高 JavaScript 編程的可管理性。如果您在幾年前曾經不得不處理動態超文字標記語言 行為,就會理解我的意思。Microsoft 用戶端 AJAX 庫是一個很大的進步,因為使用此庫編寫複雜的 JavaScript 代碼會輕鬆得多。大多數 AJAX 專業人員可能都同意這一點:要實現強大的 AJAX 編程,必須具備更豐富的 JavaScript 功能。
將您向 Dino 提出的問題和意見發送至: cutting@microsoft.com.
NEW: Explore the sample code online! - or - 代碼下載位置: CuttingEdge2007_08.exe (167KB) |