Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

An update to the ClientScriptProxy Class for ScriptManager/ClientScript


:P
On this page:

A while back I had posted a ClientScript  Manager wrapper that is able to detect whether the MS Ajax extensions are loaded and if so uses the MS Ajax ScriptManager instead of the standard Page.ClientScript to handle embedding of script resources into the page. As you probably know the UpdatePanel control requires that any controls write their output to the new ScriptManager rather than the to the Page.ClientScript object in order for MS Ajax to detect when script, resources and other page related objects are embedded into the page.

 

To make this a little more complicated than it sounds a custom control can’t make any assumptions about MS Ajax being available or even referenced in the application so you can’t actually reference the new ScriptManager directly and instead have to check for the existence first and then use Reflection to make the calls. So this class acts as a go between that checks whether MS Ajax is available and if it is calls ScriptManager, otherwise calls standard Page.ClientScript.

 

The idea is that a control developer can add an instance of this class and then simply use it to make the ClientScript calls on it instead. Note that I didn't implement all of the ClientScript methods - only a handful of the ones that I actually use frequently. I've been adding additional methods as needed. <s> It should be easy enough to add any others that need to go to the ScriptManager.

 

I originally posted this tool with Beta 2 updated it for the RC and now it requires yet another set of changes for the final release version of MS Ajax RTM. Actually one of the commenters on the last post (SpeedNet – thanks man!) pointed out a couple of issues that I originally dismissed because I had overlooked the latest update of RC1 which introduced a few more changes that I hadn’t updated for yet. So rather than keep fixing that particular post I’m reposting it here for RTM because there have been a number of changes.

 

First it includes a new instance method to check whether a ScriptManager is loaded onto a page. There are really two things that you need to check for:

 

  • Whether MS Ajax and the ScriptManager is available in the app (as Reference)
  • Whether ScriptManager is actually on the page and used for Script Compression

The first is useful for generic checking – basically when MS Ajax is loaded you generally just want to fire away at the ScriptManager rather than using ClientScript. In some situations though it’s useful to know whether there’s actually a ScriptManager on the current page (you can check on ScriptManager class with the static GetCurrent() method). For my scenario specifically I have a custom script compression module I run for compressing my own scripts and it needs to know whether MS Ajax is actually running and enabled. MS Ajax will do script compression as well, but only if the a scriptmanager is actually on the page. So if it’s not on the page I continue to compress script resources using my own module compression.

 

It’s a shame that ScriptManager doesn’t actually do things resource compression unless the ScriptManager control is on the page. It seems that it should be perfectly capable of doing this without a SM on the page – there should be no dependencies since RegisterClientScriptResource only registers the resource URL and a separate handler serves it. But alas it doesn’t so I rolled my own.

 

As far as I can tell I think that the static ScriptManager script functions don’t do anything different than pass through to the ClientScript manager when a ScriptManager is not present.

 

So anyway here’s the updated ClientScriptProxy class:

 

/// <summary>

/// This is a proxy object for the Page.ClientScript and MS Ajax ScriptManager

/// object that can operate when MS Ajax is not present. Because MS Ajax

/// may not be available accessing the methods directly is not possible

/// and we are required to indirectly reference client script methods through

/// this class.

///

/// This class should be invoked at the Control's start up and be used

/// to replace all calls Page.ClientScript. Scriptmanager calls are made

/// through Reflection

/// </summary>

public class ClientScriptProxy

{

    private static Type scriptManagerType = null;

 

    // *** Register proxied methods of ScriptManager

    private static MethodInfo RegisterClientScriptBlockMethod;

    private static MethodInfo RegisterStartupScriptMethod;

    private static MethodInfo RegisterClientScriptIncludeMethod;

    private static MethodInfo RegisterClientScriptResourceMethod;

    private static MethodInfo RegisterHiddenFieldMethod;

    private static MethodInfo GetCurrentMethod;

 

    //private static MethodInfo RegisterPostBackControlMethod;

    //private static MethodInfo GetWebResourceUrlMethod;

 

 

    /// <summary>

    /// Internal global static that gets set when IsMsAjax() is

    /// called. The result is cached once per application so

    /// we don't have keep making reflection calls for each access

    /// </summary>

    private static bool _IsMsAjax = false;

 

    /// <summary>

    /// Flag that determines whether check was previously done

    /// </summary>

    private static bool _CheckedForMsAjax = false;

 

    /// <summary>

    /// Cached value to see whether the script manager is

    /// on the page. This value caches here once per page.

    /// </summary>

    private bool _IsScriptManagerOnPage = false;

    private bool _CheckedForScriptManager = false;

           

    /// <summary>

    /// Current instance of this class which should always be used to

    /// access this object. There are no public constructors to

    /// ensure the reference is used as a Singleton to further

    /// ensure that all scripts are written to the same clientscript

    /// manager.

    /// </summary>

    public static ClientScriptProxy Current

    {

        get

        {

            return

                (HttpContext.Current.Items["__ClientScriptProxy"] ??

                (HttpContext.Current.Items["__ClientScriptProxy"] =

                    new ClientScriptProxy()))

                as ClientScriptProxy;

        }

    }

 

    /// <summary>

    /// No public constructor - use ClientScriptProxy.Current to

    /// get an instance to ensure you once have one instance per

    /// page active.

    /// </summary>

    protected ClientScriptProxy()

    {

    }

 

    /// <summary>

    /// Checks to see if MS Ajax is registered with the current

    /// Web application.

    ///

    /// Note: Method is static so it can be directly accessed from

    /// anywhere. If you use the IsMsAjax property to check the

    /// value this method fires only once per application.

    /// </summary>

    /// <returns></returns>

    public static bool IsMsAjax()

    {

        if (_CheckedForMsAjax)

            return _IsMsAjax;

 

        // *** Easiest but we don't want to hardcode the version here

        // scriptManagerType = Type.GetType("System.Web.UI.ScriptManager, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", false);

 

        // *** To be safe and compliant we need to look through all loaded assemblies           

        Assembly ScriptAssembly = null; // Assembly.LoadWithPartialName("System.Web.Extensions");

        foreach (Assembly ass in AppDomain.CurrentDomain.GetAssemblies())

        {

            string fn = ass.FullName;

            if (fn.StartsWith("System.Web.Extensions"))

            {

                ScriptAssembly = ass;

                break;

            }

        }

 

        if (ScriptAssembly == null)

            return false;

 

        scriptManagerType = ScriptAssembly.GetType("System.Web.UI.ScriptManager");

 

        if (scriptManagerType == null)

        {

            _IsMsAjax = false;

            _CheckedForMsAjax = true;

            return false;

 

        }

 

        // *** Method to check for current instance on a page - cache

        // *** since we might call this frequently

        GetCurrentMethod = scriptManagerType.GetMethod("GetCurrent");

       

        _IsMsAjax = true;

        _CheckedForMsAjax = true;

 

        return true;

    }

 

    /// <summary>

    /// Checks to see if a script manager is on the page

    /// </summary>

    /// <param name="control"></param>

    /// <returns></returns>

    public bool IsScriptManagerOnPage(Page page)

    {            

        // *** Check is done only once per page

        if (this._CheckedForScriptManager)

            return _IsScriptManagerOnPage;

       

        // *** Must check whether MS Ajax is available

        // *** at all first. Method sets up scriptManager

        // *** and GetCurrentMethod on success.

        if (!IsMsAjax())

        {

            this._CheckedForScriptManager = true;

            this._IsScriptManagerOnPage = false;

            return false;

        }

 

        // *** Now check and see if we can get a ref to the script manager

        object sm = GetCurrentMethod.Invoke(null, new object[1] { page });

        if (sm == null)

            this._IsScriptManagerOnPage = false;

        else

            this._IsScriptManagerOnPage = true;

 

        this._CheckedForScriptManager = true;

        return this._IsScriptManagerOnPage;

    }

   

    /// <summary>

    /// Returns a WebResource or ScriptResource URL for script resources that are to be

    /// embedded as script includes.

    /// </summary>

    /// <param name="control"></param>

    /// <param name="type"></param>

    /// <param name="resourceName"></param>

    public void RegisterClientScriptResource(Control control, Type type, string resourceName)

    {

        if ( this.IsScriptManagerOnPage(control.Page) )

        {

            // *** NOTE: If MS Ajax is referenced, but no scriptmanager is on the page

            //           script no compression will occur. With a script manager

            //           on the page compression will be handled by MS Ajax.

            if (RegisterClientScriptResourceMethod == null)

                RegisterClientScriptResourceMethod = scriptManagerType.GetMethod("RegisterClientScriptResource",

                                                             new Type[3] { typeof(Control), typeof(Type), typeof(string) });

 

            RegisterClientScriptResourceMethod.Invoke(null, new object[3] { control, type, resourceName });

        }

 

        //// *** If wwScriptCompression Module through Web.config is loaded use it to compress

        //// *** script resources by using wcSC.axd Url the module intercepts

        //if (wwScriptCompressionModule.wwScriptCompressionModuleActive)

        //{

        //    if (type.Assembly == this.GetType().Assembly)

 

        //        RegisterClientScriptInclude(control, type, resourceName, "wwSC.axd?r=" +

        //                                    Convert.ToBase64String(Encoding.ASCII.GetBytes(resourceName)));

        //    else

        //        RegisterClientScriptInclude(control, type, resourceName, "wwSC.axd?r=" +

        //                                    Convert.ToBase64String(Encoding.ASCII.GetBytes(resourceName)) +

        //                                    "&t=" +

        //                                    Convert.ToBase64String(Encoding.ASCII.GetBytes(type.Assembly.FullName)));

        //}       

  else

            // *** Otherwise just embed a script reference into the page

            control.Page.ClientScript.RegisterClientScriptResource(type, resourceName);

    }

   

    /// <summary>

    /// Registers a client script block in the page.

    /// </summary>

    /// <param name="control"></param>

    /// <param name="type"></param>

    /// <param name="key"></param>

    /// <param name="script"></param>

    /// <param name="addScriptTags"></param>

    public void RegisterClientScriptBlock(Control control, Type type, string key, string script, bool addScriptTags)

    {

        if (IsMsAjax())

        {

            if (RegisterClientScriptBlockMethod == null)

                RegisterClientScriptBlockMethod = scriptManagerType.GetMethod("RegisterClientScriptBlock", new Type[5] { typeof(Control), typeof(Type), typeof(string), typeof(string), typeof(bool) });

 

            RegisterClientScriptBlockMethod.Invoke(null, new object[5] { control, type, key, script, addScriptTags });

        }

        else

            control.Page.ClientScript.RegisterClientScriptBlock(type, key, script, addScriptTags);

    }

 

    /// <summary>

    /// Registers a startup code snippet that gets placed at the bottom of the page

    /// </summary>

    /// <param name="control"></param>

    /// <param name="type"></param>

    /// <param name="key"></param>

    /// <param name="script"></param>

    /// <param name="addStartupTags"></param>

    public void RegisterStartupScript(Control control, Type type, string key, string script, bool addStartupTags)

    {

        if (IsMsAjax())

        {

            if (RegisterStartupScriptMethod == null)

                RegisterStartupScriptMethod = scriptManagerType.GetMethod("RegisterStartupScript", new Type[5] { typeof(Control), typeof(Type), typeof(string), typeof(string), typeof(bool) });

 

            RegisterStartupScriptMethod.Invoke(null, new object[5] { control, type, key, script, addStartupTags });

        }

        else

            control.Page.ClientScript.RegisterStartupScript(type, key, script, addStartupTags);

 

    }

 

    /// <summary>

    /// Registers a script include tag into the page for an external script url

    /// </summary>

    /// <param name="control"></param>

    /// <param name="type"></param>

    /// <param name="key"></param>

    /// <param name="url"></param>

    public void RegisterClientScriptInclude(Control control, Type type, string key, string url)

    {

        if (IsMsAjax())

        {

            if (RegisterClientScriptIncludeMethod == null)

                RegisterClientScriptIncludeMethod = scriptManagerType.GetMethod("RegisterClientScriptInclude", new Type[4] { typeof(Control), typeof(Type), typeof(string), typeof(string) });

 

            RegisterClientScriptIncludeMethod.Invoke(null, new object[4] { control, type, key, url });

        }

        else

            control.Page.ClientScript.RegisterClientScriptInclude(type, key, url);

    }

 

    /// <summary>

    /// Returns a WebResource URL for non script resources

    /// </summary>

    /// <param name="control"></param>

    /// <param name="type"></param>

    /// <param name="resourceName"></param>

    /// <returns></returns>

    public string GetWebResourceUrl(Control control, Type type, string resourceName)

    {

        return control.Page.ClientScript.GetWebResourceUrl(type, resourceName);

    }

 

    /// <summary>

    /// Injects a hidden field into the page

    /// </summary>

    /// <param name="control"></param>

    /// <param name="hiddenFieldName"></param>

    /// <param name="hiddenFieldInitialValue"></param>

    public void RegisterHiddenField(Control control, string hiddenFieldName, string hiddenFieldInitialValue)

    {

        if (IsMsAjax())

        {

            if (RegisterHiddenFieldMethod == null)

                RegisterHiddenFieldMethod = scriptManagerType.GetMethod("RegisterHiddenField", new Type[3] { typeof(Control), typeof(string), typeof(string) });

 

            RegisterHiddenFieldMethod.Invoke(null, new object[3] { control, hiddenFieldName, hiddenFieldInitialValue });

        }

        else

            control.Page.ClientScript.RegisterHiddenField(hiddenFieldName, hiddenFieldInitialValue);

    }

 

}

 

You can also pick up the source code as part of the free wwHoverPanel library which includes the ClientScriptProxy.cs file... Note there’s one external dependency in the download code on the wwScriptCompressionModule which is commented out in the code above. That’s the hook I use to hook my own script resources to my own script resource url. More on that in a couple of days...

Posted in Microsoft AJAX  

The Voices of Reason


 

Jim
January 21, 2007

# re: An update to the ClientScriptProxy Class for ScriptManager/ClientScript

Nice update, thanks!

Am I ever glad I subscribed to RSDN (Rick Strahl Developer Network) - ha, what a dork I am ;-)

Speednet
January 22, 2007

# re: An update to the ClientScriptProxy Class for ScriptManager/ClientScript

For some reason your RSS feed did not pick up this latest entry. Maybe it's my web browser? I tried refreshing the feed, and no luck.

Thanks for the props.

Also, many thanks for continuing to update one of the most useful classes designed for ASP.NET AJAX. I use it every day, and I immediately install any updates you make.

I have created a simplified version of the IsMsAjax() function using a predicate, and I'd be happy to post it if you'd like. (I like using predicates because they isolate logic for maintainability and reusability.)

Rick Strahl
January 22, 2007

# re: An update to the ClientScriptProxy Class for ScriptManager/ClientScript

I think I might have caused this to happen by backtiming the entry. I posted it earlier then reposted and changed the date back - I think either FeedBurner or some feedreaders didn't detect the feed because of that. I've reset the time one more time to right now, so this should make sure it shows up. Hopefully this won't result in dupes.

As to the predicate code I don't think that's necessary here. It has its place, but remember predicates are still function calls so there's overhead for using them. It's great if you have logic that complex enough to require a separate piece of code by I don't think it's necesary for a single string comparison in a loop <s>...

Joe Beam
May 02, 2007

# re: An update to the ClientScriptProxy Class for ScriptManager/ClientScript

I like your naming convention for Assembly.

Luke's Blog
July 17, 2007

# Ajax UpdatePanel e FCKeditor: una soluzione definitiva


Mark Carranza
September 09, 2007

# re: An update to the ClientScriptProxy Class for ScriptManager/ClientScript

Looks like the ClientScriptProxy Class needs to be updated to include the possible use of AjaxControlToolkit.ToolkitScriptManager.

Mark Carranza
September 09, 2007

# re: An update to the ClientScriptProxy Class for ScriptManager/ClientScript

Hmm, ClientScriptProxy Class DOES work with AjaxControlToolkit.ToolkitScriptManager

Even though ToolkitScriptManager is in a different assembly than ScriptManager, when we hit this line
object sm = GetCurrentMethod.Invoke(null, new object[1] { page });

GetCurrentMethod()'s type is System.Web.UI.ScriptManager, but then object sm is set to type: AjaxControlToolkit.ToolkitScriptManager!!!

I do not understand this reflection stuff completely, but:
IsScriptManagerOnPage(Page page) correctly returns true when a ToolkitScriptManager is on a page.

Rick Strahl
September 09, 2007

# re: An update to the ClientScriptProxy Class for ScriptManager/ClientScript

It shouldn't really matter. The key is that the scriptmanager knows that scripts have been updated so they get written in partial page postbacks. I'm not sure what the toolkit script manager does in addition to the standard one, but for sending general scripts it probably doesn't matter. From what you say it looks like they are just subclassing the stock script manager and add additional functionality all of which should work fine with ClientScriptProxy.

François-Xavier Vits
May 07, 2008

# re: An update to the ClientScriptProxy Class for ScriptManager/ClientScript

Hi,

I found a problem with your class in IsMyAjax method. In fact, that method can fail if the system.web.extensions.design is loaded before system.web.extensions in the current domain.
Below you can find my bug fix :

public static bool IsMsAjax()
{
if (_CheckedForMsAjax)
return _IsMsAjax;

// *** Easiest but we don't want to hardcode the version here
// scriptManagerType = Type.GetType("System.Web.UI.ScriptManager, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", false);

// *** To be safe and compliant we need to look through all loaded assemblies
Assembly ScriptAssembly = null; // Assembly.LoadWithPartialName("System.Web.Extensions");
foreach (Assembly ass in AppDomain.CurrentDomain.GetAssemblies())
{
if (string.Compare(ass.ManifestModule.Name, "System.Web.Extensions.dll", true) == 0)
{
ScriptAssembly = ass;
break;
}
}

if (ScriptAssembly == null)
return false;

scriptManagerType = ScriptAssembly.GetType("System.Web.UI.ScriptManager");

if (scriptManagerType == null)
{
_IsMsAjax = false;
_CheckedForMsAjax = true;
return false;

}

// *** Method to check for current instance on a page - cache
// *** since we might call this frequently
GetCurrentMethod = scriptManagerType.GetMethod("GetCurrent");

_IsMsAjax = true;
_CheckedForMsAjax = true;

return true;
}


Explanation : if you look for ass.FullName.startwith("System.Web.extensions"), you will find that dll in your AppDomain.CurrentDomain.GetAssemblies(). It's ok, but scriptManagerType will be null if system.web.extensions.design appears before system.web.extensions in AppDomain.CurrentDomain.GetAssemblies(). scriptManagerType will be null because the type couldn't be found.

Hope it will be useful

See you all
FX

Tim
July 15, 2008

# re: An update to the ClientScriptProxy Class for ScriptManager/ClientScript

Just as an FYI, the content on this page is partially truncated when viewed on Firefox 3.0 / Windows Vista. The left side of the main content is displaying underneath the left nav bar. Thus, once the nav bar is cleared, all content is viewable, but for the duration of the nav bar, content is obscured by it.

Thanks,

Tim

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024