65.9K
CodeProject 正在变化。 阅读更多。
Home

Project Silk 导航 for ASP.NET Web Forms

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2011 年 12 月 1 日

CPOL

9分钟阅读

viewsIcon

33227

downloadIcon

1092

使用 Navigation for ASP.NET Web Forms codeplex 项目将 Project Silk 移植到 ASP.NET Web Forms,重点关注导航方面。

引言

Project Silk 是微软提供的用于构建跨浏览器 Web 应用程序的示例,使用 ASP.NET MVC3 和 jQuery 编写。本文演示了使用 ASP.NET Web Forms 构建会更简单,代码库会更小、更清晰、更易于维护。

我们将重点关注以下 Project Silk 导航设计原则:

  1. 必须使用 Ajax 防止整页刷新,同时保留可用的浏览器后退按钮和书签特定状态页面的能力。
  2. 在禁用 JavaScript 的情况下,网站也必须能够工作。

Project Silk 需要大量复杂的 JavaScript 代码来实现第一点,以及大量重复的控制器和视图代码来实现第二点。

本文表明,通过使用 ASP.NET Web Forms,无需编写任何 JavaScript,也无需任何重复代码即可实现这两点。

背景

您应该对 Project Silk 有基本的了解:http://silk.codeplex.com/

您应该对 Navigation for ASP.NET 框架有中等水平的了解:http://navigation.codeplex.com/。可以通过 NuGet 安装,包名为 'Navigation'。

尽管我们处理的是 Web Forms 应用程序,但其代码和文件夹结构与典型的 MVC 应用程序相同,因为它包含视图模型、控制器和存储库。为了专注于导航方面,非导航代码已尽可能简化,例如,存储库使用内存集合而不是数据库;控制器不包含业务逻辑;并且存储库模型类被用作视图模型。

数据绑定

该应用程序包含一个仪表板页面,以及车辆、加油和提醒三个功能区域的各一个 List 和一个 Details 用户控件。这些视图和控制器之间的所有通信都通过数据绑定进行。每个视图包含一个 ListViewFormView,通过 ObjectDataSource 数据绑定到控制器方法。以这种方式使用数据绑定,我们遵循了良好的编码实践,特别是,它实现了空代码隐藏和可测试的控制器。

我们将从 FillupController 上的 ListDetails 方法开始。这些是非常基本的方法,只是过滤存储库的加油列表。

public IEnumerable<FillupEntry> List(int vehicleId)
{
    return _Repository.Fillups.Where(f => f.VehicleId == vehicleId).ToList();
}

public FillupEntry Details(int vehicleId, int fillupId)
{
    return _Repository.Fillups.Single(f => f.VehicleId == vehicleId && 
    f.FillupId == fillupId);
}  

这两个方法都需要传递参数,因此要对其进行数据绑定,必须指定 ObjectDataSourceSelectParameters。然而,所有内置的 ASP.NET 参数类型都不适用,因为我们的参数需要在不同场景下从不同来源获取。例如,在禁用 JavaScript 时,QueryString 参数很合适,但在使用 ASP.NET Ajax 时,QueryString 参数就不够用了。

Navigation for ASP.NET Web Forms 框架解决了这个问题。该框架有自己的数据存储,称为 NavigationData,具有以下主要功能:

  1. NavigationData 在页面首次加载时从查询字符串初始化。
  2. NavigationData 被保存在 ControlState 中,因此在回发之间会被保留。
  3. NavigationData 可以通过编程方式管理(通过 StateContext 类的动态 Bag 属性)。

这使其成为理想的选择,因为我们无需担心数据是从原始查询字符串值(禁用 JavaScript)获取的,还是作为回发(ASP.NET Ajax)的结果更新的。Navigation 框架提供了 NavigationData 参数供声明性数据绑定使用,因此 Details 方法的 ObjectDataSource 变为:

<asp:ObjectDataSource ID="DetailsSource" runat="server" 
    TypeName="MileageStats.Web.Controllers.FillUpController" SelectMethod="Details">
    <SelectParameters>
        <nav:NavigationDataParameter Name="vehicleId" />
        <nav:NavigationDataParameter Name="fillupId" />
    </SelectParameters>
</asp:ObjectDataSource> 

当页面首次加载时,NavigationData 将为空,因此我们需要手动设置车辆和加油 ID,以便 NavigationData 参数值能够被填充。由于页面上用户控件的顺序,List 方法在 Details 方法之前调用,特别是车辆 List 方法是第一个调用的。因此,我们将在每个控制器的 List 方法中设置一个初始值,即 VehicleController 的 List 将初始化车辆 ID,FillupController 将初始化加油 ID,等等。这确保了在每个控制器方法执行之前,requisite NavigationData 都已填充。下面显示的是 List 方法以编程方式在 NavigationData 中设置加油 ID:

public IEnumerable<FillupEntry> List(int vehicleId)
{
    IEnumerable<FillupEntry> fillups = _Repository.Fillups.Where
            (f => f.VehicleId == vehicleId).ToList();
    StateContext.Bag. fillupId = fillups.First().FillupId;
    return fillups;
} 

为了使 List 中的加油项可选择,我们需要添加一个超链接,指向此仪表板页面,并传递当前的车辆 ID 和选定的加油 ID。Navigation 框架提供了一个 NavigationHyperLink 控件来帮助在页面之间进行此类移动和数据传递。NavigationHyperLink 的重要属性设置包括:

  1. ToData - 包含要传递的 *已更改* 的 NavigationData,即因为加油 ID 不同但车辆 ID 未更改,所以我们只需要传递加油 ID。
  2. Direction - 设置为 Refresh,因为我们希望超链接指向当前所在的页面。
  3. IncludeCurrentData - 设置为 true 以将当前的 NavigationDataToData 一起包含,特别是这意味着当前的车辆 ID 将被传递。

因此,将 NavigationHyperLink 添加到我们的加油 ListView 中,得到:

<asp:ListView ID="FillUps" runat="server" DataSourceID="ListSource">
    <LayoutTemplate>
        <asp:PlaceHolder ID="itemPlaceholder" runat="server" />
    </LayoutTemplate>
    <ItemTemplate>
        <nav:NavigationHyperLink ID="DetailsLink" runat="server" 
    ToData='<%# new NavigationData(){{ "fillupId", Eval("FillupId") }} %>' 
    Direction="Refresh" IncludeCurrentData="true" Text="Select"/>
    </ItemTemplate>
</asp:ListView> 

点击此加油超链接将导致选定的加油 ID 和当前车辆 ID 在查询字符串中传递,因此 NavigationData 将被正确初始化。然而,上面添加到 List 方法中的初始化 ID 的逻辑现在存在问题,因为它将覆盖传递的 NavigationData。要解决此问题,我们需要更改 List 方法,使其仅在 ID 未被填充时才进行初始化。这需要将 ID 作为参数添加到 List 方法中,以便进行检查(因此,我们还需要将相应的 NavigationData 参数添加到 ListObjectDataSource 中)。

public IEnumerable<FillupEntry> List(int vehicleId, int? fillupId)
{
    IEnumerable<FillupEntry> fillups = _Repository.Fillups.Where
            (f => f.VehicleId == vehicleId).ToList();
    if (!fillupId .HasValue)
        StateContext.Bag.fillupId = fillups.First().FillupId;
    return fillups;
} 

我们解决了在给定车辆的加油项之间移动时的问题,但在查看加油项时切换车辆时存在另一个问题。这是因为在 NavigationData 中设置的加油 ID 将指向前一辆车的加油项,而对于新选择的车辆则无效。要解决此问题,我们将更改 List 方法,使其检查该车辆是否存在加油 ID,如果不存在则进行初始化。

public IEnumerable<FillupEntry> List(int vehicleId, int? fillupId)
{
    IEnumerable<FillupEntry> fillups = _Repository.Fillups.Where
            (f => f.VehicleId == vehicleId).ToList();
    if (!fillupId .HasValue || fillups.Where(f => f.FillupId == 
                fillupId).FirstOrDefault() == null)
        StateContext.Bag.fillupId = fillups.First().FillupId;
    return fillups;
} 

只需少量代码,我们就拥有了一个在禁用 JavaScript 的情况下也能工作的仪表板。

然而,ListDetails 方法始终会执行,而不管正在查看哪个功能区域,即,即使在查看提醒时,FillupController 上的 ListDetails 方法仍然运行。这可能导致性能问题。幸运的是,我们可以利用 NavigationData 的另一个功能,即 layoutlayout 由包含它的仪表板页面及其关联的控制器维护,并跟踪正在查看哪个功能区域。我们只需要将 layout 参数添加到所有控制器方法(以及所有 ObjectDataSources 对应的 NavigationDataParameter)中,然后可以检查它以决定是否值得查询存储库。

public FillupEntry Details(string layout, int vehicleId, int fillupId)
{
    if (layout != "fillups") return null;
    return _Repository.Fillups.Single
    (f => f.VehicleId == vehicleId && f.FillupId == fillupId);
} 

Ajax

要为代码添加 Ajax 功能,我们将利用 Navigation 框架的渐进增强功能以及 ASP.NET Ajax。NavigationHyperLinks 默认渲染为普通链接,但通过将其 PostBack 属性设置为 true,它们将渲染一个 onclick 属性,因此如果启用了 JavaScript,它们将回发。

<a href="https://codeproject.org.cn/Dashboard" onclick="__doPostBack('Link');
return false" id="Link">Select</a> 

传统上,我们的代码必须知道执行的是 GET 还是 POST 请求,以便正确地获取其数据,例如,前者从查询 string 获取,后者从控件获取。然而,我们没有这个问题,因为我们正在使用 NavigationData,并且无论是否启用 JavaScript,它都会被相同地填充。因此,我们可以将 NavigationHyperLinksPostBack 属性设置为 true,而无需更改任何控制器代码。

<nav:NavigationHyperLink ID="DetailsLink" runat="server" 
    ToData='<%# new NavigationData(){{ "fillupId", Eval("FillupId") }} %>' 
    Direction="Refresh" IncludeCurrentData="true" Text="Select" PostBack="true"/> 

现在 NavigationHyperLinks 正在回发,添加 Ajax 功能变得很简单。将每个 FormViewListView 包装在 UpdatePanel 中将把所有请求都变成部分页面请求。

<asp:UpdatePanel ID="Content" runat="server" UpdateMode="Conditional">
    <ContentTemplate>
        <asp:ListView ID="FillUps" runat="server" DataSourceID="ListSource">
            <LayoutTemplate>
                <asp:PlaceHolder ID="itemPlaceholder" runat="server" />
            </LayoutTemplate>
            <ItemTemplate>
                <nav:NavigationHyperLink ID="DetailsLink" runat="server" 
        ToData='<%# new NavigationData(){{ "fillupId", Eval("FillupId") }} %>' 
        Direction="Refresh" IncludeCurrentData="true" Text="Select"' 
        PostBack="true"/>
            </ItemTemplate>
        </asp:ListView>
    </ContentTemplate>
</asp:UpdatePanel> 

我们几乎完成了 Ajax 功能的添加,但还有一个问题。点击加油项选择超链接不会导致加油项详细信息发生变化。这是因为超链接位于一个更新面板内,而详细信息位于另一个更新面板内,因此后者更新面板不会被自动更新。由于 Details FormView 在其任何 NavigationData 参数发生更改时都会进行数据绑定,我们将向 FormViewDataBound 事件添加一个监听器,并手动更新 UpdatePanel 的内容。

protected void Details_DataBound(object sender, EventArgs e)
{
    Content.Update();
} 

只需少量代码,我们就将我们的仪表板 Ajax 化了。

后退按钮和书签

ASP.NET Ajax 历史记录可用于添加后退按钮支持,并且由于它使用 URL 哈希来存储应用程序状态,因此会自动支持书签。历史记录点可以以编程方式添加,当按下后退按钮时,ScriptManager 会引发一个 Navigate 事件,传递历史状态数据。只要将此数据设置回 NavigationData,当前的控制器代码将自动工作,无需任何更改。Navigation 框架会为我们完成这个工作,只要我们调用它的 AddHistoryPointNavigateHistory 方法。

我们希望在用户每次切换车辆或功能区域时都添加历史记录。在 NavigationData 方面,这对应于车辆 ID 或布局的更改,因此我们将更改仪表板的 DataBound 事件监听器来调用 AddHistoryPoint

protected void Details_DataBound(object sender, EventArgs e)
{
    Content.Update();
    if (ScriptManager.GetCurrent(this).IsInAsyncPostBack && 
        !ScriptManager.GetCurrent(this).IsNavigating)
    {
        NavigationData toData = new NavigationData();
        toData.Bag.vehicleId = StateContext.Bag.vehicleId;
        toData.Bag.layout = StateContext.Bag.layout;
        StateController.AddHistoryPoint(this, toData, null);
    }
} 

然后,为了恢复 NavigationData,我们只需向 ScriptManagerNavigate 事件添加一个监听器,并将状态数据传递给 NavigateHistory 方法。

protected void ScriptManager_Navigate(object sender, HistoryEventArgs e)
{
    if (ScriptManager.IsInAsyncPostBack)
    {
        StateController.NavigateHistory(e.State);
    }
} 

只需少量代码,我们就为仪表板添加了后退按钮和书签支持。

结论

完成了!Project Silk 已转换为 ASP.NET Web Forms,复杂度和代码量大大降低。尽管我们在这里没有展示,但控制器代码可以进行完全单元测试。代码非常易于维护和增强,例如,要为用户在加油项之间切换时添加历史记录点,只需添加一行代码(在 Project Silk 中这会更难)。

尽管这是一个简化的仪表板版本(例如,它不支持编辑),但我已经 fork 了 Project Silk 代码,并在 NavigationSilk 上实现了一个完整的仪表板(请确保将新的 MileageStats.Web.Navigation 项目设置为启动项目,并将 Default.aspx 设置为启动页面)。

历史

  • 2011 年 12 月 1 日:首次发布
© . All rights reserved.