Friday, September 21, 2007 5:04 PM
kazimanzurrashid
Asynchronous Asp.net Page
Jeff Prosise showed us the power of Asynchronous Asp.net Page in developing truly scalable web application in his great article. But I found a lot people finding hard time implementing it in real life based upon those examples. For example, in the data bind example he uses the BeginExcecuteReader/EndExecuteReader method of the DataReader but in real life we do not use it, instead we return some custom entity collection or the DataReader/DataSet directly from the bussiness logic layer. Omar showed me why this approach will not work and even it will add extra overhead comparing the regular model.In this post, I will show you some real life usage of the asynchronous pages and how easily you can convert your regular pages with it. Let us start with a simple example, you want show all the customers of USA of the Northwind database in a GridView. You will come up something similar like the following:
protected void Page_Load(object sender,
EventArgs e)
{
if (!IsPostBack)
{
LoadCustomers();
}
}
private void LoadCustomers()
{
string SQL = string.Format("SELECT * FROM [Customers] WHERE [Country] = '{0}'", "USA");
using (IDbConnection cnn = new SqlConnection(_connectionString))
{
cnn.Open();
using (IDbCommand cmd = cnn.CreateCommand())
{
cmd.CommandText = SQL;
cmd.CommandType = CommandType.Text;
using (IDataReader rdr = cmd.ExecuteReader())
{
//Bind it with the GridView
gvCustomers.DataSource = rdr;
gvCustomers.DataBind();
}
}
}
}
Now let us convert this regular page with the asynchronous model. First add Async="true" in the page directive. Next, create a delegate with the same signature of LoadCustomers method, we will use this delegate for the async operation. The following shows the complete code:
private delegate void LoadData();
private LoadData _loadCustomers;
protected void Page_Load(object sender,
EventArgs e)
{
if (!IsPostBack)
{
AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginLoadCustomers),
new EndEventHandler(EndLoadCustomers));
}
}
private IAsyncResult BeginLoadCustomers(object sender,
EventArgs e,
AsyncCallback cb,
object state)
{
_loadCustomers = new LoadData(LoadCustomers);
return _loadCustomers.BeginInvoke(cb, state);
}
private void EndLoadCustomers(IAsyncResult result)
{
_loadCustomers.EndInvoke(result);
}
private void LoadCustomers()
{
string SQL = string.Format("SELECT * FROM [Customers] WHERE [Country] = '{0}'", "USA");
using (IDbConnection cnn = new SqlConnection(_connectionString))
{
cnn.Open();
using (IDbCommand cmd = cnn.CreateCommand())
{
cmd.CommandText = SQL;
cmd.CommandType = CommandType.Text;
using (IDataReader rdr = cmd.ExecuteReader())
{
//Bind it with the GridView
gvCustomers.DataSource = rdr;
gvCustomers.DataBind();
}
}
}
}
What we are doing is very simple, we are wrapping up the actual method with a Begin/End Pair and we are passing the pair to AddOnPreRenderCompleteAsync method. In the Begin we are creating an instance of the delegate and calling the invoke which actually fires a thread from the ThreadPool. We can also use the RegisterAsyncTask method instead of AddOnPreRenderCompleteAsync and there some extra advantage of using it instead of AddOnPreRenderCompleteAsync, among the advantages I think the thread impersonate(It is not possible to access the HttpContext.Current from the other threads) and the parallel execution of multiple async method are the most important. Let us consider that you also want to show the employees in another GridView of the Northwind database along with the customers which country is set to USA, since there is no dependency between these operations we can use two separate thread to for each operation. The following shows the complete code:
private delegate void LoadData();
private LoadData _loadCustomers;
private LoadData _loadEmployees;
protected void Page_Load(object sender,
EventArgs e)
{
if (!IsPostBack)
{
RegisterAsyncTask(
new PageAsyncTask(
new BeginEventHandler(BeginLoadCustomers),
new EndEventHandler(EndLoadCustomers), null, null, true)
);
RegisterAsyncTask(
new PageAsyncTask(
new BeginEventHandler(BeginLoadEmployees),
new EndEventHandler(EndLoadEmployees), null, null, true)
);
}
}
private IAsyncResult BeginLoadCustomers(object sender,
EventArgs e,
AsyncCallback cb,
object state)
{
_loadCustomers = new LoadData(LoadCustomers);
return _loadCustomers.BeginInvoke(cb, state);
}
private void EndLoadCustomers(IAsyncResult result)
{
_loadCustomers.EndInvoke(result);
}
private IAsyncResult BeginLoadEmployees(object sender,
EventArgs e,
AsyncCallback cb,
object state)
{
_loadEmployees = new LoadData(LoadEmployees);
return _loadEmployees.BeginInvoke(cb, state);
}
private void EndLoadEmployees(IAsyncResult result)
{
_loadEmployees.EndInvoke(result);
}
private void LoadCustomers()
{
string SQL = string.Format("SELECT * FROM [Customers] WHERE [Country] = '{0}'", "USA");
using (IDbConnection cnn = new SqlConnection(_connectionString))
{
cnn.Open();
using (IDbCommand cmd = cnn.CreateCommand())
{
cmd.CommandText = SQL;
cmd.CommandType = CommandType.Text;
using (IDataReader rdr = cmd.ExecuteReader())
{
//Bind it with the GridView
gvCustomers.DataSource = rdr;
gvCustomers.DataBind();
}
}
}
}
private void LoadEmployees()
{
string SQL = string.Format("SELECT * FROM [Employees] WHERE [Country] = '{0}'", "USA");
using (IDbConnection cnn = new SqlConnection(_connectionString))
{
cnn.Open();
using (IDbCommand cmd = cnn.CreateCommand())
{
cmd.CommandText = SQL;
cmd.CommandType = CommandType.Text;
using (IDataReader rdr = cmd.ExecuteReader())
{
//Bind it with the GridView
gvEmployees.DataSource = rdr;
gvEmployees.DataBind();
}
}
}
}
The code is almost same as the previous one except we have added another set of methods for the employees and we are using the RegisterAsyncTask method instead of AddOnPreRenderCompleteAsync in page load event. Here the most important thing in the RegisterAsyncTask method is the last parameter where we are passing true, this ensures that both the method will be executed simultaneously. If we used AddOnPreRenderCompleteAsync method or do not pass true in the last parameter of the RegisterAsyncTask method then it will first execute the customer part, once complete it start the employee part.
In the last and final section, we will see an Master/Detail example of Async Page. We will have a DropDownList which will be populated with all the categories of Northwind database and based upon the selected category we will load the products in a GridView. Since the product loading depends upon the selected category& thus it is not possible to do the parallel execution of both method, instead the product loading needs to wait until the category loads first. The following shows the compete code:
private delegate void LoadData();
private LoadData _loadCategories;
private LoadData _loadProducts;
protected void Page_Load(object sender,
EventArgs e)
{
if (!IsPostBack)
{
RegisterAsyncTask(
new PageAsyncTask(
new BeginEventHandler(BeginLoadCategories),
new EndEventHandler(EndLoadCategories), null, null)
);
RegisterAsyncTask(
new PageAsyncTask(
new BeginEventHandler(BeginLoadProducts),
new EndEventHandler(EndLoadProducts), null, null)
);
}
}
protected void Category_Changed(object sender,
EventArgs e)
{
RegisterAsyncTask(new PageAsyncTask(
new BeginEventHandler(BeginLoadProducts),
new EndEventHandler(EndLoadProducts), null, null)
);
}
private IAsyncResult BeginLoadCategories(object sender,
EventArgs e,
AsyncCallback cb,
object state)
{
_loadCategories = new LoadData(LoadCategories);
return _loadCategories.BeginInvoke(cb, state);
}
private void EndLoadCategories(IAsyncResult result)
{
_loadCategories.EndInvoke(result);
}
private IAsyncResult BeginLoadProducts(object sender,
EventArgs e,
AsyncCallback cb,
object state)
{
_loadProducts = new LoadData(LoadProducts);
return _loadProducts.BeginInvoke(cb, state);
}
private void EndLoadProducts(IAsyncResult result)
{
_loadProducts.EndInvoke(result);
}
private void LoadCategories()
{
const string SQL = "SELECT [CategoryID], [CategoryName] FROM [Categories]";
using (IDbConnection cnn = new SqlConnection(_connectionString))
{
cnn.Open();
using (IDbCommand cmd = cnn.CreateCommand())
{
cmd.CommandText = SQL;
cmd.CommandType = CommandType.Text;
using (IDataReader rdr = cmd.ExecuteReader())
{
ddlCategory.DataSource = rdr;
ddlCategory.DataBind();
}
}
}
}
private void LoadProducts()
{
string SQL = string.Format("SELECT * FROM [Products] WHERE [CategoryID] = {0}",
ddlCategory.SelectedItem.Value);
using (IDbConnection cnn = new SqlConnection(_connectionString))
{
cnn.Open();
using (IDbCommand cmd = cnn.CreateCommand())
{
cmd.CommandText = SQL;
cmd.CommandType = CommandType.Text;
using (IDataReader rdr = cmd.ExecuteReader())
{
gvProducts.DataSource = rdr;
gvProducts.DataBind();
}
}
}
}
As you can see that we are not passing true in the last argument of RegisterAsyncTask method, it ensures that the category will load first and then the products. We are also registering another async task in the selected index change event of the DropDownList.
I hope this will clear your confusion with async pages and you will be able to implement easily it in your projects.
Download: Full Source

Filed under: Asp.net