Click here to Skip to main content
15,860,972 members
Articles / Web Development / ASP.NET
Article

JavaScriptBuilder: JavaScript Handler Class for Custom Controls

Rate me:
Please Sign up or sign in to vote.
4.74/5 (39 votes)
2 Sep 200311 min read 181.7K   5.4K   109   24
The conflict between maintenance and efficiency examined and resolved.

Notes:

  • The ClickCounter DLL/code requires the JavaScriptBuilder DLL.
  • The ClickCounter control may be added to (and used from) the VS.NET toolbar but has no related icon.
  • The example page can be dropped into any Web Forms project if the DLLs are included within the application's /bin folder.
  • The DLLs above have been generated using the .NET Framework v1.1 but the code should work in v1.0

Introduction

When you start writing WebControls or even UserControls in ASP.NET, you will quickly discover the need to register pieces of JavaScript with the container page.

Doing so is reasonably easy, using the API included as part of the Page object, specifically:

IsClientScriptBlockRegisteredHas a given piece of ClientScript been registered?
RegisterClientScriptBlockRegister a piece of ClientScript
IsStartupScriptRegisteredHas a given piece of StartupScript been registered?
RegisterStartupScriptRegister a piece of StartupScript.
RegisterArrayDeclarationRegister an element in a global array which can be accessed by scripts.
RegisterHiddenFieldRegister a hidden field for use within JavaScript (useful for postback data).
RegisterOnSubmitStatementRegisters some JavaScript to be executed in the onsubmit event of the form.

The intent of this article is not to cover these individually or in detail, that may be done in another article.

This article and the downloadable class libraries are intended to address specific problems common to RegisterClientScriptBlock, RegisterStartupScript and RegisterOnSubmitStatement and to offer a remarkably simple solution.

As an overall theme, the problems involve the age-old development issues of maintenance vs. efficiency. This article attempts to maximize both in an environment that encourages neither.

Problem 1: String Handling

Each of the above methods accepts a piece of JavaScript code in string format as an argument. But building a string using the ASP.NET string object can be a horrible waste of resources. Consider the three best options:

  1. Simply concatenating strings using the + operator or even String.Concat will create a new string for each concatenation, using massive resources when building an entire slice of JavaScript code.
  2. String.Format or StringBuilder.AppendFormat can be powerful and more efficient, but tend to make JavaScript code within C# (or indeed VB.NET) less readable. It also poses a problem with opening/closing code blocks ("{" / "}") against formatting place-markers ("{0}").
  3. Repeated calls to StringBuilder.Append would be the most efficient option, but that can lead to painfully unmanageable code.

Take this simple piece of JavaScript, for example (we will refer back to this throughout the article):

JavaScript
<script language="javascript">
<!--
function MyWebControl_AlertText ()
{
    alert("MyWebControl");
}
// -->
</script>

All this script would do is throw up a message box containing the name of the WebControl. It would be registered by the control, to avoid multiple occurrences, but the function name and literal text must be overridable by inheritors, in case they decide to change the script.

So to create this code using option 1 (string + string), you would need to write the following code:

C#
string script = "<script language=\"javascript\">" + Environment.NewLine
    + "<!--" + Environment.NewLine
    + "function " + FunctionName + " ()" + Environment.NewLine
    + "{" + Environment.NewLine
    + "\talert(\" + ControlName + ");" + Environment.NewLine
    + "}" + Environment.NewLine
    + "// -->" + Environment.NewLine
    + "</script>";

This is all quite readable, but the CLR is going to create as many as 19 string objects, which are all going to sit around until the Garbage Collector gets around to picking them up. On a busy website, this could be a serious problem.

Let's have a look at option 2 (Formatting strings via StringBuilder).

C#
StringBuilder sb = new StringBuilder();
sb.AppendFormat("<script language=\"javascript\">{0}", Environment.NewLine);
sb.AppendFormat("<!--{0}", Environment.NewLine);
sb.AppendFormat("function {0} (){1}", FunctionName, Environment.NewLine);
sb.Append("{");
sb.Append(Environment.NewLine);
sb.AppendFormat("\talert(\"{0}\");{1}", ControlName, Environment.NewLine);
sb.Append("}");
sb.Append(Environment.NewLine);
sb.AppendFormat("// -->{0}", Environment.NewLine);
sb.Append("</script>");
string script = sb.ToString();

Formatting strings this way is not a bad option, it is certainly a fair compromise between the other two. It is still readable, though it was nicer when strings were placed where they should be; it is certainly more efficient than concatenating strings, but it's not as efficient as repeated calls to Append.

So let's take a look at that third and final option:

C#
StringBuilder sb = new StringBuilder();
sb.Append("<script language=\"javascript\">");
sb.Append(Environment.NewLine);
sb.Append("<!--");
sb.Append(Environment.NewLine);
sb.Append("function ");
sb.Append(FunctionName);
sb.Append(" ()");
sb.Append(Environment.NewLine);
sb.Append("{");
sb.Append(Environment.NewLine);
sb.Append("\talert(\"");
sb.Append(ControlName);
sb.Append("\");");
sb.Append(Environment.NewLine);
sb.Append("}");
sb.Append(Environment.NewLine);
sb.Append("// -->");
sb.Append(Environment.NewLine);
sb.Append("</script>");
string script = sb.ToString();

This is definitely the ideal option for a marketable server control, using StringBuilder to maximum efficiency, but just imagine for a moment coming back to a complex piece of JavaScript, built like this, some six months after you've written it. This is not a pretty image.

Problem 2: Differing Styles

Of all the above styles, I would almost certainly choose option 2. You might opt for another one entirely, for reasons that are very acceptable but do not fit my way of thinking.

Likewise, you may choose not to use Environment.NewLine, preferring \r\n or even just \n (most browsers don't really care) for its simplicity and efficiency. Personally, I would rather stick nails in my head than deal with streams of escape codes. Even the tabs I would use for indenting can be sore on the eyes after a while.

There are many other ways the above pieces of code could be improved or degraded, depending on your point of view. Again, it comes down to maintenance vs. efficiency.

Every decision you make is going to affect anyone picking up your code later (and in a work environment, this will almost certainly happen, no matter how much we all like to pretend it will not). The effect could be good or bad and at the time you are developing your control, you cannot possibly know.

Problem 3: Comments and Whitespace

JavaScript development always leaves you with a predicament when it comes to whitespace and inline comments.

Both are a waste of bandwidth and will rarely even be seen. Sometimes you may not even want the end-user to see the comments you have used in your JavaScript, or even read the code without an immense amount of effort.

On the other hand, if you choose not to use them then reading your own code can be a daunting prospect.

All of this is particularly pertinent when you are hoping to sell a server control to a web space service provider which may then be reused by dozens of amateur web developers.

A Simple Solution to Three Problems

What we really need is a class that encapsulates the StringBuilder, for the sake of efficiency, but allows us different options designed more specifically for a script-writing environment.

We also need a class that limits our options and makes it clear to any developer (even one unfamiliar with the class), what we are aiming to achieve. If we can duplicate the best features of all our earlier options then that would remove the need for different styles of JavaScript building.

Finally, we need a class with which we can easily switch from highly manageable code to highly efficient code (as we all commonly do when compiling DEBUG and RELEASE versions of software).

If we can implement just a tiny bit of debugging into that class, because JavaScript is so hard to debug, wouldn't that be great?

And so, enter the JavaScriptBuilder class.

Step 1: Create the Class

C#
using System;
using System.Text;

namespace CP.WebControls
{
    public class JavaScriptBuilder
    {
        private StringBuilder sb = new StringBuilder();
        private int currIndent = 0;
        private int openBlocks = 0;
        private bool format = false;

        public JavaScriptBuilder()
        {
        }

        public JavaScriptBuilder(bool Formatted)
        {
            format = Formatted;
        }

// Script handling code here

    }
}

There is nothing unusual about the basic structure of the class.

Apart from System, the only .NET Framework namespace we need is System.Text, for the StringBuilder class.

We need a StringBuilder object, created when the JavaScriptBuilder is instantiated. The rest of the private members handle the formatting, and whether there is going to be any. This latter question must be answered when we create the object because it is used throughout, the rest is handled as we go along.

Step 2: Add Lines and Retrieve the Result

C#
public void AddLine(params string[] parts)
{
    // Append parts of the line to StringBuilder individually
    // - much more efficient than sb.AppendFormat
    foreach (string part in parts)
        sb.Append(part);

    // Add a new line
    sb.Append(Environment.NewLine);
}
C#
public override string ToString()
{
    // Add the <script> tags and some comment blocks, so that
    // browsers that don't support scripts will not crash horribly
    return String.Format(
        "<script language=\"javascript\">{0}<!--{0}{1}{0}// -->{0}</script>", 
        Environment.NewLine, 
        sb
    );
}

The only trick here is to learn as many lessons from our first problem definition as possible.

What we truly want is the power and readability of string concatenation, combined with the efficiency of StringBuilder.Append(). Using C#'s params keyword we can do just that.

We can also add the open and close sequence for every piece of JavaScript in the ToString() function, because they should be the same in every block of script we register.

These two bits of code alone allow us to use the following code to produce our initial simple script block:

C#
JavaScriptBuilder jsb = new JavaScriptBuilder(true);
jsb.AddLine("function ", FunctionName, " ()");

jsb.AddLine("{");
jsb.AddLine("\talert(\"", ControlName, "\");");
jsb.AddLine("}");

string script = jsb.ToString();

Already much simpler and more readable than any of our initial three options and we have not lost any efficiency at all over our best-case option.

Step 3: Track the Indentation

C#
public int Indent
{
    get { return currIndent; }
    set { currIndent = value; }
}
C#
public void OpenBlock()
{
    AddLine("{");
    currIndent++;
    openBlocks++;
}
C#
public void CloseBlock()
{
    // Check that there is at least one block open
    if (openBlocks < 1)
        throw new InvalidOperationException(
            "JavaScriptBuilder.CloseBlock() called when no blocks open"
        );

    currIndent--;
    openBlocks--;
    AddLine("}");
}
C#
public void AddLine(params string[] parts)
{
    // Open line with tabs
    for (int i=0; i < currIndent; i++)
        sb.Append("\t");

    // Append parts of the line to StringBuilder individually
    // - much more efficient than sb.AppendFormat
    foreach (string part in parts)
        sb.Append(part);

    // Add a new line
    sb.Append(Environment.NewLine);
}

This code allows the developer to open and close blocks using the JavaScriptBuilder class and not have to remember how many blocks are open at any given time to insert their own tabs. It also means that you do not have to deal with streams of escape codes at the beginning of each line, which for me was a significant benefit.

Another, not so obvious, advantage of this is that you can move a piece of code from inside a block to the outside (or vice versa) and not have to worry about adjusting the number of tabs on each line.

The more observant reader is probably asking themselves why we would need to keep track of both indentation and the number of open blocks. There are situations where you might want to indent a piece of code without opening a new block, for example if one line of code is stretched across a number of lines of text.

So we need to expose the Indent property to the calling program without breaking the validation line we used in CloseBlock(). We can also include a similar validation in ToString(), so that the calling program can only convert the script to a string once all code blocks have been closed. We do not, however, care if the calling program has left indents active, because it will not make the script fail.

Note that openBlocks is not exposed in any way, it is handled entirely internally.

Going back yet again to our earlier simple script, it can now be written as easily as:

C#
JavaScriptBuilder jsb = new JavaScriptBuilder(true);
jsb.AddLine("function ", FunctionName, " ()");

jsb.OpenBlock();
jsb.AddLine("alert(\"", ControlName, "\");");
jsb.CloseBlock();

string script = jsb.ToString();

Step 4: Handle the Formatting Flag

C#
public void AddLine(params string[] parts)
{
    // Open line with tabs, where formatting is set
    if (format)
        for (int i=0; i < currIndent; i++)
            sb.Append("\t");

    // Append parts of the line to StringBuilder individually
    // - much more efficient than sb.AppendFormat
    foreach (string part in parts)
        sb.Append(part);

    // Append a new line where formatting is set or a space
    // where it isn't
    if (format)
        sb.Append(Environment.NewLine);
    else
        if (parts.Length > 0)
            sb.Append(" ");
}
C#
public void AddCommentLine(params string[] CommentText)
{
    if (format)
    {
        // Open the line with tab indent
        for (int i=0; i < currIndent; i++)
            sb.Append("\t");

        // ... and a comment marker
        sb.Append ("// ");

        // Append all the parts of the line
        foreach (string part in CommentText)
            sb.Append(part);

        // Throw in a new line
        sb.Append(Environment.NewLine);
    }
}

The changes to AddLine() mean that tabs and new line will only be added where the format flag is set. If the flag is not set then we simply separate each line of code with a single space. There is also a new method AddCommentLine() which allows us to add a comment only when the format flag is set.

To demonstrate, let's add a single comment to our simple script block:

C#
JavaScriptBuilder jsb = new JavaScriptBuilder(true);
jsb.AddLine("function ", FunctionName, " ()");

jsb.OpenBlock();
jsb.AddCommentLine("A message box showing the Control name");
jsb.AddLine("alert(\"", ControlName, "\");");
jsb.CloseBlock();

string script = jsb.ToString();

Currently, this JavaScriptBuilder will produce the following script block:

JavaScript
<script language="javascript">
<!--
function MyWebControl_AlertText ()
{
    // A message box showing the Control name
    alert("MyWebControl");
}
// -->
</script>

But if you change true to false (or allow it to default) in the constructor, the script block is unformatted as follows:

JavaScript
<script language="javascript">
<!--
function MyWebControl_AlertText () { alert("MyWebControl"); }
// -->
</script>

This saves a few bytes of bandwidth on every page that uses your control. But with a simple one-word change to your code, you can return the script to its original state, should you need to edit or debug it.

One easy way of handling this is to write the constructor as follows:

C#
#if DEBUG
JavaScriptBuilder jsb = new JavaScriptBuilder(true);
#else
JavaScriptBuilder jsb = new JavaScriptBuilder();
#endif

This then allows us to test the control using neat, readable JavaScript and, without a single line change in the code, create an efficient release version of the control.

Example: The ClickCounter Control

To show fully the power of the JavaScriptBuilder, we should create a control with a more complex script block.

The ClickCounter control will display some text along with a click counter. Each time the control is clicked, it will increment the counter and retain the initial text.

The code for this control can be downloaded at the top of the article.

Note: this could more easily be handled by using two <span> tags and only adjusting the contents of the second, but that would not suit the purposes of this article as the JavaScript would only be a couple of lines.

The control itself is simple enough, two ViewState-enabled properties (.Text and .InitialValue) with Render() overridden. I suspect that if you have read this far, you already know how to do that. This article is only of interest to Control Developers.

The script is registered as follows:

C#
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
protected virtual string IncrementScriptName
{
    get
    {
        return "ClickCounter_IncrementValue";
    }
}

protected override void Render(HtmlTextWriter output)
{
    if (!Page.IsStartupScriptRegistered(IncrementScriptName))
        Page.RegisterStartupScript(IncrementScriptName, IncrementScript);

// standard rendering code here
}

The IncrementScriptName property is designed to allow inheritors to override the script name along with the script text and, where a page includes both types of controls, both scripts can be included in the generated page without overriding the more complicated Render() method.

Now take a look at the IncrementScript property code, which is the code more relevant to this article.

C#
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
protected virtual string IncrementScript
{
    get
    {
#if DEBUG
        JavaScriptBuilder jsb = new JavaScriptBuilder(true);
#else
        JavaScriptBuilder jsb = new JavaScriptBuilder();
#endif
        jsb.AddCommentLine("Splits the inner text" + 
                         " into Text (txt) and Counter (c) parts,");
        jsb.AddCommentLine("increments (c) and" + 
                         " joins them back together again.");
        jsb.AddLine("function ", IncrementScriptName, "(elmt)");

        jsb.OpenBlock(); // function (elmt)
        jsb.AddCommentLine("Initialize variables");
        jsb.AddLine("var inner = elmt.innerText;");
        jsb.AddLine("var c = 0;");
        jsb.AddLine("var txt = \"\";");

        jsb.AddLine();

        jsb.AddCommentLine("Run through inner text string");
        jsb.AddLine("for (idx = 0; idx < inner.length; idx++)");

        jsb.OpenBlock(); // for (idx...)
        jsb.AddCommentLine("Split string into text and counter parts");
        jsb.AddLine("c = parseInt( inner.substring (idx, inner.length + 1) );");
        jsb.AddLine("txt = inner.substring(0, idx);");

        jsb.AddLine();

        jsb.AddCommentLine("If we have a number, get out of the loop");
        jsb.AddLine("if ( ! isNaN( c ) )");

        jsb.OpenBlock(); // if ( ! isNaN( c ) )
        jsb.AddLine("break;");
        jsb.CloseBlock(); // if ( ! isNaN( c ) )

        jsb.CloseBlock(); // for (idx...)

        jsb.AddLine();
        jsb.AddCommentLine("Increment counter");
        jsb.AddLine("c++;");
        jsb.AddCommentLine("Rebuild the string and put it in the inner text"); 
        jsb.AddLine("elmt.innerText = txt + \" \" + c;");
        jsb.CloseBlock(); // function (elmt)

        return jsb.ToString();
    }
}

This generates the following piece of JavaScript when compiled in DEBUG mode:

JavaScript
<script language="javascript">
<!--
// Splits the inner text into Text (txt) and Counter (c) parts,
// increments (c) and joins them back together again.
function ClickCounter_IncrementValue(elmt)
{
    // Initialize variables
    var inner = elmt.innerText;
    var c = 0;
    var txt = "";
    
    // Run through inner text string
    for (idx = 0; idx < inner.length; idx++)
    {
        // Split string into text and counter parts
        c = parseInt( inner.substring (idx, inner.length + 1) );
        txt = inner.substring(0, idx);

        // If we have a number, get out of the loop
        if ( ! isNaN( c ) )
        {
            break;
        }
    }
    
    // Increment counter
    c++;
    // Rebuild the string and put it in the inner text
    elmt.innerText = txt + " " + c;
}
// -->
</script>

This is quite readable within the control itself and very readable in the generated page. Meanwhile it is achieved with all the efficiency of repeated calls to StringBuilder.Append() and without a single escape character or Environment.NewLine in sight.

However, it does generate a lot of unnecessary code for the end user. They will rarely look at the code and again, if 1000 pages include your control and are accessed 1000 times a day, that's 1Mb of bandwidth per extra byte of code every day.

The release version of the control will have a much shorter piece of code:

JavaScript
<script language="javascript">
<!--
function ClickCounter_IncrementValue(elmt) { var inner = elmt.innerText; var c = 
0; var txt = ""; for (idx = 0; idx < inner.length; idx++) { c = parseInt( 
inner.substring (idx, inner.length + 1) ); txt = inner.substring(0, idx); if ( 
! isNaN( c ) ) { break; } } c++; elmt.innerText = txt + " " + c; }
// -->
</script>

Totally unreadable, but the functionality has not changed. This new version is considerably shorter with just under 400 bytes saved. That totals as much as 400Mb of bandwidth on your 1000 pages accessed 1000 times a day. Imagine the effect on a really complicated piece of JavaScript.

Summary

JavaScriptBuilder can be included in your code as a .cs (C#) class file or linked in from a very small (16Kb) DLL. It can be used to create JavaScript code that is readable in a generated page and easily maintained in the control's source, but without any loss of efficiency in terms of either server memory or bandwidth.

Forget about choosing between maintenance and efficiency, choose both.

Note: As with anything available on CodeProject, it is free to use, but it would be nice to hear from you if you decide to use it. If you have any ideas for improvements, I would like to hear that too.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United Kingdom United Kingdom
Paul lives in the heart of En a backwater village in the middle of England. Since writing his first Hello World on an Oric 1 in 1980, Paul has become a programming addict, got married and lost most of his hair (these events may or may not be related in any number of ways).

Since writing the above, Paul got divorced and moved to London. His hair never grew back.

Paul's ambition in life is to be the scary old guy whose house kids dare not approach except at halloween.

Comments and Discussions

 
GeneralAdding DEBUG to Constructor Pin
DeKale19-May-05 6:25
DeKale19-May-05 6:25 
GeneralRe: Adding DEBUG to Constructor Pin
Paul Riley19-May-05 9:48
Paul Riley19-May-05 9:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.