In this article, there are scenarios in which we will see what could be the least complicated ways to use grid views and update them asynchronously from the client browser using JavaScript.
Introduction
The Grid View control is one of the most complicated and resource heavy components that we can have in our ASP.NET web pages. The Grid View control makes it very easy for us to create complex data grids with multiple columns and rows. There can be custom styling for the header, footer, data rows, alternate rows and so on. This makes it crucial for everyone who needs to use grid view control to have a good understanding of how this control should be used on both the server and client side.
While it remains true that the world is quickly moving on to pure JavaScript grid libraries and other such frameworks, there are still so many applications that are still dependent on ASP.NET grid views and a good number of devs who have to face the difficult challenge of maintaining those applications.
There can be many different scenarios in which we can use this control based on our requirements; often times, we need to have multiple grids on a single web page to show a variety of information to the user. These kinds of situations make it difficult to code the gridview
properly. It becomes even more difficult when we need to update the grids asynchronously from JavaScript.
In this article, there are scenarios in which we will see what could be the least complicated ways to use grid views and update them asynchronously from the client browser using JavaScript.
Target Audience
This code should be beneficial to all those who have at some point worked with complex grid pages. Those who are already working on applications which extensively use multiple grid view controls to display loads of data can also use this article to properly manage the async updates to the grids.
Using the Code
This article will be addressing two cases where we can use single and multiple grid view controls. In the first scenario, there is a single grid which we need to update asynchronously.
The second case will be of multiple grids which we will need to update one by one, so in this case, we will need to do some kind of event chaining like we do when implementing JavaScript promises. This means that we can only make the async update of the second grid after the first grid's update has been finished.
Single Grid View Control
In this scenario, there is a grid view control inside an update panel. We will need the update panel to make async updates to the grid. The grid will be populated by test data generated by the code. If you want more robust testing, then you can wire up the data source of the grid with the actual database table.
There is an input which can be used to add more records to the grid. Whenever a new record is added, then we have to refresh the grid contents. For that purpose, we will need to fire the update event of the update panel enclosing the grid so that the server code used to bind the data to the grid can be executed.
This implementation is easy to understand. I have used __doPostBack
to update the grid from the JavaScript code, this method serves little purpose to refresh a single grid, but because our next example has multiple grids, I am using __doPostBack
so that you can understand how it works.
Let's walk through the code to see how things are working here. Add an empty ASP.NET web form to a new Web Application Project. Name the page SingleGrid.aspx and add an update panel named upMain
. Inside the update panel, add a grid view control and name it gridMain
.
Below the update panel, add a text box to input a new name to add to the data collection. Also add an HTML anchor whose click event will be wired to a JavaScript function to call the __doPostBack
function.
We will need to wire up the events to add a new record to the data and then to update the grid control. Bind the document DOMContentLoaded
event with the onLoad
function and the anchor's click event with the AddRecord
function. The AddRecord
function will call the __doPostBack
function to make an async update for the upMain
update panel.
SingleGrid.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SingleGrid.aspx.cs"
Inherits="WebApplication1.SingleGrid" %>
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<form runat="server">
<asp:ScriptManager ID="scriptManager1" runat="server"
AsyncPostBackTimeout="90"></asp:ScriptManager>
<h3>All Records</h3>
<asp:UpdatePanel ID="upMain" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:GridView ID="gridMain" runat="server"
OnDataBinding="gridMain_DataBinding" CellPadding="10">
</asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>
<h3>Add New Record</h3>
<input id="txtName" type="text" placeholder="Name" />
<br />
<a href="#" id="lnkAddRecord">Add</a>
</form>
<script>
var upMainID = '<%= upMain.ClientID%>';
document.addEventListener("DOMContentLoaded", onLoad);
var lnkAddRecord = null;
function onLoad()
{
lnkAddRecord = document.querySelector('#lnkAddRecord');
lnkAddRecord.addEventListener('click', AddRecord);
}
function AddRecord()
{
var txtName = document.querySelector('#txtName');
__doPostBack(upMainID, txtName.value);
return false;
}
</script>
</body>
</html>
The JS code statement __doPostBack(upMainID, txtName.value);
is doing all the magic here. The implementation of __doPostBack
is as follows:
function __doPostBack(eventTarget, eventArgument) {
if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
theForm.__EVENTTARGET.value = eventTarget;
theForm.__EVENTARGUMENT.value = eventArgument;
theForm.submit();
}
}
In the above, we have to pass two arguments, the eventArgument
being the optional one.
Add the methods in the .cs file to wire up the data binding event. We will be creating the test data from the code and there is a method named SetData
for that purpose. AddRecord
method is used to add a new record to our table. This table is stored in the application session state so that we can retrieve this information in subsequent partial postbacks.
SingleGrid.aspx.cs
namespace WebApplication1
{
public partial class SingleGrid : System.Web.UI.Page
{
private DataTable _table;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
SetData();
}
else
{
String parameter = Request["__EVENTARGUMENT"];
AddRecord(parameter);
}
gridMain.DataBind();
}
protected void gridMain_DataBinding(object sender, EventArgs e)
{
gridMain.DataSource = (Session["MainTable"] as DataTable).DefaultView;
}
private void SetData()
{
_table = new DataTable();
_table.Columns.Add("ID", typeof(Int32));
_table.Columns.Add("Name", typeof(String));
_table.Rows.Add(1, "David");
_table.Rows.Add(1, "Mark");
Session["MainTable"] = _table;
}
private void AddRecord(String name)
{
DataTable table = Session["MainTable"] as DataTable;
table.Rows.Add(table.Rows.Count + 1, name);
}
}
}
Whenever the page load event is fired, it is being checked if it's a postback or not. Depending on the outcome of that check, we have to either load the data for the first time or add a new record to the existing collection. We are passing the record Name
value as an event argument when we are executing the __doPostBack
from the client JavaScript. This argument's value is captured on the server side and then is being used to add a new record.
To see how this example is working, just execute the Web Form, enter a Name and click on the Add link and you will see that a new name is added to our existing data and the grid also refreshes.
Let's move on to a more involved and challenging scenario where we need to make async updates in multiple grids.
Multiple Grid View Controls
In this example, we will update a couple of grid controls based on any input's event. A drop down input will contain a list of countries and two grid controls will contain State
and City
list. The code for this example is somewhat complex than the previous example as we will utilize the page events of ASP.NET Ajax. There are two ways in which we are going to update the grids:
- Updating grids when another input is changed
- Updating second grid based on the user's interaction with the first grid
This example uses test data tables in the code to create static data. You can modify the sample code to connect the grids to an existing data source. Add a new webpage to our existing application and name it MultipleGrids.aspx. Add a drop down input and name it cboCountryList
which will contain a list of countries. Whenever the value in the input changes, we will need to refresh the grids with the correct data. We don't need to have the drop down input as a server control but if you want to fill it from the server code with a list, then it needs to have runat='server'
attribute.
Add two Grid View controls within their own Update Panels so that we can update them asynchronously. Now the problem that most of us face is how to handle the async update of multiple grid views because it is not straight forward to refresh them from JavaScript in the first place and it is weird to subscribe to their update complete event. Things get even more serious when we have to implement a promise like code structure to chain update panel refreshes.
To update multiple grids, we need to add ASP.NET Ajax page loaded event handler.
Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(pageLoaded);
In the above, the pageLoaded
function will always get called for complete and partial page loads. This is the exact place where we need to execute any code that needs to be called after the update panel is finished refreshing its contents.
The following code is for the MultipleGrids.aspx:
<%@ Page Language="C#" AutoEventWireup="true"
CodeBehind="MultipleGrids.aspx.cs" Inherits="WebApplication1.MultipleGrids" %>
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="https://code.jquery.com/jquery-1.12.0.min.js" type="text/javascript"></script>
</head>
<body>
<form id="Form1" runat="server">
<asp:ScriptManager ID="scriptManager1" runat="server"
AsyncPostBackTimeout="90"></asp:ScriptManager>
<select ID="cboCountryList">
<option value="0" selected>United States</option>
<option value="1">India</option>
</select>
<h3>States</h3>
<asp:UpdatePanel ID="upMain1" runat="server"
UpdateMode="Conditional" AutoGenerateColumns="false">
<ContentTemplate>
<asp:GridView ID="gridMain1" runat="server"
OnDataBinding="gridMain1_DataBinding"
OnRowDataBound="gridMain1_RowBound" CellPadding="10">
</asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>
<br />
<h3>Cities</h3>
<asp:UpdatePanel ID="upMain2" runat="server"
UpdateMode="Conditional" AutoGenerateColumns="false">
<ContentTemplate>
<asp:GridView ID="gridMain2" runat="server"
OnDataBinding="gridMain2_DataBinding" CellPadding="10">
</asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>
</form>
<script>
Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(pageLoaded);
var upMain1ID = '<%= upMain1.ClientID%>';
var upMain2ID = '<%= upMain2.ClientID%>';
var gridMain1ID = '<%= gridMain1.ClientID%>';
var cboCountryList = null;
document.addEventListener("DOMContentLoaded", onLoad);
function onLoad()
{
cboCountryList = document.querySelector('#cboCountryList');
cboCountryList.addEventListener('change', UpdateCountry);
loadGridMain1();
}
function loadGridMain1()
{
$('#' + gridMain1ID).find('[gridmain1row="true"]').each(function (idx, el)
{
el.onclick = function ()
{
var stateID = $(el).attr('StateID');
return function ()
{
grid1RowClick(stateID);
}
}();
});
}
function grid1RowClick(stateID)
{
__doPostBack(upMain2ID, stateID);
}
function pageLoaded(sender, args)
{
var prm = Sys.WebForms.PageRequestManager.getInstance();
if (prm.get_isInAsyncPostBack())
{
var updatedPanels = args.get_panelsUpdated();
for (var x = 0; x < updatedPanels.length; x++)
{
var panel = updatedPanels[x].id;
switch (panel)
{
case upMain1ID:
loadGridMain1();
__doPostBack(upMain2ID, '');
break;
}
}
}
}
function UpdateCountry()
{
__doPostBack(upMain1ID, cboCountryList.value);
return false;
}
</script>
</body>
</html>
When the drop down input value is changed, then all the grids are refreshing one by one. Also, when we select an item in the first grid, then the second grid refreshes. There are functions which are wired up with the change and click events. UpdateCountry
will be called when the country drop down changes its selected value, and grid1RowClick
will be called when any row in the first grid is clicked. Event binding of the HTML elements inside a grid needs to be handled both on the server and the client side. On the server side, we have to add custom attributes in the grid's Row Bound event. Later when the page loads in the browser, we can identify the grid elements based on those attributes we added and thus we can wire up those DOM elements with the JavaScript functions.
The first grid will have the State
list of the selected country and the second grid will have the City
list of the selected State
. In the pageLoaded
function, I have used PageRequestManager.get_isInAsyncPostBack()
to check if the current page load is partial or not. Based on that, we need to identify the update panel that was refreshed to proceed chaining the subsequent code blocks. So when the first update panel is finished loading, then we are calling __doPostBack
for the second grid's update panel. We cannot make async updates of multiple update panels simultaneously; doing that will prompt the JavaScript to throw a Parameter Count Mismatch error.
Let's move on to the server code now. Add the following code to the MultipleGrids.aspx.cs file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;
namespace WebApplication1
{
public enum Countries
{
UnitedStates = 0,
India = 1
}
public partial class MultipleGrids : System.Web.UI.Page
{
private DataTable _stateTable;
private DataTable _cityTable;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
Session["SelectedCountry"] = 0;
Session["SelectedState"] = 5;
SetData();
}
else
{
String target = Request["__EVENTTARGET"];
String parameter = Request["__EVENTARGUMENT"];
if (target == "upMain1")
{
Session["SelectedCountry"] = Convert.ToInt32(parameter);
switch ((Countries)Session["SelectedCountry"])
{
case Countries.UnitedStates:
Session["SelectedState"] = 5;
break;
case Countries.India:
Session["SelectedState"] = 1;
break;
}
}
else if (target == "upMain2" && parameter.Length > 0)
{
Session["SelectedState"] = Convert.ToInt32(parameter);
}
}
gridMain1.DataBind();
gridMain2.DataBind();
}
protected void gridMain1_DataBinding(object sender, EventArgs e)
{
(Session["StateTable"] as DataTable).DefaultView.RowFilter =
String.Format("CountryID = '{0}'", (Int32)Session["SelectedCountry"]);
gridMain1.DataSource = (Session["StateTable"] as DataTable).DefaultView;
}
protected void gridMain2_DataBinding(object sender, EventArgs e)
{
(Session["CityTable"] as DataTable).DefaultView.RowFilter =
String.Format("StateID = '{0}'", (Int32)Session["SelectedState"]);
gridMain2.DataSource = (Session["CityTable"] as DataTable).DefaultView;
}
private void SetData()
{
_stateTable = new DataTable();
_cityTable = new DataTable();
_stateTable.Columns.Add("CountryID", typeof(Int32));
_stateTable.Columns.Add("StateID", typeof(Int32));
_stateTable.Columns.Add("State", typeof(String));
_stateTable.Columns.Add("Area", typeof(String));
_cityTable.Columns.Add("StateID", typeof(Int32));
_cityTable.Columns.Add("City", typeof(String));
_cityTable.Columns.Add("Population", typeof(String));
#region India
...
#endregion India
#region United States
...
#endregion United States
Session["StateTable"] = _stateTable;
Session["CityTable"] = _cityTable;
}
protected void gridMain1_RowBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
DataRowView row = e.Row.DataItem as DataRowView;
e.Row.Attributes.Add("gridmain1row", "true");
e.Row.Attributes.Add("StateID", row["StateID"].ToString());
}
}
}
}
The above code is mostly working same as the first example. We are updating the state of the grids stored in the application session state based on the event arguments. After that, the test data rebinds with the grids. Because the update panel re-renders its enclosing content, the grid is refreshed as a result.
As mentioned before, the gridMain1_RowBound
event is used to add custom attribute gridmain1Row
to each grid row. The argument StateID
is also being added as an attribute so that each row can have its own state information that can be later passed on to the JavaScript event function. In the client code, we have to find each row element which has the gridmain1Row
attribute using jQuery and then loop out each of those elements to bind them to a JavaScript closure.
To test the code, change the selected country and both grids will refresh one after the other. Click on any State
grid's row and the city grid will refresh to show the correct data.
Points of Interest
The code sample is attached for you to see and play with. There can be scenarios when the grids can reside in separate ASP.NET page controls. In order to execute a chained update panel refresh in this case, we will need to register our events to a global event system such that all the user controls have access to that event system and we can execute cross control code without breaking any modules.
History
- 26th January, 2016: Initial version