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

UpdatePanels and ClientScript in custom Controls


:P
On this page:

Over the last few weeks since MS Ajax Beta rolled around I’ve been getting a number of reports of the wwHoverPanel control running into some problems when running in combination with MS Ajax. The controls themselves don’t interfere with MS AJAX directly, but if you’re sticking the controls inside of an AJAX UpdatePanel() there’s a problem as the script code that the controls spit out don’t get properly generated into the callback generated updates. With the script code missing the controls still work but exhibit some unexpected behaviors. For example a hover panel placed into an update panel will lose it’s positioning in many cases and instead of popping up at the current mouse cursor position will pop up at the border of the container control it lives in.

 

The problem is that Microosft decided in MS AJAX Beta to go with a completely separate script generation engine which is driven through the ScriptManager control. The MS Ajax ScriptManager mimics many of the ClientScript object’s methods, but provides them as static methods (thankfully! without that we’d be really screwed).

 

So methods like RegisterClientScriptBlock, ResgisterClientScriptResources – anything that deals with getting script code into the page have related static methods in ScriptManager. The ScriptManager methods pass in the Control as an additional first parameter but otherwise mimic the existing ClientScriptManager.

 

This new behavior puts existing controls into a bind though – if code uses ClientScriptManager then UpdatePanels will not be able to see the script code (if it needs updating in a callback). But at the same time the control developer can’t make the assumption that the MS Ajax ScriptManager actually exists.

 

The end result of all of this is that it’s not exactly straight forward to deal with this mismatch and what needs to happen is that a wrapper object needs to be created that can decide which control to use. The wrapper needs to deal with deciding whether MS Ajax is available in the application and if it is, using Reflection to access the ScriptManager to write out any script code.

 

I can’t take credit for this though: Eilon Lipton posted about this issue a while back and his code really was what I needed to get this off the ground, I just wrapped the thing up into a ClientScriptProxy object that I used on a handful of controls. I basically added a handful of the ClientScript methods that I use in my applications. Here’s the class:

 

[*** code updated: 1/19/2007 from comments *** ]

/// <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 RegisterPostBackControlMethod;

//private static MethodInfo GetWebResourceUrlMethod;

 

 

/// <summary>

/// Determines if MsAjax is available in this Web application

/// </summary>

public bool IsMsAjax

{

    get

    {

        if (scriptManagerType == null)

            CheckForMsAjax();

 

        return _IsMsAjax;

    }

}

private static bool _IsMsAjax = 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

/// </summary>

/// <returns></returns>

public static bool CheckForMsAjax()

{

    // *** 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;

   

 

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

    //if (ScriptAssembly != null)

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

  

    if (scriptManagerType != null)

    {

        _IsMsAjax = true;

        return true;

    }

 

    _IsMsAjax = false;

    return false;

}

 

/// <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 (this.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 (this.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 (this.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 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.IsMsAjax)

    {

        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 });

    }

    else

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

}

 

 

/// <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)

{

    //if (this.IsMsAjax)

    //{

    //    if (GetWebResourceUrlMethod == null)

    //        GetWebResourceUrlMethod = scriptManagerType.GetMethod("GetScriptResourceUrl");

 

    //    return GetWebResourceUrlMethod.Invoke(null, new object[2] { resourceName, control.GetType().Assembly }) as string;

    //}

    //else

    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 (this.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);

}

 

} 

 

The code basically checks to see whether the MS Ajax assembly can be accessed as a type and if so assumes MS Ajax is installed. This is not quite optimal – it’d be better to know whether a ScriptManager is actually being used on the current page, but without scanning through all controls (slow) I can’t see a way of doing that easily.

 

The control caches each of the MethodInfo structures to defer some of the overhead in making the Reflection calls to the ScriptManager methods. I don’t think that Reflection here is going to cause much worry about overhead unless you have a LOT of calls to these methods (I suppose it’s possible if you have lots of resources – think of a control like FreeTextBox for example). Even then the Reflection overhead is probably not worth worrying about.

 

To use this class all calls to ClientScript get replaced with call this class instead. So somewhere during initialization of the control I add:

 

protected override void OnInit(EventArgs e)

{

    this.ClientScriptProxy = ClientScriptProxy.Current;

    base.OnInit(e);

}

 

And then to use it:

 

this.ClientScriptProxy.RegisterClientScriptInclude(this,this.GetType(),

           ControlResources.SCRIPTLIBRARY_SCRIPT_RESOURCE,

           this.ResolveUrl(this.ScriptLocation));

 

Notice the first parameter is the control instance (typically this) just like the ScriptManager call, so there will be a slight change of parameters when changing over from ClientScript code.

 

Once I added this code to my controls the problems with UpdatePanel went away and it started rendering properly again even with the controls hosted inside of the UpdatePanels.

 

You can grab the code for the client script proxy and sample control code through the wwHoverPanel download.

Posted in ASP.NET  AJAX  

The Voices of Reason


 

Fredrik Kalseth
December 12, 2006

# re: UpdatePanels and ClientScript in custom Controls

Nice one :)

With respect to the problem of figuring out if Ajax is enabled:

The ScriptManager does exactly what the WebPartManager does with respect to how it ensures that only one instance ever exists on a page etc. Ie, the ScriptManager has a static GetCurrent(Page) method to get a hold of the instance for any given page.

Thus, to figure out if Ajax is enabled you could a) use reflection to call that method and see if it returns null (ie no ScriptManager control on the current page, thus no Ajax), or b) check the page context collection (Page.Items) for an entry with the key typeof(ScriptManager) (which is essentially what GetCurrent does).

Fredrik Kalseth
December 12, 2006

# re: UpdatePanels and ClientScript in custom Controls

I just made another observation while peeking at ScriptManager with Reflector; it actually register script blocks etc the regular way first (via Page.ClientScript) and then does some extra stuff only if a ScriptManager exists on the page and IsInAsyncPostBack is true for it:

 control.Page.ClientScript.RegisterClientScriptBlock(type, key, script, addScriptTags);
      ScriptManager manager1 = ScriptManager.GetCurrent(control.Page);
      if ((manager1 != null) && manager1.IsInAsyncPostBack)
      {
            manager1.ScriptRegistration.RegisterClientScriptBlockInternal(control, type, key, script, addScriptTags);
      }



So my comment above is really redundant; if a ScriptManager type is accessible then it will be able to figure out the rest itself :)

Rick Strahl
December 12, 2006

# re: UpdatePanels and ClientScript in custom Controls

Frederik - nice detetective work. Yes, it would seem that GetCurrent() will be a better choice for making sure that a manager is active that way you won't have to go through Reflection when the manager is not on the form.

Simone Busoli
December 12, 2006

# re: UpdatePanels and ClientScript in custom Controls

Nice idea Rick, but I suggest you use a context-singleton class for your proxy, or you'll risk to have double registered scripts (for example if the class is used in custom controls). I made a post on the topic here: http://dotnetslackers.com/community/blogs/simoneb/archive/2006/08/21/382.aspx, since I faced the problem while implementing my HeadScriptManager I worte about here: http://dotnetslackers.com/community/blogs/simoneb/archive/2006/07/29/247.aspx

Rick Strahl
December 12, 2006

# re: UpdatePanels and ClientScript in custom Controls

Simone, good idea, although I'm not sure that this is really necessary for ClientScript or ScriptManager. Those objects are already singletons (ClientScript on Page and ScriptManager because it's static) and this class is merely getting references to those objects - it's not creating new ones, so there should be no issue with the script entries getting duplicated.

However, I think this is still a good idea as it minimizes additional overhead of another class.

/// <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.
/// </summary>
public static ClientScriptProxy Current
{
    get
    {
            return
            ( HttpContext.Current.Items["__ClientScriptProxy"] ??
            (HttpContext.Current.Items["__ClientScriptProxy"] = 
                new ClientScriptProxy(HttpContext.Current.Handler as Page)))
            as ClientScriptProxy;
    }
}

Simone Busoli
December 12, 2006

# re: UpdatePanels and ClientScript in custom Controls

I guess you're right... maybe I was having that issue because I was doing something weird on the page ;)

Rick Strahl
December 18, 2006

# re: UpdatePanels and ClientScript in custom Controls

Note that with the RC of MS Ajax the strong name for the ScriptManager has changed, so the Reflection load check needs to look at:

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

Rick Strahl's Web Log
December 18, 2006

# ScriptManager and Resource Compression - Rick Strahl's Web Log

The MS Ajax ScriptManager can automatically compress script files when using RegisterClientScriptResource. Unfortunately it only works when a script manager instance sits on a form even though the ScriptManager method is static.

Jim
December 21, 2006

# re: UpdatePanels and ClientScript in custom Controls

This is great - I just invented this myself only to find your post a bit later!

What worries me is this part of the code.

"Version=1.0.61025.0"

Any objection to doing this?

Assembly a = Assembly.LoadWithPartialName("System.Web.Extensions");

if(a!=null)
{
Type t = a.GetType("System.Web.UI.ScriptManager");

In theory I guess it could load a fake System.Web.Extensions - but is there anyway to specify the assembly key token without the version?

Rick Strahl
December 21, 2006

# re: UpdatePanels and ClientScript in custom Controls

Jim - yes I was going to check that out at some point. It looks like that should work without any problems even in Medium trust which is what I was worried about. I had to actually think about this for a minute to see whether LoadWithPartialName would actually load the assembly into the project even when no reference exists - and thankfully it doesn't <s>...

So yes it looks like this should work. With this the code for CheckForMsAjax becomes:

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

    Assembly ScriptAssembly = Assembly.LoadWithPartialName("System.Web.UI.ScriptManager");
    if (ScriptAssembly != null)
       scriptManagerType = ScriptAssembly.GetType("System.Web.UI.ScriptManager");
   
    if (scriptManagerType != null)
    {
        _IsMsAjax = true;
        return true;
    }

    _IsMsAjax = false;
    return false;
}

# ASP.NET Forums - Ajax RC not working on production server, works on dev machine


Speednet
January 03, 2007

# re: UpdatePanels and ClientScript in custom Controls

I see the issue now. The new AJAX ScriptManager can take either a Page object or a Control object for the first argument of RegisterClientScriptResource(). (The other two arguments in both cases are type As System.Type and resourceName As String.)

Speednet
January 03, 2007

# re: UpdatePanels and ClientScript in custom Controls

UGH! Very sorry to keep adding like this, but you need to change the other GetMethod()s as well. Seems MS added support for Control or Page in each of them.

Here they all are, I'm sure you know where they go:

ClientScriptProxy.RegisterClientScriptBlockMethod = scriptManagerType.GetMethod("RegisterClientScriptBlock", New Type() {GetType(Control), GetType(Type), GetType(String), GetType(String), GetType(Boolean)})

ClientScriptProxy.RegisterStartupScriptMethod = scriptManagerType.GetMethod("RegisterStartupScript", New Type() {GetType(Control), GetType(Type), GetType(String), GetType(String), GetType(Boolean)})

ClientScriptProxy.RegisterClientScriptIncludeMethod = scriptManagerType.GetMethod("RegisterClientScriptInclude", New Type() {GetType(Control), GetType(Type), GetType(String), GetType(String)})

ClientScriptProxy.RegisterClientScriptResourceMethod = scriptManagerType.GetMethod("RegisterClientScriptResource", New Type() {GetType(Control), GetType(Type), GetType(String)})

Rick Strahl
January 03, 2007

# re: UpdatePanels and ClientScript in custom Controls

Not sure what you mean about the GetMethod() calls. Where do you see Page as a parameter to any of these methods? That makes no sense since you can get the Page reference from Control. And there's nothing in Reflector regarding these methods.

Thanks for catching the typo in the code above - you're right it should be System.Web.Extensions:

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


I've updated the full code snippet in the main article with a number of updates.

I've removed your other posts since they were really long and rambling, sorry...

Speednet
January 04, 2007

# re: UpdatePanels and ClientScript in custom Controls

After the next RC comes out you will need to change:

RegisterClientScriptIncludeMethod = scriptManagerType.GetMethod("RegisterClientScriptInclude");

TO:

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

As I explained in my "rambling" comments, there are now overloaded versions of the RegisterClientScriptInclude() (and other) methods, and your simple GetMethod() does not cut it anymore. If you don't change it you'll get a Ambiguous Match error.

I'm surprised you did not get rid of the LoadWithPartialName() as I had suggested, because it is no longer supported (see MSDN docs). Too bad you deleted the code, it works well.

Rick Strahl
January 17, 2007

# re: UpdatePanels and ClientScript in custom Controls

SpeedNet. Hmmm... somewhere in there I missed an RC <s>. I see what you're saying with the release version and I updated the code. I also changed the assembly loading code.

# A Continuous Learner's Weblog: December 2006

# A Continuous Learner's Weblog


ASP.NET Forums
May 28, 2007

# Ajax RC not working on production server, works on dev machine - ASP.NET Forums


Damir Dobric Posts
July 04, 2007

# RegisterStartupScript and RegisterClientScriptBlock can work with UpdatePanel?! - Damir Dobric Posts

c#; vista; WWF; WCF;

Azhar Khan
December 11, 2007

# re: UpdatePanels and ClientScript in custom Controls

I wanted to render a Required field with a RED box around it. And since Microsft's WebUIValidation.js does not do it, I wrote a custom WebUIValidation.js.
To service my version of WebUIValidation.js , I had to write a custom WebResource.axd handler.
I notice that when the application is run on certain servers the RED box does not get rendered properly and on some servers it does. I looked at html that gets rendered and I noticed that they were different.
The basic difference between ClientScriptManager and ScriptManager is that ClientScriptManager renders WebResource.axd handler to fetch the resource from Server whereas ScriptManager renders ScriptResource.axd handler.

Looks like RequiredFieldValidator control renders ScriptManager.axd to fetch WebUIValidation.js on offending servers.
Do I have to write a custom handler for ScriptResource.axd or there is an easier way?

ihsany
May 08, 2008

# re: UpdatePanels and ClientScript in custom Controls

nice solution
its works in everywhere (page, usercontrol,masterpage and usercontrol in grid edit form :) )
only the code below in my customcontrol's OnPreRender

thanks..

string resourceName = "ErciyesCommon.Components.MyControl.js";

            ScriptManager manager = ScriptManager.GetCurrent(this.Page);
            if ((manager != null) && manager.IsInAsyncPostBack)
            {
                Type smType  = manager.GetType();
                MethodInfo RegisterClientScriptResourceMethod = smType.GetMethod("RegisterClientScriptResource", new Type[3] { typeof(WebControl), typeof(Type), typeof(string) });
                if (RegisterClientScriptResourceMethod != null)
                {
                    RegisterClientScriptResourceMethod.Invoke(null, new object[3] { this, this.GetType(), resourceName });
                }
            }
            else
            { 
                ClientScriptManager cs = this.Page.ClientScript;
                cs.RegisterClientScriptResource(typeof(ErciyesCommon.Components.LookupBox.LookupBox), resourceName);
            }

Pat Burm
December 05, 2008

# re: UpdatePanels and ClientScript in custom Controls

Rick, just an FYI, the Current property in the code you provide for download does not match the example on this blog entry. It will check to see if HttpContext.Current.Items["__ClientScriptProxy"] is not null and use it, but it never adds it to the items collection, so it always returns a new instance.

Rick Strahl
December 05, 2008

# re: UpdatePanels and ClientScript in custom Controls

@Pat - Duh. Yes that's a silly oversight.
public static ClientScriptProxy Current
{
    get
    {
        if (HttpContext.Current == null)
            return new ClientScriptProxy();
        
        if (HttpContext.Current.Items.Contains(STR_CONTEXTID))
            return HttpContext.Current.Items[STR_CONTEXTID] as ClientScriptProxy;

        ClientScriptProxy proxy = new ClientScriptProxy();
        HttpContext.Current.Items[STR_CONTEXTID] = proxy;
        
        return proxy;
    }
}


Fixed here:

http://www.west-wind.com:8080/svn/jquery/trunk/jQueryControls/Support/ClientScriptProxy.cs

Kevin Cornwell
January 28, 2011

# re: UpdatePanels and ClientScript in custom Controls

Thank you so much for going to the trouble with this article! I spent nearly a full day working on the problem.

One question... Can a .IsClientScriptIncludeRegistered(key) method be built into your proxy class? I would like to do this so I'm not reregistering (and wasting bandwidth and resources) when the page is not AJAX'd. This would allow me to use your proxy class globally for all client script operations.


Thanks!
Kevin Cornwell


More info on .IsClientScriptIncludeRegistered...

http://msdn.microsoft.com/en-us/library/255xet9e.aspx

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