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 browsers, 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

Getting the current time in a different Time Zone (C#)

September 30th, 2009

A fast start with the solution (getting the current time in EST):

TimeZoneInfo timezone_EST = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");

DateTime now_EST = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, timezone_EST);

And now for the yada yada.

As standard as the ‘Eastern Standard Time’ might seem to you, C# doesn’t know a thing about it. In fact, it doesn’t know much about any timezone, except the local timezone (the one on the computer running the application) and the UTC.

Why that, it beats me. I would have expected to have at least the more used timezones kept inside the basic library. But I guess it wasn’t meant to be.

Instead, to get information about a specific timezone, you have to retrieve it from your system’s registry, with the FindSystemTimeZoneById method. This means you have to hardcode the timezone’s ID, and this after you made sure the computer that runs the code has your intended timezone with that ID in the registry.

To get a complete list of the timezones available on your system, you can go ahead and check the registry! (just joking, there’s a method for that)

Fortunately, these timezone IDs are pretty standard across Windows installations. And I’ll just throw here a list, to easily grab an ID when I need it (these are the timezones on my Windows 7; compared with a list I got from a Windows XP, there are ~7 different entries, rather obscure timezones):

Id Display Name Supports DST
Afghanistan Standard Time (UTC+04:30) Kabul Nay.
Alaskan Standard Time (UTC-09:00) Alaska Yay!
Arab Standard Time (UTC+03:00) Kuwait, Riyadh Nay.
Arabian Standard Time (UTC+04:00) Abu Dhabi, Muscat Nay.
Arabic Standard Time (UTC+03:00) Baghdad Yay!
Argentina Standard Time (UTC-03:00) Buenos Aires Yay!
Atlantic Standard Time (UTC-04:00) Atlantic Time (Canada) Yay!
AUS Central Standard Time (UTC+09:30) Darwin Nay.
AUS Eastern Standard Time (UTC+10:00) Canberra, Melbourne, Sydney Yay!
Azerbaijan Standard Time (UTC+04:00) Baku Yay!
Azores Standard Time (UTC-01:00) Azores Yay!
Canada Central Standard Time (UTC-06:00) Saskatchewan Nay.
Cape Verde Standard Time (UTC-01:00) Cape Verde Is. Nay.
Caucasus Standard Time (UTC+04:00) Yerevan Yay!
Cen. Australia Standard Time (UTC+09:30) Adelaide Yay!
Central America Standard Time (UTC-06:00) Central America Nay.
Central Asia Standard Time (UTC+06:00) Astana, Dhaka Nay.
Central Brazilian Standard Time (UTC-04:00) Manaus Yay!
Central Europe Standard Time (UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague Yay!
Central European Standard Time (UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb Yay!
Central Pacific Standard Time (UTC+11:00) Magadan, Solomon Is., New Caledonia Nay.
Central Standard Time (UTC-06:00) Central Time (US & Canada) Yay!
Central Standard Time (Mexico) (UTC-06:00) Guadalajara, Mexico City, Monterrey Yay!
China Standard Time (UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi Nay.
Dateline Standard Time (UTC-12:00) International Date Line West Nay.
E. Africa Standard Time (UTC+03:00) Nairobi Nay.
E. Australia Standard Time (UTC+10:00) Brisbane Nay.
E. Europe Standard Time (UTC+02:00) Minsk Yay!
E. South America Standard Time (UTC-03:00) Brasilia Yay!
Eastern Standard Time (UTC-05:00) Eastern Time (US & Canada) Yay!
Egypt Standard Time (UTC+02:00) Cairo Yay!
Ekaterinburg Standard Time (UTC+05:00) Ekaterinburg Yay!
Fiji Standard Time (UTC+12:00) Fiji, Kamchatka, Marshall Is. Nay.
FLE Standard Time (UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius Yay!
Georgian Standard Time (UTC+03:00) Tbilisi Nay.
GMT Standard Time (UTC) Dublin, Edinburgh, Lisbon, London Yay!
Greenland Standard Time (UTC-03:00) Greenland Yay!
Greenwich Standard Time (UTC) Monrovia, Reykjavik Nay.
GTB Standard Time (UTC+02:00) Athens, Bucharest, Istanbul Yay!
Hawaiian Standard Time (UTC-10:00) Hawaii Nay.
India Standard Time (UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi Nay.
Iran Standard Time (UTC+03:30) Tehran Yay!
Israel Standard Time (UTC+02:00) Jerusalem Yay!
Jordan Standard Time (UTC+02:00) Amman Yay!
Korea Standard Time (UTC+09:00) Seoul Nay.
Mauritius Standard Time (UTC+04:00) Port Louis Yay!
Mid-Atlantic Standard Time (UTC-02:00) Mid-Atlantic Yay!
Middle East Standard Time (UTC+02:00) Beirut Yay!
Montevideo Standard Time (UTC-03:00) Montevideo Yay!
Morocco Standard Time (UTC) Casablanca Yay!
Mountain Standard Time (UTC-07:00) Mountain Time (US & Canada) Yay!
Mountain Standard Time (Mexico) (UTC-07:00) Chihuahua, La Paz, Mazatlan Yay!
Myanmar Standard Time (UTC+06:30) Yangon (Rangoon) Nay.
N. Central Asia Standard Time (UTC+06:00) Almaty, Novosibirsk Yay!
Namibia Standard Time (UTC+02:00) Windhoek Yay!
Nepal Standard Time (UTC+05:45) Kathmandu Nay.
New Zealand Standard Time (UTC+12:00) Auckland, Wellington Yay!
Newfoundland Standard Time (UTC-03:30) Newfoundland Yay!
North Asia East Standard Time (UTC+08:00) Irkutsk, Ulaan Bataar Yay!
North Asia Standard Time (UTC+07:00) Krasnoyarsk Yay!
Pacific SA Standard Time (UTC-04:00) Santiago Yay!
Pacific Standard Time (UTC-08:00) Pacific Time (US & Canada) Yay!
Pacific Standard Time (Mexico) (UTC-08:00) Tijuana, Baja California Yay!
Pakistan Standard Time (UTC+05:00) Islamabad, Karachi Yay!
Romance Standard Time (UTC+01:00) Brussels, Copenhagen, Madrid, Paris Yay!
Russian Standard Time (UTC+03:00) Moscow, St. Petersburg, Volgograd Yay!
SA Eastern Standard Time (UTC-03:00) Georgetown Nay.
SA Pacific Standard Time (UTC-05:00) Bogota, Lima, Quito, Rio Branco Nay.
SA Western Standard Time (UTC-04:00) La Paz Nay.
Samoa Standard Time (UTC-11:00) Midway Island, Samoa Nay.
SE Asia Standard Time (UTC+07:00) Bangkok, Hanoi, Jakarta Nay.
Singapore Standard Time (UTC+08:00) Kuala Lumpur, Singapore Nay.
South Africa Standard Time (UTC+02:00) Harare, Pretoria Nay.
Sri Lanka Standard Time (UTC+05:30) Sri Jayawardenepura Nay.
Taipei Standard Time (UTC+08:00) Taipei Nay.
Tasmania Standard Time (UTC+10:00) Hobart Yay!
Tokyo Standard Time (UTC+09:00) Osaka, Sapporo, Tokyo Nay.
Tonga Standard Time (UTC+13:00) Nuku’alofa Nay.
US Eastern Standard Time (UTC-05:00) Indiana (East) Nay.
US Mountain Standard Time (UTC-07:00) Arizona Nay.
UTC (UTC) Coordinated Universal Time Nay.
Venezuela Standard Time (UTC-04:30) Caracas Nay.
Vladivostok Standard Time (UTC+10:00) Vladivostok Yay!
W. Australia Standard Time (UTC+08:00) Perth Yay!
W. Central Africa Standard Time (UTC+01:00) West Central Africa Nay.
W. Europe Standard Time (UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna Yay!
West Asia Standard Time (UTC+05:00) Tashkent Nay.
West Pacific Standard Time (UTC+10:00) Guam, Port Moresby Nay.
Yakutsk Standard Time (UTC+09:00) Yakutsk Yay!

 

If you like reading in-depth articles from the Creators, I highly recommend a post called A Brief History of DateTime, not by Stephen Hawking, on the BCL Team Blog. It’s about how the DateTime types evolved across .NET versions and how, in order to preserve backwards-compatibility and reliability, the usage of these types may not be that intuitive in some cases.

int? == null, or why it misbehaves in Linq-to-SQL

July 4th, 2009

Let Category be a class/table, with a nullable int field named ParentId, to help me keep an hierarchy of categories.

And the following method, which returns an enumeration of all the categories having the given ParentId:

public static IEnumerable<Category> Enumerate(int? parentId)
{
    RiotingDataContext dataContext = new RiotingDataContext();

    return dataContext.Categories.Where(c => c.ParentId == parentId);
}

Now, this works as expected… right until you pass a parentId which is null. At that moment, the returned sequence is empty, although there are categories in the database that have a null ParentId.

I ran into this more than once and, needless to say, you are quite surprised when you see this for the first time. What to do? Of course, a quick check with SQL Profiler, to see what the hell Linq-to-SQL is doing underneath.

exec sp_executesql N'SELECT [t0].[Id], [t0].[Name], [t0].[ParentId]
FROM [dbo].[Categories] AS [t0]
WHERE [t0].[ParentId] = @p0',N'@p0 int',@p0=NULL

The problem is that the parameterized stored procedure that is generated checks if our nullable int equals null by doing ParentId = NULL, when the right way to do it (SQL-style) is ParentId IS NULL. So the code we would want to be generated, when ParentId is null, is:

SELECT [t0].[Id], [t0].[Name], [t0].[ParentId]
FROM [dbo].[Categories] AS [t0]
WHERE [t0].[ParentId] IS NULL

, and is the result we will get if we modify our method like this:

public static IEnumerable<Category> Enumerate(int? parentId)
{
    RiotingDataContext dataContext = new RiotingDataContext();

    if (parentId != null)
        return dataContext.Categories.Where(c => c.ParentId == parentId);
    else
        return dataContext.Categories.Where(c => c.ParentId == null);
}

Or, in a more compact manner (and the way I like to use it):

public static IEnumerable<Category> Enumerate(int? parentId)
{
    RiotingDataContext dataContext = new RiotingDataContext();

    return dataContext.Categories.Where(c => parentId != null ? c.ParentId == parentId : c.ParentId == null);
}