提升您的 ASP.NET Web Form 应用程序的性能






4.57/5 (15投票s)
本文将介绍如何使用 Javascript / Jquery 和 Web Services 来提升您现有的 ASP.NET Web Form 应用程序的性能。
引言
有很多理由可以升级您现有的“遗留” Web Forms 应用程序。其中一个主要原因就是性能。您的页面加载是否花费的时间太长?每个表单提交响应是否需要几秒钟以上?用户通常会对此感到沮丧,尤其是当没有“加载中”面板或其他视觉指示器时。这通常是 Web 应用程序没有从头开始构建得尽可能快的标志。标准编写的 Web Forms 应用,如果页面非常“沉重”,每次用户与页面交互时都会花费很长时间来加载。这通常是因为发生了回发(postback)。回发是指用户按下表单上的提交按钮,整个页面被发送到服务器,但更重要的是,整个页面又从服务器写回,这会导致页面加载时间非常慢的“双重打击”效果。这些应用程序通常可以快速地改用轻量级的 Javascript 库,以及 Web 服务,作为从页面请求和提交数据的方式。这样,只有最少的数据量实际上被加载并与服务器之间进行传输。因此,每次用户加载页面或执行任何需要与服务器之间发送数据的操作时,只会发送必要的数据,而不是整个页面。
通过不重写应用程序来节省时间和金钱
请抵制重写整个应用程序的冲动。为什么?作为回顾,请阅读 Joel Spolsky 的经典文章:您永远不应该做的事情,第一部分。在某些极端情况下,重写可能是最佳选择,但 99% 的情况下,重写是一个巨大的错误。在考虑了不重写的主要原因之后,应该很清楚:您已经花费了很长时间(可能几年)来优化您的业务逻辑,修复了许多错误,并改进了底层数据库性能,那么为什么要从头开始并不得不重新做一遍呢?
大多数 ASP.NET Web Forms 应用程序由一系列 ASP.NET 控件(或第三方控件,如 Infragistics 或 Telerik)组成,这些控件直接放置在窗体上,并在代码隐藏(aspx.vb 或 aspx.cs)中使用少量语句。这使得只有对应用程序有基本了解的开发人员可以轻松地将代码复制粘贴到现有应用程序中。这通常会导致糟糕的编码实践和糟糕的性能。像 UpdatePanel 这样的控件会让人非常想直接将其放置在页面上,然后让 .NET 来处理其余的事情。
使用代码
举个简单的例子,我创建了一个带有标准 UpdatePanel 和代码隐藏的 ASP.NET 页面。UpdatePanel 通常用于消除完整页面回发时发生的任何屏幕刷新。
default.aspx
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<asp:UpdatePanel runat="server" ID="up1">
<ContentTemplate>
<asp:Label runat="server" ID="Label1" Text="Update Me!" /><br />
<asp:Button runat="server" ID="Button1"
Text="Postback Update" OnClick="Button1_Click" />
</ContentTemplate>
</asp:UpdatePanel>
default.aspx.cs protected void Button1_Click(object sender, EventArgs e)
{
Label1.Text = DateTime.Now.ToLongDateString();
}
很简单。点击 Button1,对当前日期/时间进行异步请求,然后将其显示为 Label1 的内容。听起来很简单,但请看看完成此部分回发所需的实际 HTTP POST 和响应。
仅为了显示一个 16 个字符的字符串,就需要来回发送大量数据到 Web 服务器!这对于不经常使用的功能是可以接受的,但对于使用频繁的生产系统来说则不行。幸运的是,Microsoft 已经为我们提供了一种更有效的方法来实现这一点,这是 ASP.NET AJAX 框架的一部分。
页面方法 (Page Methods)
页面方法允许 ASP.NET AJAX 页面使用 JSON(JavaScript Object Notation)直接执行页面上的静态方法。JSON 本质上是 SOAP 的一个极简版本,非常适合客户端和服务器之间的轻量级通信。有关如何实现页面方法和 JSON 的更多信息,请参阅 Microsoft 的 在 ASP.NET AJAX 中公开 Web 服务给客户端脚本。
与执行部分回发然后接收 HTML 标记来完全替换我们的 UpdatePanel 内容相比,我们可以使用 Web 方法仅请求我们感兴趣的信息。default.aspx
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="true" />
<script language="javascript">
function UpdateTime() {
PageMethods.GetCurrentDate(OnSucceeded, OnFailed);
}
function OnSucceeded(result, userContext, methodName) {
$get('Label1').innerHTML = result;
}
function OnFailed(error, userContext, methodName) {
$get('Label1').innerHTML = "An error occured.";
}
</script>
<asp:Label runat="server" ID="Label1" Text="Update Me!" /><br />
<input type="button" id="Button2" value="Web Method Update"
onclick="UpdateTime();" />
default.aspx.cs
[WebMethod]
public static string GetCurrentDate()
{
return DateTime.Now.ToLongDateString();
}
通过此方法,我们完全消除了 UpdatePanel 请求中存在的额外数据,并将响应减少到我们感兴趣请求的数据。

使用 JSON,整个 HTTP 往返只有 16 字节,而 UpdatePanel 则为 872 字节。这大约是 **5000% 的改进!!**,并且随着页面复杂度的增加,这种改进还会继续增加。这不仅大大减少了我们的网络占用空间,还消除了服务器实例化 UpdatePanel 控件并让它们经历其生命周期以渲染发送回浏览器的 HTML 的必要性。虽然我提倡 UpdatePanel 的简洁性,但我认为谨慎使用它们至关重要。在任何重度使用的情况下,它们很少是最佳解决方案。
开发便捷性
作为一名经验丰富的 WebForms 开发人员,您可能在想:“哇,现在我必须学习使用这些新潮的 Javascript 库。”或者,“现在我们必须花很多时间用我们从未用过的新型低级 Javascript 函数来重写每个页面!”但是,等等,有一个易于使用的替代方案,它兼具高性能和易用性。我使用 JQuery、KendoUI、Knockout 和其他高级 Javascript 库创建了一些令人惊叹的应用程序,例如一个映射应用程序(允许在页面上的两个地图之间进行拖放)、具有完整图形功能的仪表板应用程序、类似 Excel 的可编辑网格等等,所有这些都在几周内完成。与标准的 WebForms 开发相比,您使用这些高级 Javascript 库拥有的强大功能、速度和控制力要高得多。您的代码将更清晰,并且无需从服务器端代码处理到客户端代码,反之亦然,因为您所有的 UI 编码都将在 Javascript 中完成。例如,我创建了一个高性能的审计“历史记录”页面,该页面完全由 Jquery 和 Javascript 实现,以替换 ASP.NET WebForms 应用程序中现有的历史记录页面。

历史记录屏幕具有典型的审计/历史记录表单应有的所有功能。实现的功能包括:搜索/过滤、排序、每页条目数、“工具提示”用于显示过长的字段数据,以及一个很酷的功能,允许用户消除 NULL-空白的“过渡”,从而消除显示不必要的记录。您的 aspx / ascx 页面将由一个 div 组成,Jquery 代码将从该 div 创建网格,以及适当的 Javascript 函数来调用显示网格函数。
function showHistory() {
var contractkey = $("#<%= hdnCID.ClientID %>").val();
var type = 0;
var checked = $("#chkFilterBlank").prop("checked");
displayHistory(type, contractkey, checked);
}
<div id="divHistory" style="width: 900px; height: 540px; background-color: #EDECEB; display: none;">
<table id="tblHistory" class="display" cellspacing="0" cellpadding="0" border="0" style=" margin: 5px 25px 0px 0px; color:#000000; border: 1px solid #222222;">
</table><br />
<input type="checkbox" id="chkFilterBlank" checked="checked" name="filterblank" value="filter"/>Filter Blank transitions
<input id="btnRefresh" type="button" class="button-right" value="refresh" runat="server" onclick="showHistory(); return false;" />
</div>
外部 history.js Javascript 文件包含创建网格的实际函数。在这种情况下,我使用了 'datatables' 插件,它执行快速的服务器端分页,从而使历史记录表单运行速度极快,**任何加载、搜索、排序、分页或刷新操作的响应时间都小于 1 秒。**
function displayHistory(type, id, filterblank) {
try {
if (filterblank == undefined || filterblank == null)
filterblank = true;
showHistoryDialog();
var oTable = $('#tblHistory').dataTable(
{
"bDestroy": true,
"bJQueryUI": true,
"bSort": true,
"bAutoWidth": true,
"bProcessing": true,
"bServerSide": true,
"sScrollX": "1500px",
"sScrollY": "385px",
"sPaginationType": "full_numbers",
"iDisplayLength": 15,
"aLengthMenu": [[10, 15, 25, 50, 100], [10, 15, 25, 50, 100]],
"sAjaxSource": "AjaxPage.aspx",
//Extra parameters
"fnServerParams": function (aoData) {
aoData.push({ "name": "type", "value": type },
{ "name": "rowId", "value": id },
{ "name": "filterBlank", "value": filterblank },
{ "name": "CallRequest", "value": "ProcessHistory" });
},
"aoColumnDefs": [
{ "sTitle": "Table", "sWidth": "160px", "type": "text", "aTargets": [0] },
{ "sTitle": "Column", "sWidth": "110px", "type": "text", "aTargets": [1],
"fnCreatedCell": function (nTd, sData, oData, iRow, iCol) {
$(nTd).attr('title', oData[6]);
}
},
{ "sTitle": "Old Value", "sWidth": "110px", "type": "text", "aTargets": [2],
"fnCreatedCell": function (nTd, sData, oData, iRow, iCol) {
$(nTd).attr('title', oData[7]);
}
},
{ "sTitle": "New Value", "sWidth": "110px", "type": "text", "aTargets": [3],
"fnCreatedCell": function (nTd, sData, oData, iRow, iCol) {
$(nTd).attr('title', oData[8]);
}
},
{ "sTitle": "Changed", "sWidth": "195px", "sType": "date", "aTargets": [4] },
{ "sTitle": "Changed By", "sWidth": "140px", "type": "text", "aTargets": [5] }
]
});
}
catch (exception) {
}
}
您的服务器端代码可以是 .asmx 页面、.ashx 页面(.NET Handler)或甚至是 .aspx 页面。在这种情况下,我使用的是 .aspx 页面,其功能与典型的代码隐藏 aspx.vb 页面相同,但我不处理任何回发,只处理 HTTP 请求。我使用的是“GET”方法,因为这个特定的 Jquery 插件(Datatables.net)在使用服务器端分页功能时实现了此方法。
Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load Dim callRequest As String = If((Me.Request("CallRequest") Is Nothing), String.Empty, Me.Request("CallRequest")) Dim dataTable As DataTable = Nothing Dim returnValue As Boolean = False Dim strJson = Nothing If callRequest = "ProcessHistory" Then strJson = ProcessHistory() End If Me.Response.ClearHeaders() Me.Response.Clear() Me.Response.ContentType = "application/json" Me.Response.Write(strJson) Me.Response.[End]() End Sub Public Function ProcessHistory() As String 'Paging parameters: Try Dim iDisplayLength = Integer.Parse(HttpContext.Current.Request.QueryString("iDisplayLength")) Dim iDisplayStart = Integer.Parse(HttpContext.Current.Request.QueryString("iDisplayStart")) ' Sorting parameters Dim iSortCol = Integer.Parse(HttpContext.Current.Request.QueryString("iSortCol_0")) Dim iSortDir = HttpContext.Current.Request.QueryString("sSortDir_0") ' Search parameters Dim sSearch = HttpContext.Current.Request.QueryString("sSearch") Dim type = Integer.Parse(HttpContext.Current.Request.QueryString("type")) Dim rowId = Integer.Parse(HttpContext.Current.Request.QueryString("rowId")) Dim filterBlank = Boolean.Parse(HttpContext.Current.Request.QueryString("filterBlank")) Dim sEcho = HttpContext.Current.Request.QueryString("sEcho") 'TableHistoryResult Dim history = TryCast(TableAudit.GetTableHistory(DirectCast(type, TableHistoryType), rowId), IEnumerable(Of TableHistoryResult)) If filterBlank = True Then Dim historylist As List(Of TableHistoryResult) = TryCast(history, List(Of TableHistoryResult)) historylist.RemoveAll(AddressOf IsBlankTransition) End If ' TableName ColumnName ' Define an order function based on the iSortCol parameter Dim order As Func(Of TableHistoryResult, Object) = Function(hist) Select Case iSortCol Case 0 Return DirectCast(hist.TableName, Object) Case 1 Return DirectCast(hist.ColumnName, Object) Case 2 Return DirectCast(hist.OldValue, Object) Case 3 Return DirectCast(hist.NewValue, Object) Case 4 Return DirectCast(hist.ChangedDateTime, Object) Case Else Return DirectCast(hist.UserChangedByLoginName, Object) End Select End Function ' Define the order direction based on the iSortDir parameter history = If("desc" = iSortDir, history.OrderByDescending(order), history.OrderBy(order)) sSearch = sSearch.ToLower() ' prepare an anonymous object for JSON serialization history = history.Where(Function(h) (h.TableName IsNot Nothing AndAlso h.TableName.ToLower().Contains(sSearch)) OrElse (h.ColumnName IsNot Nothing AndAlso h.ColumnName.ToLower().Contains(sSearch)) OrElse (h.OldValue IsNot Nothing AndAlso h.OldValue.ToLower().Contains(sSearch)) OrElse (h.NewValue IsNot Nothing AndAlso h.NewValue.ToLower().Contains(sSearch)) OrElse (h.UserChangedByLoginName IsNot Nothing AndAlso h.UserChangedByLoginName.ToLower().Contains(sSearch))) Dim aaData2 = history.[Select](Function(h) New With {h.TableName, .ColBlank = h.ColumnName.Substring(0, Math.Min(h.ColumnName.Length, 6)) + (If(h.ColumnName.Length <= 6, "", "...")), _ .OldBlank = If(h.OldValue Is Nothing, "", (h.OldValue.Substring(0, Math.Min(h.OldValue.Length, 6)) + (If(h.OldValue.Length <= 6, "", "...")))), _ .NewBlank = If(h.NewValue Is Nothing, "", (h.NewValue.Substring(0, Math.Min(h.NewValue.Length, 6)) + (If(h.NewValue.Length <= 6, "", "...")))), _ .ChangedDateTime = h.ChangedDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ"), _ h.UserChangedByLoginName, _ h.ColumnName, h.OldValue, h.NewValue}).Skip(iDisplayStart).Take(iDisplayLength) Dim lsthistory As New List(Of String()) For Each historyitem In aaData2 Dim arrHistory As String() = New String(8) {historyitem.TableName, historyitem.ColBlank, historyitem.OldBlank, historyitem.NewBlank, historyitem.ChangedDateTime, historyitem.UserChangedByLoginName, historyitem.ColumnName, historyitem.OldValue, historyitem.NewValue} lsthistory.Add(arrHistory) Next Dim result = New With { _ Key .sEcho = sEcho, _ Key .iTotalRecords = history.Count(), _ Key .iTotalDisplayRecords = history.Count(), _ Key .aaData = lsthistory } Dim json = SerializeToJSON(result) Return json Catch ex As Exception End Try End Function
请注意 VB.NET、LINQ 和匿名类型的使用(VB.NET 处理匿名类型声明的方式与 C# 不同。)。您可以简单地调用您现有的数据层,而不是像我在此示例中那样使用 LINQ。
更新您的现有应用程序
上述示例演示了如何仅用 Javascript / Jquery 替换您的 UI 代码。因此,无需重写整个应用程序即可提高系统的性能和可维护性。您可以根据应用程序的结构应用本文学到的任何一种技术,以实现应用程序之间传输数据量的巨大减少。
您可以选择仅升级代码的 UI 部分,甚至使用 MVC。借助 .NET Framework 4.5.1,您甚至可以将 MVC 添加到您现有的 WebForms 应用程序中,而无需从头开始。这将允许您重用所有现有的业务逻辑,并且一次只升级您网站的几个区域,同时将您的 MVC 代码与现有的 WebForms 页面分开。