ASP.NET MVC 三款 jQuery 模态对话框对比
对三款 jQuery 模态对话框插件在 ASP.NET MVC 中的工作方式及优雅 JavaScript 的对比评估。
引言
最近,我需要在数据密集型的 ASP.NET MVC 应用程序中为所有 CRUD 操作集成模态对话框,这促使我写下了这篇文章。在这种情况下,我像往常一样进行了快速的 Google 搜索,找到了大约十几个可能的选项(见文章末尾的列表)。不幸的是,当我试图深入了解时,我很快发现其中许多都很难判断它们是否能满足我的需求。文档通常稀疏,有些示例似乎不起作用,在线论坛上充斥着沮丧的用户提出的问题,但回答很少或无益。
在对一些示例进行了一些研究后,我开始怀疑,这些示例不仅不完整,无法回答我的具体问题,而且对于那些天真地尝试在 MVC 应用程序中使用其中某个插件的开发者来说,还可能隐藏着未被发现的陷阱。因此,我决定创建一个简单的演示页面,包含我通常需要的两种数据录入:单个数据项(代表表中的一行)和包含多个项目的多行数据网格。然后,我尝试逐一整合不同的对话框库,看看在试图实现我想要的所有功能时会发生什么。
总的来说,我查看了大约八个库,但很快就放弃了一些,最后只剩下三个我认为值得在这篇文章中介绍的。我发现很难找到我问题的答案,这给了我一个想法,那就是提供一个完整、可工作的、从头到尾的解决方案。我希望其他人能从中受益。
项目需求
- 我这个项目的主要要求是看看我能否让这些东西中的一个工作起来,即从数据页面弹出模态对话框,修改记录,然后将其保存回数据库。
- 对话框应通过 ajax 提交更改,并对页面相关部分进行局部刷新。
- 应该可以对页面上的多个项目进行更新。
- 应该可以对同一个项目进行重复更新。
- 仅为此演示设置的一个个人要求是,测试每个对话框开箱即用的能力,而无需诉诸任何补充库,包括 MicrosoftMvcAjax 库。
第二、第三和第四项需求对所有我测试过的插件都造成了问题。局部刷新会用新的、外观相同但不再绑定的 DOM 对象替换页面加载时与对话框脚本连接的 DOM 对象,从而导致脚本以各种方式失败。典型结果是,你只能进行一次更新,然后就无法再进行任何操作,除非重新加载页面。jQuery 的 live()
函数就是为此类情况设计的,但我无法使其与某些插件可靠地工作。正如我稍后将描述的,只有在使用 UI-Dialog 时,我才设法为所有测试场景找到了解决方案。
Using the Code
提供的代码包含三个不同对话框库的完整解决方案:jQuery-UIDialog、ColorBox 和 Simple Dialog。由于每个对话框都有不同的样式和脚本要求,并且为了避免过度混淆自己,我为每个库分配了自己的空间,因此 Scripts、Views、Content 和 Controllers 文件夹都包含单独对话框文件的子文件夹。快速浏览项目文件夹就会一目了然。
起始页 (Home/Index) 显示一个菜单,指向三个示例,它们看起来都像下面的插图。每个菜单项都会导向一个单独的页面(都命名为 SalesInfo.aspx),该页面显示两组独立的数据:一组是单个客户信息,另一组是一个包含三本书籍标题的表格,每本书都有一个编辑链接。点击其中任何一个都会弹出相应的对话框来编辑该数据对象。主页顶部的服务器时间显示,以便您直观地判断任何更新是通过 ajax 还是完全回发完成的。
为了验证对话框确实可以更新记录并刷新页面(这是简单在线示例中常常遗漏的部分),我需要某种工作的数据库来存储和检索数据。为了保持简单,我使用了两个简化的 XML 文件(1 个客户和 3 本书),并使用 LINQ to XML 进行访问。
请注意,您无法添加或删除任何内容。除了 jQuery-UIDialog 示例(它们会发布到服务器但不更改 xml 数据库中的任何内容)之外,所有创建或删除记录的链接都没有连接。在其他示例中,点击这些链接会生成错误。
优雅的 JavaScript
模态对话框采用了一种有时被称为“优雅 JavaScript”或“hijax”的技术,该技术允许用户无论 JavaScript 是开启还是关闭都能进行更新或创建操作。有关此技术的讨论可以在许多文章和博客中找到,例如 Richard Kimble 的 这篇,我从中获得了一些想法。我最初并没有将此作为要求,但觉得这是一个良好的实践习惯,所以在这里贯穿使用。这本身也带来了一些问题,主要围绕需要根据视图类型更改各种按钮的可见性,因此我想简要描述一下处理方式。
您首先创建一个标准页面(例如,PersonalDetail.aspx),该页面向用户显示一个数据录入表单,用户在 JavaScript 关闭时(或出现脚本错误时)点击“编辑”按钮。该页面实际上只是一个 PartialView 的外壳,其中包含实际的编辑字段。JavaScript 开启的用户会看到模态对话框,该对话框使用 ajax 只检索 PartialView。
![]() |
![]() |
JavaScript 关闭时 | JavaScript 开启时 |
以下代码片段显示了 PersonalDetail.aspx 的全部内容。请注意,“返回”ActionLink(Visual Studio 通常会将其插入到部分视图中)已从那里移到此页面,因为它在模态对话框中点击时可能会引起问题,但这样一来,没有 JavaScript 的用户仍然可以看到它。
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<DialogDemo.Models.Person>" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h3>Edit Customer Information</h3>
<% Html.RenderPartial("_PersonDetail"); %>
<div>
<%: Html.ActionLink("Back", "SalesInfo", new { customerid = Model.PersonId })%>
</div>
</asp:Content>
相应的偏视图,根据 Kimble 的命名约定,命名为 _PersonalDetail.ascx,包含所有输入字段,如下所示。提交按钮已包装在“<div class='btn-Panel nonAjax'></div>”标签中,以便在具有内置提交按钮的对话框(如 SimpleDialog 和 jQueryUI-Dialog)中隐藏该按钮。btn_Panel 类在主机页面 (SalesInfo) 中定义为“display: none”;然后,在仅为没有 JavaScript 的用户提供的 PersonalDetail.aspx 页面中,nonAjax 样式被定义为“display: block”。
我将 Html.ValidationMessageFor
帮助器移到了 div 标签之外,因为我觉得将它们显示在 UI-Dialog 的输入字段下方看起来更好。最后,客户和书籍偏视图中的表单具有相同的 id (“target”),因为 UI-Dialog 使用相同的脚本来提交两者。
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<DialogDemo.Models.Person>" %>
<div id="form-wrapper">
<% using (Html.BeginForm("CustomerEdit", "jqUI", FormMethod.Post, new { id = "target"} ) )
{%>
<%: Html.ValidationSummary(true) %>
<div class="edit-set">
<div class="editor-label">
<%: Html.LabelFor(model => model.PersonId) %>
</div>
<div class="editor-field readonly">
<%: Html.TextBoxFor(model => model.PersonId, new { @readonly = "readonly"})%>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.FirstName) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.FirstName) %>
</div>
<%: Html.ValidationMessageFor(model => model.FirstName) %>
<div class="editor-label">
<%: Html.LabelFor(model => model.LastName) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.LastName) %>
</div>
<%: Html.ValidationMessageFor(model => model.LastName) %>
</div>
<div class="btn-Panel nonAjax"><input type="submit" value="Save" class="button"/></div>
<% } %>
</div>
边栏:一个大陷阱
还有另一个问题我想警告大家,它让我在满足需求 #4 的过程中绕了很久的圈子。测试情况是允许用户重复编辑某个条目,可能是因为犯了错误,或者遗漏了什么,直到提交后才注意到。例如,如果我将名字从 Ray 改为 Elvis,保存,然后立即再次点击编辑,模态框会再次出现,仍然显示名字是 Ray,尽管它已经在底层页面中更改为 Elvis。起初我以为是对话框脚本的问题,但事实并非如此。实际上,服务器只是重新发送了缓存数据,而没有再次命中数据库(直到缓存超时)。为了解决这个问题,我在 web.config 中定义了一个无缓存的 outputCacheProfile
。
<system.web>
... other stuff
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="ZeroCacheProfile"
duration="0"
varyByParam="None"
location="None" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>
然后,通过用 ZeroCacheProfile
装饰检索数据的任何控制器操作,可以强制服务器每次请求都访问数据库,从而解决问题。
[OutputCache(CacheProfile = "ZeroCacheProfile")]
public ActionResult CustomerEdit(int customerid)
{
// code here ...
}
对比
1. Simple Dialog
不要与 SimpleModal 混淆,Simple Dialog 是我深入研究的第一个库,我仍然很喜欢它。我不得不承认,对于生产站点来说,它可能不是一个好的选择,因为开发似乎在 2009 年中期就停止了,并且只达到了 0.1.1 版本。尽管如此,它几乎完美地工作,而且非常轻量级且易于样式化。它唯一有问题的地方是表格中的编辑链接。第一次点击可以正常工作,但点击 ajax 刷新后的任何链接都会失败(这是一个相当大的失败)。但是,如果您不介意在此情况下诉诸常规回发,它工作得很好。在编辑链接可以放置在 PartialView 之外的情况下,它可以进行 ajax 提交,但对于网格来说这是不可能的,而且似乎没有其他我能找到的解决方法。
因为它非常简单但存在一些问题,我不会详细介绍。演示应该很容易理解。一些值得注意的点:
- 每个对话框都使用一个设置脚本,该脚本引用用于打开它的链接,例如
$('a.modalDlg').simpleDialog({options})
,但这不允许您过多地操作链接属性。 - 它似乎不允许您在引用链接上使用
$.bind() 或 $.live()
,这是一个不幸的限制。 - 对话框可以从您在 PartialView 或其他地方标记为
class="close"
的任何按钮关闭。 - 对话框没有原生的提交表单功能,所以我需要拦截 PartialView 上的提交事件,以便使用 jQuery$.ajaxPost(仅适用于非表格编辑)。
- 对话框不处理成功或错误事件。相反,使用 $ajaxPost() 方法将错误报告回宿主页面。
- 表格行只能使用表单的提交按钮通过完全回发来更新。
我认为这个库可以做得更多,但此时我已决定继续前进。
2. ColorBox
我在别人的博客上看到有人提到它很好,但 ColorBox 最终成了我最不喜欢的。它有很多 Simple Dialog 的局限性,而且更多。
- 它不支持任何类型的 ajax 提交,因此所有编辑操作都使用完全回发。(您可能可以使用我上面提到的 jQuery$.ajaxPost,但我没有足够的兴趣去测试它。)
- 据我所知,您必须使用 PartialView 上的输入按钮来提交所有表单。
- 错误必须返回宿主页面。
- 它的样式很难设置。
公平地说,ColorBox 并非设计用于数据录入表单,但如果您想创建幻灯片、画廊或类似的东西,它将是一个易于使用且外观精美的选项。
3. jQuery UI-Dialog
尽管我一开始对 这个插件有些偏见,但它最终成为了明显的赢家。我避免它的一个原因是文件大小似乎过大,但这并不完全准确,因为您可以创建一个自定义下载,其中只包含您需要的部件。它似乎也很难样式化,这也有一定的道理。但是,如果您愿意花一些时间在 Firebug 中追踪无尽的 div 层,您会发现它也非常灵活。下载包含 Pepper Grinder 主题的一个微小修改版本,但您应该能够替换任何其他您喜欢的主题。
脚本与 jQuery UI 网站上的脚本略有不同,后者将所有对话框代码包装在 document.ready()
函数中。我一直在尝试一种不同的样式,每次对话框只使用一行,但我不知道这是否有区别。我的版本看起来像这样:
$(function () {
$('a.modalDlg').live("click", function(event) {loadDialog(this, event, '#customerInfo');});
$('a.abookModal').live("click", function(event) {loadDialog(this, event, '#bookInfo');});
}); /* end document.ready() */
第一行绑定所有类为“modalDlg”的 <a>
标签的点击事件,该事件调用 loadDialog()
函数来编辑客户记录。‘#customerInfo
’ 参数是包装 Customer.ascx 偏视图全部内容的 <div>
标签的 ID。成功发布后,对话框将用服务器返回的更新后的 HTML 替换 div 的内容。
第二行绑定数据网格中所有编辑链接的点击事件,并调用相同的函数来打开对话框编辑书籍记录。‘this’ 参数引用被点击的链接,该函数能够使用其 title 和 href 属性为数据类型打开正确的对话框,并设置标题栏文本。对话框能够自动调整大小以正确适应不同的内容。
jQuery $.live()
函数用于绑定页面加载时尚不存在的链接,这会在每次进行部分 ajax 刷新时发生。
打开对话框的函数大部分直接取自 jQuery UI 网站,并进行了几处重要修改。
function loadDialog(tag, event, target) {
event.preventDefault();
var $loading = $('<img src="../../Content/images/ajaxLoading.gif" alt="loading" class="ui-loading-icon">');
var $url = $(tag).attr('href');
var $title = $(tag).attr('title');
var $dialog = $('<div></div>');
$dialog
.append($loading)
.load($url)
.dialog({
autoOpen: false
,title: $title
,width: 500
,modal: true
,minHeight: 200
,show: 'fade'
,hide: 'fade'
});
$dialog.dialog( "option", "buttons", {
"Cancel": function() {
$(this).dialog("close");
$(this).empty();
},
"Submit": function () {
var dlg = $(this);
$.ajax({
url: $url,
type: 'POST',
data: $("#target").serialize(),
success: function (response) {
dlg.dialog('close');
$(target).html(response);
dlg.empty();
$("#ajaxResult").hide().html('Record saved.').fadeIn(300, function(){
var e = this;
setTimeout(function() { $(e).fadeOut(400); }, 2500 );
});
},
error: function (xhr) {
if (xhr.status == 400)
dlg.html(xhr.responseText, xhr.status); /* display validation errors in this dialog */
else
displayError(xhr.responseText, xhr.status); /* display other errors in separate dialog */
}
});
}
});
$dialog.dialog('open');
};
我略微打破了自己的“不使用外部库”的规则,包含了 jquery.effects.core.js 和 jquery.effects.fade.js 来实现淡入淡出效果。
然而,最重要的更改是这两行 $(this).empty()
,它必须出现在 Cancel 和 Submit 函数中。没有它们,在 ajax 更新后,对话框将无法重新打开,直到整个页面重新加载。我不知道为什么这能解决问题,但它确实有效。
成功发布后,$(target).html(response)
这行代码会将 <div id='xxxx'>
标签(其中 $(target) 指的是传递到函数中作为 target 参数的 div ID)中的现有 HTML 替换为从服务器返回的响应文本(即,由“_PartialView”创建)。同样,这个函数根据被点击链接的‘href’属性,打开用于编辑客户和书籍记录的相应对话框。
使用 UI-Dialog 时,最好(但非必需)使用对话框自带的内部提交和取消按钮,并隐藏 _PartialView 附带的提交按钮。$.ajax 方法做得很好,data: $("#target").serialize()
这行代码就是您需要发送序列化的数据到服务器所需的全部内容(注意,这里的“#target”根据我的命名约定,指的是“_PartialView”中的 $('form[id=target]
'),这与 loadDialog 中的 target 参数不同;这是我糟糕的命名,但我懒得现在更改)。
UI-Dialog 的另一个优点是,如果您愿意,可以轻松地在编辑对话框中显示验证错误。您会注意到 jqUIController 中有一些注释掉的代码,用于构建替代样式的错误消息,可以弹出在编辑对话框上方,但使用内置的 MVC 验证框架要简单得多。顺便说一句,控制器中有一个针对客户对象的简陋的小型验证函数,它只是为了展示如何工作而存在的。别评判我!