WebUIValidation.js gets reloaded in async calls when in CompositeScript and UpdatePanel

1.2k views Asked by At

My scenario:

  • I have a validator in an UpdatePanel.
  • I want to combine my scripts so I am using CompositeScript in ScriptManager, and including a reference to WebUIValidation.js
  • I am using .NET 4.0

My problem:

  • When I asynchronously update the panel, .NET loads WebUIValidation.js (in a Scriptresource.axd file) in the async response, even though it has been loaded in the initial CompositeScript-generated script. This is a problem because I have custom code that hijacks some functions in WebUIValidation.js, and the async response overrides my hijacks.
  • If you move the reference to WebUIValidation.js to Scripts in ScriptManager, there is no problem.
  • If you were to have WebUIValidation.js as the only item in CompositeScript (pointless I know) then there is no problem.
  • This async reload does not happen with other .NET library scripts, e.g. WebForm.js

What I want to find out:

  • is there a reason why WebUIValidation.js is loaded in the async response when it is already included in the CompositeScript?

Someone has posted a similar (but not duplicate) issue today, and is veering towards saying that WebUIValidation.js might not be handled by ScriptManager. Can anyone verify this?

To replicate use the following two files

test1.js

// To be added to the composite script
console.log('Test 1 Loaded');

test.aspx

<%@ Page Language="vb" AutoEventWireup="false"  %>

<!DOCTYPE html>

<html>
<head runat="server">
    <title></title>
</head>
<body>
    <script language="VB" runat="server" runat="server">
        Protected Sub ButtonClicked(ByVal sender As Object, ByVal e As System.EventArgs) Handles TestButton.Click
            ButtonClickFeedback.Text = "Button clicked At " & Date.Now.ToString & "; Look at the scripts in your Developer Tools, there is now a separate script for WebUIValidation.js loaded, in spite of the composite script."
        End Sub 
    </script>

    <form runat="server">
        <asp:ScriptManager runat="server">
            <CompositeScript>
                <Scripts>
                    <asp:ScriptReference Path="~/test.js" />
                    <asp:ScriptReference Name="WebUIValidation.js" Assembly="System.Web" />
                </Scripts>
            </CompositeScript>
        </asp:ScriptManager>

        <h1>WebUIValidation.js, CompositeScript and UpdatePanel test</h1>

        <asp:UpdatePanel runat="server" ID="ButtonUpdatePanel">
            <ContentTemplate>
                <asp:Label runat="server" >This is a section with a validator that is within an UpdatePanel.  If you look at the scripts loaded, you will see the composite script in the detail.</asp:Label>
                <asp:Textbox ID="TestInputForValidator" runat="server" Text="This is populated so it will validate"/>
                <asp:RequiredFieldValidator runat="server" ControlToValidate="TestInputForValidator" ErrorMessage="You must write something" /><br />
                <asp:Button ID="TestButton" Text="Click Me!" runat="server"  /><br />
                <asp:Literal ID="ButtonClickFeedback" runat="server" />
            </ContentTemplate>
            <Triggers>
                <asp:AsyncPostBackTrigger ControlID="TestButton" />
            </Triggers>
        </asp:UpdatePanel>
    </form>
</body>
</html>

If you use your Developer Tools to inspect what scripts are being loaded in, you should see an additional Scriptresource.axd (that contains WebUIValidation.js) getting loaded in after clicking the button, in spite of the existence of a Scriptresource.axd with the composite script. test.js is just a sample js file to simulate the idea of composite scripts.

1

There are 1 answers

0
Thierry_S On

Sharing my investigation on why it's loading these scripts again (WebUIValidation.js or Focus.js). I've just found about Focus.js for now.

First, the origination of the http request is in the partial update generated by the update panel. If you look at the reponse to the asynchronous xhr POST request, you'll have something like this. Note almost at the end the ScriptResource.axd url. This is processed by the ajax framework on the client side and since it's a script block with a path, it gets loaded:

1|#||4|2999|updatePanel|ctl00_LoginContent_ctl00|
<div id="ctl00_LoginContent_...">[...html content here]</div>|
0|hiddenField|__LASTFOCUS||
0|hiddenField|__EVENTTARGET||
0|hiddenField|__EVENTARGUMENT||
904|hiddenField|__VIEWSTATE|UdWhNvH6wpBcPOigY[...]SIphbw==|
8|hiddenField|__VIEWSTATEGENERATOR|25748CED|
176|hiddenField|__EVENTVALIDATION|q+FUEVGVj+t[...]AzAm|
0|asyncPostBackControlIDs|||
0|postBackControlIDs|||
26|updatePanelIDs||tctl00$LoginContent$ctl00,|
0|childUpdatePanelIDs|||
25|panelsToRefreshIDs||ctl00$LoginContent$ctl00,|
2|asyncPostBackTimeout||90|
14|formAction||./Default.aspx|
119|scriptBlock|ScriptContentNoTags|function PopulateTimeZoneOffset(){[my own js here...]}|
154|scriptBlock|ScriptPath|/ScriptResource.axd?d=Uup1Lt[...]q450&t=ffffffffd4ee116f|
31|focus||ctl00_LoginContent_LoginControl|

Now debugging server side code, loading the .net assemblies symbols from https://referencesource.microsoft.com/ (with VS configuration as described there).

PageRequestmanager.cs

    private void ProcessFocus(HtmlTextWriter writer) {
        // Roughly stolen from Whidbey Page.cs
        if (_requireFocusScript) {
            Debug.Assert(ClientSupportsFocus, "If ClientSupportsFocus is false then we never should have set _requireFocusScript to true.");
            string focusedControlId = String.Empty;

            // Someone calling SetFocus(controlId) has the most precedent
            if (!String.IsNullOrEmpty(_focusedControlID)) {
                focusedControlId = _focusedControlID;
            }
            else {
                if (_focusedControl != null && _focusedControl.Visible) {
                    focusedControlId = _focusedControl.ClientID;
                }
            }
            if (focusedControlId.Length > 0) {
                // Register focus script library
                string focusResourceUrl = _owner.GetScriptResourceUrl("Focus.js", typeof(HtmlForm).Assembly);
                EncodeString(writer, ScriptBlockToken, "ScriptPath", focusResourceUrl); 
                // *********** THIS ENCODESTRING OUTPUTS THE PROBLEM !!!

                // Send the target control ID to the client
                EncodeString(writer, FocusToken, String.Empty, focusedControlId);
            }
        }
    }

We are deep inside Page.ProcessRequest, and now in Page.Render, RenderPageCallback and ProcessFocus. The highlighted EncodeString near the end writes directly to the writer things like "len|type|id|content|", including writer.Write(content); where content is "/ScriptResource.axd?d=Uup1IW...q450&t=ffffffffd4ee116f". There is no check to see if this script is already registered with the ScriptManager, it's not calling ScriptManager.RegisterXXXX.

So it seems to me that's the cause of getting another http request for something that's already loaded. ProcessFocus is done as part of "Render", far too late to use any ScriptManager functinality.

I can't think of a way to avoid this http request (apart from not using any SetFocus type thing from the .net framework).

(Running VS 2015, .net framework 4.6.2, ajax control toolkit 17.1)