by Chris Johnson @
www.Kolaberate.com
April 20, 2014
There are many reasons to upgrade your existing ‘legacy’ applications. One of the main reasons would be performance. Are your pages just taking too long to load? Is every form submit taking more than a few seconds to respond? Users will usually get frustrated at this, especially if there is no ‘Loading’ panel or other visual indicator. This is usually the sign of a web application that was not written from the ground up to be as fast as possible. WebForms apps (written in the standard way) with very ‘heavy’ pages will take a long time to load every time the user interacts with a page. This is usually because of the fact that a postback is actually occurring. A postback is when the user presses a submit button somewhere on the form, and the entire page, is sent to the server, but more importantly, the entire page is again written back from the server, which causes a ‘double whammy’ effect of very slow page load times. These applications can usually be quickly changed to use lightweight javascript libraries, along with Web services, as a way to request and submit data from the page. In this way, only the minimum amount of data is actually loaded, and sent back and forth to the server. So, every time a user loads a page or performs any action requiring data to be sent back and forth to the server, only the essential data is sent back and forth, and not the entire page.
Save time and money by NOT rewriting your application
Resist the urge to re-write the entire application. Why? Just for a refresher, read Joel Spolsky’s classic article: Things you should never do, Part 1. There may be some extreme cases where a re-write is the best option, but 99% of the time a re-write is a huge mistake. After thinking of one of the main reasons why not to rewrite, it should be clear: You’ve already spent a long time (perhaps years) refining your Business logic, fixing many bugs, and improving your underlying database performance, so why start from scratch and have to do it all over again?
Most ASP.NET Web Forms applications consist of a number of ASP.NET (or third party controls such as Infragistics or Telerik), which are simply dropped on the form, and used with a few statements in codebehind (aspx.vb or aspx.cs). This makes it easy for developers with only a basic understanding of the application to quickly cut and paste code into an existing app. This usually leads to poor coding practices, and poor performance. Some controls such as Update Panels make it very tempting to simply drop on the page and allow .NET to take care of the rest. As a quick example, I created an ASP.NET page with a standard UpdatePanel, and codebehind. The UpdatePanel is usually used to eliminate any screen refreshes 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();
}
Simple enough. Button1 is clicked, an asynchronous request is made for the current date/time, and that is displayed as Label1′s content. As simple as it sounds, take a look at the actual HTTP post and response necessary to accomplish the partial postback:
That’s a LOT of data sent back and forth to the web server just to display a 16 character string! This may be acceptable for infrequently used functionality, but, not in a heavily used production system. Luckily, Microsoft has given us a more efficient way to do this, as part of the ASP.NET AJAX framework.
Page Methods
Page methods allow ASP.NET AJAX pages to directly execute a page’s static methods using JSON (JavaScript Object Notation). JSON is basically a minimalistic version of SOAP, which is perfectly suited for light weight communication between client and server. For more information about how to implement page methods and JSON, take a look at Microsoft’s Exposing Web Services to Client Script in ASP.NET AJAX.
Instead of performing a partial postback and then receiving HTML markup to completely replace our UpdatePanel’s contents, we can use a web method to request only the information that we’re interested in:
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();
}
Through this method, we’ve completely eliminated the extra data that was present in the UpdatePanel’s request, and reduced the response down to just the data we’re interested in requesting:
Using JSON, the entire HTTP round trip is 16 bytes, as compared to 872 bytes for the UpdatePanel. That’s roughly a 5,000% improvement!!, which will only continue to increase with the complexity of the page. Not only has this reduced our network footprint dramatically, but it eliminates the necessity for the server to instantiate the UpdatePanel’s controls and take them through their life cycles to render the HTML sent back to the browser. While I’m a proponent of the simplicity inherent in the UpdatePanel, I think that it is crucial that we use them judiciously. In any heavy use situation, they are very rarely the best solution.
Ease of Development
As a seasoned WebForms developer, you may be thinking: “Wow, now I have to learn how to use these new 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.
Great content! Keep up the good work!