Sorry for the cheesy title, but this was such a mysterious bug to track down that it seems appropriate to give it a dramatic title. Consider the following code:
<%@ Page Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="sm" runat="server" />
<asp:UpdatePanel ID="up" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:RadioButtonList ID="rbl" runat="server" AutoPostBack="true">
<asp:ListItem Value="0" Selected="true">Zero</asp:ListItem>
<asp:ListItem Value="1">One</asp:ListItem>
</asp:RadioButtonList>
<%= DateTime.Now %>
</ContentTemplate>
</asp:UpdatePanel>
</form>
</body>
</html>
Nothing very scary looking in there, just a RadioButtonList with a couple options, and my old favorite DateTime.Now to tell me when the UpdatePanel was last updated. This code does exactly what you think it should: when the page loads, "Zero" is selected, and when you switch to "One," the timestamp refreshes. You can then switch back to "Zero" (and back to "One" again, etc.), and each time you change the radio button selection, the timestamp gets updated.
That code is a little inefficient. The RadioButtonList doesn't change during the async postback, so we don't really need it to be refreshed as part of the UpdatePanel. Let's move it outside the UpdatePanel and use a trigger instead. Here's the new code:
<%@ Page Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="sm" runat="server" />
<asp:RadioButtonList ID="rbl" runat="server" AutoPostBack="true">
<asp:ListItem Value="0" Selected="true">Zero</asp:ListItem>
<asp:ListItem Value="1">One</asp:ListItem>
</asp:RadioButtonList>
<asp:UpdatePanel ID="up" runat="server" UpdateMode="Conditional">
<Triggers>
<asp:AsyncPostBackTrigger ControlID="rbl" />
</Triggers>
<ContentTemplate>
<%= DateTime.Now %>
</ContentTemplate>
</asp:UpdatePanel>
</form>
</body>
</html>
If you load this page, you'll see the same thing as before. "Zero" is selected, and if you click "One," the timestamp updates to the current time, just as before. Here's where it gets mysterious, though. If you click "Zero" again, nothing happens! If, after that, you click "One" again, the timestamp will update as expected. But click back to "Zero," and it again does nothing. Keep clicking back and forth, and you'll find that every time you click "One," the UpdatePanel is refreshing, but when you click "Zero," it never does. Hence the "half-trigger" in the title of this post.
Well, we have our mystery. Let's get a-sleuthin'.
Step one is to figure out where things are going wrong. What we expect to happen is:
- On the client, I click "Zero," which fires off an async postback trigger.
- On the server, the page does a postback, which updates the timestamp, and it sends back the contents of our UpdatePanel.
- On the client, the UpdatePanel contents get replaced by the response from the server.
I was working in parallel on the real-world version of this code with Rupesh Patric, one of our support engineers. He and I attacked the problem in two different ways but came to the same conclusion. He set a breakpoint on the server in the event handler for the RadioButtonList's SelectedIndexChanged event. (I've left that event out here to make the code simpler.) He found that his breakpoint was only being hit when he clicked on "One." I attacked the problem from the client instead and fired up Nikhil's Web Development Helper to see what the HTTP traffic was like. There I found that there was an async postback any time I clicked on "One" (as is expected), but there was no traffic at all when I clicked "Zero."
Either approach is a fine way to determine where the unexpected behavior is occurring. In this case, we learned that the problem was that clicking on the button wasn't causing an async postback.
To me, that meant it was time to look at the actual HTML source of the page in the browser to see if something looked amiss, and that's where I found the problem. Here's what the HTML looks like for the RadioButtonList:
<table id="rbl" border="0">
<tr>
<td><input id="rbl_0" type="radio" name="rbl" value="0" checked="checked" /><label for="rbl_0">Zero</label></td>
</tr>
<tr>
<td><input id="rbl_1" type="radio" name="rbl" value="1" onclick="javascript:setTimeout('__doPostBack(\'rbl$1\',\'\')', 0)" /><label for="rbl_1">One</label></td>
</tr>
</table>
Now the difference becomes fairly obvious. Because the first radio button is checked, it doesn't get the onclick="..." script, so it doesn't call __doPostBack() and doesn't cause an async postback. When it was inside the UpdatePanel, it was getting replaced each time with the opposite code ("Zero" has the onclick="..." and "One" doesn't). Outside the UpdatePanel, it's never getting replaced. The bottom line is that when I said above, "The RadioButtonList doesn't change during the async postback," I was actually wrong. The RadioButtonList does change when the selection changes.
To hit this issue, your code has to meet three conditions:
- A RadioButtonList is an async postback control. (Regular postbacks obviously update the control.)
- It's not in an UpdatePanel that gets updated during the async postback it causes. (It's either not inside an UpdatePanel or that UpdatePanel has ChildrenAsTriggers="false".)
- One of the ListItems is set with Selected="true". (Otherwise all the radio buttons get the onclick="..." script.)
The really simple fix? Just put your RadioButtonList inside an UpdatePanel (either the one it's triggering or one of its own).
Mystery solved! It all looks so simple once it's been debugged...