Wednesday, March 21, 2012

A bug in AJAX RC ScriptManager

I've been adopting MS AJAX Extensions, and founded a small bug, wich i would like to describe in that post.

It's about script registration order when using ScriptManager static methods. As I suppose, they should work same way as Page.ClientScript methods. But, they don't, wich i believe is a bug.

When someone registers more than one script via ScriptManager with different Control references, the order of script registration is undetermined. And ClientScriptManager (Page.ClientScript) registers scripts in order as they were registered in user code.

I've looked via reflector into the scriptmanager code, and it seems, the bug source is System.Web.UI.ScriptRegistrationManager+ScriptSubmitStatementEntry class wich stores registered script entries in a ScriptBlocks property of type Dictionary<Control, List<ScriptBlockEntry>>.

That effectively means, that when i register some scripts with same control entry, the order of registration is right, but when i pass different control instances - the order is undermined due to Dictionary implementation (HashTable).

So. If we have;

Control button1 = new Button();
Control button2 = new Button();

ScriptManager.RegisterClientScriptInclude(button1, ...)
ScriptManager.RegisterClientScriptInclude(button2, ...)

The second script may actually register before first one, wich can lead to a bugs. Because script registration order is important.

I'm not sure where to submit this, so i hope someone will fix it.

Hi...

I don't think that's a bug, actually. If you look at the "ClientScriptManager.RegisterStartupScript Method (Type, String, String)" documentation, under "Remarks", it notes that "...The script blocks are not guaranteed to be output in the order they are registered. ". It offers the suggestion that, if you need the scripts in a specific order, you should concatenate them together in a single string and register that.

Hope this helps.


Hm.. interesting point. There is no such remark on RegisterScriptInclude method.

And actually, it works as i described. Maintaining the order. I think scripts are stored in arbitary array or list inside page.

Still, even if it's not a formal bug, it clearly a wrong behavior. ScriptManager states that it's methods can be used simply instead Page.ClientScrip methods, and it's obviously not true in that case.

If you programmatically register includes, you often don't have an option of concatenation. And still need to maintain the correct order.


JiCheng, you're incorrect.

There is indeed a static RegisterScriptInclude method on the ScriptManager object in the RC. And no, you cannot simply use Page.ClientScript.RegisterScriptInclude instead. This won't work in asynchronous postback scenarios.

To answer the original question, multiple calls to RegisterClientScriptIncludeshould result in loading the scripts in the order you registered them. I did a quick test with just two files, and they loaded in order on the client. (I checked by putting an alert() in each, so I could watch what order the alerts popped up.) When I switched the order of the RegisterClientScriptInclude calls, the order switched on the client as well.

To sum up, I can't reproduce the behavior you're seeing (where order isn't guaranteed), but I would consider it a bug. Please share code that reproduces this bug so I can verify.


I gotScriptManager.RegisterClientScriptInclude Method (control, type, key, url) method from this document -http://ajax.asp.net/docs/mref/M_System_Web_UI_ScriptManager_RegisterClientScriptInclude_4_6597d783.aspx.

TheRegisterClientScriptInclude(Control, Type, String, String) method is used to register a client script file for a page or part of a page that is participating in partial-page updates. If your page or part of the page is not using partial-page updates and you want to register a client script file, use the ClientScriptManager class registration methods. You can get a reference to ClientScriptManager object from the ClientScript property of the page.

TheRegisterClientScriptInclude(Control, Type, String, String) method registers a client script file on the page by rendering a <script> element with a src attribute. The url parameter is used to set the src attribute of the <script> element. To resolve client URLs, use the ResolveClientUrl(String) method. This method uses the context of the URL on which it is called to resolve the path.

Both theRegisterClientScriptInclude(Control, Type, String, String) andRegisterClientScriptResource(Control, Type, String) methods load script files into the browser. If a script with the same type and key (for a script file) or type and resource name (for a script resource) is already loaded, the script is not reloaded. The script blocks that are loaded using these two methods should avoid script that executes inline functions and should instead should contain function definitions and other supporting definitions for your application.

Sample Codes as follows:

%@. Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
public void Page_Load(Object sender, EventArgs e)
{
if (!IsPostBack)
Calendar1.SelectedDate = DateTime.Today;

}
protected void Page_PreRender(object sender, EventArgs e)
{
ScriptManager.RegisterClientScriptInclude(
this,
typeof(Page),
"AlertScript",
ResolveClientUrl("~/scripts/script_alertdiv.js"));
}
protected void IncrementButton_Click(object sender, EventArgs e)
{
Calendar1.SelectedDate = Calendar1.SelectedDate.AddDays(1.0);
}
protected void DecrementButton_Click(object sender, EventArgs e)
{
Calendar1.SelectedDate = Calendar1.SelectedDate.AddDays(-1.0);
}

</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>ScriptManager RegisterClientScriptInclude</title>
<style type="text/css">
div.MessageStyle
{
background-color: Green;
top: 95%;
left: 1%;
position: absolute;
visibility: hidden;
}
</style>
</head>
<body>
<form id="Form1" runat="server">
<div>
<asp:ScriptManager ID="ScriptManager1"
runat="server"/>

<script type="text/javascript">
Sys.WebForms.PageRequestManager.instance.add_endRequest(Notify);
</script>

<asp:UpdatePanel ID="UpdatePanel1" UpdateMode="Conditional"
runat="server">
<ContentTemplate>
<asp:Calendar ID="Calendar1" runat="server"/>
<br />
Change the selected date:
<asp:Button runat="server" ID="DecrementButton" Text="-" OnClick="DecrementButton_Click" />
<asp:Button runat="server" ID="IncrementButton" Text="+" OnClick="IncrementButton_Click" />
</ContentTemplate>
</asp:UpdatePanel>

<div id="NotifyDiv" class="MessageStyle">
Updates are complete.
</div>
</div>
</form>
</body>
</html>


Note I have two typos in my first paragraph where I wrote "RegisterScriptInclude" instead of "RegisterClientScriptInclude." Sorry for any confusion.

No comments:

Post a Comment