|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Notes:
IntroductionWhen you start writing Doing so is reasonably easy, using the API included as part of the Page object, specifically:
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 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 HandlingEach of the above methods accepts a piece of JavaScript code in
Take this simple piece of JavaScript, for example (we will refer back to this throughout the article): <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 So to create this code using option 1 ( 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 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 So let's take a look at that third and final option: 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 Problem 2: Differing StylesOf 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 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 WhitespaceJavaScript 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 ProblemsWhat we really need is a class that encapsulates the 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 Step 1: Create the Classusing 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 We need a Step 2: Add Lines and Retrieve the Resultpublic 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);
}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 We can also add the open and close sequence for every piece of JavaScript in the These two bits of code alone allow us to use the following code to produce our initial simple script block: 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 Indentationpublic int Indent
{
get { return currIndent; }
set { currIndent = value; }
}public void OpenBlock()
{
AddLine("{");
currIndent++;
openBlocks++;
}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("}");
}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 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 Note that Going back yet again to our earlier simple script, it can now be written as easily as: 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 Flagpublic 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(" ");
}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 To demonstrate, let's add a single comment to our simple script block: 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 <script language="javascript">
<!--
function MyWebControl_AlertText ()
{
// A message box showing the Control name
alert("MyWebControl");
}
// -->
</script>
But if you change <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: #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 ControlTo show fully the power of the
The The code for this control can be downloaded at the top of the article. Note: this could more easily be handled by using two The control itself is simple enough, two The script is registered as follows: [
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 Now take a look at the [
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: <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 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: <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
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.
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||