views:

214

answers:

2

Hi, we are trying to get the following scenario working:

we use asp:scriptmanager / CompositeScripts to combine our scripts into a few script blocks but after each deploy to the test system we have trouble with testers not getting updated versions of css and javascript (browser cache). For the CSS we have defined our own css user control which appends a Siteversion parameter "?v=1.0.190" for instance to the css url. This siteversion is defined in web.config / appsettings and is bumped on every deploy.

We wanted to be able to use the same strategy for javascripts, but thus far I haven't had any success. When rendering the script tag. the Scriptmanager renders

<script src="/ScriptResource.axd?d=..." type="text/javascript"></script> 

Given that the current configured siteversion was 1.0.190, I would want it to render

<script src="/ScriptResource.axd?d=...&v=1.0.190" type="text/javascript"></script> 

How do I get at the "script" html output from the script manager so I can change it? It doesn't seem to be present in the stuff rendered in Render, RenderChildren or RenderControl

Yours Andreas

+1  A: 

I will suggest a diferent aproche. The chache is your problem, then change the cache Header. Here is an example that I make and test it and its works just fine. On the global.asax on the very start of the call...

protected void Application_BeginRequest(Object sender, EventArgs e)
{
    HttpApplication app = (HttpApplication)sender;

    string cTheFile = HttpContext.Current.Request.Path;

    if (cTheFile.EndsWith("ScriptResource.axd", StringComparison.InvariantCultureIgnoreCase))
    {
        // here is the trick with your version !
        string etag = "\"" + app.Context.Request.QueryString.ToString().GetHashCode().ToString() + "1.0.190" + "\"";
        string incomingEtag = app.Request.Headers["If-None-Match"];

        app.Response.Cache.SetETag(etag);

        if (String.Compare(incomingEtag, etag) == 0)
        {
            app.Response.StatusCode = (int)System.Net.HttpStatusCode.NotModified;
            app.Response.StatusDescription = "Not Modified";                            
        }
        else
        {
            app.Response.Cache.SetExpires(DateTime.UtcNow.AddMinutes(1));
            app.Response.Cache.SetMaxAge(new TimeSpan(0, 1, 0));
            app.Response.Cache.SetCacheability(HttpCacheability.Public);
        }
    }
}
Aristos
The issue with that approach is that then the browser will end up making lots of unnecessary round trips to the webserver only to find out that the file has not been modified since the last time. On a non-trivial website, this can have a significant impact on the performance of the site (round trips = bad). The advantage of using the versioned url's is that you can cache the scripts very aggressively, and only force the browser to make a round trip when the script has been updated (by increasing the version number).
Andy
+2  A: 

I dug around in reflector for a bit and it looks like this is unfortunately a tricky thing to add. MS didn't provide any good extension points that I could find. However, if you're willing to resort to a nice reflection hack, adding the following ControlAdapter for ScriptManager should do the trick:

public class VersionedScriptManagerAdapter : ControlAdapter
{
    protected new ScriptManager Control
    {
        get { return (ScriptManager) base.Control; }
    }

    protected override void OnPreRender(System.EventArgs e)
    {
        base.OnPreRender(e);

        var compositeScriptField = Control.GetType().GetField("_compositeScript",
                                                              BindingFlags.NonPublic | BindingFlags.Instance);
        var currentCompositeScript = Control.CompositeScript;
        var versionedCompositeScript = new VersionedCompositeScriptReference();
        CopyCompositeScript(currentCompositeScript, versionedCompositeScript);
        compositeScriptField.SetValue(Control, versionedCompositeScript);
    }

    private void CopyCompositeScript(CompositeScriptReference sourceCompositeScript, CompositeScriptReference targetCompositeScript)
    {
        targetCompositeScript.Path = sourceCompositeScript.Path;
        targetCompositeScript.ResourceUICultures = sourceCompositeScript.ResourceUICultures;
        targetCompositeScript.ScriptMode = sourceCompositeScript.ScriptMode;
        foreach (var scriptReference in sourceCompositeScript.Scripts)
        {
            targetCompositeScript.Scripts.Add(scriptReference);
        }
    }

    private class VersionedCompositeScriptReference : CompositeScriptReference
    {
        protected override string GetUrl(ScriptManager scriptManager, bool zip)
        {
            string version = ConfigurationManager.AppSettings["ScriptVersion"];
            return base.GetUrl(scriptManager, zip) + "&v=" + version;
        }
    }
}

Then to hook up this control adapter, you will need to create a Web.browser file and put it in your App_Browsers folder on the website. The Web.browser file should look something like this:

I tested this out and it worked for me. I hope its helpful to you.

Andy
+1 Andy I vote your answer because its closer to the question, and because its better someone get the bounty if andreas forget to select one.
Aristos
Somehow my Web.browser snippet didn't make it into my original post. Here is what it would look like:<browsers> <browser refID="Default"> <controlAdapters> <adapter controlType="System.Web.UI.ScriptManager" adapterType="MyNamespace.VersionedScriptManagerAdapter"/> </controlAdapters> </browser></browsers>
Andy
@Andy this code finally its not working ! I think that its need more work and tests.
Aristos
Have you confirmed that the control adapter is configured? Try setting a breakpoint in the OnPreRender method of the adapter and verify that the debugger hits the breakpoint. If it doesn't, then that means your .browser file is not configured correctly. The .browser file needs to live in the App_Browsers folder of your website...
Andy