Sunday, July 24, 2005
Visually Add Client Scripts to Your ASP.Net Pages
One thing that many ASP.Net developers seem to dislike or to avoid is client-side scripts. That's unfortunate because client-side scripting is the backbone of a rich browser-based application environment. Most of the best of ASP.Net depends on client-side scripting and basic HTML controls for the presentation layer.
In my own projects, one drudgery I dislike is copying and pasting the same script across multiple pages over and over again, and then having to search pages repeatedly to see if the required scripts have been added - hence the ClientScripts control:
Image 1. ClientScripts Control
The properties and methods of the ClientScript are well documented in the source code which you can view in HTML format or as part of the downloadable project so only the highlights will be covered here. The ClientScript class has seven properties that describe a client script:
The ToHtmlString method of the ClientScript class generates the HTML that is sent with the page including the SCRIPT tags.
There are some interesting attributes in the ClientScript class that deserve mentioning. At the ClientScript class level, there are two:
Also of significance is the Editor attribute that is applied to the ScriptUrl property of the ClientScript class. Because the ScriptUrl is a string value, the default editor is a TextBox in the PropertyGrid but it is much more useful to be able to browse within the project and select the script file. The editor for the ScriptUrl has an interesting name, MyHundredDollarUrlEditor that will be explained in the MyHundredDollarUrlEditor section of this article.
The ClientScripts class, and the control, is pretty simple. The ClientScripts class has only one property and one method. The rest of the functionality for the class is controlled by attributes and the classes supporting those attributes.
The single property is the Scripts property which contains a ScriptCollection of ClientScript objects. More about the ScriptCollection and ClientScript classes later. You can view the source code for the ClientScripts class in HTML format or as part of the downloadable project.
The one method of the ClientScripts class is an override of the OnPreRender event handler of System.Web.UI.Control base class. While most web server controls are rendered in the Render method, there are two good reasons for "rendering" ClientScripts in the OnPreRender event. First, the ClientScripts control has no user display that needs to be rendered visibly for the web browser client. The second reason will be apparent in the next paragraph.
RegisterStartupScript adds the script content immediately before the closing </FORM> tag. While a person might assume that startup scripts would, by definition, be put at the beginning - or start - of the page, the framework renders them at the end of the form because, in order to interact with form elements, the entire form must have been rendered prior to executing the startup script code.
RegisterClientScriptBlock, on the other hand, adds scripts immediately following the opening <FORM> tag. These scripts are generally called by other events or scripts and therefore won't be called until after the page is fully rendered. Therefore, these scripts can be rendered at the beginning of the form. That is where OnPreRender comes into play for ClientScripts. Because these scripts are rendered early in processing, they must be made available to the page before the page starts rendering. By the time OnRender is called, it is too late to generate client script blocks. If ClientScripts were to wait until OnRender to generate ClientScript blocks, those blocks defined as ClientScripts would not render to the page.
The rest of the work of the ClientScripts class is performed by its attributes. There are attributes on the class and on the Scripts property. The attributes of the ClientScripts class are:
DefaultProperty("Scripts"),The DefaultProperty attribute determines which property is selected initially in the property window when you select the control in the designer. Since most of the other properties are meaningless in the case of the ClientScripts control, the only property you'll normally deal with is the Scripts property so it is set as the default.
ToolboxData defines the code that is created in the ASPX page when you drag a ClientScripts control from the toolbox to the page.
The two remaining attributes are more interesting for the ClientScripts control:
The Designer attribute is what allows the control to render in the Visual Studio.Net designer. This attribute is how the ClientScripts control tells the IDE to use custom code for displaying the control in the webform designer and where to find that code. The ClientScriptsDesigner is discussed further later in this article.
The ParseChildren attribute has a default method of ChildrenAsProperties so our usage of ParseChildren(true,"Scripts") results in the ControlParser treating all children elements of the ClientScripts control in the ASPX file as properties of the ClientScripts control, the Scripts property to be precise, instead of as child controls.
The Scripts property has a few interesting attributes as well:
The Scripts property provides a getter only. To change the contents of the Scripts collection, edit the ScriptCollection. It is not possible to create a new SciptCollection and assign it to the Scripts property.
If it was only necessary to keep up with one script in a page, it would be pretty simple and this control may never have come about but, commonly, many pages require managing many scripts, either including or excluding them, depending on the page content and requirements. That is why the ClientScripts control was developed, to manage the entire collection of scripts that a page may require.
The ScriptCollection class is modelled after Naty Gur's article, Step by step guide for developing custom server control with custom collection, at http://weblogs.asp.net/ngur/articles/144770.aspx. Following the examples in that article helped me solve the "Ambiguous match found" error described in Microsoft Knowledge Base article 823194. You can view my implementation of what is described in these articles in HTML format or as part of the downloadable project.
The System.ComponentModel.Design.CollectionEditor gave me some problems that I was not able to otherwise resolve so I created a custom CollectionEditor for the ScriptCollection that is used in conjunction with the ClientScriptTypeConverter for the ClientScript class. The problems in the default CollectionEditor were related to naming of ClientScript objects. The source code for the ScriptCollectionEditor and ClientScriptTypeConverter described below can be viewed in HTML format or as part of the downloadable project.
First, the CollectionEditor automatically named newly added scripts as clientScript# where # was replaced by a number. The problem is that the number would start over at one again each time a new CollectionEditor was created which is, apparently, each time you reopen the page in the designer. This created duplicate named ClientScript objects unless the names were manually changed to be unique. The ScriptCollectionEditor overrides the CreateInstance method to assign a unique name to each script when it is created:
protected override object CreateInstance(Type itemType)
// Create the script instance.
ClientScript script = (ClientScript)Activator.CreateInstance(typeof(ClientScript));
if ( this.Context.Instance!=null)
string newBaseName = "clientScript";
// Get the collection instance being edited.
ClientScripts cs = (ClientScripts)this.Context.Instance;
ScriptCollection csc = cs.Scripts;
int count = csc.Count + 1;
string newID = string.Empty;
// Set the script.ID to an appropriate name for displaying in
// the control and collection editor.
newID = newBaseName + count.ToString();
} while (csc.Contains(newID));
script.ID = newID;
The second reason for the custom ScriptCollectionEditor was the left-hand box that lists the Members of the collection. The default behavior is to list the members by type only, without any unique identification in this list. For instance, all ClientScript items were listed as just ClientScript. In order to edit a specific item, one would have to select each in sequence until the desired item was found to edit. This is apparently not a bug because Microsoft treats it as acceptable behavior in many of their collection classes. The sole purpose of the custom TypeConverter in the ClientScripts control is to handle problem by overriding the ConverTo method:
public class ClientScriptTypeConverter : TypeConverter
public override object ConvertTo(ITypeDescriptorContext context,
if (destinationType == typeof(string))
ClientScript script = (ClientScript)value;
return base.ConvertTo(context, culture, value, destinationType);
The ClientScriptsDesigner is the heart and soul of the ClientScripts control - its reason for existence: Visually adding client scripts to your ASP.Net pages.
The designer creates HTML that is displayed in the Visual Studio.Net web forms designer and lists the enabled scripts that will be rendered to the page. Scripts are listed according to whether they are rendered as ClientScripts or as StartupScripts. Since it is pointless to render a script with no content, if the ScriptUrl property is empty, the script will be listed in the designer with a red asterisk (*) next to it. The CLientScriptsDesigner is pretty easy to follow in the source code that you can view in HTML format or as part of the downloadable project.
The most challenging piece of the ClientScripts control is the MyHundredDollarUrlEditor class.
In the early versions of the ClientScripts control, the path to the script file was just typed into the default TextBox provided for entering string values. One key goal of the project for publication was to add URL editing capabilities similar to other controls with URL properties in the IDE.
At first glance, this seemed a trivial exercise: Add a UrlEditor and that's all there is to it. Well, it wasn't so easy and, in fact, was not easy at all. Adding a UrlEditor to the ClientScript object worked as described in the framework documentation as long until the ScriptCollection is factored in.
As soon as I added the UrlEditor to a ClientScript object in a collection, either a default collection class or the ScriptCollection class, the UrlEditor would fail to open, displaying a message box wtih the error: "Object reference not set to an instance of an object." and no other information is provided. There's no indication of what object, what class, or anything else.
Trials with the UrlEditor class and its cousins, the XmlUrlEditor, ImageUrlEditor, and XslUrlEditor, showed that no UrlEditor class would work for a property of an object if that object was contained within a collection while it would work correctly if that object was sited directly on the page. There were no properties for any of the UrlEditor classes that could be set to correct the object reference error. Many things I tried yielded various results but none were satisfactory. I wanted the UrlEditor to work in the CollectionEditor just as it worked directly in a PropertyGrid for an item not in a collection.
After further research of the UrlBuilder class I realized the problem was that the ClientScript.Site property was empty when the ClientScript was in a collection. I spent weeks searching and posting newsgroups about how to get the Site property from the collection into the members of the collection. I was able to find many instances where other developers had asked the same questions but never any answers.
Finally, I submitted an incident to Microsoft's product services in the hopes of getting an answer. Thus the name "MyHundredDollarUrlEditor". The original Microsoft solution provided a UrlEditor named MyUrlEditor. I styled their response more to my liking and renamed the class for the price of an email support incident from Microsoft. In their defense, let me say that it was a great deal. I spent a few weeks trading emails while the support person assigned worked to solve the problem. I am sure that the labor rate for my particular incident could not have yielded any better than 5 or 10 dollars an hour after all the time that was spent resolving this.
As you can see in the source code for the MyHundredDollarUrlEditor, that you can view in HTML format or as part of the downloadable project, the class uses undocumented features of Visual Studio and the .Net framework. Though I asked for supporting documentation after getting the solution, Microsoft was unable to provide it because it was "internal information." Now I know that I could never have solved the problem on my own in my entire lifetime.
Microsoft's solution uses the undocumented Microsoft.VisualStudio.Designer.Host.DesignSite class to get the ISite associated with the designer. When the MyHundredDollarUrlEditor is instantiated to edit the ScriptUrl property, the first thing it does is check the value of the Site property of the ClientScript. If the Site property is null, the MyHundredDollarUrlEditor assigns the Microsoft.VisualStudio.Designer.Host.DesignSite Site property to the ClientScript.
That completes the ClientScripts control. If you have any suggestions or improvements, or if you use it in your application, please feel free to let me know.