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

ASP.NET 和 jQuery 极致应用 - 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (20投票s)

2011年1月8日

CPOL

12分钟阅读

viewsIcon

54774

downloadIcon

751

使用 jQuery 和 ASP.NET 进行 AJAX Web 开发的有机方法。

引言

这是我关于 ASP.NET 和 jQuery 的第二篇文章。我建议您在阅读本文之前先阅读第一部分

在本文中,我想谈论两个重要议题,它们有助于使我们新的编程方式更加完整和灵活。第一个主题是创建可以与服务器端交互的可重用组件。第二个主题是客户端的错误处理。

可重用组件

回收您的作品

一个有趣的讨论主题是如何使用文章第一部分中解释的新技术创建可重用组件。为了完成这项任务,我们依赖于 ASP.NET 用户控件功能,即.ascx 组件。事实上,我们不需要太多定制就可以重用原生的 ASP.NET 方法,但我们对可重用组件的定义意味着任何ascx

  • 都必须附带一些 JavaScript 文件:它们代表了我们管理客户端逻辑和与服务器通信的基础架构;
  • 允许访问提供服务给托管页面的端点;
  • 可以与同一个托管页面中的多个实例一起实例化:ScriptManager 在 ASP.NET 中为我们透明地解决了这个问题(它区分 ID 并删除重复的 JavaScript 引用),但在我们新的世界里,我们不再使用它了。

将我们的用户控件与 JavaScript 一起携带

装好枪

我非常喜欢将 JavaScript 文件放在其所有者页面或所有者组件附近。在所有者页面的情况下,我们通过一些强命名约定规则解决了这个问题(参见第一部分)。这样,我们就达到了将彼此链接的文件分组的目标,并在最小化过程中轻松地将所有链接的 JavaScript 文件合并成一个文件。

命名约定再次来帮助我们。如果我们决定将所有用户控件放在一个文件夹中,我们可以像我一样称其为UserControls,在演示项目中,这需要极大的想象力。但是我们也可以决定使用其他标准来组织我们的项目。无论如何,通过为项目内的命名约定和文件位置设置强规则,保持我们的解决方案井井有条和易于维护是必须的。

UserControls文件夹内,您可以找到一个用户控件(EditorControl.ascx),它完成一个简单的任务:保存和检索键值对元素。

我们可以在HostingPageUserControl.aspx页面中看到这个组件的实际效果。要测试该组件,请尝试在第一个文本框中提供一个 ID,并在上面的文本区域中输入任何文本。然后按“保存”按钮。现在尝试恢复已保存的项,提供正确的 ID 并按“获取文本”按钮。好的,我知道这是一个微不足道的任务,但我们必须专注于功能,而不是其有用性。

如果我们打开HostingPageUserControl.aspx文件,我们将看到一个简单的用户控件注册标签和一个更简单的将其包含在页面中的方式。

File: HostingPageUserControl.aspx

<%@ Register src="UserControls/EditorControl.ascx" 
           tagname="Editor" tagprefix="SmartUserControls" %>
...
<SmartUserControls:Editor ID="Editor1" runat="server" />

从这个角度来看,与传统的 ASP.NET 相比,没有什么变化,但现在让我们看看解决方案资源管理器中UserControls文件夹的内容。

005.UserControlsFolderContent.png

我们可以看到.ascx文件,两个符合我们常规命名约定规则的 JavaScript 文件,以及一个 Web Service,即.asmx文件,其中包含我们控件的端点。像往常一样,JavaScript 文件在编译时被最小化和合并,并将生成的文件(EditorControl.min.js)复制到js/min文件夹中。

打开EditorControl.ascx。我们可以看到对我们在调试模式和发布模式下的 JavaScript 文件的引用,以及组件元素的标记。

File EditorControl.ascx

<% if (HttpContext.Current.IsDebuggingEnabled) { %>
  <%= this.RegisterClientScript("~/UserControls/EditorControl.js")%>
  <%= this.RegisterClientScript("~/UserControls/EditorControl_Proxy.js")%>
<% } else { %>
  <%= this.RegisterClientScript("~/js/min/EditorControl.min.js")%>
<% } %>

<div style="float: left; margin-right: 50px;" 
        id="editControlContainer" runat="server">

出现一个问题:为什么我们使用这个奇怪的“RegisterClientScript”方法而不是我们常用的“ResolveAndVersionUrl”来在控件中引入 JavaScript 引用?

使用户控件在同一个托管页面中可重用多次

或者……我想要回脚本管理器!

如果我们使用ResolveAndVersionUrl方法包含引用的 JavaScript 文件,托管页面将正常工作,但如果我们尝试在同一页面中多次包含我们的控件,情况就会发生变化。

在演示项目中,还有另一个页面,HostingPageUserControlMulti.aspx,其中包含三个EditorControl实例。

如果我们使用ResolveAndVersionUrl,我们将找到对EditorControl.min.js的三个引用。在同一页面中对同一 JavaScript 文件进行多次引用在客户端是严重的验证错误,并降低了页面下载性能。

RegisterClientScript是位于BasePage类(在Controls文件夹内)的一个方法,它仅当该特定 JavaScript 文件尚未包含在页面中时调用常用的ResolveAndVersionUrl,否则不会添加引用。这是 ASP.NET ScriptManager在其传统的RegisterClientScriptBlock方法中在后台为开发人员提供的服务之一。

另一个需要解决的问题是控件的 ID。如果我们在一个页面中多次包含EditorControl,如果我们使用 HTML 元素 ID,我们就有可能重复这些 ID。这样,一个具有特定 ID 的 HTML 标签将在客户端无法通过 jQuery 唯一标识,并且页面无法通过验证。因此,我们被迫放弃使用 ID。控件的 JavaScript 知道它不能依赖 ID 来选择元素,而是使用类、相对位置、属性和类型。

让我们看看在演示项目中是如何实现的。

GetTextSaveText函数需要一个包含所有控件元素的divClientId。这样我们就指定了一个搜索范围。然后,在 JavaScript 代码中,元素是通过引用其类、属性或类型的选择器来找到的。

File: EditorControl.ascx

<div runat="server" id="editControlContainer" 
                    style="float:left; margin-right:50px;">
<div id="notUnique"></div>
Text id: <br />
<input type="text" value="" maxlength="3" style="width:60px;" />
<input type="button" value="Get text" 
  onclick="GetText('<%=editControlContainer.ClientID %>')" />
<input type="button" value="Save" 
       onclick="SaveText('<%=editControlContainer.ClientID %>')"
       style="float:right;" />
File: EditorControl.js

function GetText(idContainer) {
  var divContainer = $('#' + idContainer);
  var spanResult = $(divContainer).find('.spanResult');
  var textboxId = $(divContainer).find(':text');
  var textArea = $(divContainer).find('textarea');

这是区分单个控件实例从客户端角度来看的可能方法之一。如果您想创建自己的方法,请记住两点:

  • 不要使用 ID 来命名控件元素,以避免重复;
  • 使 JavaScript 能够理解它正在与哪个控件实例协同工作,使用一个唯一标识的容器。

为我们的用户控件提供端点

瞄准

每个用户控件都可以在服务器端提供一些服务。

我们已经学会了如何通过 jQuery 调用服务器端方法,但在本文的第一部分,我们只关注了 Web 方法的方式。

.ascx文件不能公开 Web 方法,所以我们可以在一个特殊的 Web Service(EditorControlService.asmx)中创建控件的端点。我们的 WS 符合常规的命名约定,并且可以以与 Web 方法完全相同的方式调用。

让我们看看 JavaScript 文件中的两个调用

File: EditorControl_Proxy.js

EditorControl_Proxy.GetText = function (idText, getTextCallback) {

  $.ajax({
    type: "GET",
    contentType: "application/json; charset=utf-8",
    url: "UserControls/EditorControlService.asmx/GetText?id=" + idText,
    success: getTextCallback,
    error: feedbackFailure
  });
}

EditorControl_Proxy.SaveText = function (idText, text, saveTextCallback) {

  var jsonData = JSON.stringify({ id: idText, content: text });

  $.ajax({
    type: "POST",
    contentType: "application/json; charset=utf-8",
    url: "UserControls/EditorControlService.asmx/SaveText",
    data: jsonData,
    success: saveTextCallback,
    error: feedbackFailure
  });
}

好吧,我不太喜欢将.asmx文件放在一个名为UserControls的文件夹中,但这样,我们可以将逻辑上严格相关的文件放在同一个物理位置。

检查结果

最后,我们的页面工作了

如果您尝试加载HostingPageUserControlMulti.aspx,您将看到三个控件实例完美地协同工作。查看 HTML 源代码,了解任何 JavaScript 文件的单个引用。当然,尝试在页面中使用这些控件,以确保每个控件都知道其正确的范围,并且仅与其元素一起工作。

作为最后的实验,尝试在EditorControl.ascx中取消注释这行

File: EditorControl.ascx

<div id="notUnique"></div>

现在重新加载页面并验证生成的标记:您将看到 ID 重复被视为严重错误。

要验证页面标记,您可以使用任何合适的工具。我倾向于使用 Firefox 的Web Developer Toolbar,它可以验证远程和本地页面。它还提供数十种其他有用功能,是必备工具。

客户端错误管理

嗯,有时我们的应用程序会出错

通常我们需要在用户执行某些操作后向他们传达某些信息。当调用的方法需要向最终用户显示反馈消息时,我们可以提前考虑一种 nice 的传达方式:使用 JavaScript alert(不是特别 nice!)、消息对话框、气泡弹窗等。

问题出现在返回的消息是意料之外的情况,例如当我们只需要传达由于错误而发生的情况时。我的解决方案是创建一个公共基础设施来管理客户端的错误消息。确实,一种非常普遍的方法是从服务器端返回纯异常,并带有自定义的、用户可理解的消息。

在客户端,您只需捕获异常并将它们以一种舒适的方式呈现给用户(好吧……我还没见过舒适的异常,但……)。

File: ErrorManagement_Proxy.js

// -----------------------------------------
// Get error from server
// -----------------------------------------
ErrorManagement_Proxy.GetError = function (successCallback) {
  $.ajax({
    type: "POST",
    contentType: "application/json; charset=utf-8",
    url: "ErrorManagement.aspx/ThrowServerError",
    success: successCallback,
    error: feedbackFailure
  });
}

// -----------------------------------------
// Failure management.
// -----------------------------------------
var feedbackFailure = function (data) {
  var message = JSON.parse(data.responseText);
  alert(message.Message);
}

这样您就可以剖析您的异常,并向用户显示唯一重要的内容:错误消息。

在服务器端,我们以一种微不足道的方式引发异常,使用像这样的指令

File: ErrorManagement.aspx

[WebMethod]
public static bool ThrowServerError()
{
  throw new Exception("Hello! This is a managed error message from an async call.");
}

现在我们可以看到响应期间真正发生的情况。

转到ErrorManagement.aspx页面,打开 Fiddler 或 Firebug 并激活 Net 标签,然后单击第一个按钮“Throw async error”。

我们简单的错误管理代码(feedbackFailure JavaScript 函数)会响应一个消息框,其中我们向用户发出错误已发生的警告,并附带我们的自定义消息。

现在分析响应的全部内容。

我们成功返回了自定义消息,但在响应堆栈跟踪中,可以读取包含客户端调用端点的 ASPX 页面的完整路径,这并不是一件好事。

ASP.NET 返回整个堆栈的原因是我们处于调试模式。打开应用程序的web.config文件并更改

File: Web.config

<compilation debug="true" >

to

<compilation debug="false" >

现在再次按“Error”按钮并分析响应。

结果现在好多了:我们只暴露了通过堆栈跟踪已经公开的端点名称。

深入检查

……或者我如何喜欢把事情搞复杂

好吧,一切似乎都运行良好。这次没有任何困难。但是敌人总是在门后,我们还没有跨过门槛。

嗯,门槛是 Web 应用程序的生产环境的部署,其中包含使其如此专业的所有细节。有一个元素对我们仍然脆弱的错误基础架构尤其危险:自定义错误重定向器。

再次打开web.config文件,尝试更改此行

<customErrors mode="Off" />

to

<customErrors mode="On" defaultRedirect="GenericError.aspx">
  <error statusCode="404" redirect="PageNotFoundError.aspx"/>
</customErrors>

因为我们假设在生产环境中,我们需要重定向至少 404 错误。

按下“Error”按钮并再次阅读弹出消息

004.Custom_Errors_On.png

那是什么!?我们那真正具有解释性的消息去哪儿了??

CustomErrors不是一个聪明的家伙。当它必须管理一个完全回发错误(即找不到页面的错误)时,它能够将浏览器重定向到正确的页面。

但是,当它管理部分回发(即异步请求)中的错误时,它无法重定向页面,但仍然会拦截错误。

“为什么?”我想问。如果你什么都做不了,请什么都别做。

我们进入黑暗隧道

……前面没有光。

好的,我们陷入僵局了。

一方面,我们希望使用原生的 .NET 异常基础结构在异步请求期间将托管错误消息返回到客户端,另一方面,我们希望使用CustomErrors在应用程序在完全回发期间抛出错误时重定向浏览器。

这个问题似乎无法解决。事实上,MS UpdatePanel在这种情况下似乎有些可疑。当CustomErrorsOff并且在UpdatePanel内部发生错误时,服务器会返回状态码 200(OK)和这个奇怪的字符串:14|error|500|(custom error text)|。

看起来 MS 在这个字符串中编码了实际的状态码(500 在这种情况下是更正确的状态码),UpdatePanel需要读取并管理它。

如果CustomErrorsOn但未设置重定向,则服务器响应类似于:0|error|500||。我们可以注意到在这种情况下,任何自定义错误消息都会丢失。

最后,当CustomErrorsOn且在 500 错误情况下设置了适当的重定向时,UpdatePanel会选择将您重定向到自定义页面,即使您在异步回发中。

我错了还是这似乎有点混乱的管理?

冲破到达另一边

门是笔直的,深邃而宽广……

解决方案很简单但又激进:一个管理响应异常的 HTTP 模块,以及Web.config中一个我称之为CustomErrorsAjax的新节,它与 HTTP 模块一起工作。

CustomErrorsAjax是一个新节,其配置方式与它那个愚蠢的兄弟CustomErrors完全相同。

它的优点在于,如果您处于异步请求中,它不会拦截错误,以便我们的应用程序可以在客户端使用错误消息。

有了这个解决方案,我们的错误管理变得线性而清晰,我们可以使用 .NET 异常将消息引导到来自客户端的任何异步请求。

只需将 ASP.NET CustomErrors重置为Off

File: Web.config

<customErrors mode="Off" defaultRedirect="GenericError.aspx">
  <error statusCode="404" redirect="PageNotFoundError.aspx"/>
</customErrors>

并激活CustomErrorsAjax节(我已经将其包含在Web.config文件中,所以取消注释它)

File: Web.config

<section name="customErrorsAjax" 
  type="EcommerceWebSite.CustomErrorAjax.CustomErrorAjaxConfig, 
  EcommerceWebSite" requirePermission="false"/>

...

<customErrorsAjax mode="On" defaultRedirect="GenericError.aspx">
  <errors>
    <clear />
    <error statusCode="404" redirect="PageNotFoundError.aspx" />
    <error statusCode="500" redirect="GenericError.aspx?e=500" />
  </errors>
</customErrorsAjax>

现在转到ErrorManagement.aspx页面并尝试单击三个按钮。

第一个按钮“Throw async error”正确地以我们的托管错误消息响应:“Hello! This is a managed error message from an async call”。

第二个按钮“Throw postback error”简单地进行完全回发并调用服务器方法btnError_Click,该方法返回一个错误。我们的 HTTP 模块检查这个错误不是异步的,然后将我们的浏览器重定向到web.config中指定的页面。在这种情况下,状态码是 500,所以目标页面是GenericError.aspx

最后一个按钮“Throw page not found error”调用一个不存在的页面,并使我们的 HTTP 模块将 404 错误重定向到正确的页面(PageNotFoundError.aspx)。

好吧,HTTP 模块是新生的,很粗糙。请随时根据您的需求进行改进,重要的是解决方案的核心。

© . All rights reserved.