4

A major complaint of ASP.NET WebForms is how the framework takes away control of the rendered document away from the developer - myriad components (both built-in to ASP.NET and third-party) are able to inject their own <script> elements and other functionality into the rendered page.

My current issue is with the ClientScriptManager (Page.ClientScript).

A project I'm working on is a legacy WebForms project with hundreds of .aspx that takes a massive dependency on a third-party web-controls and utility library which contains a copy of jQuery 1.4.1 as an embedded WebResource.

Whenever any control inside this library is used the controls register their private copy of jQuery through Page.ClientScript.RegisterClientScriptInclude, like so:

class SomeControl : WebControl {

    protected void OnInit() {

        this.Page.ClientScript.RegisterClientScriptInclude(
            this,
            typeof(Page),
            "jquery",
            this.Page.ClientScript.GetWebResourceUrl( base.GetType(), "MyJQuery.1.4.1.js" )
        );
    }
}

This is a problem because the website needs to implement ASP.NET SignalR, which has a dependency on jQuery 1.6.4 or higher - but the library's controls are included literally thousands of times (including in the .master files). I need to prevent this old jQuery library from being added to the page.

Subclassing all of the controls won't help because their code that calls RegisterClientScriptInclude is buried in their Control.Init or Control.Load code with other essential functionality, and they often make a direct call to the static ScriptManager.RegisterClientScriptInclude method, so subclassing Page and overriding the ClientScript property won't help either (it isn't a virtual property anyway).

Other people have reported the same problem and asked the same question on StackOverflow, except they had the source-code of the libraries that inject the scripts - so they were simply instructed to change their code to not register the script in the first place - this is not an option for me - all I have is a multiple-megabyte-sized DLL I can only peek into using .NET disassembly tools.

(In the third link, the solution doesn't work for me because I can't call RegisterClientScript before the bad library does, and the first caller permanently sets the value, subsequent calls will not overwrite the script registration).

My hack-ish solution was to use .NET Reflection to overwrite the internal script registration - as ASP.NET WebForms is stable it means this is unlikely to break in the near future, but it's far from ideal. I'll post my solution as an answer - but if there are any solutions that don't use reflection I'll prefer those.

Dai
  • 141,631
  • 28
  • 261
  • 374
  • Could you just replace the resource that is identified by `MyJQuery.1.4.1.js` with JQuery 1.6.2? Keep the name, update the code. – John Wu Jun 23 '17 at 19:41
  • @JohnWu Unfortunately, no - if you make a subsequent call to `RegisterClientScript` with the same key/name as an existing entry then it will be ignored, it doesn't matter if the script body is different. – Dai Jun 23 '17 at 20:40
  • I don't mean make a separate call. I mean go find the resource that gets inserted as a result of the existing call, and replace it with the resource that you want (the later version of jquery). – John Wu Jun 23 '17 at 20:42
  • @JohnWu It's an embedded resource inside the library that I'm using - and it's got a strong-name, so I can't replace it without the assembly-loader complaining. – Dai Jun 23 '17 at 20:55
  • So use a [binding redirect](https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/redirect-assembly-versions). Surely you can't expect to fix this problem without modifying any assemblies! – John Wu Jun 23 '17 at 20:58

1 Answers1

4

Here is my current solution using reflection - it's not pretty, but it works for now:

public static class WebResourceOverrides
{
    private const BindingFlags PrivateThis = BindingFlags.NonPublic | BindingFlags.Instance;

    public static void OverrideClientScript(Page page, String key, String newContent)
    {
        if( page == null ) throw new ArgumentNullException(nameof(page));
        if( key == null ) throw new ArgumentNullException(nameof(key));
        if( newContent == null ) throw new ArgumentNullException(nameof(newContent));

        Type clientScriptType = page.ClientScript.GetType();

        FieldInfo clientScriptBlocksField           = clientScriptType.GetField( "_clientScriptBlocks", PrivateThis);
        FieldInfo registeredClientScriptBlocksField = clientScriptType.GetField( "_registeredClientScriptBlocks", PrivateThis );

        ArrayList      clientScriptBlocks           = (ArrayList)clientScriptBlocksField.GetValue( page.ClientScript );
        ListDictionary registeredClientScriptBlocks = (ListDictionary)registeredClientScriptBlocksField.GetValue( page.ClientScript );

        // clientScriptBlocks contains `System.Tuple<ScriptKey,String,Boolean>`, unfortunately `ScriptKey` is internal, so we can't play with the `Tuple` value directly.
        // ...but we can still access the fields:

        foreach(Object tuple in clientScriptBlocks)
        {
            Boolean found = ProcessScriptRegistration( tuple, key, newContent );
            if( found ) break;
        }
    }

    private static Boolean ProcessScriptRegistration(Object tuple, String key, String newContent)
    {
        Type tupleType = tuple.GetType();

        FieldInfo scriptKeyField = tupleType.GetField("m_Item1", PrivateThis );
        FieldInfo htmlField      = tupleType.GetField("m_Item2", PrivateThis); // Thankfully you can overwrite a `readonly` field using Reflection

        Object scriptKeyValue = scriptKeyField.GetValue( tuple );
        String html           = (String)htmlField.GetValue( tuple );

        Type scriptKeyType = scriptKeyValue.GetType();

        FieldInfo scriptKeyKeyField = scriptKeyType.GetField("_key", PrivateThis );
        String    scriptKeyKey      = (String)scriptKeyKeyField.GetValue( scriptKeyValue );

        if( scriptKeyKey == key )
        {
            htmlField.SetValue( tuple, newContent );
            return true;
        }

        return false;
    }
}

Usage:

I call this from the .master page's OnPreRender method, so it can be sure that if the bad jQuery script is used by the page then it will be added to the internal registration list by now (as it's usually added by the library's controls' Init or Load event-handlers):

public class MyMasterPage : MasterPage {

    protected override void OnPreRender(EventArgs e) {

        base.OnPreRender( e );

        WebResourceOverrides.OverrideClientScript( this.Page, "jquery", "<!-- jQuery 1.4.1 disabled. -->" );
    }
}

This causes the page's <form> to be rendered as below. Originally a <script> block referencing the jQuery1.4.1.js file via WebResource.axd would be rendered, now it's replaced with a comment.

<form ...>
    <script src="/WebResource.axd?d=1234..."></script>
    <script src="/WebResource.axd?d=5678..."></script>
    <!-- jQuery 1.4.1 disabled. -->
    <script src="/WebResource.axd?d=90AB..."></script>

My code doesn't delete the registration because the internal state of client script registration uses an ArrayList with integer indexes, so deleting an entry would potentially break any stored indexes - and it means the bad library's controls still believe that their version of jQuery is being referenced (as some of them do call this.Page.ClientScript.IsClientScriptIncludeRegistered).

Dai
  • 141,631
  • 28
  • 261
  • 374