ASP.NET Dynamic Data 中的多功能详情页模板






4.85/5 (20投票s)
一个可以取代所有详情、编辑和插入页面的模板。
引言
ASP.NET Dynamic Data Framework 提供的页面模板和脚手架功能,可以通过将通用自定义工作限制在四个或五个页面模板来节省大量时间和精力,无论我们的数据模型中有多少实体。Dynamic Data Framework 的默认路由模式,在新的 Dynamic Data 站点中被称为独立页面模式,即为列表、详情(查看只读记录)、编辑和插入操作各有一个页面。另一种预打包的路由模式,即组合页面模式,则使用一个页面来处理列表、详情、编辑和插入操作。组合页面模式的站点模型只是在同一页面上使用一个网格和一个详情面板,而前者有一个列表、网格或页面,我们可以从中导航到编辑或插入页面。本文重点介绍如何为编辑、查看和插入操作使用一个单一的、以详情视图为导向的页面模板。
路由
独立页面模式
上面提到的两种路由模式都包含在Global.asax 的样板代码中,无论何时你在 Visual Studio 中创建一个新的 Dynamic Data 站点。独立页面模式默认是活动的。
// The following statement supports separate-page mode,
// where the List, Detail, Insert, and
// Update tasks are performed by using separate pages.
// To enable this mode, uncomment the following
// route definition, and comment out the route definitions
// in the combined-page mode section that follows.
routes.Add(new DynamicDataRoute("{table}/{action}.aspx") {
Constraints = new RouteValueDictionary(new { action = "List|Details|Edit|Insert" }),
Model = model
});
按照最近确立的惯例,注释有点令人困惑,因为它们指示你“取消注释以下路由定义”,而实际上它已经取消注释了。我想,如果你在某个时候注释掉了路由定义,并且以后忘记了它是如何工作的,这可能会有帮助。
此路由定义允许操作替换参数的四个值,并且不指定任何视图。这意味着每个操作都将被路由到它自己的视图。例如,一个带有 URL Customers/List.aspx 的请求将被路由到 List.aspx 页面,该页面将显示 Customers 表中的记录列表,而一个 URL Customers/Insert.aspx 将导致请求被路由到 Insert.aspx 页面,以插入一条新的 Customer 记录。
ASP.NET 路由的一个好处是它允许我们将“漂亮”的请求 URL(即没有文件扩展名)映射到带有文件扩展名的真实视图,因此我们可以定义一个路由,其中 URL 只需要是 Customers/List(没有“.aspx”)就可以仍然路由到 List.aspx 页面。我完全不知道为什么 Microsoft 没有在 Dynamic Data Framework 中做到这一点,但将在未来的文章中进行研究。
组合页面模式很容易激活,但在这种情况下,注释是正确的;代码确实需要取消注释。我为了清晰起见已经这样做了,但为了本文的目的,并且由于我们关注的是独立页面模式,它实际上应该保持注释状态。
组合页面模式
// The following statements support combined-page mode,
// where the List, Detail, Insert, and Update tasks are performed by using the same page.
// To enable this mode, uncomment the following routes and comment out the route definition
// in the separate-page mode section above.
//routes.Add(new DynamicDataRoute("{table}/ListDetails.aspx") {
// Action = PageAction.List,
// ViewName = "ListDetails",
// Model = model
//});
//routes.Add(new DynamicDataRoute("{table}/ListDetails.aspx") {
// Action = PageAction.Details,
// ViewName = "ListDetails",
// Model = model
//});
这些路由定义导致所有请求都被路由到 ListDetails.aspx 页面,该页面同时在同一页面上显示列表和编辑面板。这种模式非常笨拙,而且如果没有对 ListDetails.aspx 进行大量自定义,它实际上只适用于你需要维护 UI 的情况。
如果我们查看独立页面模式使用的四个页面模板,我们可以看到 Details.aspx、Edit.aspx 和 Insert.aspx 几乎完全相同。Details.aspx(显示只读数据)甚至与 Edit.aspx 具有完全相同的 ValidationSummary
和 DynamicValidator
控件!
看来,即使是 Dynamic Data 开发团队有时也会进行一些“复制粘贴编码”。
如果我们的项目不需要对这些页面模板进行大量自定义,为什么不将它们合并到一个页面中呢?即使只维护四个页面所需的工作量微乎其微,将所有布局和功能集中到一个页面中也能减少错误发生的可能性,例如我上面提到的复制粘贴问题。在没有过多考虑这个新页面模板的名称的情况下,我们称它为 DetailsEditInsert.aspx,然后继续。
实现我们的新页面模板
我假设本文的大多数读者都知道如何创建一个新的 Dynamic Data 站点,向其中添加数据模型,并在 Global.asax 中注册该数据模型。如果您想了解 Dynamic Data 的介绍,或有关启动新站点的演练,请参考 Microsoft Dynamic Data 网站,或进行搜索并找到许多涵盖基础知识的出色文章。
我将从创建一个使用数据模型和普遍存在的 Northwind 数据库的新 Dynamic Data 网站开始。您的项目是否与我在这里使用的项目或示例代码完全相同并不重要。只要您在工作的 Dynamic Data 站点中拥有一些脚手架化的表,您就能跟上我的步伐。这是 Dynamic Data Framework 及其模板化功能的美妙之处之一。
添加新页面
我创建了一个名为 DynamicDataArticles 的新解决方案,并在其中添加了一个名为 AllActions 的新网站。
让我们在 Dynamic Data\PageTemplates 文件夹中创建新的页面模板。
其他页面
现在,让我们仔细看看其他详情页面:Details.aspx、Edit.aspx 和 Insert.aspx。正如我之前提到的,它们都非常相似。这些页面都共享一个基本的结构和一套关键控件。
- 一个
DynamicDataManager
控件,位于一个UpdatePanel
之上,该UpdatePanel
包含所有其他动态数据和标准控件。DynamicDataManager
可能不在UpdatePanel
内部。 - 一个
DetailsView
和一个EntityDataSource
控件。我们关注的是DetailsView
。 - 用于 AJAX 功能的
UpdatePanel
和ScriptManageProxy
控件。
一个非常有信息量的起点是复制 Details.aspx 的所有布局和代码到 DetailsEditInsert.aspx。当然,您也可以直接复制 Details.aspx 并重命名它。让我们告诉 Dynamic Data 将所有详情类(非列表)请求路由到我们的新页面。请仔细修改 Global.asax 中单一的默认路由定义,如下所示。
routes.Add(new DynamicDataRoute("{table}/{action}.aspx") {
Constraints = new RouteValueDictionary(
new { action = "List|Details|Edit|Insert" }),
Model = model
});
routes.Add(new DynamicDataRoute("{table}/{action}.aspx") {
Constraints = new RouteValueDictionary(new { action = "Details|Edit|Insert" }),
ViewName = "DetailsEditInsert",
Model = model
});
可以看到,我们已经将此路由定义中允许的操作限制为仅三个详情类操作。这是因为我们现在指定了一个视图名称,意味着此定义的所有操作都将被路由到 DetailsEditInsert.aspx 视图,而以前,每个操作都被路由到其对应的视图。将列表操作也路由到 DetailsEditInsert.aspx 是不合适的,因此我们将其移出,添加一个新的、独立的路由定义,专门用于列表操作。
routes.Add(new DynamicDataRoute("{table}/List.aspx")
{
ViewName = "List",
Action = PageAction.List,
Model = model
});
请注意,我不再为操作使用替换参数,因为此路由定义只有一个可能的操作。由于我没有使用操作参数,因此我需要告诉 Dynamic Data 我的操作是什么,因此使用了 PageAction.List
属性。
The resource cannot be found.
Description: HTTP 404. The resource you are looking for (or one of its dependencies)
could have been removed, had its name changed, or is temporarily unavailable.
Please review the following URL and make sure that it is spelled correctly.
Requested URL: /AllActions/DynamicData/PageTemplates/List.aspx
这种情况发生是因为 Visual Studio 丢失了该站点的“启动页”的跟踪,尽管请求的资源(/PageTemplates/List.aspx)确实存在,但在 Dynamic Data 中,我们必须通过路由才能访问该资源。我们不能直接导航到那里,因为页面上的 DynamicDataManager
控件将没有足够的元数据来正确初始化页面。我们可以通过在 Visual Studio 中将 Default.aspx 设置为该网站的“启动页”来轻松解决此问题。
让我们在浏览器中查看该站点,看看我们的新页面是否正在接收请求。如果我们浏览到 Default.aspx,应该会看到数据模型中的表列表。将鼠标悬停在其中一个表链接上,注意链接的 URL。它应该是 ~/{table}/List.aspx,并且点击它应该会执行该表的列表操作,并显示包含该表内容的网格。
看看编辑、详情和插入新项链接的 URL 的最后一部分。它们都指定了与链接对应的操作。但是,如果我们点击其中任何一个链接,我们修改后的路由定义将确保我们始终停留在 DetailsEditInsert.aspx 页面。当我们点击“插入新项”链接时,情况会变得更有趣。
这里有两个细节值得注意。最主要的一个是表单已经填好了!另一个细节实际上非常小,标题写着“Entry from table Orders”(来自 Orders 表的条目),而它应该写着“Add new entry to table Orders”(向 Orders 表添加新条目)。这些都是我们的三用途 DetailsEditInsert.aspx 不知道它应该执行插入操作的线索。我们将 Details.aspx 的功能原样复制到了我们的新页面,所以不能怪它感到困惑。请注意 OrderID 字段的值。由于我们的新页面正试图显示 Order 的详情,但没有主键,因此它选择了 Orders 表中的第一条记录,该记录的 OrderID 为 10248。您可以通过例如使用 Server Explorer 查看 Orders 表中的数据来确认这一点。让我们先处理主要的细节。
尽管我希望将路由详细信息提供给 DetailsEditInsert.aspx,以便我们能够看到哪个操作路由到了此页面,但我找不到任何方法来实现。我选择了检查请求 URL。
protected void Page_Init(object sender, EventArgs e)
{
dynamicDataManager.RegisterControl(commonDetailsView);
switch (Request.Url.Segments[3])
{
case "Insert.aspx":
commonDetailsView.AutoGenerateInsertButton = true;
commonDetailsView.ChangeMode(DetailsViewMode.Insert);
break;
}
}
关于对象命名的插曲
上面代码中的两个控件名称对您来说很可能看起来很奇怪。每当我创建新的 Dynamic Data 页面模板或自定义默认模板时,我都会修改这些模板中的对象命名,以反映我自己的标准和约定。我也推荐您这样做,因为 Dynamic Data 的好处在于您很少需要这样做。不要为每个项目都做一次,而是为每个标准页面模板做一次,然后在创建新项目时简单地替换默认模板。
我已经将 DynamicDataManager1
重命名为 dynamicDataManager
,将 DetailsView1
重命名为 commonDetailsView
,将 DetailsDataSource
重命名为 detailsDataSource
。“1”后缀总是让我觉得代码或标记看起来很随意且未完成。我还重命名了与本文无关的其他控件,我命名的修订结果可以在示例项目中的 DetailsEditInsert.aspx 页面及其代码隐藏文件中看到。
我上面代码中的方法不是最优雅的,也不是最健壮的,但对于大多数用途来说,它已经足够了。URL 由 Dynamic Data 框架提供,并且至少应该有四个段,最后一个段应该总是 Details.aspx、Edit.aspx、Insert.aspx 或 List.aspx,因此我仍然可以在非生产网站中硬编码段索引和字符串。
将原始的 Insert.aspx 与我们的新页面进行比较,也会发现其他关键差异。首先,commonDetailsView
的 ItemInserted
和 ItemCommand
事件没有被处理。这两个事件的作用只是返回到列表视图;当点击“取消”链接时,ItemCommand
处理程序会执行此操作,并且 ItemInserted
处理程序在没有发生错误的情况下也会执行此操作。后者值得一提,因为如果插入操作中发生任何异常,Dynamic Data 将让您停留在插入视图中,而没有任何错误指示!
其次,detailsDataSource
也需要允许插入。默认情况下,在我们的新页面(以及 Details.aspx 页面)上,只允许删除。
<asp:EntityDataSource ID="detailsDataSource"
runat="server" EnableDelete="true"
EnableInsert="true" EnableUpdate="true">
<WhereParameters>
<asp:DynamicQueryStringParameter />
</WhereParameters>
</asp:EntityDataSource>
强烈建议读者在 Insert.aspx 和 Edit.aspx 页面模板中添加一个控件来显示错误消息,并相应地修改 ItemInserted
和 ItemUpdated
事件处理程序,以检测异常并显示错误消息。当然,使用 DetailsEditInsert.aspx 的我们只需要修改一个页面(包含两个事件处理程序)。
在进行上述代码更改(即添加事件处理程序和启用插入)后,我们使用 DetailsEditInsert.aspx 的插入操作按预期工作,插入新项并将我们带回到列表视图。让我们继续让编辑操作正常工作。
正如我们在上面 DetailsEditInsert.aspx 表单的屏幕截图中看到的(在列表视图中点击任何项目上的编辑链接后),我们的多功能页面模板不知道它应该处于编辑模式。其渲染的 UI 与原始 Details.aspx 页面完全相同,包括一个通常会带您到编辑视图的编辑链接。在这种情况下,它只是重新加载 DetailsEditInsert.aspx,其中包含一个编辑链接。
让我们重复我们之前完成的步骤来让插入操作工作。首先,我们需要告诉多功能页面它当前的用途。
protected void Page_Init(object sender, EventArgs e)
{
dynamicDataManager.RegisterControl(commonDetailsView);
switch (Request.Url.Segments[3])
{
case "Insert.aspx":
commonDetailsView.AutoGenerateInsertButton = true;
commonDetailsView.ChangeMode(DetailsViewMode.Insert);
break;
case "Edit.aspx":
commonDetailsView.AutoGenerateEditButton = true;
commonDetailsView.ChangeMode(DetailsViewMode.Edit);
break;
}
}
这是允许我们使用多功能页面执行编辑操作的最重要更改。然后,我们还必须在 entityDataSource
控件上启用编辑,即在其标记声明中添加 EnableUpdate="true"
属性,并为 commonDetailsView
的 ItemUpdated
事件添加一个处理程序。
最后的润色
我希望大多数人都对我们在该页面模板实现的三个模式中的任何一个上,多功能页面上额外的编辑和删除链接感到不安。
在上面的图像中,我们也看不出页面处于什么模式,因为相同的标题“Entry from table Region”(来自 Region 表的条目)总是出现(当然,仅适用于所有 Region 表的操作),而不管页面的模式如何。在完成我们的工作并去酒吧之前,让我们先解决这两个问题。
为什么我们的多功能模板会生成那些编辑和删除链接?查看 DetailsEditInsert.aspx 的标记,我们可以看到 commonDetailsView
的 Fields
集合中声明了一个单一的模板控件,该模板控件定义了这两个有问题的链接。从标记中删除整个 Fields
元素很简单,但为什么这些链接首先被定义?请记住,我们基于 Details.aspx 页面构建了我们的多功能页面,所以该页面可能需要这些链接;因此,只有当我们的新页面处于详情模式时才需要它们。
当我们设置模式时,添加和删除这个模板控件字段似乎有点工作量,而且在我想象这个项目时,我并不真正感兴趣。一些实验很快就表明,我们可以让 DetailsView
控件自动生成这些链接,就像我们用更新和插入链接一样。让我们在我们的模式切换中添加第三种情况。
switch (Request.Url.Segments[3])
{
case "Insert.aspx":
commonDetailsView.AutoGenerateInsertButton = true;
commonDetailsView.ChangeMode(DetailsViewMode.Insert);
break;
case "Edit.aspx":
commonDetailsView.AutoGenerateEditButton = true;
commonDetailsView.ChangeMode(DetailsViewMode.Edit);
break;
case "Details.aspx":
commonDetailsView.AutoGenerateEditButton = true;
commonDetailsView.AutoGenerateDeleteButton = true;
commonDetailsView.ChangeMode(DetailsViewMode.ReadOnly);
break;
}
现在,如果我们从列表视图中的“详情”链接,我们几乎得到了我们需要的一切,再加上那两个额外的、有问题的链接。
如果我们现在简单地删除 commonDetailsView
的 Fields
集合中的显式链接,我们会失去什么?真的只有一个东西,那就是附加到删除链接客户端 Click
事件的 JavaScript 删除确认,正如在“OnClientClick
”中为删除链接标记的 LinkButton
中所见。
<asp:LinkButton ID="DeleteLinkButton"
runat="server" CommandName="Delete" CausesValidation="false"
OnClientClick='return confirm("Are you sure you want to delete this item?");'
Text="Delete" />
如果您点击上面的图像中看到的底部删除链接,您将不会收到任何确认。显示的记录将被删除,并且您将被路由回列表视图,而不会经过开始。现在,一旦我们删除了两个“有问题的”链接(请注意,现在“有问题的”是加引号的),我们如何才能重新获得相当有用的删除确认呢?好吧,鉴于删除链接是自动生成的,我们可以相当肯定它的文本总是“Delete”。有了这个假设,一个快速而粗糙的 jQuery 解决方案就不远了。
<script src="https://:28537/AllActions/Scripts/jquery-1.3.2.js"
type="text/javascript"></script>
<script type="text/javascript">
jQuery(function($) {
$(".detailstable a:contains('Delete')").click(function() {
return confirm("Are you sure you want to delete this item?");
});
});
</script>
请注意,我不得不为 jQuery 脚本引用使用绝对 URL。这是因为 Dynamic Data 路由使用的“人工”URL。如果我使用相对 URL,例如 Scripts/jQuery-1.3.2.js,当浏览器请求一个以例如 Categories/List.aspx 结尾的 URL 的页面时,它实际上也会请求不存在的资源 Categories/Scripts/jQuery-1.3.2.js,并得到一个 404 - Not Found 的响应。这让我感到非常困惑,因为我可以看到 jQuery 在 Default.aspx 中加载了,并且无法弄清楚为什么它没有在 Categories/List.aspx 中加载。使用 Firebug 等高级调试工具,大约花了十分钟就解决了这个问题。
简而言之,上面的 jQuery 函数在文档元素在浏览器 DOM 中准备就绪时执行。它查找所有包含文本“Delete”的“a
”元素(链接),这些元素是任何带有 CSS 类“detailstable
”的元素的所有后代,并为这些链接元素的“click
”事件附加一个匿名事件处理程序,该处理程序的功能与上面为 LinkButton
定义的功能完全相同。DetailsEditInsert.aspx 页面的标记告诉我们 commonDetailsView
具有 CSS 类“detailstable
”,并且由于删除链接是自动为我们生成的,所以我们应该有信心该链接将始终包含该文本。
不熟悉 jQuery 的读者可以下载它,将其添加到简单的纯 HTML 项目中,打开 jQuery API Browser,然后花几个小时学习基础知识。这个 API 和库绝对是这样一种情况,即相当高级的技能可以从非常基本的知识中快速自学成才。我能想象到的实现删除确认的唯一的 ASP.NET 替代方案,与上面两行代码相比,似乎不仅笨拙,而且完全受限,但也许一个敏锐的读者,拥有比我更丰富的想象力,可以提供一个纯 ASP.NET 解决方案。
结语
让我们再回顾一下我们所取得的成就。我们添加了一个新的页面模板 DetailsEditInsert.aspx,它执行了 Dynamic Data 项目模板“开箱即用”的 Details.aspx、Edit.aspx 和 Insert.aspx 页面模板的所有功能。我们学习了一些关于 Dynamic Data 路由的知识,我希望其中一些可以迁移到我们对路由的总体认识中(在我开始这个项目之前,我的知识几乎为零,而且我们使用了 JavaScript 的一个最小化示例,通过 jQuery 来实现)。
这是我的第一篇文章,我希望它能成为 ASP.NET Dynamic Data 中许多精彩内容的一个系列。随着我发布未来的文章,我将修订本文中需要更详细探讨的部分,以链接到新文章,并且我将继续修订不断增长的系列,以维护一个有用的文章网络,供那些对 Dynamic Data 感兴趣的人参考。请继续关注,并在可能的情况下通过建议、纠正和批评来帮助我。