Dale Preston's Web Log
  
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:
ClientScripts Control
Image 1. ClientScripts Control
I created the ClientScripts control so that I could easily add or remove scripts from a page and can visually see which scripts have been added. In the process, I learned a great deal about controls, collections, and customizing the design environment. My initial goal in writing this article was to highlight the ClientScripts control but along the way, as I worked on rounding out the control to prepare it for publication, there were several interesting issues that I had to overcome. So while the end result of this article will be to have a tool for graphically managing client scripts in your ASP.Net applications, I will also share what I learned along the way.

ClientScript
ClientScripts
ScriptCollection
ScriptCollectionEditor
ClientScriptsDesigner
MyHundredDollarUrlEditor

ClientScript


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:
  • ID - inherited from System.Web.UI.Control.
  • PostScriptHtml - adds HTML to your page that is rendered after the script code.
  • PreScriptHtml - adds HTML ot your page before the script. This is handy if the script will be accessing HTML that must be there - render that HTML from this property. A future article will demonstrate a handy usage of this property.
  • ScriptEnabled - determines whether the script is rendered both in the designer and in the page.
  • EmbedScript - determines whether the code of the script file is embedded in the page or whether the script file is passed as the src attribute of a SCRIPT tag.
  • ScriptType - specifies whether the script is rendered as a StartupScript or as a ClientScript as discussed in the ClientScripts section.
  • ScriptUrl - specifies the URL of the file containing the 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:
  • TypeConverter, which assigns a custom TypeConverter to the ClientScript class. TypeConverters are used to convert a type to a different type. We use the custom TypeConverter for one purpose in the ScriptCollectionEditor which is covered later in the article.
  • ToolboxItem, this attribute is required because the ClientScript class inherits from System.Web.UI.Control. Normally, objects derived from System.Web.UI.Control have a user interface and are intended to be put on a page and are, therefore, available in the toolbox if the assembly is added to the toolbox. While the ClientScripts control can be added to the toolbox and to a page, the script objects are not intended to be used outside of the ClientScripts control.


  • 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.

    ClientScripts


    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.

    The job of the ClientScripts control is to "render" client-side javascripts to the web browser as part of an HTML page. The .Net framework has two methods that render client-side scripts for you:

  • RegisterStartupScript
  • RegisterClientScriptBlock


  • 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"), 
    ToolboxData("<{0}:ClientScripts runat=server></{0}:ClientScripts>"),
    Designer("DPLib.ClientScriptsDesigner"),
    ParseChildren(true,"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:
            [DesignerSerializationVisibility(DesignerSerializationVisibility.Content),
    Editor(typeof(ScriptCollectionEditor), typeof(UITypeEditor)),
    NotifyParentProperty(true),
    PersistenceMode(PersistenceMode.InnerDefaultProperty),
    Category("Data")]

  • DesignerSerializationVisibility, in an ASP.Net application, tells the IDE how to serialize properties to the ASPX page. The value of Content tells the IDE to persist the visible properties of each property of the property type.
  • Editor, this attribute specifies a custom editor for the ClientScripts collection. The default CollectionEditor did not meet all the requirements for the collection, as described in the ScriptCollectionEditor section of this article, so to handle those specific limitations, a custom editor is specified.
  • PersistenceMode.InnerDefaultProperty tells the ControlParser that all of the inner elements of the ClientScripts control belong to the default property, Scripts, and that there are no other inner element types within the control.
  • NotifyParent tells the IDE to update the ClientScripts control when the contents of the ScriptCollection is changed.


  • 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.

    ScriptCollection


    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.

    ScriptCollectionEditor


    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;

    do
    {
    // Set the script.ID to an appropriate name for displaying in
    // the control and collection editor.
    newID = newBaseName + count.ToString();
    count++;

    } while (csc.Contains(newID));
    script.ID = newID;
    }

    return script;
    }

    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,
    System.Globalization.CultureInfo culture,
    object value,
    Type destinationType)
    {
    if (destinationType == typeof(string))
    {
    ClientScript script = (ClientScript)value;
    return script.ID;
    }
    return base.ConvertTo(context, culture, value, destinationType);
    }
    }

    ClientScriptsDesigner


    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.

    MyHundredDollarUrlEditor


    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.

    Summary


    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.

    Comments: Post a Comment

    << Home

    Powered by Blogger