Project Silk 导航 for ASP.NET Web Forms





5.00/5 (8投票s)
使用 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 导航设计原则:
- 必须使用 Ajax 防止整页刷新,同时保留可用的浏览器后退按钮和书签特定状态页面的能力。
- 在禁用 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
用户控件。这些视图和控制器之间的所有通信都通过数据绑定进行。每个视图包含一个 ListView
或 FormView
,通过 ObjectDataSource
数据绑定到控制器方法。以这种方式使用数据绑定,我们遵循了良好的编码实践,特别是,它实现了空代码隐藏和可测试的控制器。
我们将从 FillupController
上的 List
和 Details
方法开始。这些是非常基本的方法,只是过滤存储库的加油列表。
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);
}
这两个方法都需要传递参数,因此要对其进行数据绑定,必须指定 ObjectDataSource
的 SelectParameters
。然而,所有内置的 ASP.NET 参数类型都不适用,因为我们的参数需要在不同场景下从不同来源获取。例如,在禁用 JavaScript 时,QueryString
参数很合适,但在使用 ASP.NET Ajax 时,QueryString
参数就不够用了。
Navigation for ASP.NET Web Forms 框架解决了这个问题。该框架有自己的数据存储,称为 NavigationData
,具有以下主要功能:
NavigationData
在页面首次加载时从查询字符串初始化。NavigationData
被保存在ControlState
中,因此在回发之间会被保留。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
的重要属性设置包括:
ToData
- 包含要传递的 *已更改* 的NavigationData
,即因为加油 ID 不同但车辆 ID 未更改,所以我们只需要传递加油 ID。Direction
- 设置为Refresh
,因为我们希望超链接指向当前所在的页面。IncludeCurrentData
- 设置为true
以将当前的NavigationData
与ToData
一起包含,特别是这意味着当前的车辆 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
参数添加到 List
的 ObjectDataSource
中)。
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 的情况下也能工作的仪表板。
然而,List
和 Details
方法始终会执行,而不管正在查看哪个功能区域,即,即使在查看提醒时,FillupController
上的 List
和 Details
方法仍然运行。这可能导致性能问题。幸运的是,我们可以利用 NavigationData
的另一个功能,即 layout
。layout
由包含它的仪表板页面及其关联的控制器维护,并跟踪正在查看哪个功能区域。我们只需要将 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,它都会被相同地填充。因此,我们可以将 NavigationHyperLinks
的 PostBack
属性设置为 true
,而无需更改任何控制器代码。
<nav:NavigationHyperLink ID="DetailsLink" runat="server"
ToData='<%# new NavigationData(){{ "fillupId", Eval("FillupId") }} %>'
Direction="Refresh" IncludeCurrentData="true" Text="Select" PostBack="true"/>
现在 NavigationHyperLinks
正在回发,添加 Ajax 功能变得很简单。将每个 FormView
和 ListView
包装在 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
参数发生更改时都会进行数据绑定,我们将向 FormView
的 DataBound
事件添加一个监听器,并手动更新 UpdatePanel
的内容。
protected void Details_DataBound(object sender, EventArgs e)
{
Content.Update();
}
只需少量代码,我们就将我们的仪表板 Ajax 化了。
后退按钮和书签
ASP.NET Ajax 历史记录可用于添加后退按钮支持,并且由于它使用 URL 哈希来存储应用程序状态,因此会自动支持书签。历史记录点可以以编程方式添加,当按下后退按钮时,ScriptManager
会引发一个 Navigate
事件,传递历史状态数据。只要将此数据设置回 NavigationData
,当前的控制器代码将自动工作,无需任何更改。Navigation
框架会为我们完成这个工作,只要我们调用它的 AddHistoryPoint
和 NavigateHistory
方法。
我们希望在用户每次切换车辆或功能区域时都添加历史记录。在 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
,我们只需向 ScriptManager
的 Navigate
事件添加一个监听器,并将状态数据传递给 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 日:首次发布