Thoughts, Tips and Tricks on what I'm currently do for a living. Currently most of my spare time is spent on contributing to Akka.NET.
Showing posts with label Ajax. Show all posts
Showing posts with label Ajax. Show all posts

Sunday, July 27, 2008

How to include scripts after asp.net ajax framework's

When registering script includes using ScriptManager.RegisterClientScriptInclude("myFile.js") the files are always included before the frameworks. This causes errors if your code depends on the ajax framework's. Errors like "Type is not defined" are common since the first line in the included script file often tries to register a namespace: Type.registerNamespace('My.Namespace');

On the other hand, files registered as ScriptReferences on the ScriptManagerProxy will be included AFTER the framework's includes.

<asp:ScriptManagerProxy runat="server" id="ScriptManagerProxy1">
    <Scripts><asp:ScriptReference Path="~/anotherFile.js" /></Scripts>
</asp:ScriptManagerProxy>

Not exactly the expected behavior and certainly not what we want.

When calling ScriptManager.RegisterClientScriptInclude() it delegates to the class ClientScriptManager. This class holds, internally, a list of registered scripts, and new entries ends up at the end. So the call ScriptManager.RegisterClientScriptInclude("myFile.js") will put myFile.js at the end of that list. Like this:

ClientScriptManagerClientScripts = { ..., "~/myFile.js" }

The references in the ScriptManagerProxies are handled a bit differently. The ScriptManager will, on the event PagePreRenderComplete, collect all references from the proxies in a list, and first in that list put the ajax framework's files.

List in ScriptManager = { ajax framework files, "~/anotherFile.js" }

After being collected the files are registered with the ClientScriptManager and are appended at the end of it's list. Since we registered myFile.js before PagePreRenderComplete (which occurs late in the asp.net page's life cycle), myFile.js is before the framework's files:

ClientScriptManagerClientScripts = { ..., "~/myFile.js", ..., ajax framework files, "~/anotherFile.js"}

These are rendered to the page in this order and it explains why anotherFile.js may use the framework directly and myFile.js not.

Solution 1. Use a proxy

The simplest solution is to put a ScriptManagerProxy on the page (or user control, or MasterPage) and either, as shown above declare a ScriptReference, or add one by code:

ScriptManagerProxy1.Scripts.Add(new ScriptReference("~/myFile.js"));

Solution 2. Register the file after PagePreRenderComplete

If you somehow manage to register myFile.js after PagePreRenderComplete (i.e. after ScriptControl has added all files) it will end up after the framework's files. But you'll need to do it before the rendering takes place. Between PreRender and Render phases is the SaveViewState phase. If we override SaveViewState on a control (Page is also a control) and registers myFile.js there it will be added to the list after the framework's files. This is sort of a hack and it's not guaranteed to work when new versions of the framework are released, but: It works.

protected override object SaveViewState()
{
    ScriptManager.RegisterClientScriptInclude(this,GetType(),"myFile","~/myFile.js");
    return base.SaveViewState();
}

Solution 3. Your own ScriptManager

This is, at least to me, the most elegant solution. Basically: you create the class MyScriptManager, which derives from ScriptManager; you replace the ScriptManager on the page (or user control, or MasterPage) with MyScriptManager; add the method RegisterClientScriptInclude(string url) to MyScriptManager and by some magic inside MyScriptManager makes sure that files registered thru the new method will be rendered after the ajax framework's includes.

When the ScriptManager collects all the ScriptReferences from the proxies (as explained above) it also collects ScriptReferences from all controls that have been registered with the ScriptManager as ScriptControls, by calling the method GetScriptReferences, that ScriptControls must implement. These files will be added after the proxies' files.

This is what we'll do: The new RegisterClientScriptInclude method will only add the url to a list, and not register it anywhere else. We let MyScriptManager be a ScriptControl (i.e. implement IScriptControl) and register itself as such. In the method GetScriptReferences we will, when requested, supply the list of registered files, and they will end up after the framework's.

Not much code is needed.

public class MyScriptManager : ScriptManager, IScriptControl
{
    private List<string> _registeredScripts = new List<string>();

    public virtual void RegisterClientScriptInclude(string url)
    {
        _registeredScripts.Add(url);
    }

    protected override void OnPreRender(EventArgs e)
    {
        //Register this instance as a ScriptControl.
        RegisterScriptControl(this);
        base.OnPreRender(e);
    }

    public new static MyScriptManager GetCurrent(Page page)
    {
        return (MyScriptManager) ScriptManager.GetCurrent(page);
    }

    #region IScriptControl Members
    IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()
    {
        return null;
    }

    IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
    {
        //For each element in _registeredScripts create a
        //ScriptReference and return the IEnumerable
        return _registeredScripts.ConvertAll(s => new ScriptReference(s));
    }
    #endregion
}

Please note that I've simplified the code in order to show the concept. In production code you'll want to encapsulate the list in a public property that creates the list if needed. And you want two protected virtual versions of GetScriptDescriptors and GetScriptReferences that the existing ones will call.

To register a file to be included after the framework's files you use:

MyScriptManager.GetCurrent(Page).RegisterClientScriptInclude("~/myFile.js");

If you're using AjaxControlToolkit change the inheritance to ToolkitScriptManager.

Note the order the files are included:

  1. ScriptManager.RegisterClientScriptInclude registered files
  2. Ajax framework's files
  3. All ScriptManagerProxy ScriptReferences
  4. MyScriptManager.RegisterClientScriptInclude registered files

If you want your files to be included between 2 and three you're in deep water. It's probably doable but far from trivial, since ScriptManager is pretty closed for extension. It would be nice if Microsoft adhered the Open/Closed-principle a bit more. :)

Friday, May 18, 2007

Passing values to behavior properties

Typically a property in a AjaxControlToolkit Extender looks like this:
[ExtenderControlProperty]
[DefaultValue("")]
public string MyProperty
{
  get { ... }
  set { ... }
}
Which will match a property in the behavior:
get_MyProperty : function() { ... },
set_MyProperty : function(value) { ... }
Values for MyProperty on the server side will end up in the behavior. Sometimes you want to pass values to the behavior without creating a property in the Extender control. To do this, simply override RenderScriptAttributes() and add the properties using AddProperty().
protected override void RenderScriptAttributes(ScriptBehaviorDescriptor descriptor)
{
  base.RenderScriptAttributes(descriptor);
  string horizontalAlignment=GetHorizontalAlignment();
  descriptor.AddProperty("HorizontalAlignment", horizontalAlignment);
}
And voilà, the HorizontalAlignment property in the beahvior will be set.

Property names & Extenders

Properties for extenders inheriting ExtenderControlBase typically looks like this:
[ExtenderControlProperty]
[DefaultValue("")]
public string MyProperty
{
  get
  {
    return GetPropertyValue("MyProperty", "");
  }
  set
  {
    SetPropertyValue("MyProperty", value);
  }
}
You must have a property with the same name in the behavior on the client side:
get_MyProperty : function() {
  return this._myProperty;
},
set_MyProperty : function(value) {
  this._myProperty = value;
}
But what if you want it to have different name on the client side? Easy. Add the ClientPropertyName attribute to the server side property. If we want MyProperty to be called myProp instead this is how it's done:
[ExtenderControlProperty]
[DefaultValue("")]
[ClientPropertyName("myProp")]
public string MyProperty
{
  get
  {
    return GetPropertyValue("MyProperty", "");
  }
  set
  {
    SetPropertyValue("MyProperty", value);
  }
}
On the client side we get:
get_myProp : function() {
  return this._myProperty;
},
set_myProp : function(value) {
  this._myProperty = value;
}
More on attributes: http://ajax.asp.net/ajaxtoolkit/Walkthrough/ExtenderClasses.aspx

Thursday, May 10, 2007

OOP in Javascript

MSDN Magazine May 2007 edition has an interesting article on how to use object oriented techniques in Javascript. A must-read if you are using Microsoft AJAX since the entire client script library is written using these techniques. http://msdn.microsoft.com/msdnmag/issues/07/05/JavaScript/default.aspx

Thursday, March 22, 2007

Validators not working with UpdatePanel??

The validators don't work very well sometimes when put in an UpdatePanel. But they used to... The reason is that in the 1.0 release of Ajax asp.net the validators that were used in place of the the ordinary asp.net validators have been removed. There was supposed to be an update available for the System.Web namespace to overcome this problem but it will take some time before we have the update. In the meantime there is a fix available. Have a look here: http://blogs.msdn.com/mattgi/archive/2007/01/23/asp-net-ajax-validators.aspx

Wednesday, January 31, 2007

Find a control in javascript (Ajax ASP.Net)

To find a control with id "MyControl" us the function $get. Example:
alert($get("MyControl").id);

Show and Hide a ModalPopupExtender from javascript

To show a modal popup handled by a ModalPopupExtender from javascript you need to set the BehaviorID property:
<ajaxToolkit:ModalPopupExtender...
BehaviorID="MyModalPopupExtender"
... /> 
The script for showing and hiding the popup:
<script language="javascript">
   function hidePopup()
   {
       $find('MyModalPopupExtender').show();
   }
   function hidePopup()
   {
       $find('MyModalPopupExtender').hide();
   }
</script>