MVC

介绍 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。

第一个问题是“Which ASP.NET technology are you currently using?”(您现在正在使用哪种 ASP.NET 技术?),该问题可能的回答要么是“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