Introduction
It happened to me again recently.
One of my colleagues showed me a legacy ASP.Net application, and wanted to know how to fix a nasty issue with it. Their webpage would start to get displayed, but the pulldown menu would appear on the screen – half-drawn, and in a mess - and freeze there for 6-7 seconds, until suddenly the page sorted itself out, and displayed itself properly.
The cause ?
The developer had decided to put all of their time-consuming code to load SQL Server data into their Page_Load
function. And once that started running, the browser would grind to a halt until the data had finished loading.
In this article, I'll take you through the simple steps to get rid of this problem, by moving the time-consuming code into a separate thread, then getting the webpage to update itself once the data is ready.
Disclaimer
It goes without saying, it's much more efficient and maintainable to use a modern technology like Angular to load and display data, as I have described in my other articles, but this article is a simple step-by-step guide to quickly improve legacy pages, without having to rewrite everything.
Follow these steps, and with very little effort, you'll make your slow, painful webpages much more responsive, and get you a payrise when you have your next annual review. Perhaps.
Legacy code
Let's start with the how the code used to look like.
Basically, our developer had put his code, to load data from SQL Server, in the Page_Load
function.
We’ll simulate this problem by creating a 10 second pause, then generating a sample DataTable
, and setting the DataGrid
to use the DataTable
as its data source.
protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack)
return;
System.Threading.Thread.Sleep(10000);
System.Data.DataTable dt = new System.Data.DataTable("Drivers");
dt.Columns.Add("UserID", Type.GetType("System.Int64"));
dt.Columns.Add("Surname", Type.GetType("System.String"));
dt.Columns.Add("Forename", Type.GetType("System.String"));
dt.Columns.Add("Sex", Type.GetType("System.String"));
dt.Columns.Add("Date of Birth", Type.GetType("System.DateTime"));
dt.Rows.Add(new object[] { 1, "James", "Spencer", "M", new DateTime(1962, 3, 19) });
dt.Rows.Add(new object[] { 2, "Edward", "Jones", "M", new DateTime(1939, 7, 12) });
dt.Rows.Add(new object[] { 3, "Janet", "Spender", "F", new DateTime(1996, 1, 7) });
dt.Rows.Add(new object[] { 4, "Maria", "Percy", "F", null });
dt.Rows.Add(new object[] { 5, "Malcolm", "Marvelous", "M", new DateTime(1973, 5, 7) });
this.grid.DataSource = dt;
this.grid.DataBind();
}
The result of this code was that the webpage was painfully slow to appear. Usually the browser would get so far in displaying the page, then freeze for several seconds, before finally managing to display the complete webpage.
Meanwhile, the user has been staring at their browser, wondering if it’s crashed, or if their laptop has hung.
It's not a pleasant experience.
Let’s make it async !
Okay, so let’s make it better.
The first step is to go into your .aspx file, and locate which section of your webpage contains the controls that you’ll want to update when your time-consuming task finishes.
In our case, this is simple, only our DataGrid
will need to be updated once our data load has completed.
<asp:DataGrid ID="grid" runat="server"></asp:DataGrid>
What we need to do is wrap this control (or group of controls) inside an UpdatePanel
and ContentTemplate
, and we need to add a Timer
control to our page.
<asp:UpdatePanel ID="panel" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:Timer ID="MyTimer" OnTick="timer_tick" Interval="1000" runat="server" />
-->
<asp:DataGrid ID="grid" runat="server"></asp:DataGrid>
</ContentTemplate>
</asp:UpdatePanel>
Our aim is to get the webpage onto the screen as quickly as possible, and once our data is loaded, we can go back and update (just) this section of the webpage.
Our next step is to separate out the data-loading code from our Page_Load
function into its own function, and to make it populate a variable (a DataTable
in this example) which is stored in a Session variable.
You’ll need one of these Session
variables for each chunk of data which you’ll be loading asynchronously.
We will also need a boolean Session
variable, bReadyToDisplayData
, which we’ll set to “true” when our data has finished loading.
System.Data.DataTable dt
{
get
{
return (System.Data.DataTable)Session["table1"];
}
set
{
Session["table1"] = value;
}
}
bool bReadyToDisplayData
{
get
{
return (bool)Session["bReadyToDisplayData"];
}
set
{
Session["bReadyToDisplayData"] = value;
}
}
protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack)
return;
bReadyToDisplayData = false;
LoadDataFromWebService();
}
private void LoadDataFromWebService()
{
System.Threading.Thread.Sleep(10000);
dt = new System.Data.DataTable("Drivers");
bReadyToDisplayData = true;
}
This looks better, but we are still calling our data-loading function synchronously, so next, lets change Page_Load
to call it asynchronously..
System.Threading.Thread thread = new System.Threading.Thread(LoadDataFromWebService);
thread.Start();
There’s just one last thing to add, to tie this all together.
You’ll have noticed that we added a Timer
control to our .aspx file. This is to allow us to periodically check whether our data has finished loading, and when it has loaded, to populate our grid controls.
To do this, we need a Timer
"tick” handler.
(Without this Timer
variable, we wouldn’t have a method of updating our UpdatePanel
.)
protected void timer_tick(object sender, EventArgs e)
{
if (bReadyToDisplayData == false)
{
return; }
this.grid.DataSource = dt;
this.grid.DataBind();
this.panel.Update();
MyTimer.Enabled = false;
}
And that’s it !
Now, when you open this aspx webpage, it’ll get displayed really quickly, go off and load it’s data, and when it’s finished, we’ll set the DataGrid
’s DataSource
to point to this data, and get it to display it.
Session variables
Just a quick note on using Session variables.
If you find that this code doesn't work for you, check whether your .aspx
page is managing to save Session variable values. To do this, put a breakpoint in your Page_Load
function, just after you've set the bReadyToDisplayData
variable.
protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack)
return;
bReadyToDisplayData = false;
System.Threading.Thread thread = new System.Threading.Thread(LoadSomeData);
If you then examine the Session
variable, its "Count
" value should be at least 1 (as we've just added a Session variable to store the bReadyToDisplayData
variable).
If this Count
value is 0, then your webpage isn't storing Session variables, and this code won't work properly.
Two things to check:
1. Does your Server Name contain underscore characters ? Apparently, this can cause problems with IIS.
2. Try adding the following lines into your Global.asax
file:
protected void Session_Load(object sender, EventArgs e)
{
Session["info"] = 1;
}
This Session variable problem seems to have been noticed mainly on Internet Explorer 11.
Summary
And, that’s it. A simple walkthrough of taking some pretty painful code, and making it more user friendly.
I’ve seen plenty of in-house webpages suffering from this problem, particularly when dealing with large quantities of data. Often the user would click on a link to open the webpage, and after 30 seconds of watching the hourglass going around, would give up and go elsewhere.
Now, we can easily modify such code, to make the code more responsive.
And you could, of course, take this further, perhaps by adding a “Please wait” message inside of your UpdatePanel
, which gets hidden once the data has finished loading.
Personally, I like showing a timer on the screen when I know my webpage will take a few seconds to prepare the data it needs to show. Users tend to trust webpages more, when they can actually see a timer counting the seconds. It’s a much friendlier experience than a browser window which just looks “locked”.