UpdatePanels and ClientScript in custom Controls
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.
Other Posts you might also like
The Voices of Reason
# re: UpdatePanels and ClientScript in custom Controls
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 :)
# re: UpdatePanels and ClientScript in custom Controls
# re: UpdatePanels and ClientScript in custom Controls
# re: UpdatePanels and ClientScript in custom Controls
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; } }
# re: UpdatePanels and ClientScript in custom Controls
# re: UpdatePanels and ClientScript in custom Controls
scriptManagerType = Type.GetType("System.Web.UI.ScriptManager, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", false);
# re: UpdatePanels and ClientScript in custom Controls
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?
# re: UpdatePanels and ClientScript in custom Controls
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; }
# re: UpdatePanels and ClientScript in custom Controls
# re: UpdatePanels and ClientScript in custom Controls
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)})
# re: UpdatePanels and ClientScript in custom Controls
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...
# re: UpdatePanels and ClientScript in custom Controls
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.
# re: UpdatePanels and ClientScript in custom Controls
# re: UpdatePanels and ClientScript in custom Controls
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?
# re: UpdatePanels and ClientScript in custom Controls
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); }
# re: UpdatePanels and ClientScript in custom Controls
# re: UpdatePanels and ClientScript in custom Controls
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
# re: UpdatePanels and ClientScript in custom Controls
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
# re: UpdatePanels and ClientScript in custom Controls
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).