Maximizing the performance of your ASP.NET WebForms application

by Chris Johnson @
www.Kolaberate.com

April 20, 2014

Should you really upgrade your existing legacy application? there are many reasons to upgrade your existing ‘legacy’ applications. If I should list one of the major reasons to upgrade your existing legacy application, the main reason would be performance.

Are your pages just taking forever to load?

Form submissions, are they taking more than a few seconds to load?

Viewers will usually get exasperated at this, especially if a  ‘Loading’ panel does not exist or other visual indicator.

This is usually the indication of a web application that was not coded with attention to any or all aspects of performance optimization.

WebForms apps (this was written in the standard way) with very ‘heavy’ pages will take forever to load every time the user opens  with a page.

This is due to the fact that a postback is actually taking place.

Postbacks take place when the user clicks a submit button someplace on the form, and the whole page, is sent to the server, but more notably, the whole page is repeatedly written back from the server, which results in a ‘double whammy’ effect of very slow page load times.

These applications can normally be briskly changed to use lightweight javascript libraries, together with Website services, as a means to request and submit data from the page.

In this way, only the least amount of data is actually loaded, and sent to and fro to the server. So, each and every time a user loads a page or carryout any action that requires data to be sent to and fro to the server,  the important data is the only one  sent to and fro, and not the whole page.

Save yourself the time and money by NOT rewriting your application

Hold out against the urge to re-write the entire application. Why is that so? Just for a refresher, read Joel Spolsky’s classic guide article to this: Things you should never do, Part 1.

However, we must have some extreme cases where a re-write may prove to be the best option, but virtually 99% of the time a re-write is not the best decision.

After meditating on one of the major reasons why not to rewrite, it should be clear that: You’ve dedicated a long time (possibly years) fixing many bugs, refining your business logic and improving your rudimentary database performance, so why do you need to start the whole process again?

Majority of ASP.NET Website Forms applications may be made up of a number of ASP.NET (or third party controls like Telerik or infragistics), which are basically dropped on the form, and utilized with a few statements in codebehind (aspx.vb or aspx.cs). This makes it more easier for developers with only rudimentary understanding of the application to swiftly cut and paste code into an existing application. This usually results to poor coding ethics, and some sort of poor performance. Controls like Update Panels make it very alluring to simply drop on the page and give access to .NET to take care of the rest.

As a quick illustration, I generated an ASP.NET page with a standard UpdatePanel, and codebehind. The UpdatePanel is usually utilized to stop any screen refreshes from occurring with a full page postback:

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();
					
}
			
Easy enough. The Button1 is pressed, an asynchronous request is made for the present date or time, and that is shown as Label1′s content. As easy as it sounds, view the actual HTTP post and response required to achieve the partial postback: 



Numerous data sent to and fro to the web server just to show a sixteen character string! This may be tolerable for irregular  used functionality, but, not in a strongly utilized production system. Fortunately, Microsoft has given us a more effectual way to achieve this, as part of the ASP.NET AJAX framework.

Page Methods

Page methods enable ASP.NET AJAX pages to directly carry out a page’s static methods using JavaScript Object Notation (JSON). Javascript Object Notation is basically a  version of Simple Object Access protocol (SOAP) for someonoe who lacks adornment in style or design., which is superbly befitting for light weight communication between server and client.

If you want to read further on implementing page methods and JSON, read this guide on Microsoft’s Exposing Web Services to Client Script in ASP.NET AJAX.

 There is a method we can use  to request only the information that we’re interested in which is called web method instead of performing a partial postback and then receiving HTML markup to totally put back our UpdatePanel’s contents,:

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();
					
}
			

Using this method, we've completely get rid of the extra data that was existing in the UpdatePanel's request, and minimized the response down to just the data we want to request: 


When we usr JSON, the entire HTTP round trip is 16 bytes, unlike updatePanel which is 872 bytes. That’s guesstimately a 5,000% upgrade!!, which will only persist to multiply with the difficulty of the page.

This has not only reduced our network footprint dramatically, but it get rid of the necessity for the server to instantiate the UpdatePanel’s controls and take them through their life span to give the HTML sent back to the browser. While I’m an advocate of the simplicity innate in the UpdatePanel, I think that it is very important that we use them prudently In any heavy use circumstance, they are very uncommonly the best bet

Ease of Development

As an established WebForms developer, you may be thinking: “Waiting, now I have to study how to use these latest fangled javascript libraries.” Or, “Now we have to spend a lot of time redoing every page with these new low level javascript functions which we’ve never used!” But, wait, there is an easy to use alternative that has both high performance and ease of use.

I’ve used JQuery, KendoUI, Knockout, and other high level javascript libraries to create some amazing applications such as a mapping application (allowing drag and drop between 2 maps on a page), Dashboard applications with full graphing capabilities, an Excel-like editable grid, and more, all in a matter of weeks. The power, speed and control you have with these high level javascript libraries are far and above that of standard WebForms development. Your code will be cleaner, and there will be no need to handle control from server side code to client side code, and vice versa, as all your UI coding will be in Javascript. As an example, I created a high performance Auditing ‘History’ page, which is entirely in Jquery and Javascript to replace an existing History Page in an ASP.NET WebForms application:


The History screen has all the features you’d expect in a typical Auditing / History form. Implemented features are: Search / Filter, Sort, Number of entries per page, “ToolTips” for extra long field data, and a cool feature that allows the user to get rid of NULL-Blank ‘transitions’, eliminating unnecessary records being displayed.

Your aspx / ascx page would consist of a div which the Jquery code will create the grid from, along with the appropriate Javascript function to call the display grid function:

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>

The external history.js javascript file consists of the actual function to create the grid (I used the ‘datatables’ plugin in this case, which allows fast server-side paging, allowing the history form to be extremely fast (< 1 second response time on any load, search, sort, page, or refresh operation.):

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) {

}

}

Your Server side code could be an .asmx page, .ashx page (.NET Handler), or even an .aspx page. In this case, I’ve used an .aspx page which acts the same as a typical codebehind aspx.vb page, except that I don’t handle any postbacks, only HTTP Requests. I’m using the “GET” method, as this particular Jquery plug-in (Datatables.net) implements this method when using the server-side paging functionality:


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 serializationhistory =     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

Note the use of VB.NET, LINQ and anonymous types (VB.NET handles anonymous type declaration differently than C#.). You can simply use a call to your existing data layer, instead of the use of LINQ as I did in this example.

Updating your existing application

This example, demonstrated how to simply replace your UI code with Javascript / Jquery. So, there would be no need to rewrite your entire application just to improve the performance and maintainability of your system. You may choose to simply upgrade your UI portion of your code, or even to use MVC. With the .NET Framework 4.5.1, you can even add MVC into your existing WebForms application without having to start from scratch. This will allow you to reuse all of your existing business logic, and only upgrade a few areas of your site at a time, while keeping your MVC code separate from your existing WebForms pages.

 

Categories:

Tags:

Leave a Reply

Your email address will not be published. Required fields are marked *