Introduction
This article demonstrates an easy method to access the data of templated controls (Repeater
, DataList
, DataGrid
) from client-side scripts. This will allow us to use this data in client-side operations such as calculation and validation.
Background
Templated controls are a special type of data-bound server controls that offer great flexibility for rendering list-like data. At design-time, a templated control enables you to use a combination of HTML and server controls to design a custom layout for the fields of a single list item. And at run-time, it binds to the data source, walks through the bound items, and produces graphical elements according to the template you designed.
.NET framework 1.0/1.1 ships with three templated controls, the Repeater
, the DataList
, and the DataGrid
. You can also build your own templated control by implementing the INamingContainer
interface.
A challenge you may encounter when using templated controls is how to access the data in the generated child controls from client-side script, because the client-side does not understand templates, it only understands the names of individual controls, and it�s hard to guess what the name of each of the child controls will be at run-time.
The problem
I�ll use a simple example to explain my idea. Let�s assume we have a database table that stores employee expenses over a certain period. The table has the following schema:
ID |
int |
ExpenseType |
varchar |
ExpenseAmount |
decimal |
We want to allow the employee to enter the amount he spent on each expense type using a DataGrid
control. As the employee updates the amount of each expense, it is required to display the total amount he entered, and to validate that this total does not exceed 1000.
These requirements are easy to achieve if we decide to make the updates row by row. This means that as the employee updates each row, the form has to make post-backs to the server to calculate the new total, and apply the validation rule.
The client-side approach
My proposed approach has several benefits, as it conserves the server and network resources, and provides a more interactive user interface.
The DataGrid
used has the following structure:
<asp:datagrid id="dgExpenses" runat="server" AutoGenerateColumns="False">
<Columns>
<asp:BoundColumn DataField="ExpenseType" HeaderText="Expense Type">
</asp:BoundColumn>
<asp:TemplateColumn HeaderText="Expense Amount">
<ItemTemplate>
<asp:TextBox Text='<%# DataBinder.Eval(Container.DataItem,"ExpenseAmount")%>'
Runat="server" ID="txtExpAmount"></asp:TextBox>
</ItemTemplate>
</asp:TemplateColumn>
</Columns>
</asp:datagrid>
The code that populates the DataGrid
is straightforward:
SqlConnection conn = new
SqlConnection("server=localhost;database=pubs;uid=sa;pwd=");
SqlCommand cmd = new SqlCommand("select * from timesheet", conn);
conn.Open();
SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
dgExpenses.DataSource = rdr;
dgExpenses.DataBind();
rdr.Close();
To expose the data to the client-side script, I use a hidden field to store the initial values of the ExpenseAmount
field as a string of comma-separated values.
hdnRowArr.Value = "";
string expAmount;
foreach(DataGridItem dgi in dgExpenses.Items)
{
if(dgi.ItemType == ListItemType.Item ||
dgi.ItemType == ListItemType.AlternatingItem)
{
expAmount = ((TextBox)dgi.Cells[1].Controls[1]).Text;
hdnRowArr.Value += expAmount + ",";
}
}
hdnRowArr.Value = hdnRowArr.Value.TrimEnd(',');
In the client-side, I split the comma-separated values into an array. Using an array makes it easier to update the values when the user makes a change. It also simplifies calculating the total. I use the following function to fill the array. Then I calculate the initial total to update the UI. This function is invoked in the Body OnLoad
event.
function refreshArray()
{
var commaSeparated = document.getElementById("hdnRowArr").value;
arrRows = commaSeparated.split(",");
sumArray();
}
Now we need a way to detect any changes made by the user, update the array, and re-calculate the total. I add the following code to the DataGrid
ItemDataBound
event.
if(e.Item.ItemType == ListItemType.Item ||
e.Item.ItemType == ListItemType.AlternatingItem)
{
int rowIndex = e.Item.ItemIndex;
TextBox txtExpAmount = (TextBox)e.Item.Cells[1].Controls[1];
string evtHandler = "updateValue(this," + rowIndex.ToString() + ")";
txtExpAmount.Attributes.Add("onblur", evtHandler);
}
This code will add an event handler for the onblur
event of each TextBox
in the ExpenseAmount
template column. The event will be handled by the updateValue
client-side function. I pass to the function a reference to the child control, and the index of the row that contains the control. The index is used to locate the array element that corresponds to the DataGrid
row.
function updateValue(field, rowNum)
{
arrRows[rowNum] = field.value;
sumArray();
}
Calculating the total and updating the UI is done by this function:
function sumArray()
{
var sum = 0;
var expAmount;
for(var i = 0;i < arrRows.length; i++)
{
expAmount = parseFloat(arrRows[i]);
if(!isNaN(expAmount))
sum += expAmount;
}
document.getElementById("txtTotalExp").value = sum;
}
Finally, we can associate any validator with the txtTotalExp
TextBox
. We have to set the "ReadOnly
" property of the TextBox
to "true
" to prevent the user from updating the total manually. So, now with every change to the values of the ExpenseAmount
TextBox
es, the total will be automatically re-calculated.
Using the code
In order to apply this method for any templated control, you can follow these steps for each column you want to access from the client-side:
- Add a hidden field, and fill it with the initial data that are retrieved from the data source. Pass the data to the field as comma-separated values.
- Add an
Array
variable, and the corresponding refreshArray
, updateValue
, and sumArray
JavaScript functions to the form. You can replace the sumArray
function with another function to perform any other operation than calculating the total.
- In the Body
OnLoad
event, add a call to the refreshArray
function.
- Use the
ItemDataBound
event of the templated control to add a call to the updateValue
function to handle the child control client-side event.
- Add a
Label
or a TextBox
to display the result of the client-side operation. You can then add a suitable validator and associate it with the TextBox
.
Points of interest
- The client-side approach can be extended to address a wide range of templated control validation requirements. For example, if you have a
DataGrid
that has a template column with a CheckBox
(shown in picture), and you want to put a limit on the maximum or minimum number of CheckBox
es that can be checked, it can easily be implemented by applying the same concept introduced here. This example is included in the sample code.
- It is important to note that the goal of having the validation or any other operation done on the client-side is not to move a part of the data processing from the server to the client, but rather to avoid the repetitive round trips to the server while the user is entering the data. So in the example I used here, when the user decides to submit the data he entered, I should have the server re-calculate and validate the total, and not rely on the result calculated on the client-side, as this could be tampered with by a malicious user.