rioting bits

jedes herz ist eine revolutionäre zelle

Accessing controls inside a Repeater from Javascript

December 1st, 2009

The way ASP.NET assigns IDs for controls has long been criticized. In order for them to be unique for a given page, the IDs get forms like ctl00_ContentPlaceHolder1_lblCountTotal, depending on the server controls that contain them. This means that when the IDs of the containing controls change, the child’s ID changes too, breaking your Javascript code that directly accessed the control by its full ID.

The usual way to handle this issue is by using the ClientID property in Javascript, like <%= lblCountTotal.ClientID %>. However, there are some situations when you can’t use the ClientID of a control. A notable one is when the control is inside the ItemTemplate of a Repeater. So if you’re thinking, for example, to update all the label values in your Repeater on a Javascript event, you realize you can’t really access them and start thinking of a work-around. I’m going to describe you how I handled the issue.

My use case

In a couple of my recent projects at work, we are developing some in-house web applications for a network of hospitals and part of the web apps is porting all their reporting logic. More exactly, they have a lot of excels with different data input forms and, from those filled-in forms, reports must be generated.

Now, when recreating those data input forms on the web, we try to keep as much of the existing look & feel as possible, so we won’t confuse the users too much with the changes. This means that those calculations Excel makes “on the fly” have to work the same way when the user goes from field to field on our webform. And if the forms are large (and they usually are) and we want those calculations to run as fast as not-noticeable, we have to use Javascript (so purely client-side code) to do them.

The ‘didactic’ example

In order not to bore you to death and make some sense with what I’m talking about, I made a small form to help you follow me.

 form_screenshot
As the user fills in the fields with the number of visitors per time zone, the form must calculate the total number of visitors and the percentages (visitors for a time zone / total number of visitors). Each time a field has been changed, the total is changed and consequently all the percentages.

Some of the ASP.NET mark-up for the page:

<tr style="background-color: #FFFFCC;">
    <td>
        Total
    </td>
    <td>
        <asp:Label ID="lblCountTotal" runat="server">0</asp:Label>
    </td>
    <td>
        &nbsp;
    </td>
</tr>
<asp:Repeater ID="rptVisitors" runat="server">
    <ItemTemplate>
        <tr style="background-color: #EEEEEE;">
            <td>
                <%#Eval("TimeZoneName") %>
            </td>
            <td>
                <asp:TextBox ID="txtCount" runat="server" onblur='<%# "UpdateValues(" + Eval("Index") + ", this.value)" %>'></asp:TextBox>
            </td>
            <td>
                <asp:Label ID="lblPercent" runat="server">n/a</asp:Label>
            </td>
        </tr>
    </ItemTemplate>
</asp:Repeater>
<asp:HiddenField ID="repeaterRowsCount" runat="server" />

 
The Repeater there is bound using GetSystemTimeZones(), a method I talked about in my previous blog post:

protected void Page_Load(object sender, EventArgs e)
{
    int index = 0;
    rptVisitors.DataSource = TimeZoneInfo.GetSystemTimeZones()
                                         .OrderBy(tz => tz.StandardName)
                                         .Select(tz => new
                                         {
                                             TimeZoneName = tz.StandardName,
                                             Index = index++
                                         });
    rptVisitors.DataBind();

    repeaterRowsCount.Value = rptVisitors.Items.Count.ToString();
}

 
You can see in the ASP.NET mark-up how, on the onblur event of txtCount, so when the user tabs to the next field, the UpdateValues() Javascript method is called. UpdateValues() calculates the total number of visitors and then the percentages, and the catch is setting those percentages for each lblPercent.

As I said, you can’t use the ClientID property to access controls inside Repeaters. The labels get IDs like ctl00_ContentPlaceHolder1_rptVisitors_ctl01_lblPercent, ctl00_ContentPlaceHolder1_rptVisitors_ctl02_lblPercent, and the trick I finally used was building their IDs on the go, by getting the base part from the ClientID of the Repeater.

This is how my Javascript code turned out to look:

var repeaterId = '<%= rptVisitors.ClientID %>';
var repeaterRowsCount = 0;
var values = [];

window.onload = function() {
    repeaterRowsCount = eval(document.getElementById('<%=repeaterRowsCount.ClientID %>').value);

    for (i = 0; i < repeaterRowsCount; i++)
        values[i] = 0;
}

function UpdateValues(row, value) {
    values[row] = 0;
    if (value != '' && !isNaN(value))
        values[row] = eval(value);

    var totalCount = 0;
    for (i = 0; i < repeaterRowsCount; i++)
        totalCount += values[i];

    document.getElementById('<%= lblCountTotal.ClientID %>').innerHTML = totalCount;

    for (i = 0; i < repeaterRowsCount; i++) {
        var percent = 'n/a';
        if (totalCount > 0)
            percent = ((values[i] / totalCount) * 100).toFixed(1) + "%";

        var rowNo = i;
        if (rowNo < 10)
            rowNo = "0" + rowNo;

        document.getElementById(repeaterId + '_ctl' + rowNo + '_lblPercent').innerHTML = percent;
    }
}

I highlighted there the two lines, getting the ClientID of the Repeater, and then building the ID for each label. Besides providing a way to access the labels, using the Repeater’s ClientID as a base ensures that this code won’t break when subsequent changes are made to the project.

I’ve let the rest of the code there, so you’d see the whole solution. Most notably, the way I got the number of items in the Repeater, which I needed in the Javascript code, by using the repeaterRowsCount hidden field.

Alternatives

The first thought I had when I started working at the webforms was making the calculations in code-behind, by using AJAX calls. But, even if it would have been nicer, I soon found out that it was too slow for forms with lots of fields. As soon as you got at about 200 fields, you could notice the delay, and I even had forms with 1600 fields!

Another nice solution would have been jQuery’s ‘ends-with’ selector. Actually, on my first take I was using, instead of this:

document.getElementById(repeaterId + '_ctl' + rowNo + '_lblPercent').innerHTML = percent;

, this:

$('id$=_ctl' + rowNo + '_lblPercent').html(percent);

Which is, of course, shorter, and worked perfectly on my IE8 browser. However, the ‘ends-with’ selector is terribly slower on older browser, as Dave Ward points out (execution time of a ‘ends-with’ selector, in milliseconds):

encosia-ends-with-comparison
And this made my form unusable on IE7, for example, so I had to ditch the jQuery. I must note, however, that jQuery is a wonderful library, it just wasn’t the “tool for the job”.

Update: You can get the whole project, so you can play around with it, if you want: here

2 Responses to 'Accessing controls inside a Repeater from Javascript'

Subscribe to comments with RSS or TrackBack to 'Accessing controls inside a Repeater from Javascript'.

  1. If you’re interested, you could avoid the ID substring search by doing something like this:

    $('tbody tr').each(function(i, item) {
    var $item = $(item);

    var percentage = $item.find('input').val() / totalVisitors * 100;

    percentage = percentage.toFixed(1) + '%';

    $item.find('label').text(percentage);
    });

    That will select everything with getElementsByTagName, which is fast even in IE6. You’d just need to change your markup slightly, wrapping the Repeater in a tbody so it’s faster/easier to exclude the heading rows.

    I put a rough example up on JSBin. It could use some refactoring, but should perform decently: http://jsbin.com/izexu/edit

    Dave Ward

    1 Dec 09 at 19:57

  2. Woa… Thanks for giving your thought on the problem. Quite an honour, actually.

    I’ve put your code into my solution. I had to change just some minor things, like using “span” in JS instead of “label” (the Label in asp.net renders as a span) and checking for ” and NaN when adding values.
    Anyway… It is a nicer approach, but it’s still slower than going after their built-up IDs, with the most basic methods.
    My bechmarking was not the most thouroughly, but still (execution time of the “update calculations” method):
    My basic JS way:
    - IE8: ~17ms
    - IE8-CM: ~22.5ms
    This jQuery approach:
    - IE8: ~143ms
    - IE8-CM: ~170ms
    (CM – Compatibility Mode)

    It’s not at all too much for this example, but for a big form I had at work, it takes like 1 sec for the full calculations. And 2 sec would be too much.

    I do have to admit the jQuery-newbie in me learned some stuff from your code.

    Dan Dumitru

    3 Dec 09 at 00:35

Leave a Reply