Share via


ASP。網

介紹 ASP.NET Web 表單框架的導航

Graham Mendick

下載代碼示例

ASP.NET Web 表單框架的導航是一個託管在 navigation.codeplex.com 上的開源專案,該框架使您可以採用單元測試範圍來編寫 Web 表單代碼,同時遵循“切勿重複”(DRY) 原則,從而使 ASP.NET MVC 應用程式羡慕不已。

雖然存在一些放棄 Web 表單並改為使用 MVC 的現象,但因為有些開發人員越來越厭倦大量代碼隱藏不可進行單元測試,而使這一新框架(與資料綁定一起結合使用)成為用全新的眼光看待 Web 表單的有說服力的理由。

使用 ObjectDataSource 控制項進行的資料綁定自 Visual Studio 2005 以來已經形成,可允許對簡潔的代碼隱藏和資料檢索代碼進行單元測試,但是存在一些妨礙其實施的問題,例如,引發異常是將業務驗證失敗情況報告給 UI 的唯一方法。

為即將發佈 Visual Studio 所做的絕大部分 Web 表單開發工作均投入在了資料綁定上,借用 MVC 的模型綁定概念來解決這些問題,例如,引入模型狀態來解決業務驗證失敗的通信問題。 但是,在與導航和資料傳遞相關的資料綁定方面仍留有兩個難點,不過您可以使用 ASP.NET Web 表單框架的導航(以下簡稱“導航框架”)將它們輕鬆剔除。

第一個難點是沒有導航邏輯抽象,在 MVC 中它是被封裝在控制器方法返回類型中。 這會在資料綁定方法內重定向調用,從而阻止其進行單元測試。 第二個難點是 ObjectDataSource 參數的類型確定其值來自哪裡,例如,QueryStringParameter 總是從查詢字串中獲取其資料。 這樣會阻止在不同的導航上下文(如回發和非回發)中使用相同的資料來源,使得在讓人生畏的代碼隱藏中沒有實體邏輯。

導航框架可通過採用整體性的導航和資料傳遞方法來解決這些難點。 無論所執行導航的類型如何(可以是超連結、回發、AJAX 歷史記錄或單元測試),始終採用同一方式保留所傳遞的資料。 在以後的文章中,我將介紹這一點如何產生具有完全經過單元測試的資料檢索和導航邏輯的空代碼隱藏,以及對於啟用和禁用 JavaScript 的方案如何產生無需代碼複製的適合搜尋引擎優化 (SEO) 且逐步增強的單頁應用程式。 本文介紹了導航框架,並通過構建示例 Web 應用程式演示了有關導航框架的一些基本但很重要的概念。

示例應用程式

此示例 Web 應用程式是一項網上調查。 該調查只有兩個問題並在完成後顯示一則“thank you”(非常感謝)消息。 每個問題均由單獨的 ASPX 頁面表示,分別稱為 Question1.aspx 和 Question2.aspx,而“thank you”消息也有其單獨的頁面,稱為 Thanks.aspx。

問第一個問題是,"哪些 ASP 等。網路技術? 你正在使用",哪些可能的答案是"Web 表單"或"MVC"。因此,Question1.aspx 我會添加和硬編碼的選項按鈕答案的問題:

<h1>Question 1</h1>
<h2>Which ASP.NET technology are you currently using?</h2>
<asp:RadioButtonList ID="Answer" runat="server">
  <asp:ListItem Text="Web Forms" Selected="True" />
  <asp:ListItem Text="MVC" />
</asp:RadioButtonList>

第二個問題是“Are you using the Navigation for ASP.NET Web Forms framework?”(您使用的是 ASP.NET Web 表單框架的導航嗎?),其回答為“是”或“否”,可採用類似的方式進行標記。

開始使用

為使用導航框架而建立調查 Web 專案最直接的方法就是使用“NuGet Package Manager”(NuGet 套裝程式管理器)來安裝它。 從套裝程式管理器主控台內運行“Install-Package Navigation”命令將添加所需的引用和配置。 如果您未使用 Visual Studio 2010,則可在 navigation.codeplex.com/documentation 中找到手動安裝說明。

導航配置

導航框架可以看作是一個狀態機,其中每一個不同的狀態均表示一個頁面,從一個狀態移動到另一個狀態或在頁面之間進行導航稱為轉換。 這組預定義的狀態和轉換是在 NuGet 安裝所創建的 StateInfo.config 檔中進行配置的。 如果沒有此基礎配置,運行調查應用程式將引發異常。

因為狀態實質上就是頁面,所以該調查應用程式需要三個狀態,三個頁面每個頁面一個狀態:

<state key="Question1" page="~/Question1.aspx">
</state>
<state key="Question2" page="~/Question2.aspx">
</state>
<state key="Thanks" page="~/Thanks.aspx">
</state>

從現在起,我將使用其各自的鍵名來指代不同的狀態,即 Question1、Question2 和 Thanks,而不使用它們表示的頁面。

因為轉換說明了狀態之間可能的導航,所以調查應用程式需要兩個轉換。 一個是針對從 Question1 到 Question2 的導航,另一個是針對從 Question2 到 Thanks 的導航。 轉換表現為要退出狀態的子項,並通過其“to”(轉換到)屬性來指向要進入的狀態:

<state key="Question1" page="~/Question1.aspx">
  <transition key="Next" to="Question2"/>
</state>
<state key="Question2" page="~/Question2.aspx">
  <transition key="Next" to="Thanks"/>
</state>
<state key="Thanks" page="~/Thanks.aspx">
</state>

對話方塊是配置的最後元素並表示狀態的邏輯分組。 該調查應用程式只需要一個對話方塊,因為 Question1、Question2 和 Thanks 實際上是單一導航路徑。 對話方塊的“initial”(初始)屬性必須指向開始狀態,也就是說,Question1:

<dialog key="Survey" initial="Question1" path="~/Question1.aspx">
  <state key="Question1" page="~/Question1.aspx">
    <transition key="Next" to="Question2"/>
  </state>
  <state key="Question2" page="~/Question2.aspx">
    <transition key="Next" to="Thanks"/>
  </state>
  <state key="Thanks" page="~/Thanks.aspx">
  </state>
</dialog>

您會注意到,每個對話方塊、狀態和轉換均有一個鍵屬性。 我選擇使用頁面名稱來給狀態鍵命名,但沒有必要這樣做。 但要注意,所有的鍵在其父項內必須是唯一的;例如,您不能使用具有相同鍵的同級狀態。

將 Question1.aspx 作為起始頁,調查應用程式現在將以 Question1 狀態成功啟動。 但是,調查會一直滯留在此狀態,因為沒有辦法繼續進行到 Question2。

導航

將不同類型的 Web 表單導航分為兩大陣營是很有用的。 非回發陣營是控制項從一個 ASPX 頁面傳遞到另一個頁面的地方,採用的是超連結、重定向或轉移的形式。 回發陣營是控制項一直處於同一頁面的地方,採用的是回發、部分頁面請求或 AJAX 歷史記錄的形式。 後者在以後討論單頁介面模式的文章中會加以說明。 在本文中,我將重點介紹第一種導航類型。

要在頁面之間進行移動,必須建立一個 URL。 在 Visual Studio 2008 之前的版本中,唯一可用的選項是根據硬編碼的 ASPX 頁面名稱手動構建 URL,這樣會導致頁面之間發生緊密耦合的現象,從而使應用程式非常脆弱且難以維護。 路由的引入緩解了此問題,並用可配置的路由名稱代替了頁面名稱。 然而,路由在用於 Web 環境外部時會引發異常,這一事實加上路由對類比的抗拒使路由成為單元測試的一大絆腳石。

導航框架會保留路由提供的鬆散耦合,並且不會對單元測試造成任何妨礙。 與路由名稱的用法相似,這是上一節配置的對話方塊和轉換鍵(代碼中引用了這些鍵),而非硬編碼 ASPX 頁面名稱;導航到的狀態取決於各自的“initial”(初始)和“to”(轉換到)屬性。

返回到調查,“下一個”轉換鍵可用於從 Question1 移動到 Question2。 我會將“下一個”按鈕添加到 Question1.aspx,並將下麵的代碼添加到與其關聯的按一下處理常式中:

protected void Next_Click(object sender, EventArgs e)
{
  StateController.Navigate("Next");
}

傳遞到 Navigate 方法的鍵與 Question1 狀態所配置的子轉換相匹配,隨後即會顯示由“to”(轉換到)屬性標識的狀態,即 Question2。 我會將同一按鈕和處理常式添加到 Question2.aspx。 如果您運行該調查,就會發現您可以通過按一下“下一個”按鈕在這三個狀態中導航。

您可能已經注意到第二個問題是針對 Web 表單的提問,如此一來,如果第一個問題的回答選擇了“MVC”,則第二個問題就無關緊要了。 因此,需要更改代碼來解決此問題,即直接從 Question1 導航到 Thanks,完全跳過 Question2。

當前配置不允許從 Question1 導航到 Thanks,因為所列出的轉換只是到 Question2。 因此,我將通過在 Question1 狀態下添加第二個轉換來更改該配置:

<state key="Question1" page="~/Question1.aspx">
  <transition key="Next" to="Question2"/>
  <transition key="Next_MVC" to="Thanks"/>
</state>

有了這個新的轉換,就可以很容易地調整“下一個”按鈕按一下處理常式以根據所選的答案來傳遞不同的轉換鍵:

if (Answer.SelectedValue != "MVC")
{
  StateController.Navigate("Next");
}
else
{
  StateController.Navigate("Next_MVC");
}

不允許使用者更改答案的調查將不會是好的調查。 目前,沒有辦法返回到上一個問題(除了流覽器後退按鈕以外)。 要導航回上一頁面,您可能認為需要在 Thanks 下麵添加兩個轉換,分別指向 Question1 和 Question2,同時在 Question2 下麵添加另一個轉換,指向 Question1。 雖然這樣操作也可以奏效,但是這是不必要的,因為後退導航功能是導航框架本身附帶的。

痕跡導航是一組連結,使使用者可訪問每個當前所流覽頁面的上一頁面。 Web 表單將痕跡導航內置於其網站地圖功能中。 但是,由於網站地圖由固定的導航結構來表示,因此,對於所給定的頁面來說,這些痕跡始終相同,與所採用的路由無關。 它們無法處理調查中出現的有時排除 Question2 直接路由到 Thanks 的類似情況。 通過跟蹤發生導航時訪問的狀態,導航框架可構建實際所採用路由的痕跡記錄。

為了進行演示,我將超連結添加到 Question2.aspx,並在代碼隱藏中使用後退導航以程式設計方式設置其 NavigateUrl 屬性。 必須傳遞距離參數,以指示要返回到的狀態有多少,值 1 意味著緊鄰的前一狀態:

protected void Page_Load(object sender, EventArgs e)
{
  Question1.NavigateUrl = StateController.GetNavigationBackLink(1);
}

如果您運行應用程式且第一個問題的答案是“Web 表單”,您將看到 Question2.aspx 上的超連結會將您返回到第一個問題。

我會對 Thanks.aspx 執行同樣的操作,儘管這樣做有點棘手,因為需要兩個超連結(每個問題一個),且使用者可能無法同時看到這兩個問題(也就是說,如果他/她對第一個問題的回答是“MVC”)。 在決定如何設置超連結之前,可以先檢查先前的狀態數(請參見圖 1)。

圖 1 動態後退導航

protected void Page_Load(object sender, EventArgs e)
{
  if (StateController.CanNavigateBack(2))
  {
    Question1.NavigateUrl = StateController.GetNavigationBackLink(2);
    Question2.NavigateUrl = StateController.GetNavigationBackLink(1);
  }
  else
  {
    Question1.NavigateUrl = StateController.GetNavigationBackLink(1);
    Question2.Visible = false;
  }
}

現在,該調查功能正常,可允許您填寫問題和修改先前的回答。 但是,如果這些回答不投入使用,那麼調查幾乎沒有意義。 我將介紹回答資料是如何從 Question1 和 Question2 傳遞到 Thanks 的,在這裡回答資料將顯示在摘要表單中。

資料傳遞

和導航一樣,在 Web 表單中傳遞資料的方式也是多種多樣。 對於非回發導航,其中控制項從一個頁面傳遞到另一個頁面(通過超連結、重定向或轉移),可以使用查詢字串或路由資料。 對於回發導航,其中控制項一直處於同一頁面(通過回發、部分頁面請求或 AJAX 歷史記錄),控制項值、視圖狀態或事件參數可能是候選物件。

在 Visual Studio 2005 之前的版本中,代碼隱藏承擔處理此傳入的資料,因此,它們充滿了值提取和類型轉換邏輯。 資料來源控制項和選擇參數(在下一版本的 Visual Studio 中為“值提供程式”)的引入大大減輕了它們的負擔。 然而,這些選擇參數受限於特定資料來源,它們無法根據導航上下文動態切換源。 例如,它們不能從控制項或從查詢字串(具體取決於它是否為回發)中有選擇地檢索其值。 處理這些限制會導致代碼回漏到代碼隱藏,從而使問題退回到具有過多且不可測試的代碼隱藏的起點。

導航框架可通過提供單一資料來源(稱為狀態資料,無論涉及何種導航)來避免出現此類問題。 第一次載入頁面時,狀態資料將使用導航期間傳遞的任何資料來填充,方法類似于查詢字串或路由資料。 但是,顯著的差別在於狀態資料不是唯讀資料,因此當發生後續回發導航時,狀態資料可以進行更新以反映頁面當前最新內容。 當我在本部分末尾重新訪問導航時,將證明這一點是非常有益處的。

我將更改調查,以便將第一個問題的回答傳遞到 Thanks 狀態,在這裡資料將重新顯示在使用者面前。 在通過鍵值對集合進行導航的同時傳遞資料,這稱為 NavigationData。 我將更改 Question1.aspx 的“下一個”按一下處理常式,以便將第一個問題的回答傳遞到下一個狀態:

NavigationData data = new NavigationData();
data["technology"] = Answer.SelectedValue;
if (Answer.SelectedValue != "MVC")
{
  StateController.Navigate("Next", data);
}
else
{
  StateController.Navigate("Next_MVC", data);
}

此 NavigationData 是在導航期間傳遞的,用來初始化通過 StateContext 物件上的 Data 屬性供下一個狀態使用的狀態資料。 我會將標籤添加到 Thanks.aspx,並將該標籤的 Text 屬性設置為顯示傳入的回答:

Summary.Text = (string) StateContext.Data["technology"];

如果您運行該調查,則會注意到僅在第一個問題的回答是“MVC”時才顯示此摘要資訊;從不會顯示“Web 表單”的回答。 這是因為 NavigationData 只能用於下一個狀態,但不可用於後續導航產生的所有狀態。 因此,“Web 表單”的回答存在於 Question2 狀態資料中,但在到達 Thanks 時不可用。 解決此問題的一個方法是更改 Question2.aspx,以便它將回答中繼到第一個問題,也就是說,在 Question2.aspx 導航時將回答從其狀態資料中取出並將該回答傳遞到 Thanks:

NavigationData data = new NavigationData();
data["technology"] = StateContext.Data["technology"];
StateController.Navigate("Next", data);

這種方法並不理想,因為它將 Question1 和 Question2 緊密耦合到一起,強迫後者狀態注意前者正在傳入的資料。 例如,如果不對 Question2.aspx 進行相應的更改,就無法在第一個和第二個問題之間插入新的問題。 前瞻性的實施包括創建新的包含所有的 Question2 狀態資料的 NavigationData;可通過向 NavigationData 構造函數傳遞 true 來實現這一點:

NavigationData data = new NavigationData(true);
StateController.Navigate("Next", data);

狀態資料與查詢字串或路由資料之間的另一個關鍵的區別在於,採用狀態資料您並不局限于傳遞字串。 我會將一個布林值傳遞給 Thanks(即用 true 值對應于“是”),而不是像對 Question1 和 Question2 所做的那樣以字串的形式傳遞答案:

NavigationData data = new NavigationData(true);
data["navigation"] = Answer.SelectedValue == "Yes" ?
true : false;
StateController.Navigate("Next", data);

您可以看到,在從 Thanks 狀態資料中檢索它後將保留其資料類型:

Summary.Text = (string) StateContext.Data["technology"];
if (StateContext.Data["navigation"] != null)
{
  Summary.Text += ", " + (bool) StateContext.Data["navigation"];
}

該調查已完成,但還有一個問題:使用後退導航超連結時問題的回答不會被保留。 例如,當從 Thanks 返回到 Question1 時,上下文即會丟失,因此預設的“Web 表單”選項按鈕始終處於選中狀態,這與所給答案無關。

在上一部分中,您瞭解到後退導航相對於靜態網站地圖痕跡的優點。 網站地圖生成的痕跡的另一個限制就是它們不攜帶任何資料。 這意味著跟隨它們可能會丟失上下文資訊。 例如,它們在從 Thanks 返回到 Question1 時無法傳遞先前選擇的“MVC”答案。 通過跟蹤與發生導航時訪問的狀態相關聯的狀態資料,導航框架可構建上下文相關的痕跡記錄。 在後退導航期間,此狀態資料將被還原,從而允許重新創建與之前完全相同的頁面。

借助上下文相關的後退導航,我可以更改調查,從而在重新訪問狀態時使答案得到保留。 第一階段是在離開頁面之前,將回答設置到“下一個”按一下處理常式中的狀態資料中:

StateContext.Data["answer"] = Answer.SelectedValue;

現在,在重新訪問 Question1 或 Question2 時,狀態資料將包含之前選擇的答案。 這樣,採用 Page_Load 方法檢索此答案和預先選擇相關的選項按鈕就是一件非常簡單的事情:

protected void Page_Load(object sender, EventArgs e)
{
  if (!Page.IsPostBack)
  {
    if (StateContext.Data["answer"] != null)
    {
      Answer.SelectedValue = 
        (string)StateContext.Data["answer"];
    }
  }
}

調查現已完成,不容易受到在使用者按下流覽器後退按鈕(或同時打開多個流覽器視窗)時 Web 應用程式中經常遇到的錯誤的影響。 在特定于頁面的資料保存在伺服器端會話中時,通常會出現此類問題。 雖然只有一個會話物件,但是單個頁面可能會有多個“當前”版本。 例如,使用後退按鈕從流覽器緩存中檢索“陳舊”版本的頁面可能會造成用戶端和伺服器出現不同步情況。 導航框架就不會面臨這類問題,因為它沒有任何伺服器端緩存。 實際上,狀態、狀態資料和痕跡記錄均保留在 URL 中。 然而,這意味著使用者可以通過編輯 URL 來對這些值進行更改。

製作 MVC 嫉妒

之前我說過您可以使用導航框架創建 Web 表單代碼,使 MVC 羡慕嫉妒恨。 在一番大膽的言辭之後,您可能通過調查示例應用程式感覺到了一點小小的改變,因為這可能使 MVC 忍氣吞聲地避免其代碼隱藏發出的不盡人意的氣息。 但是請不要絕望;這僅僅是介紹核心概念。 後續文章將重點介紹體系結構的完整性,尤其會將著重點放在單元測試和 DRY 原則上。

在第二期中,我將採用空代碼隱藏來構建一個資料綁定示例並完成單元測試代碼範圍。 此範圍甚至會包括在 MVC 應用程式中號稱測試老大難的導航代碼。

在第三期中,我將構建 SEO 友好的單頁應用程式。 在這裡,將使用逐步增強的方式,在啟用 JavaScript 時採用 ASP.NET AJAX,在禁用 JavaScript 時妥善降級,在這兩種情況下使用的資料綁定方法相同。 同樣,在 MVC 應用程式中很難辦到這一點。

如果這激發了您的興趣,您迫不及待地要嘗試一些更多高級功能,請務必從 navigation.codeplex.com 中下載全面的功能文檔和示例代碼。

Graham Mendick 是 Web 表單最忠實的粉絲,希望向大家展示 Web 表單也能夠像 ASP.NET MVC 一樣擁有合理的架構。他撰寫了 ASP.NET Web 表單框架的導航,他相信將其與資料綁定結合使用一定能給 Web 表單注入新的活力。

衷心感謝以下技術專家對本文的審閱:Damian Edwards