|
|||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThe new state management and postback features of ASP.NET are indeed very exciting. They provide developers with a whole new range of mechanisms for producing dynamic web pages. The ability to write your own custom controls takes that ability to a whole new level allowing you to write a control with custom functionality that can easily be reused in multiple pages by simply defining custom tags, similar to any other HTML element. The person implementing the layout for a page no longer needs to know all the details of how to write client-side code to get the dynamic behavior that has become so popular. However, there are some pitfalls that developers need to be aware of. ASP.NET promotes server-heavy designs. Network traffic can be dramatically increased as each client-side event can potentially cause a round trip to the server. Many of the effects that result from these frequent trips to the server can easily be accomplished with a few simple JavaScript functions. Calls to the server should be kept to a minimum, with as much being done on the client as possible. Using custom controls to generate client-side script we can take advantage of Dynamic HTML on the client while still providing a measure of separation between the layout and the logic. Client-Side script generationOne of the goals of generating script from a custom control is to allow a developer to create the control and specify it's behavior then publish it for others to use without having to know how the code works. We want to encapsulate the implementation of the control and tightly couple the HTML rendering to the script that works with it to reduce the possible points of failure associated with more traditional methods of web component reuse (i.e. cut & paste, and include files). The most straight forward approach to script generation is to write the script along with the control in the namespace Spotu
{
public class HelloWorld : Control
{
protected override void Render (
HtmlTextWriter writer
)
{
writer.Write(@"
<script>
function HelloWorld()
{
document.all('_msg').innerText = 'Hello World';
}
</script>");
writer.Write("<button onclick='javascript:HelloWorld()'>"
+ "Click Me</button>");
writer.Write("<div id=_msg></div>");
}
}
}
The code block below shows an example of a page using the <%@ Page language="c#" %>
<%@ Register Namespace='Spotu'
TagPrefix='spotu'
Assembly ='helloworld' %>
<html>
<body>
<form runat="'server'">
<spotu:HelloWorld runat="'server'/">
</form>
</body>
</html>
This approach works, and does solve the initial problem of allowing a developer to write a custom control that someone else can use in their page to provide dynamic capabilities without having to post back to the server. However, it is not very elegant, and it does have some shortcomings, most notably we cannot include this control in a page multiple times, doing so would cause multiple divisions to be created with the same id. Even if we do uniquely name the elements in this example it is still inefficient because the JavaScript gets written out with every reference to this control. This can produce a lot of overhead, transmitting the same script down to the client for each instance of the control. We need some way to have a control generate script, but generate it only once, even if multiple instances of the control are used on the same page. Fortunately for us the developers at Microsoft thought of this and provided a way to register a script block to ensure we only write out a section of script once using the using System;
using System.Web;
using System.Web.UI;
namespace Spotu
{
public class HelloWorld : Control
{
protected override void OnInit(EventArgs e)
{
string strCode = @"
<script>
function HelloWorld(id)
{
document.all(id).innerText = 'Hello World';
}
</script>";
Page.RegisterClientScriptBlock("Spotu_HelloWorld",
strCode);
}
protected override void Render(HtmlTextWriter writer)
{
writer.Write("<button onclick='javascript:HelloWorld(\""
+ this.UniqueID + "\")'>"
+ "Click Me</button>");
writer.Write("<div id='" + this.UniqueID
+ "'></div>");
}
}
}
This approach is much better but there is still a problem. If the script that is being register is lengthy, or if there are a lot of calculations, data accesses, etc. in generating our script we will still take a performance hit when the page loads as the control creates this huge block of script that ends up being tossed out because it is already registered. Once a block of script is registered we can test for it using the protected override void OnInit(EventArgs e)
{
if (!Page.IsClientScriptBlockRegistered("Spotu_HelloWorld"))
{
string strCode = @"
<script>
function HelloWorld(id)
{
document.all(id).innerText = 'Hello World';
}
</script>";
Page.RegisterClientScriptBlock("Spotu_HelloWorld",
strCode);
}
}
Using client script generation from custom controls provides a clean encapsulated method of enabling dynamic behavior in web pages while still shielding the page designer from having to know the details of how to produce the desired effect. Developers are now free to concentrate on how to get a control to do what you want it to do without being bogged down with were to put it on the page, or being pestered by the marketing guy to move a control around, add a new one, or take one away. By combining this approach with designer integration controls with dynamic behaviors can easily be customized and reused across multiple pages with little or no developer interaction and with out the pitfalls of server side includes or cut & paste code reuse. CachingSome of you might be inclined to ask: How do I cache this script so it doesn't get downloaded every time? After all, client-side script tends to be fairly static, not needing to be downloaded every time a web page is loaded. There are a couple of options for caching the output of your control. The ASP.NET approach would be to take advantage of output caching. There are a myriad of output caching options, but most of them place the responsibility of setting up that caching on the person doing the presentation by using directives and flags in the .aspx page. Also, caching the entire page may not be the desired effect. Some pages are extremely dynamic. In such cases the ideal would be to just cache the control, or some portion of the control. ASP.NET does have some support for this, but that support is reserved primarily for user controls (.ascx files), which doesn't provide the reuse we are looking for. For custom controls providing generated script we may want to consider using an external script file. As we have already noted, most script does not change often, if at all and can readily be cached on the client. Instead of writing out the script directly from out custom control we can instead place the script in an external script file and simply write out a using System;
using System.Web;
using System.Web.UI;
using System.Collections.Specialized;
namespace Spotu
{
public class Calculator : Control, IPostBackDataHandler
{
const string sc_strStyleClass = "calcButton";
private string _strNumButton;
private string _strOpButton;
private string _strScriptSrc;
private string _strStyleHref;
private string _strSavedValue;
private int _intCalcValue = 0;
// Custom property for explicitly setting the location
// of the script file
public string ScriptSrc
{
get { return _strScriptSrc; }
set { _strScriptSrc = value; }
} // End ScriptSrc
// Custom property for explicitly setting the location
// of the stylesheet file
public string StyleSrc
{
get { return _strScriptSrc; }
set { _strScriptSrc = value; }
} // End StyleSrc
#region IPostBackDataHandler
// LoadPostData gets call when the 'save' button
// rendered by this control is clicked
public virtual bool LoadPostData (
string postDataKey,
NameValueCollection values
)
{
_strSavedValue = "Saved Value: "
+ values[UniqueID + "_display"];
return false;
} // end LoadPostData
// Needed to implement IPostBackDataHandler
public virtual void RaisePostDataChangedEvent()
{
} // End RaisePostDataChangedEvent
#endregion
// Loads the state of the control from the
// viewstate managed by .NET
protected override void LoadViewState (
object savedState
)
{
_strSavedValue = savedState as string;
} // End LoadViewState
// Saves the state of the control
protected override object SaveViewState()
{
return _strSavedValue;
} // End SaveViewState
// Init event handler, called to initialize any state
// in the object before the viewstate is restored.
protected override void OnInit (
EventArgs e
)
{
_strNumButton = string.Format("<button "
+ "onclick='javascript:g_{0}.EnterNumber(this.innerText);'"
+ " class='{1}'>", this.UniqueID, sc_strStyleClass);
_strOpButton = string.Format("<button "
+ "onclick='javascript:g_{0}.OnOperator(this.innerText);' "
+ "class='{1}'>", this.UniqueID, sc_strStyleClass);
if (_strScriptSrc == null)
{
_strScriptSrc = Context.Request.ApplicationPath
+ "/includes/calc.js";
}
if (_strStyleHref == null)
{
_strStyleHref = Context.Request.ApplicationPath
+ "/includes/calcStyle.css";
}
string strScriptBlock = "<script src='"
+ _strScriptSrc
+ "'></script>";
Page.RegisterClientScriptBlock("Spotu_Calculator",
strScriptBlock);
string strStyle = "<link rel='stylesheet' "
+ "type='text/css' href='"
+ _strStyleHref
+ "'></link>";
Page.RegisterClientScriptBlock("Spotu_Calculator_Style",
strStyle);
} // End OnInit
// Load Event Handler. Retrieve the value posted in the
// display field of the calculator so we can keep the
// state of the display regardless of how the form is
// submitted
protected override void OnLoad (
EventArgs e
)
{
if (Page.IsPostBack)
{
_intCalcValue =
Int32.Parse(Context.Request.Form[UniqueID
+ "_display"]);
}
} // End OnLoad
// Render out the control
protected override void Render (
HtmlTextWriter writer
)
{
string strHtml = string.Format(@"
<script> var g_{0} = new Calc('{0}_display'); </script>
<table>
<tr colspan='*'>
<input type='text'
name='{0}_display'
readonly=true
value={4}>
</input>
</tr>
<tr><td>{1}7</button></td>
<td>{1}8</button></td>
<td>{1}9</button></td>
<td>{2}/</button></td>
<td>
<button
class='{3}'
onclick='javascript:g_{0}.OnClear();'>
C
</button>
</td>
</tr>
<tr><td>{1}4</button></td>
<td>{1}5</button></td>
<td>{1}6</button></td>
<td>{2}*</button></td>
</tr>
<tr><td>{1}1</button></td>
<td>{1}2</button></td>
<td>{1}3</button></td>
<td>{2}-</button></td>
</tr>
<tr><td>{1}0</button></td>
<td></td>
<td>{1}.</button></td>
<td>{2}+</button></td>
<td>
<button
class='{3}'
onclick='javascript:g_{0}.OnEqual();'>
=
</button>
</td>
</tr>
</table>", UniqueID,
_strNumButton,
_strOpButton,
sc_strStyleClass,
_intCalcValue);
writer.Write(strHtml);
writer.Write("<INPUT type='submit' name='"
+ this.UniqueID + "' value='Save'></INPUT>");
writer.Write("<H3 id='" + UniqueID + "_savedVal'>"
+ _strSavedValue + "</H3>");
} // End Render
}
}
calculator.aspx<%@ Page %>
<%@ Register Namespace='Spotu'
TagPrefix='spotu'
Assembly ='calc' %>
<html>
<body>
<form runat="'server'">
<spotu:Calculator runat="'server'/">
<hr>
<spotu:Calculator runat="'server'/">
</form>
</body>
</html>
JavaScript source file for calculatorfunction Calc(dispId)
{
this.intCurrentVal = 0;
this.intLastNum = 0;
this._op = "";
this.bEqual = false;
this.displayId = dispId;
this.EnterNumber = function(num)
{
if (this.bEqual)
this.OnClear()
if (this.intLastNum != 0)
this.intLastNum += num;
else
this.intLastNum = num;
document.all(this.displayId).value = this.intLastNum;
}
this.ComputeValue = function()
{
switch (this._op)
{
case '+':
this.intCurrentVal = Number(this.intCurrentVal)
+ Number(this.intLastNum);
break;
case '-':
this.intCurrentVal -= this.intLastNum;
break;
case '*':
this.intCurrentVal *= this.intLastNum;
break;
case '/':
this.intCurrentVal /= this.intLastNum;
break;
default:
this.intCurrentVal = this.intLastNum;
}
document.all(this.displayId).value = this.intCurrentVal;
}
this.OnOperator = function(op)
{
if (!this.bEqual)
this.ComputeValue();
this.bEqual = false;
this.intLastNum = 0;
this._op = op;
}
this.OnEqual = function()
{
this.ComputeValue();
this.bEqual = true;
}
this.OnClear = function()
{
this._op = "";
this.intCurrentVal = 0;
this.intLastNum = 0;
this.bEqual = false;
document.all(this.displayId).value = this.intCurrentVal;
}
}
Style sheet for calculator buttons.calcButton
{
width=25;
}
Examining the codeOne item to note is that the reference to the style-sheet that defines the style for the calculator buttons is located in the There is one block of script that is written out when the control is rendered instead of being included in the .js file. This allows multiple instances of the calculator control to be used in the same page. The The locations of the style sheet and the external script file default to an /includes directory located at the virtual application root. However, there are two custom properties provided that allow the designer to override where those file are located. By using the ConclusionUsing custom controls to generate client-side script can have tremendous benefits. The custom control will look and behave similar to any other control written with ASP.NET making it easy to reuse and shielding the page designer from needing to know the details of how the code works. By using client-side scripting to create the dynamic behaviors you can greatly increase the responsiveness of the individual pages and the overall performance of your web site by significantly decreasing the number of calls that are made to the server. Using external files for your script has both positives and negatives. The pros include taking advantage of browser caching and easy access for customizability. The cons include a more complex deployment both in the production environment as well as the design time environment. DownloadsDownload and unzip the demo project into the root of a virtual application. The calculator.aspx file should be in the root directory of the virtual app, the calc.js and caclStyle.css files in a /includes directory under the virtual app., and the calc.dll in a /bin directory under the virtual application.
|
||||||||||||||||||||||||||||||