![]() |
|
Visual Studio 2005 Web Application Project Option
The Visual Studio 2005 Web Application Project Model is a new web project option for Visual Studio 2005 that
provides the same conceptual web project approach as VS 2003 (a project file based structure where all code in
the project is compiled into a single assembly) but with all the new features of VS 2005 (refactoring,
class diagrams, test development, generics, etc) and ASP.NET 2.0 (master pages, data controls, membership/login,
role management, Web Parts, personalization, site navigation, themes, etc).
ConfigurationManager Gotcha
Quick Gotcha... Up to now, you use the ConfigurationSettings.AppSettings property to read an entry from the AppSettings section of the config file.
In ASP.NET 2.0, this property is marked as obsolete; you must now use the ConfigurationManager class. However, this does not seem to exist in the System.Configuration namespace. Annoying.
Actually, it does exist in this namepsace, but you must add a referene to the System.Configuration assembly first (this assembly is not one of the default references).

Introduction
This article can be considered as a follow up to my former Modeling a Draggable Layer and Loading Dynamic Content to it via XML HTTP article. Have a look at it, if you have not done already so that you can catch up faster.
In this article, we will try to
- Create a cross-browser, draggable DHTML Modal Dialog
- Send an AJAX request using
HTTP-POST.
- Get the response and display it inside the modal dialog
And best of all, we will do all these in less than 20 lines of code, with the help of sardalya API.
Please note that the API enclosed in this article's source archive is a rather simplified version of sardalya. You can visit sardalya's web site for the latest full version of it.
Before starting, you may want to see the final result it in action first.
Creating the View
The HTML of our ModalDialog is fairly simpe: <div id="ModalBG"></div>
<div id="DialogWindow">
<div id="DialogHeader">
<span id="DialogTitle">Title comes here</span>
<img id="DialogActionBtn" src="icn_close.png" alt="close icon" title="" />
</div>
<div id="DialogIcon"><img id="DialogIcon" src="icn_alert.png" alt="alert icon" title="" /></div>
<div id="DialogContent">...</div>
</div>
"ModalBG" is a layer that is placed between the page content and the ModalDialog window so that we prevent accidential clicks on other page elements and in the same time put some visual emphasis on the ModalDialog by fading the page in the background.
The CSS
To make our "DialogWindow" layer resemble an actual modal dialog, we need some CSS tweaks: Master.css
#DialogWindow
{
border: 1px #FFFFFF outset;
width: 550px;
display: none;
background: #FFFFFF;
z-index: 1000;
position: absolute;
top: 0;
left: 0;
}
#DialogContent
{
float: right;
margin-right: 10px;
margin-bottom: 10px;
margin-top: 24px;
width: 450px;
display: block;
font-size: 90%;
}
#DialogIcon
{
padding: 10px;
float: left;
}
#DialogHeader
{
border-bottom: 1px #00449E outset;
background: #00449E;
text-align: right;
}
#DialogTitle
{
float: left;
padding: 8px;
color: #FFFFFF;
}
#DialogActionBtn
{
cursor:pointer;
}
And we need an "Opacity.css" for transparency support: Opacity.css
#ModalBG
{
width:100%;
display:none;
background-color:#333333;
position:absolute;
top:0;
left:0;
height:100%;
z-index:999;
opacity:.40;
filter:alpha(opacity=40)
}
With the help of this CSS, our boxes will pretty much look like a Modal Dialog, except for certain browsers:
Be Kind to Opera
I hear you say "Why am I to be kind to Opera all the time? why is never Opera kind to me?!" and I truly understand you :) But let us be kind to Opera once again:
To make our transparent background work on Opera we need several more CSS tweaks: Master.css
/* transparency support for Opera */
.modalOpera
{
background-image: url("maskBg.png") !important;
}
That's it! Opera does not understand CSS transparency, but it fully supports .png transparency, therefore a transparent mask as a background will make our ModalDialog work equally good in Opera.
There is one remaining issue here, we need to selectively apply this class only if and only if the user agent is Opera. That is other browsers, such as Mozilla, does not require this transparency hack and we should not use "modalOpera" class if our user agent is one of them. We will address this issue in a second.
The Script
First of all we need to prepare the Modal Dialog at page load: window.onload=function()
{
/* Sweep unnecessary empty text nodes. */
DOMManager.sweep();
/*
* Attach supporting css bind required
* classes for transparency support in Opera.
*/
addExtensionsForOpera();
/* Attach opacity css. */
attachOpacityCSS();
/* Adjust height. */
adjustHeight();
/* Create the modal dialog */
g_Modal=new ModalDialog("ModalBG","DialogWindow",
"DialogContent","DialogActionBtn");
/* Bind an event listener to double-click event. */
EventHandler.addEventListener(document,"dblclick",document_dblclick);
};
sweep is a utility method of sardalya's DOMManager object. It removes empty text nodes from the <CODE>DOM structure.
Now let us look at other methods one by one
function addExtensionsForOpera()
{
/* classes for opera */
var ModalBG=new CBObject("ModalBG").getObject();
if(typeof(window.opera)!="undefined")
{
ModalBG.className="modalOpera";
}
}
As seen, we only append "modalOpera" className to the "ModalBG" if the user agent is Opera.
Note that we do not sniff the user agent (navigator.userAgent) but do an "object detection" instead (window.opera).
Browsers love to fool scripts by sending false user agent strings and therefore object detection is the way to go. Although details of it is the subject of an entire article, I can say that browser sniffing is soo '90s. As a rule of thumb always use object detection.
Then comes attach opacity css piece: function attachOpacityCSS()
{
/*
* CSS for opacity support
* Note that this can be directly added to the body.
* If you do not care about blindly adhering to standards
* you can directly include the rules into Master.css
*
* Do I care? Yes and No.(visit http://www.sarmal.com/Exceptions.aspx
* to learn how I feel about it).
*/
var opacityCSS = document.createElement("link");
opacityCSS.type="text/css";
opacityCSS.rel="stylesheet";
opacityCSS.href="Opacity.css";
document.getElementsByTagName("head")[0].appendChild(opacityCSS);
}
And height adjustment: function adjustHeight()
{
/* get the available height of the viewport */
var intWindowHeight=WindowObject.getInnerDimension().getY();
var dynModalBG=new DynamicLayer("ModalBG");
var intModalHeight=dynModalBG.getHeight();
/*
* if modal background's height is less than the viewport's
* available height, increase its height.
*/
if(intModalHeight<intWindowHeight)
{
dynModalBG.setHeight(intWindowHeight);
}
}Then we create the modal dialog in just a single line: g_Modal=new ModalDialog("ModalBG","DialogWindow",
"DialogContent","DialogActionBtn");
"ModalBG" is the ID of transparent background, "DialogWindow" is the ID of modal dialog container, "DialogContent" is where messages is displayed when calling the show method of ModalDialog, and "DialogActionBtn" is the ID of the close button.
And finally we attach a double-click event to the document which will trigger an AJAX action: /* Bind an event listener to double-click event. */
EventHandler.addEventListener(document,"dblclick",document_dblclick);
Now let us have a look at document_dblclick method: function document_dblclick(evt)
{
/* create an AJAX request */
var ajax = _.ajax();
/*
* Note that _.ajax(); is a shorthand notation
* for new XHRequest();
* Visit http://sardalya.pbwiki.com/Shortcuts for details.
*/
/*
* You can add as many fields as you like to the post data.
* Normally the server will use this data to create an
* output that makes sense which may be an XML, a JSON String
* or an HTML String.
*/
ajax.removeAllFields();
ajax.addField("name","John");
ajax.addField("surname","Doe");
/* These events will be fired when server posts back a response. */
ajax.oncomplete=ajax_complete;
ajax.onerror=ajax_error;
/* Set a default waiting message. */
g_Modal.show("Fetching data... Please wait...");
/*
* Disable close action if you want to force the user
* to wait for the outcome of the AJAX request.
* Although it is generally not recommended
* this may be necessary at certain times.
*/
g_Modal.disableClose();
/* Post data to the server. */
ajax.get("externalScript.html");
/* Stop event propagation. */
new EventObject(evt).cancelDefaultAction();
}
The comments should be self-explanatory. And finally the two methods that are triggered after the server's post back. /* Triggered when a successful AJAX response comes from the server.*/
function ajax_complete(strResponseText,objResponseXML)
{
g_Modal.show(strResponseText);
/* Re-activate close button. */
g_Modal.enableClose();
}
/* Triggered when server generates an error. */
function ajax_error(intStatus,strStatusText)
{
g_Modal.show("Error code: ["+ intStatus+ "] error message: [" +
strStatusText + "].");
/* Re-activate close button. */
g_Modal.enableClose();
}
That's it!
What about those nasty SELECTs ?
ModalDialog object internally handles it, by replacing them with SPAN elements with class "modalWrap" whenever ModalDialog opens. This sorts out the well known "SELECTs bleed through my top layer" issue.
Here is the CSS of it for the sake of completeness: .modalWrap
{
border: 2px #ffffcc inset;
background:#ffffcc;
margin:5px;
}
You can add as many rules as you like to it. The more the SPAN resembles a SELECT element, the better (you can apply width and line-height, set display to inline-table... etc, I did not change it too much to keep it simple)
For those who wonder how the replacement of those SPANs and SELECTs are done, the corresponding private method is given below. You can observe the source code of the article's zip file for more details.
In the former version, we were simply hiding the SELECTs by setting their CSS visibility to hidden. Having tested it in real-life scenarios, I saw that the "all of a sudden" dissappearance of SELECTs was annoying to some of the users.
imho, transforming the SELECTs is much better than hiding them completely.
... And no, I do not want to use IFRAMEs :)
Here follows the code: _this._replaceCombos=function(blnReplaceBack)
{
var arSelect = document.getElementsByTagName("select");
var len=arSelect.length;
var objSel=null;
var strNodeValue="";
var objSpan=null;
var o=null;
if(!blnReplaceBack)
{
blnReplaceBack=false;
}
for(var i=0;i<len;i++)
{
objSel=arSelect[i];
strNodeValue=objSel.childNodes[objSel.selectedIndex
].childNodes[0].nodeValue;
objSpan=new CBObject(objSel.id+"_ModalWrap");
if(objSpan.exists())
{
o=objSpan.getObject();
o.parentNode.removeChild(o);
}
objSpan=document.createElement("span");
objSpan.id=objSel.id+"_ModalWrap";
objSpan.appendChild(document.createTextNode(strNodeValue));
objSpan.className="modalWrap";
objSel.parentNode.insertBefore(objSpan,objSel);
if(blnReplaceBack)
{
new DynamicLayer(objSpan).collapse();
new DynamicLayer(objSel).expandInline();
}
else
{
new DynamicLayer(objSpan).expandInline();
new DynamicLayer(objSel).collapse();
}
}
};
ConclusionIn conclusion, we modeled and created a draggable DHTML Modal dialog, established an AJAX connection to an external script; we did some cross-browser tweaks to make our application work on as many browsers as possible, and we did some OO coding.
And as always, Happy coding!
History
- 2006-06-02 : Article created.
About volkan.ozcelik

| Volkan is a java enterprise architect who left his full-time senior developer position to venture his ideas and dreams. He codes C# as a hobby, trying to combine the .Net concept with his Java and J2EE know-how. He also works as a freelance web application developer/designer.
Volkan is especially interested in database oriented content management systems, web design and development, web standards, usability and accessibility.
He was born on May '79. He has graduated from one of the most reputable universities of his country (i.e. Bogazici University) in 2003 as a Communication Engineer, and is currently about to finish his MBA in a second university.Click here to view volkan.ozcelik's online profile. |
Discussions and Feedback 5 comments have been posted for this article. Visit http://www.codeproject.com/useritems/ModalDialogV2.asp to post and view comments on this article.
Flicker Fix
Summary
This page tells you how to use
ASP.NET
to eliminate the
flicker
sometimes seen in
menus
implemented with cascading style sheets (CSS).
Background
For as long as authors have been experimenting with
pure CSS menus
there have been reports of flicker problems.
The emerging view is that the problem is ultimately
caused by the client browser failing to cache images used in hover styles (CSS).
One way to eliminate this flicker is to specially
configure
your web server (e.g., IIS). Then, when these sorts of rollover images are requested, your web server will
add specific caching requirements to the HTTP headers. These tell the browser to cache the images
regardless of the default caching policy it normally uses.
Unfortunately, this sort of solution is only practical when you have access to your web server's
configuration. Most web hosting services do not give you access to the IIS management console.
Even when IIS can be directly accessed, configuring caching policies is scary and unfamiliar to
many authors.
Happily, there is a much simpler solution if you are developing a web site with
ASP.NET.
It's is easy to implement and doesn't involve configuring your web server.
Solution
To implement this solution you will:
-
Add an HTTP
handler
to the root of your web site.
-
Tweak your CSS styles to refer to this HTTP
handler
instead of the image files that are flickering.
-
Add a few elements to the
appSettings
section of your web site's web.config file.
After adding the HTTP handler
PersistantImage.ashx
to the root of your web app, locate styles that refer to background images. Imagine
your web site has a subfolder just below the root where you put all your style sheets. It might
have a class that defines the background image used in a menu:
| CSS |
1
2
3
4
5
|
.someClassName
{
background:#AABBCC url(someFlickeringImage.gif) repeat-x;
}
|
If you find that the menu flickers, you could modify this style to be:
| CSS |
1
2
3
4
5
|
.someClassName
{
background:#AABBCC url(../PersistantImage.ashx?key=SomeFlickeringImage) repeat-x;
}
|
Then you would add an entry defining the query string key, SomeFlickeringImage, in the
appSettings
section of the web.config in the root of your web app:
| CSS |
1
2
3
4
5
6
7
8
9
10
|
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="SomeFlickeringImage" value="~/myStyles/someFlickeringImage.gif"/>
</appSettings>
<system.web>
</system.web>
</configuration>
|
This forces the browser to request the background image via the HTTP handler,
PersistantImage.ashx,
rather than requesting that image file directly. You provide
PersistantImage.ashx
with a nickname for the image via the key in the query string.
PersistantImage.ashx
looks up that nickname in the
appSettings
to find the true path to the image to send down to the browser with special caching instructions.
Because the browser now caches the image it doesn't re-request it from the server again and
again, which is what causes the flicker effect.
Using nicknames for images enhances your web site's security. Your web.config contains a
white list
of images that you explicitly allow to be exposed. No one can invoke
PersistantImage.ashx
directly to gain access to other files on your web site.
As an added security measure, the HTTP hander
PersistantImage.ashx
will only respond to requests for files with a web image extension: jpg, jpeg, gif or png.
If you are using the
CSS Friendly ASP.NET Control Adapters
you will find it helpful to modify the
style sheet
that defines the background images used for menus.
One of the styles to change is:
| CSS |
1
2
3
4
5
|
.PrettyMenu ul.AspNet-Menu li
{
background:#4682B4 url(bg_nav.gif) repeat-x;
}
|
It should become:
| CSS |
1
2
3
4
5
|
.PrettyMenu ul.AspNet-Menu li
{
background:#4682B4 url(../../PersistantImage.ashx?key=BasicBgNav) repeat-x;
}
|
Then, in the
web.config
you would modify the <appSettings> to be:
| CSS |
1
2
3
4
5
6
7
8
9
10
|
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="BasicBgNav" value="~/App_Themes/Basic/bg_nav.gif"/>
</appSettings>
<system.web>
</system.web>
</configuration>
|
You may find that you need to use
PersistantImage.ashx
in several different styles in order to fully eliminate the menu flicker. In practise it is probably easiest
to adopt a simple policy of using
PersistantImage.ashx
for all of the menu-related styles that refer to background images.
Introduction
One annoying task that most every developer has had to face in the past is form validation. Since forms are an integral
part of dynamic, data-driven Web sites, it is essential that a user's query into a form fit the specified guidelines.
For example, in a website like eBay where users are entering shipping and billing information, it is vital that the
user enter credit card numbers, zip codes, email addresses, and other information in an acceptible format.
Prior to ASP.NET, form validation was a frustrating and tedious process. The page developer was responsible for creating
the client-side and server-side logic for each and every validation check. ASP.NET version 1.0 helped end this tedium with
a host of validation Web controls, making validation was as simple as adding the appropriate validation control to
the web page and setting a few properties.
While validation controls in ASP.NET version 1.x greatly improved the developer's experience with adding page validation,
they still lacked some important functionality. For one, the client-side script emitted by the validation controls used
the JavaScript object document.all, which is an object that is not in the JavaScript standard and only
supported by Microsoft's Internet Explorer browser. This meant that browsers other than Internet Explorer could not
enjoy the client-side benefits of the validation controls. Additionally, the validation controls could not be logically
grouped, which proved irksome with pages divided up into different sections.
ASP.NET 2.0 doesn't add any new validation controls, but it does fix the validation control shortcomings in version 1.x,
along with adding additional features. In this article we'll dissect the validation controls in version 2.0. Read on to learn
more!
(This article does not cover the basics of ASP.NET's validation controls; for more information on that refer to
Form Validation with ASP.NET - It Doesn't Get Any Easier!
and User Input
Validation in ASP.NET.)
Validation Groups
One of the annoyances with ASP.NET version 1.x validation controls was that they all were checked against when a postback
occurred. Granted, you could suppress all validators from firing when a particular Button control was clicked
using the CausesValidation property, but oftentimes a finer degree of control is needed. Oftentimes
Web Forms have multiple "sections," where each section has a set of input controls and a Button that, when clicked, performs some
action based on that section's controls. Ideally, each section would have section-dependent validation controls, but with
ASP.NET version 1.x's limitations, developers were either forced to forgo the validation controls altogether (or
forgo them in all but one section) or turn to a third-party validation solution, such as Peter
Blum's Professional Validation And More (VAM). (Read a short review
of mine on VAM and Peter's other product, Peter's Date Package.)
Thankfully with ASP.NET version 2.0 validation controls can be grouped. All validation controls now contain the
ValidationGroup property. The default value of this property is the empty string, which indicates that
the validation control is not part of a group. But those controls that have the same ValidationGroup values
are considered grouped. The Button, LinkButton, DropDownList, and other controls that are commonly used to submit a Web Form also contain
the ValidationGroup property. When a Button with a ValidationGroup property value is clicked
all of the validation controls on the page with the same ValidationGroup property value are fired.
On postback, the Page.IsValid property reflects the validity of all of the validation controls that have been
validated. By default, this is the set of validation controls whose ValidationGroup property value equals the
ValidationGroup property value of the control that instigated the postback. However, additional validation
controls can be programmatically checked by calling the validation control's Validate() method.
In ASP.NET version 1.x the Page class had a Validate() method that could be called programmatically
which would check all validator controls in the page; furthermore, there was a Validators property that provided
a collection of all validation controls. With version 2.0, the Page class's Validate() method
has been enhanced and a new GetValidators() method has been added. (The Validation property remains
for backwards compatibility, and still returns all validation controls in the page.)
With 2.0 the Validate() method can be called without passing in any parameters, just like in version 1.x. Doing
so validates against all of the validation controls in the page. The Validate() method has an overload
that takes in a string parameter and validates against those controls whose ValidationGroup property equals
the passed-in string to Validate(). When a Button control causes a postback and its CausesValidation
property is True, internally the Page.Validate(ValidationGroupPropertyValue) method gets called.
As in version 1.x, the Page.Validators property returns a collection of all validation controls. The
GetValidators(ValidationGroup) method returns a collection of validation controls that belong to the
specified validation group.
The behavior of these properties and methods is illustrated in the ValidationMethod.aspx demo available for
download at the end of this article...
Focusing On Invalid Form Fields
One of the nifty features of ASP.NET version 2.0 is that all server controls now have a Focus() method.
Calling this method injects a bit of client-side JavaScript to set focus to a particular control on page load. In addition
to this nifty little method, the validation controls offer similar functionality through their SetFocusOnError
property. This property, if set to True, causes the Web control the validation control is operating on to receive focus
if the validation control is invalid. This is a nice little usability touch that your users will likely appreciate.
Client-Side Validation Support in Non-Microsoft Browsers
One of the unfortunately aspects of ASP.NET version 1.x's validation controls was that they used a bit of proprietary
JavaScript to provide client-side validation functionality, making the validation controls inoperable in non-Microsoft
browsers. This topic was discussed in detail in a previous article of mine, Client-Side Validation in Downlevel Browsers.
The workaround for version 1.x was to either:
- Build up your own validation control library from scratch,
- Use a free or third-party validation package that has done the dirty work for you. (The two options I mentioned in
my article were Paul Glavich's free DomValidators
controls or Peter Blum's Professional Validation And More (mentioned earlier in this article).
The client-side script used by the validation controls in 2.0 still includes calls to document.all, but also
includes equivalent checks using the standard-compliant document.getElementById(id). The end result
is that the client-side validation features of the validation controls now work with any browser that supports JavaScript
1.2 or higher. This includes Internet Explorer's two largest competitors: FireFox
and Opera.
Conclusion
The validation controls in ASP.NET version 2.0 have been improved from version 1.x. While there are no new validation controls,
the existing controls have been enhanced to include support for validation groups and client-side support for browsers other
than just Internet Explorer. While these enhancements do make ASP.NET's built-in validation controls much more useful in
real-world settings, they still lack many of the more advanced features found in third-party validation packages. For most
developers, however, these new additions to the validation controls will make the built-in controls sufficient for real-world use.
Happy Programming!
By Scott Mitchell
Attachments
Download the code examples (ZIP format)
|
Building and using a 3-tiered data architecture with ASP.NET 2.0
Welcome to a series of tutorials that will explore techniques for implementing these common data access patterns in ASP.NET 2.0. These tutorials are geared to be concise and provide step-by-step instructions with plenty of screen shots to walk you through the process visually. Each tutorial is available in C# and Visual Basic versions and includes a download of the complete code used.
What's the deal with Databinder.Eval and Container.DataItem?
The databinding expression <%# some expression %> is evaluated in the language of the page (VB, C#, etc.) This can have a big impact on the current syntax, so be very careful when you are looking at docs for the language you are using. Container.DataItem is a runtime alias for the DataItem for this specific item in the bound list. For a grid which displays 10 rows of data, this is one row from the datasource. The actual type of DataItem is determined by the type of the datasource. For example, if the datasource is a Dataview, the type of DataItem is DataRowView. If the type of the datasource is an array of strings, the type of DataItem is String. If the datasource is a collection of strongly-typed objects (for example "Employees" objects), the type of DataItem is Employees. Each of these cases requires a slightly different databinding expression, with further differences between VB and C#. In every case, you want the databinding expression to produce a string that can be displayed in the page. Here are some examples: Array of Strings: VB/C# <%# Container.DataItem %> Field from DataView: VB <%# Container.DataItem("EmployeeName") %> C# <%# ((DataRowView)Container.DataItem)["EmployeeName"] %> Property from a collection of objects: VB <%# Container.DataItem.CustomerName %> C# <%# ((Customer)Container.DataItem).CustomerName %> Non-String property from a collection of objects: VB <%# CStr(Container.DataItem.CustomerID) %> C# <%# ((Customer)Container.DataItem).CustomerID.ToString() %> As you can see the syntax is tricky, especially for C#, which requires explicit casting. So we've provided a DataBinder.Eval() helper method that figures out the syntax for you and formats the result as a string. It's really convenient, with a couple of caveats: it's late bound (uses reflection at runtime to figure out the data types), and it only supports basic data types in the fields: string, int, datetime, etc. DataBinder.Eval takes 2 or 3 arguments. The first arg is the data object to bind to. In the case of DataGrid, DataList and Repeater, Container.DataItem is the single row. The second arg the string name of the field from the data object you which to display. DataBinder.Eval uses these two pieces of information to work out the rest of the expression syntax. An optional third argument is a .NET formatting string. DataBinder.Eval will handle a single replacement for {0} only. So the example below: <a href='<%# "default.aspx?CategoryId=" + Cstr(Databinder.Eval(Container.DataItem, "ID"))%>'> could be simplified to: <a href='<%# Databinder.Eval(Container.DataItem,"ID","default.aspx?CategoryId={0}" ) %>'> Wrapping DataBinder.Eval in CStr() is unnecessary as the compiler will wrap the statement with a Convert.ToString like this:
control1.SetDataBoundString(0, Convert.ToString(DataBinder.Eval(item1.DataItem, "ID"))); Best of all, the Databinder.Eval syntax is the same for VB and C#.
Small point...what Databinding syntax does everyone use?
posted on Tuesday, December 09, 2003 4:25 PM
UPDATE: OK, before anyone points out the obvious (actually after many people have!) - I am aware that DataBinder.Eval promotes code reuse to some degree; you still have to know what your fields are called of course! My point in this post was that I rarely saw the alternative syntax used and that in my experience I have found almost no drawbacks in using the 'Strong' syntax. This was also a trawl for comments to get the opinions of other developers...with that in mind, let the comments recommence...
Incidentally, in the post below, if anything looks screwy, sorry, having problems with the stupid Rich Text editor again!
Reason I ask is that most of the examples I see all over the web use the: <%# databinder.eval(container.dataitem,="" "price","{0:c}"))%="">format... now the reason for this seems to be that this is allegedly 'easier' to use and read. To be honest, I have never used that format, I tend to use this: <%# string.format("{0:c}",((dbdatarecord)container.dataitem)["price"]%=""> format now, I understand that this looks more complex but as it points out here, "It is important to note that DataBinder.Eval can carry a noticeable performance penalty over the standard data binding syntax because it uses late-bound reflection. Use DataBinder.Eval judiciously, especially when string formatting is not required. ". Now, in my tests (I benchmark everything...obsessive? Quite possibly :-)), the Eval syntax is a LOT slower, like up to 20% slower - so, what is the actual advantage of using it? OK, my opinion on why you wouldn't use the 'explicit casting' syntax (i.e., the one I use):
- You have to import the correct namespace depending on whether you use a DataSet or a SqlDataReader (or any IDataReader) (System.Data and System.Data.Common respectively).
- You have to cast to the 'correct' object so for DataSet it's DataRowView and for SqlDataReader (or any IDataReader) it's DbDataRecord...for any other objects, it's the correct object obviously :-)
- It's more 'wordy'
- Very few examples show this syntax...
Now, these are pretty good reasons to use the DataBinder.Eval syntax, but to be honest, I just don't find them compelling...so here's some reasons to use the 'explicit casting' one:
- You have to import the correct namespace depending on whether you use a DataSet or a SqlDataReader (or any IDataReader) (System.Data and System.Data.Common respectively) - same argument...but I have to say, I like this! It's more explicit about what namespace you're actually using and forces you to know this stuff!
- You have to cast to the 'correct' object so for DataSet it's DataRowView and for SqlDataReader (or any IDataReader) it's DbDataRecord...for any other objects, it's the correct object obviously - again, the same as above...knowing what type of object you're using is, in my opinion, a good thing, it gives a whole lot of control in how you represent the object; for example you get access to all the nice DateTime formatters in the .ToString() method - same as you would in code!
- It's faster - now this is not a 'be all and end all' argument, but it is important, especially in high-hit sites or when you have to bind really long lists of data - faster = less time for which a thread is used!
Makes you look like a smart ass - OK, just me then :-)
Anyway, I really am interested in hearing people's opinions on this one...which do you prefer, and why?
ASP.NET 2.0 Localization (Video, Whitepaper, and Database Provider Support)
ASP.NET 2.0 and VS 2005 add a bunch of features that make localizing ASP.NET applications much easier.
To learn more about how the localization features work, I'd recommend first checking out this excellent 13 minute video in the ASP.NET 2.0 "How Do I?" video series. In it Scott Stansfield walks-through how to build and localize an ASP.NET application from scratch, and how to support both dynamically picking the language used based on the incoming user-agent string of the client, as well as allowing a user to explictly choose their language preference from a dropdownlist. I think you will walk away being surprised at how easy it is.
Wei-Meng Lee has also written a great walkthrough article on ASP.NET 2.0 localization that you can read for free on the O'Reilly network here. For more detailed information on ASP.NET 2.0 localization, you can also then read Michele Bustamente's excellent ASP.NET 2.0 Localization article on MSDN. Bilal Haidar also has a great artlce on ASPAlliance here.
The articles and videos above use XML .resx files to store localized resource strings. These can either by compiled into binaries or deployed as XML source with an ASP.NET 2.0 application. Jeff Modzel recently published an article that shows how to build a custom Resource Provider that stores the resource strings in a database instead. You can read about how he built this as well download his sample code here.
Hope this helps,
Scott
How to: Use Resources to Set Property Values in Web Server Controls
In an ASP.NET page, you can use the following methods to retrieve values from resource files that are compiled by ASP.NET and managed by the .NET Framework resource manager: -
Implicit localization, wherein ASP.NET fills property values from the resource manager based on matching the resource key to the properties of the control. -
Explicit localization, by creating an expression that retrieves a specific resource from the .NET Framework resource manager. -
Programmatically, by retrieving resource values in code. For detailed information, see How to: Retrieve Resource Values Programmatically. To use implicit localization-
Make sure that you have local resource files (.resx files) that meet the following criteria: -
They are in an App_LocalResources folder. -
The base name matches the page name. For example, if you are working with the page named Default.aspx, the resource files are named Default.aspx.resx (for the default resources), Default.aspx.es.resx, Default.aspx.es-mx.resx, and so on. -
The resources in the file use the naming convention resourcekey."property". For example, key name Button1."Text". -
In the control markup, add an implicit localization attribute. For example: <asp:Button ID="Button1" runat="server" Text="DefaultText"
meta:resourcekey="Button1" /> All resource files are compiled and the .NET Framework resource manager is used by ASP.NET at run time to retrieve the culture-appropriate resource for each resource in the default resource file. For each resource, ASP.NET looks for a corresponding resourcekey."property" combination (in the preceding example, resourcekey="Button1") in the page, and then substitutes the resource for the retrieved value.
To use explicit localization-
In the markup for a control, use a resource expression to set the value for each property that you want to replace with a resource. The syntax is as follows: <%$ Resources:Class, ResourceKey %> Use these values: -
Class is the resource file class, which is based on the .resx file name. A resource file named WebResources.resx uses the class name WebResources. All culture variant resource files use the same class name as the culture neutral resource file. If you want to obtain a resource from the local resource file that is associated with a page, Class is optional. -
ResourceKey is the name of a resource in the specified class. For example, a Button control that is configured to set the Text property from a global resource file might look similar to the following code example: <asp:Button ID="Button1" runat="server"
Text="<%$ Resources:WebResources, Button1Caption %>" />
Example The following code examples show implicit and explicit localization. The first code example shows how to use implicit localization, in which each control is marked with the meta attribute. At run time, ASP.NET matches resources to control properties. The second code example shows a page that uses resource expressions (explicit localization) to set the values of the Text property of various controls and of the ImageUrl property of an Image control. In the first example, change <default> in the first line to a valid culture name. For a list of culture names, see the "Culture Names and Identifiers" table in CultureInfo. Security Note |
|---|
| This example has a text box that accepts user input, which is a potential security threat. By default, ASP.NET Web pages validate that user input does not include script or HTML elements. For more information, see Script Exploits Overview. |
<%@ Page Language="VB" Culture="auto:<default>"
meta:resourcekey="PageResource1" UICulture="auto" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML
1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html >
<head runat="server">
<title>Implicit Localization Sample</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<h1>
<asp:Localize runat=server
ID="WelcomeMessage"
Text="Welcome!" meta:resourcekey="Literal1" />
</h1>
<p>
<asp:Image runat="server" ID="Logo" ImageUrl=""
meta:resourcekey="Logo" />
</p>
<br />
<br />
<asp:Localize runat="server"
ID="NameCaption"
Text="Name: " meta:resourcekey="Literal2" />
<asp:TextBox runat="server" ID="TextBox1"
meta:resourcekey="TextBox1" />
<br />
<br />
<asp:Button ID="Button1" runat="server" Text="Submit"
meta:resourcekey="Button1"/><br />
</div>
</form>
</body>
</html>
<%@ Page Language="C#" Culture"auto:<default>"
meta:resourcekey="PageResource1" UICulture="auto" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML
1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html >
<head runat="server">
<title>Implicit Localization Sample</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<h1>
<asp:Localize runat=server
ID="WelcomeMessage"
Text="Welcome!" meta:resourcekey="Literal1" />
</h1>
<p>
<asp:Image runat="server" ID="Logo" ImageUrl=""
meta:resourcekey="Logo" />
</p>
<br />
<br />
<asp:Localize runat="server"
ID="NameCaption"
Text="Name: " meta:resourcekey="Literal2" />
<asp:TextBox runat="server" ID="TextBox1"
meta:resourcekey="TextBox1" />
<br />
<br />
<asp:Button ID="Button1" runat="server" Text="Submit"
meta:resourcekey="Button1"/><br />
</div>
</form>
</body>
</html>
<%@Page Language="VB"
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML
1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html >
<head runat="server">
<title>Explicit Localization Sample</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<h1>
<asp:localize runat="server"
Text="<%$ Resources:WebResources, WelcomeMessage %>" />
</h1>
<asp:Image runat="server" id="Logo"
ImageUrl="<%$ Resources:WebResources, LogoUrl %>" />
<p>
<asp:Localize runat="server"
Text="<%$ Resources:WebResources, EnterNameCaption %>" />
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<br />
<br />
<asp:Button ID="Button1" runat="server"
Text="<%$ Resources:WebResources, SubmitButtonCaption %>" />
</p>
</div>
</form>
</body>
</html>
<%@ Page Language="C#" UICulture="auto" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML
1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html >
<head runat="server">
<title>Explicit Localization Sample</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<h1>
<asp:localize runat="server"
Text="<%$ Resources:WebResources, WelcomeMessage %>" />
</h1>
<asp:Image runat="server" id="Logo"
ImageUrl="<%$ Resources:WebResources, LogoUrl %>" />
<p>
<asp:Localize runat="server"
Text="<%$ Resources:WebResources, EnterNameCaption %>" />
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<br />
<br />
<asp:Button ID="Button1" runat="server"
Text="<%$ Resources:WebResources, SubmitButtonCaption %>" />
</p>
</div>
</form>
</body>
</html>
In the @ Page directive, the UICulture attribute is set to auto to specify that the page should set the current UI culture to the information that is passed by the browser. In Web pages, resources, such as graphics, are included by using a URL to reference an external file. Typically, you localize graphics and other resources by creating different versions of the graphics. You can then set the URL of a control using a resource expression that specifies a string resource to the appropriate path. See Also
WebResource ASP.NET 2.0 explained
Introduction
This code drop is part of Redux series. This article started out to be about a dropdown date/time ASP.NET 2.0 server control. So I built the control and all its ancillary JavaScript, along with the images, and then everything was put into a zip file for uploading to the CodeProject. The installation instructions for the control went something like this:
- Unzip the contents to a folder.
- Copy the DLL to the Bin folder of your new project.
- Copy the JavaScript file.
- Copy the CSS file.
- Copy the images.
Hey, wait a minute, this is really lame, why all the extra files, why not embed them into the DLL as resources. That was the beginning of three days of hell trying to understand and work with WebResources. I have boiled it down to about twenty lines of working code.
Nothing else on the net gives you an actual working sample and a lot of the information is simply wrong (possibly because it was based on beta versions). Well, this code compiles and runs on the release version of .NET 2.0.
Code snippets
Create a project which looks like this (you can get the control from the download and then just add an ASP.NET project to the solution):

Highlight the three files in MyResources, and in the Properties window, set Build Action to Embedded Resource:

The source for the control is as follows: namespace MyWebResourceProj
{
[ToolboxData("<{0}:MyWebResource runat=server></{0}:MyWebResource>")]
public class MyWebResource : System.Web.UI.WebControls.TextBox
{
protected override void RenderContents(HtmlTextWriter output)
{
output.Write(Text);
}
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
this.Page.ClientScript.RegisterClientScriptInclude(
this.GetType(), "Test",
Page.ClientScript.GetWebResourceUrl(this.GetType(),
"MyWebResourceProj.MyResources.Test.js"));
// create the style sheet control
// and put it in the document header
string csslink = "<link href='" +
Page.ClientScript.GetWebResourceUrl(this.GetType(),
"MyWebResourceProj.MyResources.Test.css")
+ "' rel='stylesheet' type='text/css' />";
LiteralControl include = new LiteralControl(csslink);
this.Page.Header.Controls.Add(include);
}
}
This is not a discussion on server controls, so let's focus on the parameters of GetWebResourceUrl:
this.GetType() - mandatory, just do it.
"MyWebResourceProj.MyResources.Test.js" - this is the most misunderstood parameter and the source of just about all errors. It is composed of [Assembly of this project].[Folder containing resource].[Filename of resource].
No matter what you read anywhere else, including Microsoft's official documentation (which is wrong), if you don't do it this way, you will fail (I know this from personal experience).
Now on to the AssemblyInfo.cs: [assembly: System.Web.UI.WebResource(
"MyWebResourceProj.MyResources.Test.css", "text/css")]
[assembly: System.Web.UI.WebResource(
"MyWebResourceProj.MyResources.Test.js",
"text/javascript", PerformSubstitution = true)]
[assembly: System.Web.UI.WebResource(
"MyWebResourceProj.MyResources.Test.gif", "image/gif")]
You need to specify the resource name exactly as you did in your program code. You need to specify the mime-type. If the file contains JavaScript, you may want to have ASP.NET perform text substitution. For example, you may have a line in JavaScript like: document.write("<img src='Test.gif'>");
When you embed your images in a resource, you no longer know the name of it as it appears in the resource. To fix this, code as follows: document.write("<img src='<%=WebResource("MyWebResourceProj" +
".MyResources.Test.gif")%>'>");
All the "official documentation" and all the articles on the web say that you can leave [AssemblyName].[FolderName] out. But they are dead wrong!
Note: if you are using this control as the base class of another, see yc4king's note below.
Update
KonstantinG has pointed out an error which only becomes apparent when your namespace is not the same as your assembly name. Previously, I had indicated that the name of the resource is: [Namespace of this project].[Folder containing resource].[Filename of resource], but really it is [Assembly of this project].[Folder containing resource].[Filename of resource]. As always, the best way to find information on this is by using Lutz Roeder's .NET Reflector available at aisto.com.
SmashGrab / Redux series
I have recently started two series of articles here at CodeProject.
Smash and Grab is intended as a series of short articles on one specific code technique. Redux is intended as a series of longer articles which attempts to reduce a complicated topic (like WebResource) into its basic constituent parts and show that once you have all the information, it isn't really that hard. To find the Smash and Grab articles, search for the keyword SmashGrab. To find the Redux articles, search for the keyword Redux. I welcome any contributions to either series, but please follow the guidelines when submitting articles to either.
Conclusion
So there you have it, everything you need to know to use WebResources. This article documented how to place CSS files in the <head> section, extract JavaScript files, dynamically change the contents of a JavaScript file.
Gary Dryden
| Click here to view Gary Dryden's online profile. |
A Resource Server Handler Class For Custom Controls
Introduction
When developing custom controls for ASP.NET, it may be necessary to create some client-side script that is used to interact with the custom control. There may also be image files that are used for certain elements of the control, such as buttons or style sheets that set the look of the control. A decision has to be made about how to deploy these resources with the custom control assembly.
The script can be built up using a StringBuilder or a static string, and inserted directly into the page using RegisterClientScriptBlock. This is okay for small scripts, but is unwieldy for much larger scripts, especially if they are complex enough to need debugging to work out problems during development. Scripts can also be embedded as resources in the assembly, retrieved at runtime, and again inserted into the page using RegisterClientScriptBlock. This is better than the StringBuilder approach for large scripts, but it is still inserted into the page and is rendered in its entirety, every time the page is requested. The more script code you have or the more controls on the page that render supporting script code, the larger the page gets. The script also cannot be cached on the client to save time on subsequent requests.
The scripts can be distributed as separate files along with the assembly. This solves the problem of the code being rendered in the page on each request and it can be cached on the client. However, it may complicate distribution of the custom control. It is no longer a simple XCOPY deployment as now scripts have to be installed along with the assembly. A number of factors such as whether it is a production or development server, whether or not the application is using SSL, and how the end-user's applications are set up, can affect where the scripts go, and you may end up with multiple copies in several locations. Versioning issues may also come into play if the scripts are modified in future releases of the control.
To solve these issues, I developed a class that implements the System.Web.IHttpHandler interface and acts as a resource server of sorts. The idea was inspired by examples I saw that showed how to render dynamic images using an ASPX page as the source of the image tag. The concept is basically the same for the resource server handler. You embed the resources in the control assembly and then add a simple class to your custom control assembly that implements the IHttpHandler interface. A section is added to the application's Web.Config file to direct resource requests to the handler class. The handler class uses parameters in the query string to determine the content type and sends back the appropriate resource such as a script, an image, a style sheet, or any other type of data that you need.
By having the resources embedded in the assembly and serving them as needed, there is as little code rendered in the page as possible by the controls. The resource handler responses can also be cached on the client, so performance can be improved as less information is sent to the client in subsequent page requests that utilize the same resources. This is most beneficial for users with slow dial-up connections, especially on forms that utilize controls with auto-postback enabled. The resources do not have to be deployed separately along with the assembly either, thus solving the problem of where to install the resources, as well as any issues involving versioning. We are back to a simple XCOPY deployment again.
The use of a resource server handler is not restricted to custom controls. It also adds the ability to do such things as request dynamic content, such as XML, using client-side script. For example, a request could be made to retrieve the results of a database query as XML using client-side script. The results could be used to populate a control or a popup window with information when it is needed rather than sending everything along with the page when first loaded. The following sections describe how to setup and utilize the resource server handler class.
A word about ASP.NET 2.0
The following will allow you to embed and serve resources in ASP.NET 1.1 applications as well as ASP.NET 2.0 applications. However, with ASP.NET 2.0 the ability to serve embedded web resources is a built-in feature and is simpler to implement. It makes use of embedded resources as described in here but utilizes attributes to define them along with a built-in Page.ClientScript method to render a link to them to the client. It does not require you to write a handler and does not require any entries in the Web.config file to define the handler or to allow anonymous access to them. Gary Dryden has already written a good article on how this works so I will just refer you to it rather than repeating what it has to say (WebResource ASP.NET 2.0 Explained[^]).
Add resources to your project
To keep things organized, store the resources in separate folders grouped by type (Scripts for script files, Images for image files, etc.). To create a new folder in the project, right click on the project name, select Add..., select New Folder, and enter the folder name. Add a new resource to the folder by right clicking on it, and selecting Add... and then Add New Item... to create a new item, or Add Existing Item... if you copied an existing file to the new folder. Once added to the project folder, right click on the file and select Properties. Change the Build Action property from Content to Embedded Resource. This step is most important as it indicates that you want the file to be embedded as a resource in the compiled assembly.
Add the ResSrvHandler class to your project
Add the ResSrvHandler.cs source file to your control's project and modify it as follows. TODO: comments have been added to help you find the sections that need modification.
Modify the namespace so that it matches the one for your custom control: // TODO: Change the namespace to match your control's namespace.
namespace ResServerTest.Web.Controls
{
Modify the cResSrvHandlerPageName constant so that it matches the name you will use in the application's Web.Config file to direct resource requests to the class. I have chosen to use the custom control namespace with an .aspx extension. This keeps it unique and guarantees that it won't conflict with something in the end-user's application: // TODO: Modify this constant to name the ASPX page that will be
// referenced in the application Web.Config file to invoke this
// handler class.
/// <summary>
/// The ASPX page name that will cause requests to get routed
/// to this handler.
/// </summary>
public const string cResSrvHandlerPageName =
"ResServerTest.Web.Controls.aspx";
Modify the cImageResPath and cScriptResPath constants to point to your script and image paths. Add additional constants for other resource type paths as needed. The names of the embedded resources in the assembly are created by using the default namespace of the project plus the folder path to the resource. The default namespace is usually the same as the assembly name but you can modify it by right clicking on the project name, selecting Properties, and changing the Default Namespace option in the General section of the Common Properties entry. For the demo, the default namespace has been changed to match the namespace of the control, ResServerTest.Web.Controls, and the resource paths are Images and Scripts. As such, the constants are defined as shown below. The trailing "." should also be included. The resource name will be appended to the appropriate constant when loading it from the assembly.
Note that if you are using VB.NET, the default behavior of the compiler differs from the C# compiler. It will not append the default namespace to the front of the resource filename unless you explicitly include the command line option to tell it to do that. As such, for VB.NET projects, you can omit the path constants or set them to empty strings: // TODO: Modify these two constants to match your control's
// namespace and the folder names of your resources. Add any
// additional constants as needed for other resource types.
/// <SUMMARY>
/// The path to the image resources
/// </SUMMARY>
private const string cImageResPath =
"ResServerTest.Web.Controls.Images.";
/// <SUMMARY>
/// The path to the script resources
/// </SUMMARY>
private const string cScriptResPath =
"ResServerTest.Web.Controls.Scripts.";
The ResourceUrl method can be called to format the URL used to retrieve an embedded resource from an assembly. The first version will retrieve the named resource from the assembly that contains the class. Simply pass it the name of the resource and it returns a URL that points to the resource.
The second version of the method can be used to extract embedded resources from assemblies other than the one containing the resource server class. Pass it the name of the assembly that contains the resource (without a path or extension, System.Web for example), the name of the resource handler that can retrieve it (i.e., the class defined in the cResSrvHandlerPageName constant), and the name of the resource to retrieve. When using this version, the name of the resource will be matched to the first resource that ends with the specified name. This allows you to skip the path name if you do not know it or extract resources from VB.NET assemblies which do not store the path: /// <summary>
/// This can be called to format a URL to a resource name that is
/// embedded within the assembly.
/// </summary>
/// <param name="strResourceName">The name of the resource</param>
/// <param name="bCacheResource">Specify true to have the
/// resource cached on the client, false to never cache it.</param>
/// <returns>A string containing the URL to the resource</returns>
public static string ResourceUrl(string strResourceName,
bool bCacheResource)
{
return String.Format("{0}?Res={1}{2}", cResSrvHandlerPageName,
strResourceName, (bCacheResource) ? "" : "&NoCache=1");
}
/// <summary>
/// This can be called to format a URL to a resource name that is
/// embedded within a different assembly.
/// </summary>
/// <param name="strAssemblyName">The name of the assembly that
/// contains the resource</param>
/// <param name="strResourceHandlerName">The name of the resource
/// handler that can retrieve it (i.e. the ASPX page name)</param>
/// <param name="strResourceName">The name of the resource</param>
/// <param name="bCacheResource">Specify true to have the
/// resource cached on the client, false to never cache it.</param>
/// <returns>A string containing the URL to the resource</returns>
public static string ResourceUrl(string strAssemblyName,
string strResourceHandlerName, string strResourceName,
bool bCacheResource)
{
return String.Format("{0}?Assembly={1}&Res={2}{3}",
strResourceHandlerName,
HttpContext.Current.Server.UrlEncode(strAssemblyName),
strResourceName, (bCacheResource) ? "" : "&NoCache=1");
}
The IHttpHandler.IsReusable property is implemented to indicate that the object instance can be reused for other requests. The IHttpHandler.ProcessRequest method is implemented to do all of the work. The first step is to determine the requested resource's name and its type. I use the filename's extension to determine the type. The code assumes that the query string parameter is called Res. Adjust this if you choose a different parameter name. Likewise, you can modify the code to determine the resource name and type in any number of ways depending on your needs: /// <summary>
/// Load the resource specified in the query string and return
/// it as the HTTP response.
/// </summary>
/// <param name="context">The context object for the
/// request</param>
public void ProcessRequest(HttpContext context)
{
Assembly asm;
StreamReader sr = null;
Stream s = null;
string strResName, strType;
byte[] byImage;
int nLen;
bool bUseInternalPath = true;
// TODO: Be sure to adjust the QueryString names if you are
// using something other than Res and NoCache.
// Get the resource name and base the type on the extension
strResName = context.Request.QueryString["Res"];
strType = strResName.Substring(strResName.LastIndexOf(
'.') + 1).ToLower();
The next step is to clear any current response and set up the caching options. If the NoCache query string parameter has not been specified, the class sets the necessary page caching options in the context.Response.Cache object. If it has been specified, the options are set such that the response will never be cached. The class defaults to having the response cached for one day. Adjust this as necessary for your controls. The response is set to vary caching by parameter name. The default class only has one parameter called Res. If you have additional parameters, be sure to add them as additional VaryByParams entries: context.Response.Clear();
// If caching is not disabled, set the cache parameters so that
// the response is cached on the client for up to one day.
if(context.Request.QueryString["NoCache"] == null)
{
// TODO: Adjust caching length as needed.
context.Response.Cache.SetExpires(DateTime.Now.AddDays(1));
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetValidUntilExpires(false);
// Vary by parameter name. Note that if you have more
// than one, add additional lines to specify them.
context.Response.Cache.VaryByParams["Res"] = true;
}
else
{
// The response is not cached
context.Response.Cache.SetExpires(DateTime.Now.AddDays(-1));
context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
}
The next section checks to see if the resource resides in another assembly. If the Assembly query string option has been omitted, it assumes the resource is in the same assembly as the class. If specified, it looks for the named assembly and, if found, searches for the resource within its manifest. When loading from a different assembly, the internal class path names are ignored and the name matched during the search is used instead: // Get the resource from this assembly or another?
if(context.Request.QueryString["Assembly"] == null)
asm = Assembly.GetExecutingAssembly();
else
{
Assembly[] asmList =
AppDomain.CurrentDomain.GetAssemblies();
string strSearchName =
context.Request.QueryString["Assembly"];
foreach(Assembly a in asmList)
if(a.GetName().Name == strSearchName)
{
asm = a;
break;
}
if(asm == null)
throw new ArgumentOutOfRangeException("Assembly",
strSearchName, "Assembly not found");
// Now get the resources listed in the assembly manifest
// and look for the filename. Note the fact that it is
// matched on the filename and not necessarily the path
// within the assembly. This may restricts you to using
// a filename only once, but it also prevents the problem
// that the VB.NET compiler has where it doesn't seem to
// output folder names on resources.
foreach(string strResource in asm.GetManifestResourceNames())
if(strResource.EndsWith(strResName))
{
strResName = strResource;
bUseInternalPath = false;
break;
}
}
As given, the class can serve up various image and script types, some styles for the demo, plus an additional XML file to demonstrate the NoCache option. A simple switch statement is used to determine what type to send back. The context.Response.ContentType property is set accordingly, the resource is retrieved, and it is then written to the response stream. You can expand or reduce the code to suit your needs: switch(strType)
{
case "gif": // Image types
case "jpg":
case "jpeg":
case "bmp":
case "png":
case "tif":
case "tiff":
if(strType == "jpg")
strType = "jpeg";
else
if(strType == "png")
strType = "x-png";
else
if(strType == "tif")
strType = "tiff";
context.Response.ContentType =
"image/" + strType;
if(bUseInternalPath == true)
strResName = cImageResPath + strResName;
s = asm.GetManifestResourceStream(strResName);
nLen = Convert.ToInt32(s.Length);
byImage = new Byte[nLen];
s.Read(byImage, 0, nLen);
context.Response.OutputStream.Write(
byImage, 0, nLen);
break;
case "js": // Script types
case "vb":
case "vbs":
if(strType == "js")
context.Response.ContentType =
"text/javascript";
else
context.Response.ContentType =
"text/vbscript";
if(bUseInternalPath == true)
strResName = cScriptResPath + strResName;
sr = new StreamReader(
asm.GetManifestResourceStream(strResName));
context.Response.Write(sr.ReadToEnd());
break;
case "css": // Some style sheet info
// Not enough to embed so we'll write
// it out from here
context.Response.ContentType = "text/css";
if(bUseInternalPath == true)
context.Response.Write(".Style1 { font-weight: bold; " +
"color: #dc143c; font-style: italic; " +
"text-decoration: underline; }\n" +
".Style2 { font-weight: bold; color: navy; " +
"text-decoration: underline; }\n");
else
{
// CSS from some other source
sr = new StreamReader(
asm.GetManifestResourceStream(strResName));
context.Response.Write(sr.ReadToEnd());
}
break;
case "htm": // Maybe some html
case "html":
context.Response.ContentType = "text/html";
sr = new StreamReader(
asm.GetManifestResourceStream(strResName));
context.Response.Write(sr.ReadToEnd());
break;
case "xml": // Even some XML
context.Response.ContentType = "text/xml";
sr = new StreamReader(
asm.GetManifestResourceStream(
"ResServerTest.Web.Controls." + strResName));
// This is used to demonstrate the NoCache option.
// We'll modify the XML to show the current server
// date and time.
string strXML = sr.ReadToEnd();
context.Response.Write(strXML.Replace("DATETIME",
DateTime.Now.ToString()));
break;
default: // Unknown resource type
throw new Exception("Unknown resource type");
}
For simple text-based resources such as scripts, the StreamReader.ReadToEnd method can be used to retrieve the resource. For binary resources such as images, you must allocate an array and use StreamReader.Read to load the image into the array. Once loaded, you can write the array out to the client as shown above.
If an unknown resource type is requested or if it cannot be loaded from the assembly, an exception is thrown. For script resource types, the exception handler will convert the response to the appropriate type and send back a message box or alert so that the exception is displayed when the page loads. This will give you a chance to see what failed during development. For an XML resource, the exception handler will send back an XML response containing nodes with the resource name and the error description. For all other resource types, nothing is returned. Images will display a broken image placeholder, which serves as an indication that you may have done something wrong: catch(Exception excp)
{
XmlDocument xml;
XmlNode node, element;
string strMsg = excp.Message.Replace("\r\n", " ");
context.Response.Clear();
context.Response.Cache.SetExpires(
DateTime.Now.AddDays(-1));
context.Response.Cache.SetCacheability(
HttpCacheability.NoCache);
// For script, write out an alert describing the problem.
// For XML, send an XML response containing the exception.
// For all other resources, just let it display a broken
// link or whatever.
switch(strType)
{
case "js":
context.Response.ContentType = "text/javascript";
context.Response.Write(
"alert(\"Could not load resource '" +
strResName + "': " + strMsg + "\");");
break;
case "vb":
case "vbs":
context.Response.ContentType = "text/vbscript";
context.Response.Write(
"MsgBox \"Could not load resource '" +
strResName + "': " + strMsg + "\"");
break;
case "xml":
xml = new XmlDocument();
node = xml.CreateElement("ResourceError");
element = xml.CreateElement("Resource");
element.InnerText = "Could not load resource: " +
strResName;
node.AppendChild(element);
element = xml.CreateElement("Exception");
element.InnerText = strMsg;
node.AppendChild(element);
xml.AppendChild(node);
context.Response.Write(xml.InnerXml);
break;
}
}
finally
{
if(sr != null)
sr.Close();
if(s != null)
s.Close();
}
Using the resource server handler in your control
Using the resource server handler in the custom control is very
simple. Just add code to your class to render the attributes, script
tags, or other resource types such as images that utilize the resource
server page name. This is done by calling the ResSrvHandler.ResourceUrl
method with the name of the resource and a Boolean flag indicating
whether or not to cache it on the client. The demo control contains
several examples. // An image img = new HtmlImage();
// Renders as: // src="ResServerTest.Web.Controls.aspx?Res=FitHeight.bmp" img.Src = ResSrvHandler.ResourceUrl("FitHeight.bmp", true);
// Call a function in the client-side script code registered below img.Attributes["onclick"] = "javascript: FitToHeight()";
this.Controls.Add(img);
// Register the client-side script module // Renders as: <script type='text/javascript' // src='ResServerTest.Web.Controls.aspx?Res=DemoCustomControl.js'> // </script> this.Page.RegisterStartupScript("Demo_Startup", "<script type='text/javascript' src='" + ResSrvHandler.ResourceUrl("DemoCustomControl.js", true) + "'></script>");
// Register the style sheet // Renders as: <link rel='stylesheet' type='text/css' // href='ResServerTest.Web.Controls.aspx?Res=Styles.css'> this.Page.RegisterScriptBlock("Demo_Styles", "<link rel='stylesheet' type='text/css' href='" + ResSrvHandler.ResourceUrl("Styles.css") + "'>\n");
As noted earlier, the lack of the NoCache query string option will cause the resources to be cached on the client. To turn off caching for a resource, simply specify false for the cache parameter of the ResourceUrl method, or add the NoCache
parameter to the query string if hand-coding the URL. The demo ASPX
page contains an example that retrieves an XML document from the
control assembly. It uses the no caching option so that it displays the
current time on the server every time the XML resource is requested. It
also contains a couple of examples that retrieve resources from
assemblies other than the custom control's assembly. <script type='text/javascript'> // Demonstrate the loading of uncached, // dynamic resources outside the // control class. This gets some XML // from the resource server page. function funShowXML() { window.open( 'ResServerTest.Web.Controls.aspx?Res=Demo.xml&NoCache=1', null, 'menubar=no,personalbar=no,resizable=yes,' + 'scrollbars=yes,status=no,' + 'toolbar=no,screenX=50,screenY=50,' + 'height=400,width=800').focus() } </script>
Using the control and the resource server handler in an application
In the application's project, add a reference to your custom
control's assembly and add your custom control to the application's
forms in the normal fashion. To use the resource server handler, add an
entry in the <system.web> section of your application's Web.Config file like the following: <!-- Demo Control Resource Server Handler Add this section to map the resource requests to the resource handler class in the custom control assembly. --> <httpHandlers> <add verb="*" path="ResServerTest.Web.Controls.aspx" type="ResServerTest.Web.Controls.ResSrvHandler, ResServerTest.Web.Controls"/> </httpHandlers>
Modify the path attribute so that it matches the ResSrvHandler.cResSrvHandlerPageName constant. Modify the type
attribute to reference your resource server handler's class name
(including its namespace) followed by a comma and then the name of the
assembly. This entry causes any requests containing the page name
specified in the path attribute, regardless of the folder, to get mapped to your resource handler class.
Allowing anonymous access to resources when using forms-based authentication
When using forms-based authentication to secure an entire
applic
Themes In ASP.NET 2.0
Posted by
scott
on
Sunday, August 07, 2005
This article will provide an in-depth look at the Themes feature in ASP.NET.
|
|
In a previous article, we looked at Master Pages in ASP.NET 2.0. Master pages allow you to dictate the layout and common content for the pages in your application using template files with a .master extension. The Themes feature in ASP.NET 2.0 allows you to dictate the appearance of controls in your application using template files with a .skin extension, and with style sheets. In this article, we will examine the Themes feature in-depth.
There is some overlap in what you can do with themes and master pages, as we will see later in the article. What you ultimately can achieve with the combination of these two features is the following:
- Easily build a web application with consistent layout and appearance across all pages
- Easily change the layout and appearance of all pages just by modifying a few template files
- Easily personalize an application at run time for a specific user by letting the user chose their favorite look from a number of appearance and layout options
|
ASP.Net 2.0 - Master Pages: Tips, Tricks, and Traps
Posted by
scott
on
Tuesday, April 11, 2006
MasterPages are a great addition to the ASP.NET 2.0 feature set, but are not without their quirks. This article will highlight the common problems developers face with master pages, and provide tips and tricks to use master pages to their fullest potential.
|
|
Master pages are a great addition to the ASP.NET 2.0 feature set. Master pages help us build consistent and maintainable user interfaces. Master pages, however, are not without their quirks. Sometimes master page behavior is surprising, and indeed the very name master page can be a bit misleading. In this article, we are going to examine some of the common problems developers run into when using master pages, and demonstrate some practical advice for making effective use of master pages. |
Event Bubbling From Web User Controls in ASP.NET (C#)
Posted by
scott
on
Saturday, February 14, 2004
This C# code example demonstrates event bubbling from a web user control (ASCX) to a parent page (ASPX) and the shows the flow of events through execution.
|
|
Some user controls are entirely self contained, for example, a user control displaying current stock quotes does not need to interact with any other content on the page. Other user controls will contain buttons to post back. Although it is possible to subscribe to the button click event from the containing page, doing so would break some of the object oriented rules of encapsulation. A better idea is to publish an event in the user control to allow any interested parties to handle the event.
This technique is commonly referred to as “event bubbling” since the event can continue to pass through layers, starting at the bottom (the user control) and perhaps reaching the top level (the page) like a bubble moving up a champagne glass. |
Introduction [ Back To Top ] In ASP.NET 1.x it was fairly difficult to create a custom configuration section in ASP.NET 2.0. Many enterprise applications then relied upon the AppS
How to: Apply ASP.NET Themes Programmatically
In addition to specifying theme and skin preferences in page declarations and configuration files, you can apply themes programmatically. You can set both page themes and style sheet themes programmatically; however, the procedure for applying each type of theme is different. To apply a page theme programmatically-
In a handler for the page's PreInit method, set the page's Theme property. The following code example shows how to set a page's theme conditionally based on a value passed in the query string. Protected Sub Page_PreInit(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles Me.PreInit
Select Case Request.QueryString("theme")
Case "Blue"
Page.Theme = "BlueTheme"
Case "Theme2"
Page.Theme = "PinkTheme"
End Select
End Sub
Protected void Page_PreInit(object sender, EventArgs e)
{
switch (Request.QueryString["theme"])
{
case "Blue":
Page.Theme = "BlueTheme";
break;
case "Pink":
Page.Theme = "PinkTheme";
break;
}
}
To apply a style sheet theme programmatically-
In the page's code, override the StyleSheetTheme property and in the get accessor, return the name of the style sheet theme. The following code example shows how to set a theme named BlueTheme as the style sheet theme for a page: Public Overrides Property StyleSheetTheme() As String
Get
Return "BlueTheme "
End Get
Set(ByVal value As String)
End Set
End Property
public override String StyleSheetTheme
{
get { return "BlueTheme "; }
}
To apply control skins programmatically-
In a handler for the page's PreInit method, set the control's SkinID property. The following code example shows how to set the SkinID property of a Calendar control. Sub Page_PreInit(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles Me.PreInit
Calendar1.SkinID = "BlueTheme"
End Sub
void Page_PreInit(object sender, EventArgs e)
{
Calendar1.SkinID = "MySkin";
}
Dynamic Themes assignment
If you’ve been following this Blog you probably know I’ve been mucking around with a bunch of infrastructure stuff recently and one of the things I’ve played with a lot is trying to build a generic solution for skinnable UIs. To that effect I ended up using a mixture of Themes and Master Pages in a separate set of template directories that match the Themes templates.
The idea if you change the Theme the Master Page template changes along with the theme. I rigged this up a while ago but it was a bitch to get this to work right in ASP.NET 2.0 stock projects because of various inheritance issues. In the end I got it to work though although I had to make some compromises in what goes onto the master pages (it works fine in WAP though).
Today I finally put that stuff live on my site, and just for kicks I decided to try and switch themes on the pre-compiled site. To my dismay – nothing happened. I changed the theme in the web.config file, but it no effect at all on the site... What? Isn’t that the whole point of Themes?
Back here in my dev environment it works fine, so it must have something to do with precompilation. I double checked by compiling my app into a local deployment directory and then setting up a virtual for it and sure enough I see the same behavior – the app is stuck with whatever Theme was used in web.config when the project was compiled and changing the value in Web.config has zero effect. Chalk that one up to another unexpected ASP.NET 2.0 project behavior where design time and runtime vary.
This project is a stock project compiled with fixed file options, non-updateable and then set up using ASPNET_MERGE into a single assembly. What’s really odd is that the web.config setting that is during compile time seems to determine which Theme the app is stuck on. I haven’t had the inclination to check this out further, but this seems pretty silly if theming adjustments through web.config doesn’t work with a precompiled site somehow. I must be missing something here...
I didn’t bother digging deeper because I wanted to add dynamic application driven theming to the site anyway, and so the web.config setting was just a temporary solution. In the end I wanted it so that Themes can be interactively changed through the Configuration API of the application. Thankfully setting the Theme through Page.OnPreInit works correctly and that's what I'm doing now.
In order to select set the theme (which sets both the Theme as well as the Master Page template) at runtime I use code like this:
protected override void OnPreInit(EventArgs e)
{
this.Theme = App.Configuration.Theme;
// *** Use a templated MasterPage
if (this.TemplateMasterPage != "" && !string.IsNullOrEmpty(this.Theme))
this.MasterPageFile = this.GetThemeTemplatePath(this.TemplateMasterPage);
base.OnPreInit(e);
}
/// <summary>
/// Figures out the appropriate theme specific Template path
/// that is used to hold Master Pages and other templates.
///
/// This version returns a fully qualified file Url.
/// </summary>
/// <param name="FileName"></param>
/// <returns></returns>
public string GetThemeTemplatePath(string Filename)
{
if (Filename == null)
return "~/App_Templates/";
return "~/App_Templates/" + this.Theme + "/" + Filename;
}
This code is defined in my page base class for the app. It first gets the Active Theme for the application from my Configuration store and then assigns that explicitly which effective changes the theme. The Theme is then used to look up the appropriate Master Page template by filename. Nice and simple and it works effectively.
In the configuration form the Theme is then displayed and selected and stored in the global configuration object. To get a list of themes I found this code from K. Scott Allen.
/// <summary>
/// Loads the list of available themes into txtTheme dropdown
/// </summary>
private void LoadThemes()
{
VirtualDirectory ThemeDir = HostingEnvironment.VirtualPathProvider.GetDirectory("~/App_Themes");
foreach (VirtualDirectory Dir in ThemeDir.Directories)
{
this.txtTheme.Items.Add( Dir.Name );
}
}
I had been parsing the directories out of the App_Themes directory and like Scott I was worried about the permissions issues that might not allow access there.
The form displays among other things the list of themes and lets the admin pick the theme that is used on the site.
I’ve got what I needed working but I’m still thinking about this funky behavior where the web.config setting isn’t changing the them... Any body have any thoughts?
<configuration>
<!-- application specific settings -->
<appSettings>
<add key="connString" value="user id=sa;pwd=;server=<serverName>\VSdotNET;integrated security=SSPI;trusted_connection=true;initial catalog=<Database>;" />
</appSettings>
....
In C# access looks like this....
SqlConnection conn = new SqlConnection(ConfigurationSettings.AppSettings["connString"]);
try {
conn.Open();
...
But, that still doesn't solve any login failures I had....
Even though I added my specified login because I wasn't using sa; actually I still am not using sa I setup another login. I thought I could just have it but you need to allow ASPNET to login too. Anyhow, here's how....
Microsoft sql server -> enterprise manager.
Microsoft SQL Server
SQL Server Group
serverName\VSDOTNET (Windows NT)
Security
Logins <right click>
New Login
Name <click ....>
scrolll down to the ASPNET (aspnet wp account)
You can set default database if you wish here too.
After that I went to the specific database I was interested in
and set permissions to allow the various database commands.
Sending Email in ASP.NET 2.0
By Scott Mitchell
Introduction
Email serves as a ubiquitous, asynchronous notification and information distribution system. Not surprisingly, there are many
web development scenarios where server-side code needs to generate an email and scuttle it off to the intended recipient.
The email may be destined for a user of the website, informing them of their newly created user account, reminding them of
their forgotten password, or emailing them an invoice. Or it may be destined for a web developer or site administrator, providing
information of an unhandled exception that just transpired or user feedback.
Fortunately, ASP.NET makes sending email a breeze. The .NET Framework version 1.x included a number of classes in the
System.Web.Mail class that allowed programmatically sending an email with a few scant lines of code.
While this namespace and these classes still exist in the .NET Framework version 2.0, they have been deprecated in favor
of new mail-related classes found in the System.Net.Mail
namespace. (For an article on sending email in ASP.NET version 1.x, see Sending
Email from an ASP.NET 1.x Web Page or consult www.SystemWebMail.com.)
In this article we'll look at the classes in the System.Net.Mail namespace and see how to send an email from
an ASP.NET 2.0 page's code-behind class. We'll also look at specifying relay server information in Web.config and
how this information can be used in some of the built-in ASP.NET server controls for sending emails (such as when a user creates
an account or needs a password reminder/reset). Read on to learn more!
Exploring the Classes in the System.Net.Mail Namespace
There are 16 different classes in the System.Net.Mail namespace, all related to send email to a specified
Simple Mail Transfer Protocol (SMTP) server for delivery. The two core classes
in this namespace are:
MailMessage -
represents an email message; has properties like From, To, Subject, Body,
and so on.
SmtpClient -
sends a specified MailMessage instance to a specified SMTP server.
When sending an email from an ASP.NET 2.0 page you will, typically:
- Create a
MailMessage object
- Assign its properties
- Create an instance of the
SmtpClient class
- Specify details about the SMTP server to use (if they're not already specified within
Web.config)
- Send the
MailMessage via the SmtpClient object's Send method
Steps 1 and 2 may be bypassed as the SmtpClient class's Send method can accept either
a MailMessage object or four strings, representing the from, to, subject, and body contents of the email message.
The System.Net.Mail namespace's other classes allow for more advanced email functionality. There are classes
that can be used to add attachments to an email message, to embed objects within an email, to specify SMTP server authentication
information, and Exception-derived classes for handling SMTP-specific exceptions. We'll examine using some of
these other classes for more advanced scenarios in future articles.
Providing the SMTP Server's Details
When sending an email to a friend from Outlook or GMail, the email program establishes a connection with a relay
server and sends the contents of the email message, along with information such as the date the email was composed, the email body's
format (text or HTML, for example), the recipient(s), and so on. The relay server accepts the message and then
connects to the recipient's SMTP server and sends the message. Once the message has been delivered, the recipient can, at some later
point in time, pull down the message using a different protocol (such as IMAP
or POP3).
Therefore, to send an email from an ASP.NET page we need to provide the SmtpClient class with information about
the relay server. Along with the hostname of the relay server, you can specify the port (typically port 25 is used),
whether or not to use SSL when communicating your email message contents to the relay server, and authentication
credentials (if necessary). Alternatively, if you have a local SMTP service installed on your web server, it may periodically
monitor a particular "drop-off" directory, sending any messages that appear in that directory. You can configure whether
the SmtpClient class relays its email messages to a separate relay server or if it drops it off in a specified
pickup directory through the DeliveryMethod property.
The relay server information used by the SmtpClient class can be specified programmatically, through the
SmtpClient class's properties, or can be centralized in Web.config. To use the Web.config
approach, add a <system.net> element within the <configuration> element. Then, add
a <mailSettings> element that contains
an <smtp> element whose settings are
specified within its <network> child element, like so:
<configuration>
<!-- Add the email settings to the <system.net> element -->
<system.net>
<mailSettings>
<smtp>
<network
host="relayServerHostname"
port="portNumber"
userName="username"
password="password" />
</smtp>
</mailSettings>
</system.net>
<system.web>
...
</system.web>
</configuration>
|
The host attribute contains the relayServerHostname. If you are using an external relay server, the relayServerHostname
might be something like smtp.yourisp.com. If the relay server's port number is something other than the typical
port 25, specify it through the port attribute. Most external relay servers require authentication of some sort
(in order to prevent anonymous spammers from sending their garbage through the relay). The userName and password
attributes can be provided in the case where username/password authentication is needed.
Only a subset of the SmtpClient properties can be specified through settings in Web.config. To
customize the other SmtpClient properties - EnableSsl, Timeout, and so on - set them
programmatically when sending the email (step 4 from the list of five steps examined earlier in this article).
Sending an Administrator Email Through a Feedback Web Page
To illustrate sending an email using the MailMessage and SmtpClient classes, I've created a simple
feedback page example. In this page the user is prompted for their email address, the subject of their feedback, and their
feedback.
<table border="0">
<tr>
<td><b>Your Email:</b></td>
<td><asp:TextBox runat="server" ID="UsersEmail" Columns="30"></asp:TextBox></td>
</tr>
<tr>
<td><b>Subject:</b></td>
<td><asp:TextBox runat="server" ID="Subject" Columns="30"></asp:TextBox></td>
</tr>
<tr>
<td colspan="2">
<b>Body:</b><br />
<asp:TextBox runat="server" ID="Body" TextMode="MultiLine" Columns="55" Rows="10"></asp:TextBox>
</td>
</tr>
<tr>
<td colspan="2" align="center">
<asp:Button runat="server" ID="SendEmail" Text="Send Feedback" />
</td>
</tr>
</table>
|
Once the user has supplied the feedback information and clicked the "Send Feedback" button, a postback ensues and the
Button's Click event fires. Inside the event handler, a MailMessage object is created and its To,
From, Subject, and Body properties are set according to the information provided by
the user. With the MailMessage object created and its properties populated, the email is then sent through
the SmtpClient class's Send method.
Protected Sub SendEmail_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles SendEmail.Click
'!!! UPDATE THIS VALUE TO YOUR EMAIL ADDRESS
Const ToAddress As String = "you@youremail.com"
'(1) Create the MailMessage instance
Dim mm As New MailMessage(UsersEmail.Text, ToAddress)
'(2) Assign the MailMessage's properties
mm.Subject = Subject.Text
mm.Body = Body.Text
mm.IsBodyHtml = False
'(3) Create the SmtpClient object
Dim smtp As New SmtpClient
'(4) Send the MailMessage (will use the Web.config settings)
smtp.Send(mm)
End Sub
|
We didn't need to set any of the SmtpClient class's properties here in code because they have been specified
in Web.config (download the complete code at the end of this article to run this application on your computer).
Conclusion
Along with a plethora of other improvements from ASP.NET 1.x, the email sending capabilities in ASP.NET 2.0 have been updated
and moved to a new namespace, System.Net.Mail. In 2.0 the relay server settings can easily be decoupled from the
ASP.NET code and moved into the Web.config file, as we saw in this example. Moreover, there's better support for
relay server authentication. Future articles will explore more advanced email scenarios, such as: crafting HTML-formatted emails,
sending attachments, embedding objects within the email body, handling SMTP/relay server-related exceptions, and so on.
Happy Programming!
By Scott Mitchell
Attachments
Download the complete code samples examined in this article (in ZIP format)
Suggested Readings
Sending Email with System.Net.Mail (includes a C# example)
www.SystemNetMail.com (a great set of FAQs and samples for sending email using the System.Net.Mail namespace classes)
Sending Email in ASP.NET 2.0: HTML-Formatted Emails, Attachments, and Gracefully Handling SMTP Exceptions
Sending Email in ASP.NET 2.0: HTML-Formatted Emails, Attachments, and Gracefully Handling SMTP Exceptions
By Scott Mitchell
Introduction
As detailed in last week's article, Sending Email in ASP.NET 2.0,
the .NET Framework version 2.0 includes a new namespace (System.Net.Mail) and new classes for sending email.
(The namespace (System.Web.Mail) and classes used in the .NET Framework version 1.x still exist for backwards
compatibility.) Last week we examined how to use the MailMessage and SmtpClient classes in the
System.Net.Mail namespace for sending simple, plain-text email messages.
This article looks at the more advanced email-related options. We'll see how to send HTML-formatted emails, how to include
attachments, and how to gracefully handle SMTP exceptions when sending an email (such as invalid relay server credentials or
if the relay server is offline). Read on to learn more!
This article assumes that you are already familiar with sending simple, plain-text emails from an ASP.NET 2.0 web page; if not,
please first read Sending Email in ASP.NET 2.0 before
tackling this article...
In Sending Email in ASP.NET 2.0 we saw how to send plain-text emails by assigning the contents of the email to the
MailMessage class's
Body property.
To send HTML-formatted emails, simply set the Body property to the HTML content to send, and then mark the
MailMessage class's
IsBodyHtml property
to True.
To demonstrate sending an HTML-formatted message, I created a sample named HtmlEmail.aspx available
for download at the end of this article. The germane code follows:
'(1) Create the MailMessage instance
Dim mm As New MailMessage(FromEmailAddress, ToEmailAddress)
'(2) Assign the MailMessage's properties
mm.Subject = "HTML-Formatted Email Demo Using the IsBodyHtml Property"
mm.Body = "<h2>This is an HTML-Formatted Email Send Using the <code>IsBodyHtml</code> Property</h2><p>Isn't HTML <em>neat</em>?</p><p>You can make all sorts of <span style=""color:red;font-weight:bold;"">pretty colors!!</span>.</p>"
mm.IsBodyHtml = True
'(3) Create the SmtpClient object
Dim smtp As New SmtpClient
'(4) Send the MailMessage (will use the Web.config settings)
smtp.Send(mm)
|
As you can see, simply set the Body property to the HTML content to send and the IsBodyHtml property
to True, and you're done! The actual email content that gets sent to the relay server (and eventually down to the recipient's
email client), looks something like the following:
x-sender: ToEmailAddress
x-receiver: FromEmailAddress
mime-version: 1.0
from: FromEmailAddress
to: ToEmailAddress
date: 25 Jul 2006 15:06:44 -0700
subject: HTML-Formatted Email Demo Using the IsBodyHtml Property
content-type: text/html; charset=us-ascii
content-transfer-encoding: quoted-printable
<h2>This is an HTML-Formatted Email Send Using the <code>IsBodyHtml</code>=
Property</h2><p>Isn't HTML <em>neat</em>?</p><p>You can make all sorts=
of <span style=3D"color:red;font-weight:bold;">pretty colors!!</span>.</p>
| Viewing the Email Content Sent to the Relay Server |
Interested in viewing the actual content sent to the relay server by the SmtpClient class (like the content
shown above)? In Sending Email in ASP.NET 2.0 we discussed how the SmtpClient class can be configured
to send the email to a relay server or have it dropped off in a specified directory. Using the latter option, we can
explore the actual email content that would otherwise have been sent to the relay server. Check out the Web.config
file in the code available for download at the end of this article - there's a commented out <smtp> element
that shows how to configure the SmtpClient class to dump the email's contents to a specified directory.
|
The email client - assuming it supports HTML-formatted emails - will display the HTML content within the email.
| Caveats on Sending HTML-Formatted Emails |
|
When sending HTML-formatted emails, understand that the HTML
you see on your screen may differ quite a bit from what your recipients see. Most all email clients strip out potentially
dangerous HTML content (ActiveX controls and the like), many prevent JavaScript from running, and most handle external styles
poorly. For a more thorough discussion on potential problems with sending HTML-formatted emails, check out
Top 10 HTML Email Mistakes and
CSS and Email, Kissing in a Tree.
|
Including Attachments
The MailMessage class has an Attachments property
that is a collection of Attachment class
instances. You can attach an existing file on the web server to the email message or base the content's attachment on a
Stream. To illustrate sending emails will attachments, I created a demo where the visitor can fill out a feedback-like form
to have an email sent to administrator. However, this feedback form allows the visitor to pick a file from their computer
to be attached to the email sent from the web page (much like how the web-based email web applications - Hotmail, GMail, etc. -
allow you to attach a file from your computer when sending an email).
To allow the visitor to attach a file from their computer, we need to allow the user to upload a file from their machine to
the web server. This can be easily accomplished using the FileUpload
control (which is new to ASP.NET 2.0). Let's look at the declarative syntax used for creating the user interface for this
demo:
<table border="0">
<tr>
<td><b>Your Email:</b></td>
<td><asp:TextBox runat="server" ID="UsersEmail" Columns="30"></asp:TextBox></td>
</tr>
<tr>
<td><b>File to Send:</b></td>
<td>
<asp:FileUpload ID="AttachmentFile" runat="server" />
</td>
</tr>
<tr>
<td colspan="2">
<b>Body:</b><br />
<asp:TextBox runat="server" ID="Body" TextMode="MultiLine" Columns="55" Rows="10"></asp:TextBox>
</td>
</tr>
<tr>
<td colspan="2" align="center">
<asp:Button runat="server" ID="SendEmail" Text="Send Feedback" />
</td>
</tr>
</table>
|
The FileUpload control renders as a <input type="file" ... /> HTML element, which, in the browser, is
displayed as a textbox with a Browse button. When clicked, a dialog box is opened from which the user can pick a file from their
computer.
After filling in the feedback form, selecting a file to upload, and clicking the "Send Feedback" button, a postback occurs, sending
the contents of the selected file up to the web server. In the "Send Feedback" Button's Click event handler,
a MailMessage object is created and an attachment is added. Since the FileUpload provides a Stream to the uploaded data,
we can simply point the new Attachment object at this Stream. There's no need to save the uploaded file to the
web server's file system.
'Make sure a file has been uploaded
If String.IsNullOrEmpty(AttachmentFile.FileName) OrElse AttachmentFile.PostedFile Is Nothing Then
Throw New ApplicationException("Egad, a file wasn't uploaded... you should probably use more graceful error handling than this, though...")
End If
'(1) Create the MailMessage instance
Dim mm As New MailMessage(FromEmailAddress, ToEmailAddress)
'(2) Assign the MailMessage's properties
mm.Subject = "Emailing an Uploaded File as an Attachment Demo"
mm.Body = Body.Text
mm.IsBodyHtml = False
'Attach the file
mm.Attachments.Add(New Attachment(AttachmentFile.PostedFile.InputStream, AttachmentFile.FileName))
'(3) Create the SmtpClient object
Dim smtp As New SmtpClient
'(4) Send the MailMessage (will use the Web.config settings)
smtp.Send(mm)
|
The Attachment constructor overload used in the code sample above expects two inputs: a reference to the
Stream that contains the data to attach, and the name to use for the attachment. The FileUpload's InputStream
and FileName properties are used for these two values.
Handling SMTP Exceptions
When sending an email from an ASP.NET page, what happens if the relay server is down, or if the authentication information
used is invalid? In the face of an SMTP error, the SmtpClient class will throw an
SmtpException exception.
To gracefully handle such problems, we can add exception handling code around the code that sends the email. If there's an
SmtpException we can then display a more friendly and informative error message (or, perhaps, write the email's
contents to a file to be sent later).
In the download at the end of this article I've included a demo that allows the visitor to specify the relay server to use,
along with authentication information. If there's an error in attempting to send an email, a client-side alert box is displayed,
explaining the problem. To test this out, enter an invalid relay server hostname or invalid credentials for a relay server that
requires authentication.
Try
'(1) Create the MailMessage instance
Dim mm As New MailMessage(FromEmailAddress, ToEmailAddress)
'(2) Assign the MailMessage's properties
mm.Subject = "Test Email... DO NOT PANIC!!!1!!!111!!"
mm.Body = "This is a test message..."
mm.IsBodyHtml = False
'(3) Create the SmtpClient object
Dim smtp As New SmtpClient
'Set the SMTP settings...
smtp.Host = Hostname.Text
If Not String.IsNullOrEmpty(Port.Text) Then
smtp.Port = Convert.ToInt32(Port.Text)
End If
If Not String.IsNullOrEmpty(Username.Text) Then
smtp.Credentials = New NetworkCredential(Username.Text, Password.Text)
End If
'(4) Send the MailMessage (will use the Web.config settings)
smtp.Send(mm)
'Display a client-side popup, explaining that the email has been sent
ClientScript.RegisterStartupScript(Me.GetType(), "HiMom!", String.Format("alert('An test email has successfully been sent to {0}');", ToAddress.Replace("'", "\'")), True)
Catch smtpEx As SmtpException
'A problem occurred when sending the email message
ClientScript.RegisterStartupScript(Me.GetType(), "OhCrap", String.Format("alert('There was a problem in sending the email: {0}');", smtpEx.Message.Replace("'", "\'")), True)
Catch generalEx As Exception
'Some other problem occurred
ClientScript.RegisterStartupScript(Me.GetType(), "OhCrap", String.Format("alert('There was a general problem: {0}');", generalEx.Message.Replace("'", "\'")), True)
End Try
|
This code catches both SMTP-specific error messages and general ones (such as assigning invalid email addresses to the
MailMessage object's To or From properties). In either case, a client-side alert box is
displayed informing the user of the details of the error.
Conclusion
In this article we saw how to send HTML-formatted emails, send emails with attachments, and gracefully handle exceptions arising
from sending an email message. Sending an HTML-formatted email is as simple as specifying the HTML content in the Body property
and setting the IsBodyHtml property to True. The real challenge comes in making sure the HTML content used is rendered
as expected by the popular email clients. To add an attachment to an email, simply add an Attachment object to the
MailMessage's Attachments collection. The data for the attachment can come from a file on the web server
or from a Stream. Finally, to handle SMTP-level exceptions, add exception handling code that catches the SmtpException
thrown by the SmtpClient class when unable to transport the message to the relay server.
Happy Programming!
By Scott Mitchell
Attachments
Download the complete code samples examined in this article (in ZIP format)
Suggested Readings
www.SystemNetMail.com (a great set of FAQs and samples for sending email using the System.Net.Mail namespace classes)
The New Two-Way Data Binding Syntax
When building pages that use list or grid controls in ASP.NET 2.0, the new data source controls make it much easier than the v1.x approach of using code in the page to generate a DataReader or DataSet instance, and then binding this to the control. In version 2.0, you simply place a data source control on the page, and link it to the list or grid control using the DataSourceID attribute of property of the control. You saw an example of this in the earlier section "Using Multiple Data Bound Values in a Hyperlink Control".
This approach can be used with any of the list controls that are provided with ASP.NET 2.0, including those that were originally introduced in version 1.0 (for example, the DataGrid, DataList, Repeater, ListBox, and HtmlSelect controls). However, data source controls really come into their own when used with the new GridView, DetailsView and FormView controls.
Automatic Updates with the New Data Source Controls
The original version's (1.0) list and grid controls are effectively "read-only", in that you have to write code that is executed in response to events raised by controls within the list or grid (for example, when the user clicks an Update or Delete link). This code must collect the values in the relevant row and then execute some code you've written to push these values back into the data store - possibly using stored procedures or explicit SQL statements. In other words, data binding is only being used to populate the list or grid control.
In the GridView, DetailsView and FormView controls, this is no longer the case. As long as the data source is updateable, and you have provided the appropriate update, insert and delete commands, the control will automatically push changes to the values in the list or grid control to which it's attached back into the data source. You can specify the update, insert and delete commands as explicit SQL statements or use stored procedures if you need to interact with a more complex database table structure.
What's important with respect to the topic of this article, however, is how these new controls affect the syntax used for data binding. When the columns or rows for a GridView or DetailsView control are auto-generated, or specified using a the pre-defined field types such as BoundField, HyperLinkField, or CheckBoxField, the data source control can figure out what to do when it's time to push changes back into the data store by looking at which columns each of the field types is bound to.
But, if you use a TemplateField in a GridView or a DetailsView, you must declare all the content for that column or row yourself, just as you would when using a TemplateColumn in a DataGrid control. And, if you use a FormView control, you always have to declare all the content yourself. Like the v1.0 Repeater control, the FormView control does not support pre-defined column types - it uses only templates for content generation.
Inside the templates of a TemplateField, or in the templates of a FormView control, you use data binding statements to connect the controls you declare there with the columns in the data source control. The interesting point is that the data binding technology must, in this case, support both "read" and "write" operations, rather than being "read-only" as has always been the case in the past. This is termed two-way data binding, and is supported through a new method called Bind.
The Bind Method
As with the Eval method, the Bind method has two overloads, which means that it can be used with or without the "format" parameter:
<%# Bind("expression"[, "format"]) %>
In fact, other than specifying the new method name, the Bind method is used just like the Eval method. To bind an attribute of a control to the CompanyName column, you use:
<%# Bind("CompanyName") %>
If you want to bind an attribute of a control to a value that requires formatting, you add the format parameter:
<%# Bind("OrderDate", "{0:dddd d MMMM}") %>
To demonstrate the Bind method, the following code shows a SqlDataSource control with parameterized SQL statements specified for the UpdateCommand and InsertCommand property attributes. The data source control exposes a two-column rowset containing the ID and name of products from the SQL Server Northwind sample database:
<asp:SqlDataSource id="datasource1" runat="server"
SelectCommand="SELECT ProductID, ProductName FROM Products"
UpdateCommand="UPDATE Products SET ProductName=@ProductName
WHERE ProductID=@ProductID"
InsertCommand="INSERT INTO Products (ProductName)
VALUES (@ProductName)"
/>
The next listing shows how the templates of a FormView control can be declared to allow the product name to be edited or a new product to be added. For this to work, the FormView control must know which is the primary key column in the source data, and this is declared using the DataKeyNames attribute (multiple column primary keys are specified using a comma-delimited list of column names in the control declaration, or a String array of column names at runtime).
<asp:FormView id="formview1" DataSourceID="datasource1" runat="server"
DataKeyNames="ProductID" AllowPaging="True">
<ItemTemplate>
<b><%#Eval("ProductName")%></b><p />
<asp:LinkButton id="btnEdit" runat="server"
CommandName="Edit" Text="Edit Details" /><br />
<asp:LinkButton id="btnInsert" runat="server"
CommandName="New" Text="Add New Product" />
</ItemTemplate>
<EditItemTemplate>
<asp:Label id="lblEditID" runat="server"
Text='<%# Bind("ProductID")%>' />
<asp:TextBox id="txtEditName" runat="server"
Text='<%# Bind("ProductName") %>' />
<asp:LinkButton id="btnUpdate" CommandName="Update"
Text="Update" runat="server" />
<asp:LinkButton id="btnCancel" CommandName="Cancel"
Text="Cancel" runat="server" />
</EditItemTemplate>
<InsertItemTemplate>
<asp:TextBox id="txtInsertName" runat="server"
Text='<%# Bind("ProductName") %>' />
<asp:LinkButton id="btnAdd" CommandName="Insert"
Text="Add" runat="server" />
<asp:LinkButton id="btnAbandon" CommandName="Cancel"
Text="Cancel" runat="server" />
</InsertItemTemplate>
</asp:FormView>
Then each template section declares a control to either display or allow editing of the product name. In the ItemTemplate, where the value is just being displayed, the code can use the Eval method to bind to the ProductName column. In the EditItemTemplate and the InsertItemTemplate, the product name is displayed in a TextBox control, allowing it to be edited or entered. Because we want to push these values back into the database, the Bind method must be used here.
One important point to note is that the ProductID column is the primary key in the table, and so it is "read-only" and cannot be changed in the EditItemTemplate. However, it is required to populate the @ProductID parameter of the SQL UPDATE statement - which means it must be placed in an ASP.NET server control (in this case a Label) and bound to the source rowset using the Bind method. If the Eval method is used here, the control will not be able to populate the @ProductID parameter in the SQL statement.
The ProductID is not required in the InsertItemTemplate, because this is an IDENTITY column in the database - and so it will be populated automatically when a new row is added. For this reason, the SQL INSERT statement has no parameter for the ProductID; hence, there is no requirement to display the value in a server control within this template, or use the Bind method. The only value that is required is the new product name, and the Bind method used with the TextBox where this is entered will push the new value into the database table.
The screenshot below shows the results. This example, bind-method.aspx, is included in the samples you can download for this article from http://www.daveandal.net/articles/databinding-syntax/.
Figure 3
To cause an update, delete or insert operation to take place in a FormView control, you must provide the relevant links or buttons to initiate the process yourself. However, as long as you use the appropriate values for the CommandName property attributes ("Edit", "New", "Update", "Insert", "Delete" or "Cancel") the control will automatically wire them up to the events so that you don't have to write any code. For more details of the FormView control, see the System.Web.UI.WebControls namespace in the Reference section of the SDK installed with the .NET Framework, or available online from http://msdn.microsoft.com.
When To Use the Bind Method
When you use templates in a GridView, DetailsView or FormView control, you should be aware of a few points:
- The Bind method is responsible for providing the values for the parameters declared in the SQL statements that push changes to the data back into the database. Therefore, all the controls that provide values for the parameters in the SQL statement for the current operation must use the Bind method. In the earlier example, the control for the ProductID in the EditItemTemplate is a Label that doesn't allow the value to be edited (it is the primary key for the row). However, the value is required to populate the @ProductID parameter in the SQL UPDATE statement, and so the Bind method is used, rather than the Eval method. However, because ProductID is an IDENTITY column within the database, a value isn't required in the SQL INSERT statement - and a bound control is not required in the InsertItemTemplate section.
- In general, the Bind method should only be used in an EditItemTemplate and an InsertItemTemplate. It should not be used (or, in fact, required) in an ItemTemplate, AlternatingItemTemplate, or SelectedItemTemplate.
- A list control in which the Bind method is used must be populated by a data source control that is connected to the list control through the DataSourceID property or attribute. It cannot be used for controls that are populated (as in ASP.NET 1.x) by assigning a rowset to their DataSource property.
- Every control that uses the Bind method to populate one or more of its attributes must have a unique user-declared value for its ID property (as shown for the controls in the listing above).
- The Bind method cannot be used in a DataList or a Repeater control, or in any other type of control, and it cannot be used at Page level outside all of the controls. In effect, the only controls to where you can use the Bind method are the GridView, DetailsView and FormView controls.
Gotcha: Fixing Error with VS 2005 SP1 Beta and older Web Application Project Templates
Two weeks ago we released the VS 2005 SP1 Beta. You can download it for free by visiting and registering on the Microsoft Connect Site. This release contains several hundred fixes for customer reported bugs with VS 2005. It also includes built-in support for the VS 2005 Web Application Project, which we previously released earlier this year as a standalone download (SP1 adds a few additional features to it, fixes a few bug, and has some additional performance tunings for it). Before installing VS 2005 SP1 (both the Beta and final release), you'll need to uninstall any previous installs of the VS 2005 Web Application Project standalone download that you have on your machine. One bug that we've seen some developers run into is an issue where - if they have installed an older build of the VS 2005 Web Application Project on their machine before - they get the below error dialog message when they try to add new items to a Web Application project immediately after installing the new SP1 Beta Support for it: Error: this template attempted to load an untrusted component 'Microsoft.VisualStudio.Web.Application, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. For more information on this problem and how to enable this template, please see documentation on Customizing Project Templates. This is caused by a bug with the SP1 Beta setup. You can fix this with the SP1 Beta by closing VS 2005, and then open up the "Visual Studio 2005 Command Prompt" (there is an icon for it in your Start Menu->Visual Studio 2005->Visual Studio Tools folder), and run this command from within it: devenv.exe /InstallVSTemplates This will correctly re-configure the add item templates and make everything work fine. Hope this helps, Scott P.S. The setup issue will be fixed for the final SP1 release - so the steps above are only needed with the Beta.
Creating Visual Studio Project Templates
by Ron Petrusha
10/17/2006
Often, developers perform the same repetitive operations when creating a new Visual Studio project. They frequently find themselves adding the same references and sometimes the same project items to their projects. Sometimes, developers even add more or less identical "framework" code (that is, code that performs essential operations, such as instantiating top-level objects found in an application object model). When these repetitive operations involve adding references or otherwise empty project items, they are just mildly annoying and inefficient. When they require adding repetitive code, they often become irritating and, depending on how the repetitive code is generated, error-prone as well. However, Visual Studio 2005 offers a solution to the inefficiencies of creating largely identical projects: custom project and item templates, supplemented by wizards, can automate the process of project creation and eliminate the need for developers to add the same references, the same project items, or even largely identical code to new projects.
Note: This article applies to Visual Studio project templates and wizards for Visual Studio 2005 only. In particular, it introduces a new object model for wizard development that is completely incompatible with previous versions of Visual Studio .NET.
Inside Visual Studio Project Templates
The starting point for creating a Visual Studio project is to select the type of project to be created from the New Project dialog, as shown in Figure 1. Each project that the user can create is represented by a Visual Studio template, which in turn corresponds to a Zip file stored in a predetermined location. Visual Studio recognizes two different kinds of templates:

Figure 1: The Visual Studio New Project dialog.
- Standard templates, which are installed along with Visual Studio. The root directory of standard project templates is defined by the ProjectTemplatesLocation property defined in the Visual Studio settings (.vssettings) file, while the root directory of standard item templates is defined by the ProjectItemTemplatesLocation property in the Visual Studio settings file.
- User templates, which are custom templates. The root directory of custom project templates is defined by the UserProjectTemplatesLocation value in the HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\8.0 registry key, while the root directory of custom item templates is defined by the UserItemTemplatesLocation value in the same key.
If we open the Zip file that defines a particular Visual Studio project template, we see that it includes a number of files that will eventually be added to a newly created project. For example, in Figure 2, WinZip is displaying the files contained within the Visual Basic Console Application project template, ConsoleApplication.zip. Of the 10 files contained in the Zip file, each corresponds to a file included in a new console mode application and accessible from the Visual Studio Solution Explorer, with just two exceptions:
- consoleapplication.vbproj, the Visual Basic project file. Although it is not displayed in Solution Explorer, it is a standard .NET project file whose values are reflected on the Application tab of the project's Properties dialog.
- consoleapplication.vstemplate, a Visual Studio template file.

Figure 2: The files contained in the ConsoleApplication.zip project template file. (Click for full-size image.)
Since it is the template file that distinguishes this collection of files as a Visual Studio project template, it is instructive to examine its contents. As Figure 3 shows, consoleapplication.vstemplate turns out to an XML file that consists of two sections, TemplateData (which provides information about how the template is to be handled) and TemplateContent (which defines the individual items to be included in the project).

Figure 3: The contents of a .vstemplate file. (Click for full-size image.)
In the TemplateData section, the ProjectType tag defines the project language. The DefaultName and ProvideDefaultName tags determine whether Visual Studio provides a root project name (such as ConsoleApplication followed by a number), which it increments depending on the number of projects found in a particular solution directory to form a unique project name.
In the TemplateContent section, each ProjectItem tag defines a file to be included in a new project. The TargetFileName attribute determines the location of the project item if it is not placed in the project directory. Most interesting is the ReplaceParameters attribute, which suggests that the project item files stored in the template have replaceable parameters. In the Console Application template, only some items, including Module1.vb, AssemblyInfo.vb, and the three Designer files, have replaceable parameters. We can get some sense of what these replaceable parameters are by looking at the files whose ProjectItem tag has a ReplaceParameters attribute set to true. For example, Figure 4 shows Module1.vb, the file used to store developer code in a console application. Its replaceable parameter, $safeitemname$, is easy to pick out, since it is delimited by dollar signs. Table 1 contains a complete list of replaceable parameters used in the Visual Basic console application project template, including those found in the consoleapplication.vbproj file.

Figure 4: A project item with a replaceable parameter.
| File |
Replaceable Parameter |
| Module1.vb |
$safeitemname$ |
| AssemblyInfo.vb |
$projectname$ |
| AssemblyInfo.vb |
$registeredorganization$ |
| AssemblyInfo.vb |
$year$ |
| AssemblyInfo.vb |
$guid1$ |
| MyApplication.Designer.vb |
$clrversion$ |
| Resources.Designer.vb |
$clrversion$ |
| Resources.Designer.vb |
$safeprojectname$ |
| Settings.Designer.vb |
$clrversion$ |
| Settings.Designer.vb |
$safeprojectname$ |
| consoleapplication.vbproj |
$safeprojectname$ |
Table 1: A list of replaceable parameters in the Visual Basic console mode application template.
To get a better sense of how the contents of the Visual Studio project template relate to a new project, it's also worthwhile to take a look at the project file stored in a template. Example 1 shows it in full. In addition to defining build and debug settings and determining the path of the output assembly, the project file is also responsible for defining the following:
http://schemas.microsoft.com/developer/msbuild/2003">
Debug
AnyCPU
{00000000-0000-0000-0000-000000000000}
Exe
$safeprojectname$.Module1
$safeprojectname$
$safeprojectname$
Console
true
full
true
true
bin\Debug\
$safeprojectname$.xml
42016,41999,42017,42018,42019,42032,42036,42020,42021,42022
pdbonly
false
true
true
bin\Release\
$safeprojectname$.xml
42016,41999,42017,42018,42019,42032,42036,42020,42021,42022
True
Application.myapp
True
True
Resources.resx
True
Settings.settings
True
VbMyResourcesResXFileCodeGenerator
Resources.Designer.vb
My.Resources
Designer
MyApplicationCodeGenerator
Application.Designer.vb
SettingsSingleFileGenerator
My
Settings.Designer.vb
Example 1: The project file in the Visual Basic console application template
- The references automatically added to a project when it is created.
- The namespaces that are automatically imported by the compiler. They are available on a project-wide basis without your having to use the Visual Basic Imports statement or the C# using statement.
- The files to be included in the compilation. Note that "child" files, such as designers that are dependent on "parent" files, are marked with a DependentUpon tag.
- Any resources to be included in the compilation.
A Visual Studio project template along with its project file allows you to define the source code files (which of course can contain custom code) to be included in a project, replace parameter strings with their values, define the assemblies references by the project, and indicate which namespaces the compiler imports on a project-wide basis.
Modifying or Creating a Visual Studio Project Template
One of two approaches can be used to modify a Visual Studio project template:
- You can extract the files from an existing template that it similar to the template you'd like to create, make whatever modifications you'd like (either using a text editor or by accessing the files as an existing project from the Visual Studio environment), and store the new files either in the same Zip file or a new Zip file.
- You can create a new project, make whatever modifications you'd like, and use the Export Template option on the Visual Studio File menu to walk you through the process of generating the template.
Although the second is the easier option, it suffers from a major limitation: If you create the project that you plan to export from a template, all replaceable string parameters will be replaced by their values, and the value of your template will be substantially reduced. This makes the first alternative the most attractive. However, you can combine these approaches without losing a project template's replaceable string parameters. To do this, follow these steps:
- Create a directory in which to store your project. If you store multiple projects in a single solutions directory, also create a solutions directory as the parent of the project directory.
- Locate the Zip file containing the Visual Studio template that most closely resembles the template you plan to create.
- Extract the contents of the Zip file to the project directory you've just created. If the Zip file does not do it for you, you should create the directory structure that would be created if you created the project from a template in Visual Studio. For example, in Visual C#, the AssemblyInfo.cs file is placed in the Properties subdirectory of the project directory; in Visual Basic, AssemblyInfo.vb is placed in the My Project subdirectory of the project directory.
- Delete the template (*.vstemplate) file.
- Review the files listed in the project (*.vbproj or *.csproj) file. All of the files listed must be present for Visual Studio to successfully generate a template. Modify the contents of the project file accordingly. This is also a good opportunity to rename files. The project file itself is a particularly good candidate for renaming.
- In Visual Studio, select the Open Project option from the File menu, and navigate to the project file that you'd like to open.
- Make whatever changes you desire to create the new template.
- Use the Export Template option from the File menu to generate the new template.
- Since this approach does not create a new project from an existing template, it preserves replaceable parameter strings in the project's files. This does, however, have one disadvantage: because the Visual Basic and C# compilers will not recognize the parameter strings as valid language elements, the project cannot be compiled. Instead, you'll have to use your template to create a project, which you can then compile and debug.
Visual Studio and the Templates Cache
Once a Visual Studio project session starts, it stores its templates in a cache. This means that any changes you make to a template will not be reflected until Visual Studio is closed and restarted. This is an important detail to keep in mind: you can waste hours making changes to a template, only to find that they do not appear to be reflected in new project or project items generated from that template.
Some Template Customizations
You can use this technique to generate templates that you've customized by adding new references, importing new namespaces, and using replaceable string parameters. In this section, we'll examine how to perform these three kinds of customizations before generating a new Visual Studio project template.
Adding References
To generate a template that includes additional references to .NET assemblies, right click on the Visual Basic or C# project in the Solution Explorer and select the Add Reference option from the context menu. (In Visual Basic, you can also open the project's Properties dialog and navigate to its References pane.) You can then select the references that you'd like to add to the project.
When you save your project and then export it as a template, the references you've added are present in any projects are created from that template. Both Visual Basic and C# projects behave identically in this regard.
Importing Namespaces in Visual Basic
Another of the repetitive operations that developers have to perform in each project is importing the namespaces whose types will be referenced by the project's code. Most commonly, developers find themselves importing the same namespaces into their projects, usually by adding an Imports statement (in Visual Basic) or the using statement (in C#) to the beginning of each source code file. (The statement applies only to the source code file in which it is placed, rather than to a project as a whole.)
Visual Basic provides direct support for importing namespaces into a project, rather than importing them on a file by file basis. To import a namespace for the project as a whole, open the project's Properties dialog and select the References tab. The Imported Namespaces list box, which is shown in Figure 5, allows you to see which namespaces are globally imported for your project, as well as to import additional namespaces found in the assemblies references by your project.

Figure 5: The Imported Namespaces list box
Project-wide imports are reflected in Import tags in the projects .vbproj file. For example, the namespaces imported in Figure 5 result in the following entries in the project's .vbproj file:
<ItemGroup>
<Import Include="Microsoft.VisualBasic" />
<Import Include="System" />
<Import Include="System.Collections" />
<Import Include="System.Collections.Generic" />
<Import Include="System.Data" />
<Import Include="System.Diagnostics" />
</ItemGroup>
Unlike Visual Basic, C# does not recognize the Import tag, and therefore does not allow you to import namespaces on a project-wide basis. However, you can modify the individual source code (.cs) files in your template to reflect the namespaces that you most commonly import.
Using Replaceable String Parameters
In addition to adding references to .NET assemblies and to importing namespaces, you can also add replaceable string parameters within project templates. Visual Studio automatically recognizes the following replaceable parameters:
| Parameter Name |
Description |
$guid1$ |
The GUID that identifies the project if it is exposed to COM. |
$guid2$ |
A second project-defined GUID. |
$guid3$ |
A third project-defined GUID. |
$guid4$ |
A fourth project -defined GUID. |
$guid5$ |
A fifth project -defined GUID. |
$guid6$ |
A sixth project -defined GUID. |
$guid7$ |
A seventh project -defined GUID. |
$guid8$ |
An eighth project -defined GUID. |
$guid9$ |
A ninth project -defined GUID. |
$guid10$ |
A tenth project -defined GUID. |
$time$ |
The current time. |
$year$ |
The current year. |
$username$ |
The name of the authorized user who is creating the project. |
$userdomain$ |
The domain of the authorized user who is creating the project. |
$machinename$ |
The name of the computer on which the project is crated. |
$clrversion$ |
The version of the .NET runtime under which the project will run. |
$registeredorganization$ |
The organization for which the project is being created. |
$runsilent$ |
A flag indicating whether the template should display any user interface
elements. |
$projectname$ |
The name of the project. This is the name that the user has entered into
the Name textbox when creating the project. |
$safeprojectname$ |
The name of the project with any unsafe characters removed. |
$installpath$ |
The path to the directory containing the project template used to generate
the project. |
$exclusiveproject$ |
Determines whether the current solution is closed and a new one opened
before the new project is created. If True, the solution is closed; if
False, the new project is added to an existing solution. |
$destinationdirectory$ |
The path to the project directory. |
Table 2.
For example, you may find that, when creating console mode projects, you always change the name of the class or module in your source code to match that of your project. By making the following simple modification to the Console Application template, you can have Visual Studio make this change for you when it generates the project's files:
' Modification to Module1.vb in Visual Basic projects
Module $safeprojectname$
// Modification to Program.cs in C# projects
class $safeprojectname$
In addition to making use of existing replaceable strings in your custom templates, you can also add your own custom replaceable strings. These are defined by the <CustomParameters> node in the Visual Studio template (.vstemplate) file and take the following form:
<CustomParameters>
<CustomParameter Name="$name$" Value="value" />
</CustomParameters>
To add custom replaceable string parameters to a project, you first have to generate the Visual Studio project or item template, extract and edit its .vstemplate file. The <CustomParameters> node should be inserted just before the closing </TemplateContent> tag. Then replace the old version of .vstemplate file in the template Zip file with the new one.
Custom string parameters are most useful when a Visual Studio project or item template is linked with a wizard. Even without custom templates, however, they can be useful. For example, a single project template might be used to target different versions of a particular object model. To take a very simple example, version 1.0 of a small object model is contained in a class named CompanySDK1 in an assembly of the same name. Version 2.0 of the object model is contained in a class named CompanySDK2, also in an assembly of the same name. CompanySDK1 includes a DisplaySDKVersion method, which is inherited by CompanySDK2, that displays the version of the SDK used by the application. A single template can be developed to handle both versions. For example, the code contained in a template that instantiates the versioned application object and calls its DisplaySDKVersion method might appear as follows:
Imports CompanySDK
Module Module1]
Sub Main()
Dim versionedSDK As New $versionedClass$
versionedSDK.DisplaySDKVersion ()
End Sub
End Module
Two templates that differ only in their single custom parameter can then be generated from this project. The template supporting version 1.0 of the SDK has the following <CustomParameters> node:
<CustomParameters>
<CustomParameter Name="$versionedClass$" Value="CompanySDK1" />
</CustomParameters>
The template supporting version 2.0 of the SDK has the following <CustomParameters> node:
<CustomParameters>
<CustomParameter Name="$versionedClass$" Value="CompanySDK2" />
</CustomParameters>
Ron Petrusha
is the author and coauthor of many books, including "VBScript in a Nutshell."
Upgrading VS 2003 Web Projects to be VS 2005 Web Application Projects
The below tutorial helps explain how you can now use the VS 2005 Web Application Project
to more easily migrate existing web projects in VS 2003 to VS 2005.
Migrating from VS 2003 to VS 2005 using the Web Application Project
There are a couple of different strategies you can take when migrating a VS 2003 Web Project
to VS 2005. One option (which is the default with the shipping VS 2005 bits) is to migrate the
project to use the new VS 2005 Web Site project model. For more information click here
An alternative (and now much easier) approach, is to migrate a VS 2003 Web Application Project
to use the VS 2005 Web Application Project option instead. The VS 2005 Web Application Project model uses
the same conceptual approach as VS 2003 (project file to include/exclude files, and compilation to a single assembly, etc),
and so doesn't require any architectural changes (the only things that might impact you are deprecated API warnings -- which
can optionally be ignored -- and/or language changes).
Once VS 2005 Web Application Project RC1 is installed, the default migration wizard in VS 2005
is to migrate VS 2003 Web Projects to VS 2005 Web Application Projects.
This model is much less invasive and requires fewer changes.
Please follow the below steps in exact order. If you have problems
using the below steps, please post in this forum and
VS team members will be able to help.
Step 0: Install the VS 2005 Web Application Project Preview
VS 2005 Web Application Project support is not built-into the shipping VS 2005 (although it will be included
with the SP1 release). So the first step (if you haven't done it already) is to install it.
You can learn more about it and install the release candidate build from here.
After installing it, please spend a few minutes following the tutorials on this site to test it out and learn the basics of
how it works.
Step 1: Backup Your VS 2003 Projects
Please make sure to save a backup of your VS 2003 Web Projects and Solutions before attempting any of
the below steps. There is the chance that you might need to roll-back to the VS 2003 solution if
you run into issues, or start over from any step.
Step 2: Copy Remote Frontpage Projects to your local machine
If you use Frontpage Server Extensions to access your project on a remote server, you will need to copy it locally
before migration (if your web projects run locally then you can skip this step). VS 2005 Web Application Projects do not support accessing projects remotly via the Frontpage
Server Extensions. If you do not copy the project locally first, migration will convert the project to a VS 2005
Web Site project instead.
To copy the remote site locally use the VS 2003 Copy Project menu command under the project menu.
This will lauch the copy project dialog.
Change the destination to "http://localhost/ProjectName" and be sure to select "All project files".
After the project has been copied locally you should remove the remote one from the solution and use "Add Existing Project" navigating
to the c:\inetpub\wwwroot\VS03Web folder where your project was copied.
Make sure the project still builds in the IDE.
Step 3: Open Your VS 2003 Web Project and Make Sure it Compiles Clean
Before attempting to migrate anything, please make sure that you open up your existing VS 2003 solution and perform
a clean-re-compile of the entire solution -- and then run the application. Spending 2 minutes to verify everything is
working before you migrate can save a lot of grief later (especially if the cause is because of a last-minute directory
move, etc).
Step 4: Temporarily remove the VS 2003 Solution from Source Control
If your project is currently under source control, you should temporarily remove/unbind it from source control
prior to converting it. Once converted, you can then add it back under source control.
Step 5: Open the solution or project in VS 2005 and perform a project migration
Close the solution in VS 2003, and start VS 2005. Choose "File->Open Project" from the File menu, and select the .sln
or .vbproj file for the solution you wish to migrate. This will cause the VS 2005 Migration Wizard dialog to launch:
Choose a location to backup the project:
The conversion wizard will then perform a few steps:
1) Update the project file to be a VS 2005 MSBuild based project file
2) Add an xhtmlcompliance section within your web.config file to enable "legacy" html rendering
3) Update your web project to go directly against IIS instead of use FrontPage Server Extensions
4) Fixup any language changes between .NET 1.1 and .NET 2.0
When complete, the solution will open up within VS 2005:
Step 6: Compiling and Running your Web Project
You are now ready to compile your web project. Choose "Build->Build Solution" or Ctrl-Shift-B to do this.
The most common issue I've seen people run into at this stage are compile conflicts with new class names
introduced with V2.0. For example: ASP.NET now has a built-in menu and treview control (as well as built-in profile,
membership and rolemanagent APIs) -- so if you declare these types in your VS 2003 web projects today you may
get an "ambiguous type" compiler error when you compile (note: the compiler lists each line and typename
for you). If this happens, you should update your declarations to fully qualify the TreeView/Menu/Etc.
you want to use (for example: change "TreeView" to "MyCompany.MyControls.TreeView").
You may or may not also run into some API changes when moving to V2.
This site
lists the known set of breaking API or functionality changes that were made between V1.1 and V2.0. You should consult
this if you run into a problem.
Once your project is compiling clean, you should be able to run and debug in on IIS using ASP.NET 2.0.
Step 7: Run the Web Application
Run the application to verify that the application is working fine. Fix up any runtime issues that you find.
This site
lists the known set of breaking API or functionality changes that were made between V1.1 and V2.0, and will be
able to help if you run into any issues.
Step 8: Convert to Partial Classes
When you migrate your project using the above steps, none of your code-behind page code or classes are modified.
This means that the code should look (and work) just like it did in VS 2003. This makes it much easier to migrate
existing code to VS 2005.
You can optionally choose to keep your code in this format. Doing so will require you to manually update the
control field declarations within your code-behind file -- but everything else will work just fine in VS 2005.
Alternatively, you can update your pages/controls to use the new partial-class support in VS 2005 to more cleanly
organize the tool-generated code from your code-behind logic. This is done by separating out your current code-behind
files into two separate files -- one for your code and event handlers, and the other a
.designer.vb file that contains the control field declarations for the code-behind. Please review
this tutorial for more details on how this
new code-behind model works.
To migrate your code to use this new model you should follow these two steps:
1) Make sure your web project is compiling clean without errors. You want to make sure that all code is
compiling clean without problems before attempting to update it to use partial classes. Please test this
before proceeding.
2) Right click on the root web project node within the solution explorer and choose the "Convert to
Web Application" menu item. This will then iterate through each page or user control within your project
and migrate all control declarations to be declared within the .designer.vb file, and event handlers
to be declaratively specified on the controls in the .aspx markup.
You should then do a clean re-build of your web solution. This should hopefully compile clean without errors (the
cases where I've seen errors are situations where you've done custom modification of the control declarations within
your code-behind class, and the upgrade wizard mishandles them). If you have any errors in your task list, you can
navigate to the appropriate page in the solution explorer and choose the "View Code" and "View CodeGen" context menu
items to examine the code and fix any issues.
If for some reason the .designer.vb file doesn't have a control declaration added, you can manually
declare it within the code-behind file of the page (just like you would in VS 2003). One issue we've sometimes
seen reported are cases where a developer has specifically overriden the type of a Usercontrol declaration in a VS 2003 code-behind
file (for example: MyControl1 instead of the generic UserControl base class), and the type isn't correctly
transferred to the .designer.vb file (producing a compile error). If the correct user-control type
declaration isn't added to the .designer.vb file, you can optionally just delete it from the .designer.vb and add it the code-behind file
of the page instead. VS 2005 will then avoid re-adding it to the .designer.vb file (it first looks in the
code-behind file for declarations before updating the .designer.vb file on changes).
Note: as an advanced option, you can also upgrade each page on a page-by-page basis (just right-click on the page
and choose the "Convert to Web Application" option on it). You might consider doing this if you want to watch
closely the changes made on each page.
Create Reusable Project And Item Templates For Your Development TeamMatt Milner This article discusses:- Consuming existing templates
- Creating new project and item templates
- Customizing templates through XML metadata
- Extending the new project wizard
| This article uses the following technologies: Visual Studio 2005, XMLCode download available at: CodeTemplates.exe (139KB)
|
O ver my years of working with clients on building Microsoft ® .NET Framework-based applications, I have often heard a common feature request for Visual Studio ® .NET: "It would be great if we could create our own project types and item types so that all developers in our organization would have access to them." Many orgs want to create shared templates for projects such as Web sites or for project items, such as a default Web page or forms. Visual Studio 2005 introduces a new model for defining templates and starter kits for projects and items that make this not only possible, but relatively simple. I'll take a look at how to consume, create, and customize these templates. In the past, users have been able to create project or item templates, but it took a lot of inside knowledge of arcane text file formats, JavaScript files, and folder naming conventions, as well as a little bit of magic, to make it work. With the new model, a user can create a simple ZIP file of the items to be included in the template and an XML metadata file that describes the template and its contents. This new model supports creating item templates such as a Web page, a C# or Visual Basic® code file, or a configuration file. It also supports single-project templates like Web sites, class libraries, or smart client applications, as well as multiproject solutions. In fact, most of the built-in project and item templates that you use to create new solutions in Visual Studio 2005 are based on this same template mechanism. Consuming an Existing TemplateThe easiest way to understand how these templates work is to first take a look at how to consume a template that has already been created. In Visual Studio 2005, you can configure the locations to search for templates, and these locations can use any Universal Naming Convention (UNC) path. In the Options dialog box, under the Projects and Solutions settings node, you can configure the user-specific locations to look for project or item templates (see Figure 1). The default values for these settings point to template folders in your My Documents folder. The templates included with Visual Studio 2005 are located under the Visual Studio installation directory and are included in the project setup dialog boxes by default. Figure 1 Template Location OptionsBy dropping a ZIP file with the appropriate metadata file in the user-configured template location, the new project or item template will appear in the respective dialog boxes. The sample code included with this article includes sample project item and project templates that can be copied into these directories for testing purposes. After adding the ZIP file, start Visual Studio 2005, choose File | New | Project, and notice that the new project template is included under the My Templates section of the dialog box (see Figure 2). Selecting this project template will create a new project with the items included in the ZIP file. Figure 2 Your Templates Appear in the New Project Dialog BoxThere are several benefits gained by being able to consume templates in this way. First, because the user locations are configurable, development groups can define a file share on a common server where templates are deployed for the group. All developers in the group can configure their environment to look to that file share for templates. When a new template is deployed to the server by copying the ZIP file to the share, all developers immediately have access to it from within Visual Studio. After the initial deployment, changes to the template can be made in one place instead of having to be deployed to each developer's workstation. In addition, because the template is used as a true template, it remains unchanged and can be used repeatedly to create new projects. This is a vast improvement over the old "copy and paste" approach where one developer would create a file or project and hand it off to another. Without careful management, the original was quickly lost and modified with no chance to get back to a known good configuration. Because the project and item template locations are configured independently, an organization can choose to have only project templates centralized and allow developers to create their own item templates. In addition to templates, Visual Studio 2005 includes a starter kit concept. Starter kits enable broad consumption of samples through community sites from which templates can be downloaded. From the New Project dialog box, one of the options is Search Online Templates. Choosing this option allows you to search for starter kits online by keyword. When a starter kit is found, you can choose to download and install it into the appropriate directory so that you can then use it as a template for adding items or projects. As the name implies, starter kits are generally used as learning tools or samples to help convey knowledge or provide a starting point for larger projects. Creating TemplatesConsuming templates is a great start, but most developers will want to know how to create their own templates. Visual Studio 2005 makes it extremely easy to create your own templates right in the IDE. Once they are created, you have the ability to extend and change them. The simplest type of template to create is an item template. To do so, simply open a project that includes the file you want to use as a template and then chooses File | Export Template to run the Export Template Wizard shown in Figure 3. Figure 3 Exporting an Item TemplateThe wizard will prompt you to indicate the item to export and any assembly references to include. Adding references allows you to ensure that assemblies your template depends on are included in the project when the item is added. For example, if an item template uses the System.Data.SqlXml assembly to access SQL Server™ XML functionality, it is important to make sure this assembly gets referenced by the project when the template is added. The references available to choose from in the wizard include the default assembly references for the current project type as well as any references that have been added to the current project before running the wizard. In addition to the assembly references, the export wizard is intelligent enough to export resource files required by the item be exported. For example, if an ASP.NET Web page is the item being exported, the C# or Visual Basic codebehind file and resource file for that page will also get exported and included when the template is used to create new items. The wizard also edits these files to add placeholders for class names and namespaces so that when they are added to a new project they can be named differently and show up in the correct code namespace. For example, instead of having a class that's always named BusinessObjectClass when you add your template, the user can specify a name for the new code file, and this will be used as the name for the class in your template. When the parameters have been selected for the item template—including a name, a description, and the icon to use when displaying the template—you have the option of installing the template. When exporting the template, it gets created in your My Documents\Visual Studio 2005\My Exported Templates folder. Templates in this folder are not included in the New Item or New Project dialog boxes by default. In order to make the template available for use, it must be installed by copying it to the configured directory for templates. Checking the box to install the template simply means that the ZIP file will get copied to the folder you configured for templates in addition to the exported templates folder. Not installing the template allows you to edit the metadata directly or add other files to the ZIP file before installing the template into the appropriate folder. For example, if you created a Master Page template for use in Web sites, you might want to include a CSS file that includes the styles used on the Master Page to ensure that the styles used are also included when the Master Page template is selected. In this case, you would not want to install the template at this point until you have had a chance to add the stylesheet file to the ZIP file and update the metadata file to include the stylesheet when installing the template. I will look at the steps required to customize a template like this later in the article. Creating a project template works in much the same way with the exception that assembly references are not needed as they are inferred from the current project configuration. The main difference between the project template and an item template is that a project template provides the means to create a new project, whereas the item template can only be added to an existing project. Both template types, however, allow for multiple items to be added to a project when the wizard is run. Customizing TemplatesAs you can see, creating a project or item template has been made extremely simple in Visual Studio 2005. However, like many wizards, the default template created may not be exactly what you want. Fortunately, because the design of the templates involves a ZIP file and a metadata file, it is not difficult to extend a template with new features once it's created. The template metadata file is an XML file that describes the template and indicates the files and resources to be included in with the project or item. A sample metadata file created by using the wizard to export an item template is shown in Figure 4. The first thing to notice is the general structure of this metadata, which includes a root VSTemplate node and two child nodes: TemplateData and TemplateContent. The VSTemplate node simply acts as a wrapper for the other two items and indicates the type of the template. Because of the XML namespace declaration in the file, any developer editing this file in Visual Studio 2005 will automatically get IntelliSense® support for the items that are allowed, based on the schema for template files. The TemplateData element contains information that is used to provide details about the template in the New Project or New Item dialog box. The name, description, and icon elements are all displayed directly in the dialog box as you would expect. The DefaultName element is used as the root of the default name provided for the new item or project. The ProjectType element indicates the type of project that the item can be included in and affects the location in the dialog box. The example shown in Figure 4 will cause this item template to be available only for C# Web projects. Figure 5 shows some other optional elements that can be used in the TemplateData section of the metadata file, as well as options for the items already mentioned. The TemplateContent element is where you define the files and references for your item template. The TemplateContent element references any assemblies to be included with the item or project and generally will not need to be edited since you can specify these in the export wizard. Each item in the ZIP file that is to be included in the project is called out with a ProjectItem element. In addition to identifying the source files, the attributes of the ProjectItem element allow you to indicate whether an item has a subtype, whether parameters in the file should be replaced, and whether the target file name differs from the source file name. The subtype attribute can include values such as Form or Component, which provide Visual Studio with hints about how to display the item in the correct designer when it is opened. Replacing parameters allows the New Item wizard to create namespaces, class names, and similar items dynamically so that the template is not entirely static. Without specifying true for this attribute, the template files will appear in the new project exactly as they are in the ZIP file. The TargetFileName attribute allows you to specify either a constant or parameterized value for the file name as it should appear in the new project. This allows you to reuse a template file to create multiple instances of the item in the same project. For example, a class file could be added to the same project twice with different TargetFileName attributes. When the item is added, two new classes will be added to the project based on the same template file. Allowing for this type of reuse within the template makes it much easier to manage the files involved in creating a template. When creating project templates, the process is very similar to that used for item templates, but there are a few slight differences in the metadata file. First, because this is a project, all ProjectItem elements are included under a Project element, which indicates the project file name and whether parameters should be replaced when naming the project file. An abbreviated project template file is shown in Figure 6. Notice that in addition to the Project element that identifies the project information, this example also uses the Folder element to indicate that the ZIP file contains a folder containing the project items indicated. The folder will be included in the project structure when the template is used. The project template also provides different options for the TemplateData section to influence the user interaction with the New Project dialog box. These items are shown in Figure 7. Web projects are configured in a slightly different manner as they use a different wizard for project creation. Instead of using a language for the project type such as Visual Basic and then a ProjectSubType such as Windows, a Web project uses Web as the project type, and the language as the ProjectSubType. Templates for Multiple ProjectsIn addition to being able to configure a template for a single project, it is often useful to create an entire solution as a template or starting point. For example, a development group might not simply want a single Web project as the starting point for a new Web solution as they may want to include business object and data access projects. One resolution would be to create a template for each project and allow the developer to add each project they needed individually. However, a much simpler approach, and one that provides more consistency, is to create a template for the entire solution and give a developer all of the projects they need to get started. Multi-project templates still involve only one ZIP file, but they include one master metadata file and an individual metadata file for each project in the solution. The project metadata files are no different than what has been discussed so far, but the solution template file now provides the template data to the wizard through the TemplateData element and its children and provides pointers to the other project metadata files through the TemplateContents element and its children. The abbreviated sample in Figure 8 shows a solution metadata file that includes three projects: a Web site, a business logic layer, and a data access layer. Figure 9 Solution FolderNotice that the Type for the template is a ProjectGroup and that the template content is simply a collection of links to the other project templates. A friendly name can be given to the projects, and this is how they will be named in the Solution Explorer. The paths to the other projects are based on the folder structure of the items in the ZIP file. The ZIP file used with the example template metadata in Figure 8 would look something like Figure 9. Each project in the solution has its own folder and its own vstemplate metadata file, and the links in the example provide relative paths to the project template files based on the folder structure in the ZIP file. Providing Guidance In addition to providing templates, it is often helpful to provide guidance or extra information about how to use the template. One way to do this is to provide well-commented template code files, but this requires a user to dig through your template code to understand how to use it. Visual Studio 2005 templates provide a better mechanism for not only delivering the documentation your users will need, but for making sure they are presented with the documentation after adding an item or project from your template. In both project and item templates, the ProjectItem element can have several attributes applied to it that allow for displaying items to the user. The OpenInEditor Boolean attribute allows a project item to be opened in the default editor based on the file type of that item. For example, an XML file included in your project and marked with OpenInEditor set to true will be opened in the XML editor. The OpenInWebBrowser attribute allows a text or HTML file to be opened in a Web browser window. This is an easy way to distribute help or guidance with your templates. Simply create an HTML file providing your guidance and include it in the template. Users will view your help file whenever they use your template so you can be sure they have the information they need to properly use your template files once installed. In addition to having files open in the browser or editor windows, you can control the order in which the items appear when you have multiple items opening. You should use the OpenOrder attribute on the ProjectItem element in order to control the order of the displayed items in Visual Studio. The lower the number supplied for OpenOrder, the higher the priority and the closer to the front the item will be displayed. Parameter ReplacementIn many of the examples so far, parameter replacement has been used to allow for the dynamic creation of item and project names and namespaces. Parameter replacement involves using placeholders in the metadata file or in the included source files to indicate where a certain parameter value should be inserted. When you create a new item or project from a template, Visual Studio inserts actual values for the placeholders. There are several built-in parameters that can be used; it is also possible to add your own parameters to be passed to the wizard for replacement. For parameter replacement to occur, the ReplaceParameters attribute must be set to true on either the Project element or the ProjectItem element. These are the two main locations where parameter replacement is used. When the wizard processes the project files and the included items, for each item where ReplaceParameters is set to true, the wizard searches through a dictionary of values to see if it has any known keys that match the parameters in the file. Parameters with a match are replaced with the stored values. Figure 10 shows the built-in parameters that are available to be used in metadata files and source files. Custom parameters can also be added to the template and are then available in the item templates and metadata file as well. To add custom parameters, you add a CustomParameters section within the TemplateContent element of the metadata file. For each parameter you want to add, you can specify a CustomParameter element with a name and the value (see Figure 11). Name your values using the $parametername$ syntax in this file in order to use the same naming convention in the source files. Extending the WizardIf all the extensibility and parameter replacement is not enough to meet your needs and you really have to write some code to provide the dynamic data you need for your template, you can write a wizard extension and configure it in the template metadata. An example of when you might need this type of extension is a case where you have several items in a template and need to be able to include or exclude some at run time. With a wizard extension, you can augment the standard New Project or New Item wizard with your own code and make decisions about what items to include or exclude as well as adding dynamic custom parameters for replacement in the project files. In order to create your own wizard extension, you need to create a .NET library project in your managed language of choice and add a reference to the Microsoft.VisualStudio.TemplateWizardInterface.dll and EnvDTE.dll assemblies. Create a class in your library project and implement the Microsoft.VisualStudio.TemplateWizardInterface.IWizard interface. The IWizard interface members are listed in the following code:
public interface IWizard
{
void BeforeOpeningFile(EnvDTE.ProjectItem projectItem) ;
void ProjectFinishedGenerating(EnvDTE.Project project);
void ProjectItemFinishedGenerating(EnvDTE.ProjectItem projectItem);
void RunFinished();
void RunStarted(object automationObject,
Dictionary<string, string> replacementsDictionary,
WizardRunKind runKind,
object[] customParameters);
bool ShouldAddProjectItem(string filePath);
}
The first three methods provide access to the IDE extensibility model for the items that have been added and might be opening. The RunFinished and RunStarted methods provide a means for running initialization and clean-up code. RunStarted is a common method to provide an implementation for and to write logic that sets flags used by the ShouldAddProjectItem method; these flags will determine whether an item from the template should be added to the project. In order to configure the extension, you must modify the VSTemplate element in the template metadata file to add a WizardExtension element that defines the assembly information for your custom wizard extension. The abbreviated template file in Figure 12 shows how this configuration works. When the wizard is run, the wizard extension will be loaded and its methods called at the appropriate time to augment the wizard processing. ConclusionVisual Studio 2005 has greatly improved the template capabilities and the ease with which you can create reusable, dynamic templates to provide robust starting points for projects and project items. The use of a simple ZIP file and an XML metadata file make customizing templates straightforward. In addition, the export features in Visual Studio make starting a template as simple as opening an existing project. The samples available for download provide several simple illustrations of the items discussed in this article put to use. See the Visual Studio Template Schema Reference in the MSDN® Library for additional details about adding customization to your template metafiles. Matt Milner is an independent software consultant specializing in Microsoft technologies including .NET, Web services, XML, and BizTalk Server. As an instructor for Pluralsight, Matt teaches courses on Web services, BizTalk Server and ASP.NET. Matt lives in Minnesota with his wife Kristen and his son Max.

From the January 2006 issue of MSDN Magazine.
Figure 4 Metadata File for an Item Template http://schemas.microsoft.com/developer/vstemplate/2005"
Type="Item">
__TemplateIcon.ico
MasterPage.master
Example Org Master Page
The default master page for Example Org web sites
Web
CSharp
System, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=B77A5C561934E089
mscorlib, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=B77A5C561934E089
master~1.mas
master~1.cs
Figure 5 TemplateData Elements for an Item | Element Name | Description |
|---|
| ProjectType | Indicates the type of project that this item can be added to. Possible values include CSharp, VisualBasic, VisualC, JSharp, and Web. | | SupportsMasterPage | For items being added to Web projects only, this Boolean option allows you to indicate that the user should be able to select a master page to go along with your item. This is useful for custom Web page item templates that you create. | | SupportsCodeSeparation | For items being added to Web projects only, this Boolean option allows you to indicate that the user should be able to choose if they want their code separated from their presentation. Again, this is most useful for custom Web page item templates that you might create. | | SupportsLanguageDropdown | If a template is identical for multiple languages, providing a value of true in this element will allow the user to choose a language for the template. This is only used in Web projects where the template may come from different languages such as the WebForm template. When the language is changed, the metadata file from the new template location is used and the dialog updated. For example, if you switch the language from C# to Visual J# when adding a Web form, the code separation option is disabled because the Visual J# template does not support code separation. |
Figure 6 Metadata for a Project Template http://schemas.microsoft.com/developer/vstemplate/2005"
Type="Project">
...
...
Class1.cs
Class2.cs
Figure 7 TemplateData Elements for a Project | Element Name | Description |
|---|
| LocationField | Options include Hidden, Disabled, or Enabled. This allows you to indicate whether the user should be able to specify a location for their new project. Defaults to Enabled. | | EnableLocationBrowseButton | Indicates if the user should be able to browse the file system for a location to choose for the new project. Defaults to True. | | CreateNewFolder | Indicates that the wizard should create a new folder for the project at the location selected. Defaults to True. | | AllowCreationWithoutSave | Indicates if this project template can be an in-memory temporary project. This element provides support for a new feature in Visual Studio 2005 which allows a user to create a new project and work on it, but if the project is not saved, it is discarded. This allows for quick testing or prototyping without having to fill up the My Projects folder with a lot of projects that will never get used. | | ProjectSubType | Used by internal templates to further restrict the location within the dialog to certain project subtypes such as Windows, Office, or Database. Used by Web projects to specify the language type. |
http://schemas.microsoft.com/developer/vstemplate/2005"
Type="ProjectGroup">
...
.\DAL\DAL.vstemplate
.\BAL\BAL.vstemplate
.\WebSite\WebSite.vstemplate
| Parameters | Description |
|---|
| itemname | The user-provided name from the dialog. This value is often used for naming classes and files. | | safeitemname | The same as itemname but with all unsafe characters removed. | | safeitemrootname | The name of the root item, which can be used for multi-item templates. For example, if you have partial classes, or a code-beside or code-behind scenario, you can use this item in the secondary files when you need the safeitemname of the main item being created. | | projectname | The user-provided name of the project for a project template. | | safeprojectname | The user-provided name of the project for a project template with all unsafe characters removed. | | rootnamespace | The root namespace of the project for an item template, which can be used to replace the namespace in a project item source file. | | guid[1-10] | A GUID that can be used as a unique identifier for uses such as project GUID in the project file. You can specify up to 10 different GUID parameters using the syntax: guidx, where x is a number between 1 and 10. | | time | The current time in the format DD/MM/YYYY HH:MM:SS. | | year | The current four-digit year. | | username | The Windows username of the logged-in user. | | userdomain | The Windows domain of the logged-in user. | | machinename | The name of the machine where the template is being used. | | clrversion | The version of the common language runtime being used. | | registered-organization | The registered organization of the user based on the system settings. | | wizarddata | A single string that can be any XML or string data included in the WizardData element in a template metadata file. |
http://schemas.microsoft.com/developer/vstemplate/2005"
Type="Item">
...
...
http://schemas.microsoft.com/developer/vstemplate/2005"
Type="Item">
...
...
MSDNMagazine.TemplateWizardExtension.dll
MSDNMagazine.TemplateWizardExtension.ExampleOrgWebPageExtension
© 2006 Microsoft Corporation and CMP Media, LLC. All rights reserved; reproduction in part or in whole without permission is prohibited.
|
Related Articles From MSDN Magazine:
|
10 Tips for Writing High-Performance Web Applications
This article discusses:- Common ASP.NET performance myths
- Useful performance tips and tricks for ASP.NET
- Suggestions for working with a database from ASP.NET
- Caching and background processing with ASP.NET
| This article uses the following technologies: ASP.NET, .NET Framework, IIS
|
W riting a Web application with ASP.NET is unbelievably easy. So easy, many developers don't take the time to structure their applications for great performance. In this article, I'm going to present 10 tips for writing high-performance Web apps. I'm not limiting my comments to ASP.NET applications because they are just one subset of Web applications. This article won't be the definitive guide for performance-tuning Web applications—an entire book could easily be devoted to that. Instead, think of this as a good place to start. Before becoming a workaholic, I used to do a lot of rock climbing. Prior to any big climb, I'd review the route in the guidebook and read the recommendations made by people who had visited the site before. But, no matter how good the guidebook, you need actual rock climbing experience before attempting a particularly challenging climb. Similarly, you can only learn how to write high-performance Web applications when you're faced with either fixing performance problems or running a high-throughput site. My personal experience comes from having been an infrastructure Program Manager on the ASP.NET team at Microsoft, running and managing www.asp.net, and helping architect Community Server, which is the next version of several well-known ASP.NET applications (ASP.NET Forums, .Text, and nGallery combined into one platform). I'm sure that some of the tips that have helped me will help you as well. You should think about the separation of your application into logical tiers. You might have heard of the term 3-tier (or n-tier) physical architecture. These are usually prescribed architecture patterns that physically divide functionality across processes and/or hardware. As the system needs to scale, more hardware can easily be added. There is, however, a performance hit associated with process and machine hopping, thus it should be avoided. So, whenever possible, run the ASP.NET pages and their associated components together in the same application. Because of the separation of code and the boundaries between tiers, using Web services or remoting will decrease performance by 20 percent or more. The data tier is a bit of a different beast since it is usually better to have dedicated hardware for your database. However, the cost of process hopping to the database is still high, thus performance on the data tier is the first place to look when optimizing your code. Before diving in to fix performance problems in your applications, make sure you profile your applications to see exactly where the problems lie. Key performance counters (such as the one that indicates the percentage of time spent performing garbage collections) are also very useful for finding out where applications are spending the majority of their time. Yet the places where time is spent are often quite unintuitive. There are two types of performance improvements described in this article: large optimizations, such as using the ASP.NET Cache, and tiny optimizations that repeat themselves. These tiny optimizations are sometimes the most interesting. You make a small change to code that gets called thousands and thousands of times. With a big optimization, you might see overall performance take a large jump. With a small one, you might shave a few milliseconds on a given request, but when compounded across the total requests per day, it can result in an enormous improvement. Performance on the Data TierWhen it comes to performance-tuning an application, there is a single litmus test you can use to prioritize work: does the code access the database? If so, how often? Note that the same test could be applied for code that uses Web services or remoting, too, but I'm not covering those in this article. If you have a database request required in a particular code path and you see other areas such as string manipulations that you want to optimize first, stop and perform your litmus test. Unless you have an egregious performance problem, your time would be better utilized trying to optimize the time spent in and connected to the database, the amount of data returned, and how often you make round-trips to and from the database. With that general information established, let's look at ten tips that can help your application perform better. I'll begin with the changes that can make the biggest difference. Tip 1—Return Multiple ResultsetsReview your database code to see if you have request paths that go to the database more than once. Each of those round-trips decreases the number of requests per second your application can serve. By returning multiple resultsets in a single database request, you can cut the total time spent communicating with the database. You'll be making your system more scalable, too, as you'll cut down on the work the database server is doing managing requests. While you can return multiple resultsets using dynamic SQL, I prefer to use stored procedures. It's arguable whether business logic should reside in a stored procedure, but I think that if logic in a stored procedure can constrain the data returned (reduce the size of the dataset, time spent on the network, and not having to filter the data in the logic tier), it's a good thing. Using a SqlCommand instance and its ExecuteReader method to populate strongly typed business classes, you can move the resultset pointer forward by calling NextResult. Figure 1 shows a sample conversation populating several ArrayLists with typed classes. Returning only the data you need from the database will additionally decrease memory allocations on your server. Tip 2—Paged Data AccessThe ASP.NET DataGrid exposes a wonderful capability: data paging support. When paging is enabled in the DataGrid, a fixed number of records is shown at a time. Additionally, paging UI is also shown at the bottom of the DataGrid for navigating through the records. The paging UI allows you to navigate backwards and forwards through displayed data, displaying a fixed number of records at a time. There's one slight wrinkle. Paging with the DataGrid requires all of the data to be bound to the grid. For example, your data layer will need to return all of the data and then the DataGrid will filter all the displayed records based on the current page. If 100,000 records are returned when you're paging through the DataGrid, 99,975 records would be discarded on each request (assuming a page size of 25). As the number of records grows, the performance of the application will suffer as more and more data must be sent on each request. One good approach to writing better paging code is to use stored procedures. Figure 2 shows a sample stored procedure that pages through the Orders table in the Northwind database. In a nutshell, all you're doing here is passing in the page index and the page size. The appropriate resultset is calculated and then returned. In Community Server, we wrote a paging server control to do all the data paging. You'll see that I am using the ideas discussed in Tip 1, returning two resultsets from one stored procedure: the total number of records and the requested data. The total number of records returned can vary depending on the query being executed. For example, a WHERE clause can be used to constrain the data returned. The total number of records to be returned must be known in order to calculate the total pages to be displayed in the paging UI. For example, if there are 1,000,000 total records and a WHERE clause is used that filters this to 1,000 records, the paging logic needs to be aware of the total number of records to properly render the paging UI. Tip 3—Connection PoolingSetting up the TCP connection between your Web application and SQL Server™ can be an expensive operation. Developers at Microsoft have been able to take advantage of connection pooling for some time now, allowing them to reuse connections to the database. Rather than setting up a new TCP connection on each request, a new connection is set up only when one is not available in the connection pool. When the connection is closed, it is returned to the pool where it remains connected to the database, as opposed to completely tearing down that TCP connection. Of course you need to watch out for leaking connections. Always close your connections when you're finished with them. I repeat: no matter what anyone says about garbage collection within the Microsoft® .NET Framework, always call Close or Dispose explicitly on your connection when you are finished with it. Do not trust the common language runtime (CLR) to clean up and close your connection for you at a predetermined time. The CLR will eventually destroy the class and force the connection closed, but you have no guarantee when the garbage collection on the object will actually happen. To use connection pooling optimally, there are a couple of rules to live by. First, open the connection, do the work, and then close the connection. It's okay to open and close the connection multiple times on each request if you have to (optimally you apply Tip 1) rather than keeping the connection open and passing it around through different methods. Second, use the same connection string (and the same thread identity if you're using integrated authentication). If you don't use the same connection string, for example customizing the connection string based on the logged-in user, you won't get the same optimization value provided by connection pooling. And if you use integrated authentication while impersonating a large set of users, your pooling will also be much less effective. The .NET CLR data performance counters can be very useful when attempting to track down any performance issues that are related to connection pooling. Whenever your application is connecting to a resource, such as a database, running in another process, you should optimize by focusing on the time spent connecting to the resource, the time spent sending or retrieving data, and the number of round-trips. Optimizing any kind of process hop in your application is the first place to start to achieve better performance. The application tier contains the logic that connects to your data layer and transforms data into meaningful class instances and business processes. For example, in Community Server, this is where you populate a Forums or Threads collection, and apply business rules such as permissions; most importantly it is where the Caching logic is performed. Tip 4—ASP.NET Cache APIOne of the very first things you should do before writing a line of application code is architect the application tier to maximize and exploit the ASP.NET Cache feature. If your components are running within an ASP.NET application, you simply need to include a reference to System.Web.dll in your application project. When you need access to the Cache, use the HttpRuntime.Cache property (the same object is also accessible through Page.Cache and HttpContext.Cache). There are several rules for caching data. First, if data can be used more than once it's a good candidate for caching. Second, if data is general rather than specific to a given request or user, it's a great candidate for the cache. If the data is user- or request-specific, but is long lived, it can still be cached, but may not be used as frequently. Third, an often overlooked rule is that sometimes you can cache too much. Generally on an x86 machine, you want to run a process with no higher than 800MB of private bytes in order to reduce the chance of an out-of-memory error. Therefore, caching should be bounded. In other words, you may be able to reuse a result of a computation, but if that computation takes 10 parameters, you might attempt to cache on 10 permutations, which will likely get you into trouble. One of the most common support calls for ASP.NET is out-of-memory errors caused by overcaching, especially of large datasets. Figure 3 ASP.NET CacheThere are a several great features of the Cache that you need to know. The first is that the Cache implements a least-recently-used algorithm, allowing ASP.NET to force a Cache purge—automatically removing unused items from the Cache—if memory is running low. Secondly, the Cache supports expiration dependencies that can force invalidation. These include time, key, and file. Time is often used, but with ASP.NET 2.0 a new and more powerful invalidation type is being introduced: database cache invalidation. This refers to the automatic removal of entries in the cache when data in the database changes. For more information on database cache invalidation, see Dino Esposito's Cutting Edge column in the July 2004 issue of MSDN®Magazine. For a look at the architecture of the cache, see Figure 3. Tip 5—Per-Request CachingEarlier in the article, I mentioned that small improvements to frequently traversed code paths can lead to big, overall performance gains. One of my absolute favorites of these is something I've termed per-request caching. Whereas the Cache API is designed to cache data for a long period or until some condition is met, per-request caching simply means caching the data for the duration of the request. A particular code path is accessed frequently on each request but the data only needs to be fetched, applied, modified, or updated once. This sounds fairly theoretical, so let's consider a concrete example. In the Forums application of Community Server, each server control used on a page requires personalization data to determine which skin to use, the style sheet to use, as well as other personalization data. Some of this data can be cached for a long period of time, but some data, such as the skin to use for the controls, is fetched once on each request and reused multiple times during the execution of the request. To accomplish per-request caching, use the ASP.NET HttpContext. An instance of HttpContext is created with every request and is accessible anywhere during that request from the HttpContext.Current property. The HttpContext class has a special Items collection property; objects and data added to this Items collection are cached only for the duration of the request. Just as you can use the Cache to store frequently accessed data, you can use HttpContext.Items to store data that you'll use only on a per-request basis. The logic behind this is simple: data is added to the HttpContext.Items collection when it doesn't exist, and on subsequent lookups the data found in HttpContext.Items is simply returned. Tip 6—Background ProcessingThe path through your code should be as fast as possible, right? There may be times when you find yourself performing expensive tasks on each request or once every n requests. Sending out e-mails or parsing and validation of incoming data are just a few examples. When tearing apart ASP.NET Forums 1.0 and rebuilding what became Community Server, we found that the code path for adding a new post was pretty slow. Each time a post was added, the application first needed to ensure that there were no duplicate posts, then it had to parse the post using a "badword" filter, parse the post for emoticons, tokenize and index the post, add the post to the moderation queue when required, validate attachments, and finally, once posted, send e-mail notifications out to any subscribers. Clearly, that's a lot of work. It turns out that most of the time was spent in the indexing logic and sending e-mails. Indexing a post was a time-consuming operation, and it turned out that the built-in System.Web.Mail functionality would connect to an SMTP server and send the e-mails serially. As the number of subscribers to a particular post or topic area increased, it would take longer and longer to perform the AddPost function. Indexing e-mail didn't need to happen on each request. Ideally, we wanted to batch this work together and index 25 posts at a time or send all the e-mails every five minutes. We decided to use the same code I had used to prototype database cache invalidation for what eventually got baked into Visual Studio® 2005. The Timer class, found in the System.Threading namespace, is a wonderfully useful, but less well-known class in the .NET Framework, at least for Web developers. Once created, the Timer will invoke the specified callback on a thread from the ThreadPool at a configurable interval. This means you can set up code to execute without an incoming request to your ASP.NET application, an ideal situation for background processing. You can do work such as indexing or sending e-mail in this background process too. There are a couple of problems with this technique, though. If your application domain unloads, the timer instance will stop firing its events. In addition, since the CLR has a hard gate on the number of threads per process, you can get into a situation on a heavily loaded server where timers may not have threads to complete on and can be somewhat delayed. ASP.NET tries to minimize the chances of this happening by reserving a certain number of free threads in the process and only using a portion of the total threads for request processing. However, if you have lots of asynchronous work, this can be an issue. There is not enough room to go into the code here, but you can download a digestible sample at www.rob-howard.net. Just grab the slides and demos from the Blackbelt TechEd 2004 presentation. Tip 7—Page Output Caching and Proxy ServersASP.NET is your presentation layer (or should be); it consists of pages, user controls, server controls (HttpHandlers and HttpModules), and the content that they generate. If you have an ASP.NET page that generates output, whether HTML, XML, images, or any other data, and you run this code on each request and it generates the same output, you have a great candidate for page output caching. By simply adding this line to the top of your page
<%@ Page OutputCache VaryByParams="none" Duration="60" %>
you can effectively generate the output for this page once and reuse it multiple times for up to 60 seconds, at which point the page will re-execute and the output will once be again added to the ASP.NET Cache. This behavior can also be accomplished using some lower-level programmatic APIs, too. There are several configurable settings for output caching, such as the VaryByParams attribute just described. VaryByParams just happens to be required, but allows you to specify the HTTP GET or HTTP POST parameters to vary the cache entries. For example, default.aspx?Report=1 or default.aspx?Report=2 could be output-cached by simply setting VaryByParam="Report". Additional parameters can be named by specifying a semicolon-separated list. Many people don't realize that when the Output Cache is used, the ASP.NET page also generates a set of HTTP headers that downstream caching servers, such as those used by the Microsoft Internet Security and Acceleration Server or by Akamai. When HTTP Cache headers are set, the documents can be cached on these network resources, and client requests can be satisfied without having to go back to the origin server. Using page output caching, then, does not make your application more efficient, but it can potentially reduce the load on your server as downstream caching technology caches documents. Of course, this can only be anonymous content; once it's downstream, you won't see the requests anymore and can't perform authentication to prevent access to it. Tip 8—Run IIS 6.0 (If Only for Kernel Caching)If you're not running IIS 6.0 (Windows Server™ 2003), you're missing out on some great performance enhancements in the Microsoft Web server. In Tip 7, I talked about output caching. In IIS 5.0, a request comes through IIS and then to ASP.NET. When caching is involved, an HttpModule in ASP.NET receives the request, and returns the contents from the Cache. If you're using IIS 6.0, there is a nice little feature called kernel caching that doesn't require any code changes to ASP.NET. When a request is output-cached by ASP.NET, the IIS kernel cache receives a copy of the cached data. When a request comes from the network driver, a kernel-level driver (no context switch to user mode) receives the request, and if cached, flushes the cached data to the response, and completes execution. This means that when you use kernel-mode caching with IIS and ASP.NET output caching, you'll see unbelievable performance results. At one point during the Visual Studio 2005 development of ASP.NET, I was the program manager responsible for ASP.NET performance. The developers did the magic, but I saw all the reports on a daily basis. The kernel mode caching results were always the most interesting. The common characteristic was network saturation by requests/responses and IIS running at about five percent CPU utilization. It was amazing! There are certainly other reasons for using IIS 6.0, but kernel mode caching is an obvious one.  Tip 9—Use Gzip CompressionWhile not necessarily a server performance tip (since you might see CPU utilization go up), using gzip compression can decrease the number of bytes sent by your server. This gives the perception of faster pages and also cuts down on bandwidth usage. Depending on the data sent, how well it can be compressed, and whether the client browsers support it (IIS will only send gzip compressed content to clients that support gzip compression, such as Internet Explorer 6.0 and Firefox), your server can serve more requests per second. In fact, just about any time you can decrease the amount of data returned, you will increase requests per second. The good news is that gzip compression is built into IIS 6.0 and is much better than the gzip compression used in IIS 5.0. Unfortunately, when attempting to turn on gzip compression in IIS 6.0, you may not be able to locate the setting on the properties dialog in IIS. The IIS team built awesome gzip capabilities into the server, but neglected to include an administrative UI for enabling it. To enable gzip compression, you have to spelunk into the innards of the XML configuration settings of IIS 6.0 (which isn't for the faint of heart). By the way, the credit goes to Scott Forsyth of OrcsWeb who helped me figure this out for the www.asp.net severs hosted by OrcsWeb. Rather than include the procedure in this article, just read the article by Brad Wilson at IIS6 Compression. There's also a Knowledge Base article on enabling compression for ASPX, available at Enable ASPX Compression in IIS. It should be noted, however, that dynamic compression and kernel caching are mutually exclusive on IIS 6.0 due to some implementation details. Tip 10—Server Control View StateView state is a fancy name for ASP.NET storing some state data in a hidden input field inside the generated page. When the page is posted back to the server, the server can parse, validate, and apply this view state data back to the page's tree of controls. View state is a very powerful capability since it allows state to be persisted with the client and it requires no cookies or server memory to save this state. Many ASP.NET server controls use view state to persist settings made during interactions with elements on the page, for example, saving the current page that is being displayed when paging through data. There are a number of drawbacks to the use of view state, however. First of all, it increases the total payload of the page both when served and when requested. There is also an additional overhead incurred when serializing or deserializing view state data that is posted back to the server. Lastly, view state increases the memory allocations on the server. Several server controls, the most well known of which is the DataGrid, tend to make excessive use of view state, even in cases where it is not needed. The default behavior of the ViewState property is enabled, but if you don't need it, you can turn it off at the control or page level. Within a control, you simply set the EnableViewState property to false, or you can set it globally within the page using this setting:
<%@ Page EnableViewState="false" %>
If you are not doing postbacks in a page or are always regenerating the controls on a page on each request, you should disable view state at the page level. ConclusionI've offered you some tips that I've found useful for writing high-performance ASP.NET applications. As I mentioned at the beginning of this article, this is more a preliminary guide than the last word on ASP.NET performance. (More information on improving the performance of ASP.NET apps can be found at Improving ASP.NET Performance.) Only through your own experience can you find the best way to solve your unique performance problems. However, during your journey, these tips should provide you with good guidance. In software development, there are very few absolutes; every application is unique. See the sidebar "Common Performance Myths". Rob Howard is the founder of Telligent Systems, specializing in high-performance Web apps and knowledge management and collaboration systems. Previously, Rob was employed by Microsoft where he helped design the infrastructure features of ASP.NET 1.0, 1.1, and 2.0. You can contact Rob at rhoward@telligentsystems.com.
Cache Object - Dino Esposito
One of the most compelling improvements that ASP.NET brought to ASP programming was the Cache object. The Cache has some similarities to the Application object and is a container of global data (as opposed to session-specific data) that features a fair number of innovative characteristics. At its core, the ASP.NET Cache is a sealed data container class built around a hashtable and defined in the System.Web.Caching namespace. The Cache object is central to the whole ASP.NET infrastructure. Various runtime components use the ASP.NET Cache to store working data. For example, the output of cached pages and controls is stored there. Likewise, applications that keep session state in memory use the ASP.NET Cache to store it. The Cache object is made up of two independent data stores—a public and a private cache. The private cache is reserved for system components and is accessed through a private API. The public cache is the one that is available to developers. In order to enable you to loop through the public store, the Cache class implements the IEnumerable interface. Unlike data stored in the Application object, items stored in the Cache object can have a number of additional attributes applied to them, including an expiration policy, a priority, and one or more dependencies. These attributes form the substrate of the advanced Cache features such as the ability to remove least-used items, have items expire at a given time, and set logical connections between a group of items and disk files. Like many other ASP.NET features, the Cache object has whetted the appetites of developers and given them more ideas for features to request in future versions. The most requested new Cache features are most likely support for database-driven data invalidation and custom dependencies. And if they aren't, they should be. The good news is that these two features will be available in ASP.NET 2.0 through a dazzling new class, SqlCacheDependency, which links cached items to database tables, and through the newly unsealed CacheDependency class. The SqlCacheDependency class detects changes to tables in SQL Server™ 7.0, SQL Server 2000, and SQL Server 2005 (formerly code-named "Yukon") and invalidates all dependent cache entries. In ASP.NET 2.0, CacheDependency has new virtual members and, more importantly, is unsealed and can be derived from. SqlCacheDependency derives from CacheDependency. In this column, I won't go through all the new caching features of ASP.NET 2.0, but I'll demonstrate how to implement similar custom cache dependencies in ASP.NET 1.x. A good understanding of cache dependencies is crucial if you are to create custom dependencies. So without further ado, let's start with a quick refresher of what a cache dependency is and how it works. What's a Cache Dependency, Anyway?
The whole ASP.NET cache dependency mechanism is encapsulated in the CacheDependency class. This class represents a relationship between a cached item and an array of objects like files, directories, and other cached objects. To establish a dependency between a cached item and an external component, you add the item to the ASP.NET Cache object using a specific overload of the Insert method, like so:
CacheDependency dep = new CacheDependency(fileName);
cache.Insert(key, value, dep);
In this code snippet, the item is fully identified by the key/value pair and its lifetime is bound to the timestamp of the specified file name. The item will automatically be removed from the Cache when the timestamp of the specified file changes. To add a new item to the Cache you can also use the Add method or the set accessor of the Item property:
Cache[key] = value;
The Add method is not overloaded and it differs from the Insert method in that it throws an exception if the specified item already exists. (Insert, on the other hand, will overwrite the existing item.) You should note that if you use the set accessor of the Item property to add a new item, the item is correctly inserted into the cache, but no dependency is created. As a result of this, the item will be removed only when the application shuts down or if the item is explicitly removed. A cached item can also be bound to other cached items instead of or in addition to file dependencies. The following code snippet shows how to accomplish this:
CacheDependency dep;
dep = new CacheDependency(fileNames, otherKeys);
Cache.Insert(key, value, dep);
When either the files or the other cached items change, the recently added cache object is invalidated and then removed. To make an item dependent only upon a cached item, you set the file name parameter to null. A cache dependency can be subordinate to another cache dependency. This feature is particularly useful for implementing cascading changes to stored items. So much for the programming interface of the CacheDependency class; now it's time to take a closer look at its internal implementation and interaction with the Cache data store.Overall, items are automatically removed from the cache based on time, file, and key dependencies. It is interesting to look at how each type of dependency is handled. Time and key dependencies are managed by the Cache object itself. Items with an expiration are associated with a timer and removed at the end of the specified countdown interval. Resolving key dependencies is just one of the many tasks important to the update of the cache. Whenever a write operation occurs, an internal update process fires and frees all of the items that have a broken dependency. The file change notification mechanism keeps track of file dependencies. It's an operating system function that various ASP.NET modules, including the HTTP runtime, use extensively. When a file dependency is created, the ASP.NET Cache starts to monitor that file or directory. Thanks to the capabilities of the OS, any change on a monitored resource results in an event raised to the Cache object. The handler of this internal event takes care of the removal of the linked item. That's the only way in ASP.NET 1.x to force the Cache to evict stored elements. This means that any form of custom dependency must be implemented in terms of file, time, or, more likely, key dependencies. So what's a custom cache dependency? It is primarily a class that listens to a data source for changes. When a change is detected, the class bubbles that change up to a particular stored cache item so that the item is evicted from the cache. This loose description can be translated into three actions that any custom cache dependency class should take in ASP.NET 1.x.
- Define the data source to listen to
- Define a channel with the Cache to bubble changes up
- Define a custom API
The first two points are common to all versions of ASP.NET. However, only ASP.NET 2.0 provides an API that will create and manage custom dependencies. Let's proceed with a quick look at ASP.NET 2.0 cache dependencies before taking the plunge into my ASP.NET 1.x implementation. Cache Dependencies in ASP.NET 2.0
As I mentioned, in ASP.NET 2.0 the CacheDependency class is unsealed and therefore inheritable. Subsequently, a custom dependency is simply a class that inherits from CacheDependency and implements a custom algorithm to detect changes in a given data source. Each detected change then causes an appropriate action to invalidate items in the cache. The use of inheritance guarantees that no breaking changes will ever be introduced in code ported from ASP.NET 1.x applications. In addition, there is no risk that your class misbehaves with the Cache object. The base class will handle all the wiring of the dependency object to the ASP.NET Cache object and all the issues surrounding synchronization and disposal. On the other hand, inheritance means that the memory footprint of your dependency class might be bigger than needed because your cache dependency class picks up all base class functionality, whether it needs it or not. Such functionality includes constructors that accept arrays of files or create dependencies on other cached items. A good example of a custom dependency class is the aforementioned SqlCacheDependency. It implements database dependencies by listening to table changes on SQL Server 7.0, SQL Server 2000, and SQL Server 2005. Figure 1 lists the new members added to the CacheDependency class to support custom dependencies. A custom dependency class relies on its parent for any needed interaction with the Cache object. The NotifyDependencyChanged method is called by classes that inherit from CacheDependency to tell the base class that the dependent item has changed. In response to this, the base class updates the values of the HasChanged and UtcLastModified properties. Any cleanup code needed when the custom cache dependency object is dismissed should go into the DependencyDispose method. The structure of a custom dependency object follows the pattern outlined in Figure 2. The class uses a timer to poll the data source at regular intervals and maintains a reference to the current data. When the data downloaded from the source is newer than the current copy, the corresponding item in the cache is invalidated. A call to the new NotifyDependencyChanged method breaks the internal dependency so that the Cache update process can evict the item. Figure 3 SqlCacheDependency with SQL Server 2005Note that in ASP.NET (no matter what the version) there are only two possible models for detecting changes—polling and notification. Polling means that the dependency class will periodically check the data source for changes. The notification mechanism is based on an external component that pushes changes to the dependency class in an asynchronous manner. A good example is the aforementioned file change notification mechanism. In ASP.NET 2.0, an implementation of the notification model is represented by the SqlCacheDependency class when it is connected to a SQL Server 2005 database. The overall picture is shown in Figure 3. The following is the code to set up the dependency:
// data is a DataTable filled using this same command
SqlCommand cmd = new SqlCommand(
"SELECT * FROM Customers WHERE country='USA'", conn);
SqlCacheDependency dep = new SqlCacheDependency(cmd);
Cache.Insert("SqlSource", data, dep);
In this case the SqlCacheDependency class listens to data coming from a SQL Server 2005 component—the Notification Delivery Service. The service monitors all the tables involved with the given query and invokes a callback function whenever something happens that modifies the resultset generated by the query. Internally, SqlCacheDependency exploits the new notification feature of SQL Server 2005 and runs code like the following:
// cmd is the SqlCommand object defined above
SqlDependency dep = new SqlDependency(cmd);
dep.OnChanged += new OnChangedEventHandler(OnDepChanged);
The callback function defined by SqlCacheDependency—the OnDepChanged method—calls NotifyDependencyChanged to invalidate the corresponding cache entry. Custom Cache Dependencies in ASP.NET 1.x
At its core, a custom cache dependency is a class that incorporates a timer to periodically check the data source or that listens for event notifications pushed to it. You must link an instance of this class to a cached item. Because the CacheDependency class is sealed in ASP.NET 1.x, you can't rely on the Cache.Insert method to establish this link automatically. In addition, an instance of the custom dependency class must be stored in the ASP.NET Cache to survive for the application's lifetime. When the dependency class detects a change on the data source, it must do something to invalidate the cached item. To do so, you can only use one of the base ASP.NET 1.x expiration techniques—time, file, or key. Time just doesn't apply here. File or key dependencies are both acceptable and I don't see a reason to create or edit a temporary file. So I'll associate each item that has a custom dependency with an additional helper entry. By modifying the helper entry on changes to the monitored data source, you'll automatically invalidate the cached item. A new API is needed to transparently create the helper entry upon insertion of the item with a custom dependency:
AmazonBooksCacheDependency dep =
new AmazonBooksCacheDependency(key, author);
CacheHelper.Insert(key, dataSet, dep);
The AmazonBooksCacheDependency class represents a dependency on the list of books written by a given author. Some of the information from this data source is relatively static (books, titles, publishers); some other data, however, may vary on a regular, even daily basis. Price, sales rank, reviews, and rating are parameters that may change more than once a day. The previous code sample inserts a DataSet with book information into the ASP.NET cache and makes it dependent on the raw information published on the Amazon Web site. Whenever new information is posted, the cached DataSet is invalidated and refreshed. You can't just pass an instance of the AmazonBooksCacheDependency class to the Cache.Insert method. That's why I have written a helper method: CacheHelper.Insert. As you can see in Figure 4, the overall programming interface is nearly identical to that of ASP.NET 1.x for standard dependencies and to that of ASP.NET 2.0 for custom dependencies. Figure 4 shows the source code of the helper API to cache items with a custom dependency. CacheHelper is a class with a couple of static methods, the most important of which is Insert:
static void Insert(string key, object value, MsdnMag.CacheDependency dep)
This method wraps the overload of the Cache's Insert method that takes a CacheDependency object. The Insert method accepts a custom dependency object and does some extra work. Each new cache entry is paired with a second key whose name is programmatically built by concatenating the unique name of the entry with a standard (but arbitrary) string:
internal static string GetHelperKeyName(string key)
{
return key + ":MsdnMag:CacheDependency";
}
The GetHelperKeyName is marked as internal (called friend, in Visual Basic® .NET) because I want it to be accessible from within other classes defined in the same assembly, but I don't want it to be callable from just any client. The Insert method in the CacheHelper class does two key things. First, it creates a helper key and stores the custom cache dependency object in it. The helper key is needed to convey notification changes to the principal item to which it is linked. Once placed in the cache, the custom dependency object is up and running all the time and can periodically query the data source looking for changes. When a change is detected, the custom dependency object updates its last modified date property in the cache. In doing so, it breaks the standard dependency between the helper item and the base item. As a result, the base item is evicted from the cache based on a change on an external data source. Figure 5 contains the source code of the MsdnMag.CacheDependency class. The class is built around a timer and an abstract method—HasChanged. The timer is instantiated in the constructor and executes a given callback method at specified intervals whose length is controlled by the Polling protected member. Polling indicates the extent of the interval in seconds, whereas the .NET Framework timers count in terms of milliseconds. The constructor of the CacheDependency custom class requires the name of the dependent key. This information is essential for establishing a link between the dependency object and the dependent item. With default ASP.NET dependencies (in both 1.1 and 2.0) this link is implicitly created by the Cache.Insert method when a cache entry is created or updated. The name of the linked item is stored in the DependentStorageKey protected property. Finally, the UtcLastModified date member contains the time of the last update to the dependency. When a change in the data source is detected, an update to this property triggers the eviction of the linked item from the cache. Setting Up the Timer
As mentioned, there are two basic ways for a dependency object to know about changes in a monitored resource—polling and notification. Basically, either the dependency object sets up a timer and executes a method at specified intervals or registers to receive notifications about changes from an external module. The availability of a notification service is specific to the data source you're working with. SQL Server 2005 and Windows® have one for query and file changes, respectively. If you're going to create a dependency based on a message queue (for example, MSMQ), then a notification model might be appropriate to implement. For the sample dependency object covered in this column I'll use the polling model and implement it through a timer:
if (InternalTimer == null)
{
TimerCallback func = new TimerCallback(InternalTimerCallback);
int ms = Polling*1000;
InternalTimer = new Timer(func, this, ms, ms);
}
The timer is an instance of the System.Threading.Timer class. The core of the timer is the delegate for the method that will be called periodically. In addition, the timer's constructor lets you specify a due time and a period. The due time is the time to wait before the first execution of the method; the period is the time to wait between subsequent executions of the callback method. It is important to note that you must keep a reference to the timer alive for as long as you use the object. Like any other managed object, the timer is subject to garbage collection if it goes out of scope. The timer callback is a TimerCallback delegate:
public delegate void TimerCallback(object state);
The parameter indicates an object containing specific information relevant to the method. The callback doesn't execute in the thread that created the timer; instead it executes in a separate thread that is provided by the system. The following code snippet illustrates the default implementation of the timer callback for a custom dependency object:
private void InternalTimerCallback(object sender)
{
// CacheDependency is the custom dep object
CacheDependency dep = (CacheDependency) sender;
if (dep.HasChanged())
NotifyDependencyChanged(dep);
}
The callback calls the HasChanged method on the dependency class and based on the return value invokes the NotifyDependencyChanged method to break the dependency with the cache item. The NotifyDependencyChanged method I wrote belongs to the ASP.NET 1.x custom dependency class, but I named it after the equivalent method in ASP.NET 2.0 just to help you get familiar with ASP.NET 2.0 features more quickly. The method retrieves the name of the helper key and modifies its content. The content is just the dependency object modified to reflect the date of the last update:
dep.UtcLastModified = DateTime.Now;
HttpRuntime.Cache.Insert(key, dep);
Since this is running on a background thread, in this case you must use the HttpRuntime object to get a valid reference to the ASP.NET Cache. If you try to reach the Cache through the current HttpContext object—what you normally would do from within a codebehind class—you'll run into a null exception:
HttpContext.Current.Cache.Insert(key, dep);
This line of code is equivalent to calling Cache through HttpRuntime but only within the context of a request, which is not necessarily the case in this instance.The NotifyDependencyChanged method is invoked by the timer callback, and the timer works independently of page requests. Basically, the dependency class, once placed in the ASP.NET Cache, remains active and running as long as the application lives, as does the timer it incorporates. The timer lives in the context of an ASP.NET application, but not in the context of a particular HTTP request. For this reason, HttpContext.Current just returns null. However, this doesn't mean that the Cache object is unavailable or unreachable. You simply have to take the right route to it; you have to use the HttpRuntime.Cache property. Creating a Web Service Dependency
As you can see, there's a difference between the ASP.NET 2.0 customized dependency objects and my sample class. In ASP.NET 2.0, the base class doesn't incorporate any timer; each derived class can create its own timer if needed. In this ASP.NET 1.x implementation, I tried to reduce the amount of code that characterizes a particular dependency. I've accomplished this by deriving a new class from MsdnMag.CacheDependency and overriding the abstract HasChanged method:
protected abstract bool HasChanged();
The method is expected to check the monitored data source and return true if changes have occurred. Let's put it all together and write a custom dependency that gets broken if the output of a Web service method changes. (Rob Howard presented a similar ASP.NET 2.0 example at the Microsoft PDC 2003. You can find slides and source code on the ASP.NET site at http://www.asp.net/whidbey/downloads/WSV330_rhoward_demos.zip.) I'll build a dependency class to monitor books available on Amazon. In general, imagine you have an application that needs to work with relatively static data—data that changes, but not as frequently as to justify reading it back at every postback. So you download it the first time and place it in the ASP.NET Cache. How do you deal with changes? Well, if data is known to change often by your standards, you should use a time dependency that guarantees that your data is updated at regular intervals. If the frequency of changes is low, you can opt for a custom dependency—whenever the data changes on the server, your cached snapshot is automatically invalidated. When the cache entry is accessed next, it returns null and you know that it's time for you to get the records from the server. As you'll see in a moment, my sample code goes beyond this and automatically replaces the data in the cache. Figure 6 shows the full source code of the AmazonBooksCacheDependency class. The class inherits CacheDependency and defines two constructors—one takes the author's name, and one takes both a name and a poll time. The author's name is a parameter specific to the task of this dependency. The HasChanged method downloads book information for a given author and compares that to the snapshot currently stored in the cache. If the two don't match, the method returns false and the current cached item is freed:
private void Refresh()
{
// Is the author cached already?
string author = AuthorName.Text;
if (!IsAuthorCached(author))
PrepareCacheForAuthor(author);
BindData(author);
}
When the user clicks the Refresh button, the code verifies that the books of the specified author have been cached. If not, a new entry in the cache is prepared:
private void PrepareCacheForAuthor(string author)
{
string key = GetAuthorKey(author);
AmazonBooksCacheDependency dep;
dep = new AmazonBooksCacheDependency(key, author);
// Create the cache entry
CacheHelper.Insert(key, GetBooksInfo(author), dep);
}
GetBooksInfo uses the Amazon Web service to grab up-to-date book information. The data is an XML string that can be easily loaded into a DataSet. It is stored as a string in an ASP.NET Cache item named after the author. As mentioned, CacheHelper.Insert adds two keys to the cache—one with the actual data and one with the dependency object. The dependency object checks the Amazon Web service periodically and invalidates the cached item when changes have been detected. BindData uses a special method to read from the cache, as shown here:
string ReadFromCacheForAuthor(string author)
{
string key = GetAuthorKey(author);
string data = (string) Cache[key];
if (data == null || data == string.Empty) {
data = GetBooksInfo(author);
Cache[key] = data;
}
return data;
}
If the contents of the cached item is null or empty, the method refills it with a new call to the Web service. Note that in order to develop with the Amazon Web service you must get a developer token, which you can apply for at http://xml.amazon.com. You'll also find licensing and reuse information there. It is worth noting that the solution outlined so far works, but could be improved upon. The key problem is with the implementation of the HasChanged method. In the Amazon example, book information is retrieved at every timer interval and compared to the contents currently stored in the cache. If a change is detected, the contents of the cache are freed and a second download occurs with the next read from the cache. As I've learned from sample implementations in ASP.NET 2.0, you should design the Web service to expose information about data changes through a simple Boolean method. A round-trip is still needed, but this trick will significantly reduce the amount of data being moved over the wire. ASP.NET 2.0 applies this pattern to database dependencies. Triggers set on monitored tables write a flag on a small helper table whenever a change is recorded. The helper table has just a few records—one for each monitored table in a database. The timer callback queries the small table looking for all records with the flag set. The result is guaranteed and the performance is optimal. If you don't have control over the Web service (which I don't have with Amazon), then I suggest a slightly different approach. Modify the HasChanged method so that it replaces data in the cache when changes are detected. In addition, have it always return false to prevent the custom dependency object from breaking the link and invalidating the data. This guarantees that no double read is needed when changes occur. Database Dependencies in ASP.NET 1.x
Database dependencies are not supported in ASP.NET 1.1, but Jeff Prosise demonstrated how to achieve them in the April 2003 issue of MSDN® Magazine. The idea is that you define a trigger on a SQL Server table and configure it to monitor insertions, deletions, and updates. When fired, the trigger calls an extended stored procedure to modify a disk file. If you set a dependency between a cached item and this file, the ASP.NET Cache will dispose of it whenever the SQL Server table undergoes some changes. This solution can be reworked and optimized a bit in light of the ASP.NET 2.0 code. For example, you can create a helper table to record changes and a second cache entry to invalidate the cached data whenever changes to your data occur. Conclusion
Once again, I have presented some code you can use now which is a much simplified version of code that will be completely built when ASP.NET 2.0 is released. You might as well get the functionality you want today, and you'll be ahead of the curve in understanding ASP.NET 2.0 tomorrow.
Date and Time Format Patterns
| All the patterns: |
|---|
| 0 | MM/dd/yyyy | 11/06/2006 | | 1 | dddd, dd MMMM yyyy | Monday, 06 November 2006 | | 2 | dddd, dd MMMM yyyy HH:mm | Monday, 06 November 2006 02:09 | | 3 | dddd, dd MMMM yyyy hh:mm tt | Monday, 06 November 2006 02:09 AM | | 4 | dddd, dd MMMM yyyy H:mm | Monday, 06 November 2006 2:09 | | 5 | dddd, dd MMMM yyyy h:mm tt | Monday, 06 November 2006 2:09 AM | | 6 | dddd, dd MMMM yyyy HH:mm:ss | Monday, 06 November 2006 02:09:50 | | 7 | MM/dd/yyyy HH:mm | 11/06/2006 02:09 | | 8 | MM/dd/yyyy hh:mm tt | 11/06/2006 02:09 AM | | 9 | MM/dd/yyyy H:mm | 11/06/2006 2:09 | | 10 | MM/dd/yyyy h:mm tt | 11/06/2006 2:09 AM | | 11 | MM/dd/yyyy HH:mm:ss | 11/06/2006 02:09:50 | | 12 | MMMM dd | November 06 | | 13 | MMMM dd | November 06 | | 14 | yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffffK | 2006-11-06T02:09:50.0259680-05:00 | | 15 | yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffffK | 2006-11-06T02:09:50.0259680-05:00 | | 16 | ddd, dd MMM yyyy HH':'mm':'ss 'GMT' | Mon, 06 Nov 2006 02:09:50 GMT | | 17 | ddd, dd MMM yyyy HH':'mm':'ss 'GMT' | Mon, 06 Nov 2006 02:09:50 GMT | | 18 | yyyy'-'MM'-'dd'T'HH':'mm':'ss | 2006-11-06T02:09:50 | | 19 | HH:mm | 02:09 | | 20 | hh:mm tt | 02:09 AM | | 21 | H:mm | 2:09 | | 22 | h:mm tt | 2:09 AM | | 23 | HH:mm:ss | 02:09:50 | | 24 | yyyy'-'MM'-'dd HH':'mm':'ss'Z' | 2006-11-06 02:09:50Z | | 25 | dddd, dd MMMM yyyy HH:mm:ss | Monday, 06 November 2006 02:09:50 | | 26 | yyyy MMMM | 2006 November | | 27 | yyyy MMMM | 2006 November |
| The patterns for DateTime.ToString ( 'd' ) : |
|---|
| 0 | MM/dd/yyyy | 11/06/2006 |
| The patterns for DateTime.ToString ( 'D' ) : |
|---|
| 0 | dddd, dd MMMM yyyy | Monday, 06 November 2006 |
| The patterns for DateTime.ToString ( 'f' ) : |
|---|
| 0 | dddd, dd MMMM yyyy HH:mm | Monday, 06 November 2006 02:09 | | 1 | dddd, dd MMMM yyyy hh:mm tt | Monday, 06 November 2006 02:09 AM | | 2 | dddd, dd MMMM yyyy H:mm | Monday, 06 November 2006 2:09 | | 3 | dddd, dd MMMM yyyy h:mm tt | Monday, 06 November 2006 2:09 AM |
| The patterns for DateTime.ToString ( 'F' ) : |
|---|
| 0 | dddd, dd MMMM yyyy HH:mm:ss | Monday, 06 November 2006 02:09:50 |
| The patterns for DateTime.ToString ( 'g' ) : |
|---|
| 0 | MM/dd/yyyy HH:mm | 11/06/2006 02:09 | | 1 | MM/dd/yyyy hh:mm tt | 11/06/2006 02:09 AM | | 2 | MM/dd/yyyy H:mm | 11/06/2006 2:09 | | 3 | MM/dd/yyyy h:mm tt | 11/06/2006 2:09 AM |
| The patterns for DateTime.ToString ( 'G' ) : |
|---|
| 0 | MM/dd/yyyy HH:mm:ss | 11/06/2006 02:09:50 |
| The patterns for DateTime.ToString ( 'm' ) : |
|---|
| 0 | MMMM dd | November 06 |
| The patterns for DateTime.ToString ( 'r' ) : |
|---|
| 0 | ddd, dd MMM yyyy HH':'mm':'ss 'GMT' | Mon, 06 Nov 2006 02:09:50 GMT |
| The patterns for DateTime.ToString ( 's' ) : |
|---|
| 0 | yyyy'-'MM'-'dd'T'HH':'mm':'ss | 2006-11-06T02:09:50 |
| The patterns for DateTime.ToString ( 'u' ) : |
|---|
| 0 | yyyy'-'MM'-'dd HH':'mm':'ss'Z' | 2006-11-06 02:09:50Z |
| The patterns for DateTime.ToString ( 'U' ) : |
|---|
| 0 | dddd, dd MMMM yyyy HH:mm:ss | Monday, 06 November 2006 02:09:50 |
| The patterns for DateTime.ToString ( 'y' ) : |
|---|
| 0 | yyyy MMMM | 2006 November |
Send E-Mail from your .NET application using your GMail Account
ASP.NET Tip: Sending Mail with ASP.NET 2.0
Rating: none
In ASP.NET 2.0, Microsoft deprecated the System.Web.Mail namespace and replaced it with System.Net.Mail. The new library introduces some new features, but it also includes some bugs in how mail is sent. Before discussing some of these in detail, let's go through some code sample (which assumes you've added a using System.Net.Mail at the top of the file):
MailMessage msg = new MailMessage(); msg.From = new MailAddress("address@domain.com", "Person's Name"); msg.To.Add(new MailAddress("destination@domain.com", "Addressee's Name"); msg.To.Add(new MailAddress("destination2@domain.com", "Addressee 2's Name"); msg.Subject = "Message Subject"; msg.Body = "Mail body content"; msg.IsBodyHtml = true; msg.Priority = MailPriority.High; SmtpClient c = new SmtpClient("mailserver.domain.com"); c.Send(msg);
The code is similar with some minor changes to how you address the message. Instead of constructing an address, you can let the system do that for you. If you specify an e-mail address and a name, it will automatically display in the message as this:
"Person's Name" <destination@domain.com>
This is the "proper" form for an e-mail address. You can add multiple addresses to the To, CC, and BCC collections in the same way as shown above. This is programmatically easier to do than sending lots of messages—just add multiple addresses to the BCC property in order to send a mass mailing.
As previously mentioned, this new namespace has a couple of bugs. The first is when you send a message the headers are all added in lowercase letters. While the RFC for SMTP mail doesn't specify how the headers should be capitalized, many spam filters restrict messages where the headers are not properly capitalized.
The other bug deals with the Priority setting, which should mark a message as important within the mail client. Because of the way the header is formatted, my mail program (Eudora) doesn't recognize it as the priority flag and doesn't mark the message as important. While this seems trivial, it's a change from the System.Web.Mail version for no apparent reason. I'm continuing to research this and if I can't find a good fix, I may switch back to System.Web.Mail and deal with the warning messages that Visual Studio displays about System.Web.Mail being deprecated.
Eric Smith is the owner of Northstar Computer Systems, a Web-hosting company based in Indianapolis, Indiana. He is also a MCT and MCSD who has been developing with .NET since 2001. In addition, he has written or contributed to 12 books covering .NET, ASP, and Visual Basic. Send him your questions and feedback via e-mail at questions@techniquescentral.com.
Moving from Full Trust to partial trust with Code Access Security
What do you do if your application has been developed using FullTrust, but you now find that it is necessary to move it to partial trust?
This could arise from a risk assessment that requires mitigation against code injection attacks. An example of such an attack would be to insert a web page into an Asp.Net application using sql injection, although there are many other vectors that could get bad (malicious or poorly written) code onto a web server.
So, after all the development and testing that's taken place, you find that altering the <trust /> element from Full to, say, Medium stops the application working. Perhaps it's so bad that you don't even get a detailed error message - just a line saying that the application requires more permissions than are granted by the security policy.
Well, I've had to move applications to partial trust for a number of clients. The basic guide is that you must try to make your .aspx pages as dumb as possible.
This means that Asp.Net pages should only contain code that relates to the user interface. Anything else (that accesses server resources such as files or a database etc.) should be moved to assemblies. Place these assemblies into the GAC, so that they have full trust.
Next, create a business facade layer. A business facade roughly corresponds to use case diagrams for your system. The facade exposes methods that embody all interaction between the user and the system. Place this code into the GAC also.
The facade methods will have Assert statements that stop the stack walk progressing further. This means that your web app won't be stopped by CAS Demands due to the lower trust level.
Create a custom permission. Make demands for this permission within the business facade methods (remember - always combine an Assert with a Demand to ensure that the component is not being called by bad code or the wrong application).
Now add the custom permission to the Asp.Net permission set for the web application in the security config file. This ensures that only this web app can call the business facade, rather than another web app or web service on the same server.
You've now got a situation where CAS will only allow a specified web app to call the business facade. Supposing malicious code is placed onto the server, it can only call the business facade methods, which should not really let it do much more than use the intended system functionality anyway.
If you've cut down the permissions declared in the Asp.Net permission set, then other server resources are locked down so that they can't be called directly by malicious code in this web app.
Although CAS can be applied in different ways, this pattern fits with the advice in the 'Improving Web Application Security' document created by Patterns and Practices.
Introduction
I have developed ASP and ASP.NET sites for many years and one of the most common
end user problems (apart from basic stupidity ;-) is that while the user is entering
information into a web form or HTML edit box, the session timeout period will elapse
and they lose all the work they have done.
I have tried solutions such as making JavaScript alert the user to click a button or
refresh page, but this has restrictions, especially if they are not able to submit the
form yet due to required field limitations.
Solution
I recently came across some code which attempted to fix this problem but that was
unsuccessful because the author had forgotten the issue of client side caching.
Add to your page the following code:
private void Page_Load(object sender, System.EventArgs e) { this.AddKeepAlive(); }
Collapseprivate void AddKeepAlive() { int int_MilliSecondsTimeOut = (this.Session.Timeout * 60000) - 30000; string str_Script = @" <script type='text/javascript'> //Number of Reconnects var count=0; //Maximum reconnects setting var max = 5; function Reconnect(){
count++; if (count < max) { window.status = 'Link to Server Refreshed ' + count.toString()+' time(s)' ;
var img = new Image(1,1);
img.src = 'Reconnect.aspx';
} }
window.setInterval('Reconnect()',"+ _ int_MilliSecondsTimeOut.ToString()+ @"); //Set to length required
</script>
";
this.Page.RegisterClientScriptBlock("Reconnect", str_Script);
}
This code will cause the client to request within 30 seconds of the session timeout
the page Reconnect.aspx.
The Important Part
Now this works the first time but if the page is cached locally then the request is
not made to the server and the session times out again, so what we have to do is specify
that the page Reconnect.aspx cannot be cached at the server or client.
This is done by creating the Reconnect.aspx page with this content:
<%@ OutputCache Location="None" VaryByParam="None" %> <html> </html>
The OutputCache directive prevents this from being cached and so the
request is made each time. No code behind file will be needed for this page so no
@Page directive is needed either.
And that's it.
Accessing Embedded Resources through a URL using WebResource.axd
By Scott Mitchell
Introduction
Many of the built-in ASP.NET server controls require additional, external resources in order to function properly. For example,
when using any of the ASP.NET validation controls, the
controls rely on a bevy of JavaScript functions to perform their client-side validation. While each validation control could
emit such script directly into the page's content, a more efficient approach would be to package these JavaScript functions into
an external JavaScript file and then include that file in the page using <script src="PathToExternalJavaScriptFile" type="text/javascript" >.
This would reduce the total page size and would allow the browser to cache the external JavaScript file (rather than having to
send the JavaScript code down to the browser on each and every page visit/postback).
Prior to ASP.NET 2.0, such external resources that needed to be accessible to the visitor's browser had to be implemented as
actual files on the file system. If you've worked with ASP.NET 1.x's validation controls, your pages have included a reference
to a JavaScript file /aspnet_client/system_web/version/WebUIValidation.js and there is an actual file
with that name residing in the web application's root. Such external resources hamper deployment - if you deploy your
application from the testing server to production, it's imperative that the production server have the same external resources
(WebUIValidation.js, in this case), in the same locations in the file system.
To remedy this, ASP.NET 2.0 allows for external resources to be embedded within the control's assembly and then be accessed
through a specified URL. With the external images, JavaScript files, CSS files embedded in the control's assembly,
deployment is a breeze, as all of the resources are now contained within the assembly (the .dll file).
There are no external resources whose file names and location on the file system must map up. Once embedded into the assembly,
these resources can be accessed from an ASP.NET 2.0 web page through a special URL (WebResource.axd).
In this article we'll examine how to embed external resources into an assembly and how to retrieve these resources on the web
page using a special URL. This technique helps simplify installation and deployment for creating custom, compiled server
controls that are shipped to customers. Read on to learn more!
When working with a lengthy web page that spans many vertical “pages,” scrolling down and then doing something that causes a postback results with the posted back page sitting back up at the top rather than auto-scrolling down to the location where the postback was triggered. With ASP.NET 1.x, this problem can be easily overcome with a bit of JavaScript and server-side code. See Maintaining Scroll Position on Postback for more information.
ASP.NET 2.0 makes maintaining scroll position on postback much easier, although it's not a well-document or oft-discussed feature. When I show this little tip at User Group talks, to clients, or to my class, I'd say more than 90% of the people didn't know about this, are impressed, and wonder aloud, “Why isn't this common knowledge?”
Anyway, to have a single page remember scroll position on postback, simply set the MaintainScrollPositionOnPostback attribute in the @Page directive to True. That's it! The page will then automagically inject the needed JavaScript and server-side logic to make this feature a reality.
<%@ Page Language="..." MaintainScrollPositionOnPostback="true" ... %>
Alternatively, you can enable this feature for all pages in the site by setting it in the <pages> element in Web.config.
<pages maintainScrollPositionOnPostBack="true" />
See my article Client-Side Enhancements in ASP.NET 2.0 for more information on this feature, as well as additional client-side features added to ASP.NET 2.0!
Recipe: Deploying a SQL Database to a Remote Hosting Environment (Part 1)
Scenario: You finish building a great ASP.NET application, have everything tested and working right on your local system, are taking full advantage of the new ASP.NET 2.0 Membership, Role and Profile features, and are ready to publish it to a remote hosting environment and share it with the world. Copying the .aspx files and compiled assemblies to the remote system is pretty easy (just FTP or copy them up). The challenge that confronts a lot of developers, though, is how to setup and recreate the database contents - both schema and data - on the remote hosted site. Unfortunately there hasn't historically been a super-easy way to accomplish this. The good news is that this week the SQL Server team published the release candidate of a new SQL Server Hosting Toolkit that will make it much, much easier to deploy your SQL solutions remotely to a hosted environment. The toolkit allows you to work with SQL Express, SQL Server 2000, and SQL Server 2005 databases locally, and then easily transfer your schema and data and install them into a shared hosting remote SQL Server account. The below post describes how you can start using this today.
Free refactoring tools for ASP.NET code in Visual Studio 2005
DevExpress today announced the availability of Refactor! for ASP.NET -- a free add-on to Visual Studio 2005 that enables very cool refactoring capabilities for ASP.NET code. The add-on can be downloaded from here:
http://www.devexpress.com/refactorasp
The add-on includes the following ASP.NET refactorings:
- Add Validator
- Extract ContentPlaceHolder
- Extract ContentPlaceHolder (and create master page)
- Extract Style (Class)
- Extract Style (id)
- Move to Code-behind
- Move Style Attributes to CSS
- Rename Style
- Surround with Update Panel
Sorting the Visual Studio "Add New Item" Dialog with PowerShell
The items in the "Add New Item" dialog box of Visual Studio appear in an arbitrary order. After a bit of sleuthing, I put together a brute force Powershell script to sort my items alphabetically. Now "Code File" appears near "Class", and I can always find "XML File" near the bottom of the dialog.
SortOrder, and my sanity, is restored.
What follows is the script. Download sort-vsItems.ps1. Let me caveat this script by saying it has only been tested on two machines. If you have any problems, do let me know.
# sort-vsItems # scott@OdeToCode.com # Use at your own risk! # Script will make a backup of each template ... just in case...
#vjslib for .zip support [System.Reflection.Assembly]::Load( "vjslib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" ) | out-null
# Get list of VS installed templates $installDir = (gp HKLM:Software\Microsoft\VisualStudio\8.0).InstallDir $installDir += "ItemTemplates\" $templateFiles = gci -recurse $installDir | ? {$_.extension -eq ".zip"}
# Append list of custom templates $installDir = $home $installDir += '\My Documents\Visual Studio 2005\Templates\ItemTemplates' $templateFiles += gci -recurse $installDir | ? {$_.extension -eq ".zip"}
$templateFiles = $templateFiles | sort name
$i = 1 $count = 0 $tmpDir = new-item ([System.IO.Path]::GetRandomFileName()) -type directory $buffer = new-object System.SByte[] (8192)
foreach($templateFile in $templateFiles) { write-host "Processing" $templateFile.FullName #extract all files (no methods appear available to modify zip in place) $zip = new-object java.util.zip.ZipFile($templateFile.FullName) $entries = $zip.entries() while($entries.hasMoreElements()) { $zipEntry = $entries.nextElement() $in = $zip.getInputStream($zipEntry) $out = new-object java.io.FileOutputStream( $tmpDir.FullName + "\" + $zipEntry.getName() ) while(($count = $in.read($buffer, 0, $buffer.Count)) -gt 0) { $out.write($buffer, 0, $count) } $out.Close() $in.Close() } $zip.Close()
#tweak the sort order element $vst = gci -recurse $tmpDir | ? {$_.extension -eq ".vstemplate"}
$xmlDoc = new-object System.Xml.XmlDocument $xmlDoc.Load($vst.FullName) if($xmlDoc.VSTemplate.TemplateData.SortOrder -ne $null) { $xmlDoc.VSTemplate.TemplateData.SortOrder = ($i++).ToString() $xmlDoc.Save($vst.FullName) }
#backup existing zip file $backupName = $templateFile.FullName + ".bak" if(test-path $backupName) { remove-item $backupName } move-item $templateFile.FullName $backupName
#zip up modified version $files= gci -recurse $tmpDir $zip = new-object java.util.zip.ZipOutputStream( new-object java.io.FileOutputStream( $templateFile.FullName)) foreach($file in $files) { $zipEntry = new-object java.util.zip.ZipEntry($file.Name) $zip.putNextEntry($zipEntry) $in = new-object java.io.FileInputStream($file.FullName) while(($count = $in.read($buffer, 0, $buffer.Count)) -gt 0) { $zip.write($buffer, 0, $count) } $in.close() $zip.closeEntry() } $zip.close() del $tmpDir\* }
del $tmpDir -force -recurse
write-host "Running Visual Studio to refresh templates" $vstudio = (gp HKLM:Software\Microsoft\VisualStudio\8.0).InstallDir & $vstudio\devenv /setup
Powershell is beauty!
TRULY Understanding ViewState
ViewState is a very misunderstood animal. I would like to help put an end to the madness by attempting to explain exactly how the ViewState mechanism works, from beginning to end, and from many different use cases, such as declared controls vs. dynamic controls. There are a lot of great articles out there that try to dispel the myths about ViewState. You might say this is like beating a dead horse (where ViewState is the horse, and the internet is the assailant). But this horse isn't dead, let me tell you. No, he's very much alive and he's stampeding through your living room. We need to beat him down once again. Don't worry, no horses were harmed during the authoring of this article.
DOM events in the Microsoft AJAX Library
In previous CTPs, the client-side DOM event model was the IE model. You would use attachEvent and get the event data from window.event. In other words, we had just implemented the IE model in Firefox and Safari. This didn't fly as well as we expected for a number of reasons. For instance, it wasn't very well received from a philosophical point of view: making standards-compliant browsers behave like the one non-compliant browser was interpreted by some as a malicious attempt by the Evil Empire to undermine the standardization of the Web by enforcing proprietary APIs. It wasn't. It just seemed at the time like a smart way to build cross-browser compatibility and the reason we did it this way and not the other way around is that both Safari and Firefox have extensible DOM Element prototypes whereas IE doesn't. In other words, there was no way we could make IE behave like the standard, but we could make the others behave like IE. Any other way to make a library cross-browser has to introduce a third API that abstracts the standard and proprietary APIs. This third API is of course just as proprietary as the IEism, whereas our previous approach had the advantage of not introducing a new one. Still, the implementation was fairly complex and relied on the presence of extensibility points that we had no guarantee we would find on other browsers that we may want to support in the future. Another problem with our first implementation was its reliance on the server to detect browsers and selectively send the compat scripts to the client. So we decided to change our compat layer and come back to a more conventional approach that will be easier to adapt to new browsers and that doesn't rely on the server, let alone on browser detection. The new model for DOM events is thus introducing a new API, but at least it's closely modeled after the standard APIs so it should feel pretty familiar. There are many differences in the implementations of DOM events that we needed to abstract. The first one is in the names of the methods that you call to add an event. In standard browsers, you use add/removeEventListener, in IE it's attach/detachEvent. The event names themselves are different: "click" is "onclick" in IE. Then, you have to abstract the signature of the event handlers themselves: in IE the parameters come from the global window.event object, in other browsers they are passed as a parameter. Finally, the contents of the event parameter object are themselves widely divergent from one browser to the other: mouse buttons don't have the same values for example, and some very useful stuff like mouse positions is missing altogether from the standard. Here's how you register a click event handler in the AJAX Library now: $addHandler(myDomElement, "click", someFunction);
As you can see, we use the standard event name here. $addHandler is an alias for Sys.UI.DomEvent.addHandler. You can unhook an event using $removeHandler. For instance, you should do that from your dispose methods to break circular references between your JavaScript objects and the DOM and to prevent memory leaks. From $addHandler, we wrap your function pointer into a closure that will be what will really be attached as the DOM event handler to abstract browser differences. What's nice is that you don't need to worry about that, you just provide a function that takes the event parameters object as its argument, and you write this function exactly the way you would write it for a standard-compliant browser. That means that even in IE, the event parameter object will contain standard fields: the key codes will be the right ones, as will be the mouse button values. By the way, so that you don't need to use integers when testing keys and mouse buttons, we have two enums, Sys.UI.Key and Sys.UI.MouseButton, that you can use in your event handlers: function myClickHandler(e) {
if (e.button === Sys.UI.MouseButton.leftButton) {
//...
}
}
function myKeyUpHandler(e) {
if (e.keyCode === Sys.UI.Key.enter) {
//...
}
}
From the event handler, it's worth mentioning what the "this" pointer means. Just like in a standard event handler, it represents the DOM element the event was attached to, not necessarily the element that triggered the event. Those are different if the event bubbled up. For example, you may have subscribed to the click event of a div element and what was really clicked was a button inside of it. In this case, "this" represents the div, not the button, but you can still get to the button using the target field of the event parameter object. Now, if you're wiring events from a component, chances are you're using delegates as your handler functions. In this case, "this" still refers to your component, not to any DOM element. One more thing to note is that the native, proprietary event object can still be got from the rawEvent field of the event parameter object.
Another "interesting" divergence is the way you cancel an event or prevent it from bubbling up. In IE, you set returnValue to false (resp. set cancelBubble to true), whereas the standard is to call preventDefault (resp. stopPropagation). The event parameter object that you get as the argument of your handler has the two standard methods (preventDefault and stopPropagation) so you can use them without having to worry about IE.
The last things I'd like to show on the new DOM event model are some of the helpers we've added to make component developers' lives easier. In a control or behavior, you typically have to wire up multiple handlers. For example, an accessible hover behavior might want to subscribe to mouseover, mouseout, focus and blur. To do that, you'd typically create delegates to your handlers and then wire up these delegates to the DOM events one by one. From your "dispose" method, you'd also have to remove those handlers one by one and get rid of the delegates. Seeing that this pattern was repeated over and over again in almost any control or behavior sample, we decided to add helpers to batch those operations. So here's how you would wire up all those events: $addHandlers(this.get_element(), {
mouseover: this._onHover,
mouseout: this._onUnhover,
focus: this._onHover,
blur: this._onUnhover
}, this);
No need to create delegates here, this will be done for you under the hood. From dispose, it gets even simpler as we are keeping track of everything that was added using the $add APIs: $clearHandlers(this.get_element());
In a future post, I'll also look at AJAX class events, which are events that you can expose from your own objects, and that are closer to .NET events than to the DOM events I've been showing here.
Drag and Drop Ajax Programming With Atlas
by Jesse Liberty
08/01/2006
Just about every book or article I've read about Atlas has given me a headache. They seem to have two messages in common: (1) all my server-side ASP.NET applications are now obsolete, and (2) the only way to fix them is to add really complicated JavaScript. Woe is me.
These books and articles (which shall remain nameless because lawyers are so expensive these days) all seem to imply that to be a real Atlas programmer, I should stop hiding behind black-box controls and understand Ajax and JavaScript and, for good measure, XMLHTTPRequest objects! It's like a nightmare flashback to 1985 when, to be a real C programmer, you really needed to understand Assembler so that when things went wrong, you could drop down into the snap-shot debugger to see what was in the registers. (Haven't done that for a while, and I don't really miss it much.)
No! I won't have it. The move up the ladder of abstraction is a good thing. We were right when we said that by using a higher level of abstraction, we can better focus on the problem domain, leaving the "plumbing" to well-tested controls provided by Microsoft and other vendors. I'll be damned if I want to slip back into writing type-unsafe non-object-oriented JavaScript. Twenty years of progress up in smoke, down the drain, and out the door. Feh. (Feh is Yiddish for Yuck.)
Fortunately, it is a chimera, appearing on the horizon through the fractured lens of an early adopter's telescope. The script-centric approach is an artifact of pre-release technology being explored by those who know the underlying foundation of JavaScript best. When Ajax started to get very hot, the first ones to write about it were, naturally enough, JavaScript aficionados. They were the ones who could create JavaScript client-side code that made an appreciable difference in the user experience.
Atlas expertise has become equated with script expertise; a false identity needs to be exposed and broken as quickly as possible. When Programming ASP.NET,4th Edition is released, we will have a great deal of Atlas, but very little JavaScript. I feel very strongly that application programmers should focus on applications, and controls programmers should focus on building nicely encapsulated controls (no user servable parts, opening black box may void warranty).
This Article Shows No JavaScript
For this article, I am going to start top-down, using Atlas controls and drag-and-drop within Visual Studio 2005, and I am not going to look inside the black box at all if I can help it. (Almost seems impossible, or at least sacrilegious, no? But of course, that is exactly how I would teach normal ASP.NET controls.)
Will I cover everything? Nope. But we'll get a darn good look at how quickly and easily you can improve a working ASP.NET program with tested and working Atlas controls that you can download for free from Microsoft. And my guess is they'll be turning them out almost as fast as you can learn them.
What You Need to Write Atlas Applications
The Microsoft Atlas website provides a very good explanation of what you need to download and how to install the files (as well as how to integrate them into Visual Studio), so I won't waste your time recapitulating the instructions here. Please navigate to the ASP.NET Atlas home page and click on the Download icon to retrieve the JUNE CTP (presumably if there is a later CTP by now, you'll want that, and the example code for this article will work as is or with minor modifications). The June CTP comes in three parts: Documentation, Samples, and Setup. Only the third part is required, but you'll want all three. When you are done, return to the home page and click on the Atlas Control Toolkit; you'll need that later in the article.
The downloads include directions on adding two tabs to your toolbox (Atlas and Atlas Toolkit) and populating them with the controls (a matter of browsing for the appropriate DLLs). It is pretty easy, but if the drag-and-drop approach doesn't work, you can always right-click on the toolbox tab and select "Choose Items," then click Browse, find the Bin directory, and choose the DLL. Doing so will add all the Atlas controls from that dll to the tab, as shown in Figure 1.

Figure 1. Adding controls from the dll
Starting with a Working Program
In May, I published Building a Web-Based Bug Tracking Application (Part 2). The article and complete source is available on my website (click on Books, then Articles). We'll use that application, which builds Tiny Bug Tracker as a starting point, adding Atlas functionality to improve the existing application, much as you might add Atlas controls to improve any web application you have already written.
The purpose of the Tiny Bug Tracker is to provide a single programmer (or a very small group of programmers) with a simple web-based application for tracking the progress of bugs reported, worked on, fixed, and closed by various members of the team. You begin by creating users and roles using the Web Site Administrator Tool, as shown in Figure 2.

Figure 2. Adding a user with the WAT (for more on ysabell see Discworld)
Users then sign in to use the application, as shown in Figure 3.

Figure 3. Logging In
Users can click on the menu choice to Enter a bug, which brings them to the New Bug form (also used to edit bugs) as shown in Figure 4.

Figure 4. Enter a bug
All of these bugs are placed into the TBTData database, with one entry for each bug in the Bugs table, and one entry for each modification of the bug in the BugHistories table, as indicated in Figure 5.

Figure 5. Bug Database
Once you have bugs in your database, you can review them in TBTReview.aspx. Clicking on Details brings up the Details panel for that bug, revealing the fields that do not fit in the grid. Clicking History brings up a grid with each change for the given bug, and you can then reveal the details for that change, as shown in Figure 6.

Figure 6. Review
There are a few problems with this application that Atlas can help us resolve. Each time you move from the details of one bug to the details of the next, the entire page is posted back to the server; there is a delay and hence, there is flicker. Also, as you scroll the review, you may realize you want to enter a new bug, but the menu is up on top, which forces you to scroll back. It would be nice to have the menu available. Finally, if you click Cancel when entering a new bug, it cancels--potentially throwing away a good deal of work accidentally.
Creating the Atlas Version
To make life easier, rather than adding Atlas controls to the existing application, I'm going to build a new Atlas application and then add the pages from the previous application. This ensures that Visual Studio will set up all the references that Atlas requires, and by using the pages from the previous article, I know that the basic application code should work, and I won't have to take time re-examining the underlying functionality. Note that I'm using the June CTP of Atlas; if you are using a different release, your mileage may vary.
What follows will make much more sense if you take a few moments to read through the original article. I'll wait here. Take your time. I'll work on other projects; wake me when you are ready.
First Improvement: Reduce Flicker--Only Update Part of Each Page
One of the biggest problems with the Tiny Bug Tracker application is that each time you ask for details on a different bug, the entire page must be redrawn, causing annoying flicker. (Due to caching, the database is not "hit" each time, but the round trip to the server does cause a noticeable pause).
We can use Atlas to ensure that we update only that part of the page that must be updated. To do so, we'll add four update panels to the TBTReview Page. An Update Panel is an Atlas control specifically designed to be updated independently of anything else on the page. The details of how it works may well be fascinating, but all you really care about is that it works asynchronously; that is, there is no need for a full-page postback. Even better, you can create each update panel by dragging it from the toolbox right onto the page and hey! presto! Visual Studio does the work for you. (You can also create an Update Panel by adding one by hand in source view, or you can create one dynamically in C# at runtime).
Creating the Atlas Application Step-by-Step
You can create your new application by starting with your existing application and adding the Atlas controls, libraries, and references. Really. But as I said above, I'm not going to do that because with beta software, I like to let Visual Studio set everything up just right. So, I'll create a new website called Atlas Bug Tracker by creating a New Web Site and choosing "Atlas Web Site" from the templates, as shown in Figure 7.

Figure 7. Create new Atlas website
The initial site will be built with a few files included by default: a readme.txt, a eula.rtf, a default.aspx, a web.config and, within bin, a copy of Microsoft.Web.Atlas.dll. Delete the readme, the eula, and Default.aspx; you won't be needing them.
Open the WAT, and on the security tab, choose forms-based authentication, and create the user names and roles you had in the previous application (Alex, Dan, Jesse, and Stacey and Developer, Manager, QA, and User).
Copy over the pages TBTMaster.master, TBTReview.aspx, TBTBug.aspx, and TBTWelcome.aspx and their codebehind pages, and add them to the project. Do not copy over the web.config file.
The WAT has modified the web.config file created for you when you chose the Atlas project, so you now have the combined configuration of both: an Atlas project with forms-based security.
You'll want to run the program and make sure everything is right before you begin making changes.
Note: the complete source code is available for download from my website. (Click on Books, and then on Articles), along with a link to my private support forum and other material that may be of interest.
The Script Manager
The key control for Atlas is the ScriptManager. Virtually every Atlas control requires that its page has a ScriptManager whose job is to coordinate all the Atlas controls on that page. Fortunately, this application has a master page, so we can drop our ScriptManager on the master, providing one ScriptManager for every page in the application (One ScriptManager to rule them all, one ScriptManager to find them...*).
The one attribute that we will need to add to the ScriptManager is EnablePartialRendering="True" because it turns on the ability to render part of the page asynchronously without a full-page postback.
You can place the ScriptManager control anywhere in the master page. I've put mine right below the login status control:
<atlas:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="True" />
Dragging and dropping the control from the toolbox is probably the easiest way to add it to the page, and doing so ensures the control is properly registered on the page. When you do, however, you are likely to see the Destination File Exists dialog box, as shown in Figure 8.

Figure 8. Destination File Exists
When the control is added to the page, it brings Microsoft.Web.Atlas.dll with it. There is no reason not to click Yes and refresh the file.
Breaking TBTReview into Sections
With the ScriptManager in place, you are ready to add asynchronous updates for the various grids and DetailsView objects on the TBTReview page. To do so, drag four UpdatePanel objects from the Atlas tab of the Toolbox onto the page. Each UpdatePanel represents a block of UI that can be updated independently. I placed BugReviewGrid and its data source into the first, BugDetailsView and its data source into the second, BugHistoryGrid and its data source into the third, and BugHistoryDetailsView and its data source into the fourth. You can do this with drag-and-drop in Design view, or by moving the HTML in Source view.
For example, in Figure 9, I've dragged UpdatePanel2 onto the form, and I'm dragging BugDetailsView into the Panel.

Figure 9. Dragging into the Update Panel
When I release the mouse, BugDetailsView and its SqlDataSource will be inside the Update panel, as reflected in the HTML in Source view (severely abridged here):
<span class="style1"><atlas:UpdatePanel ID="UpdatePanel2" runat="server">
<ContentTemplate></span>
<asp:DetailsView ... </asp:DetailsView>
<asp:SqlDataSource ...> </asp:SqlDataSource>
<span class="style1"></ContentTemplate>
</atlas:UpdatePanel></span>
</span>
The change in performance is instant and remarkable. Suddenly, the page stops flickering. When you move from one bug's details to another, it is smooth, instantaneous, and flicker-free. To make sure the difference is real, remove the attribute EnablePartialRendering="True" from the ScriptManager in the master page and rerun the application. You can't miss it.(Don't forget to put the attribute back!)
Look Ma, No JavaScript
Stop a moment and notice that at no time did you write JavaScript. You didn't touch XMLHttpRequest, and you have no idea how the work is done; it is all every bit as magical as how, for example, the DataGrid control itself works. Here's what I say: that is not only OK, that is jolly good. This ignorance lets you focus on what you're trying to do, and lets Microsoft focus on building the controls. (Hey! Stop whimpering. If you really want to know how it works, I promise, we have lots of books on JavaScript and on Programming Atlas that go into all the gory details. But isn't it nice, just for the moment, to focus on getting the job done?)
But There's More...
OK, we've accomplished asynchronous updates, and we could stop there and feel pretty good about Atlas, but let's take a peek at control extenders. You get quite a few with the Atlas Toolkit (and more are on the way all the time). One I really like already is the AlwaysVisible extender. This allows you to take any control and say, "I want this to stay visible on my page as the user scrolls through it." Not only is this impressive as all get-out, it is actually quite useful. It gives you the power of frames without any of the hassle (and frames, I'm told by my designer friends, are oh-so-five minutes ago).
Our master page has a few items on it, one of which is a menu that lets the user add new bugs. It would be nice if the user could get to that menu even if s/he has scrolled down on the review page. This turns out to be so easy as to be embarrassing.
Open TBTMaster.master and drag an instance of AlwaysVisibleControlExtender onto the page. Do this in source view. Between the open and close tags, you'll add one element (Intelligence will offer it for you) of type AlwaysVisibleControlProperties. Intelligence will help with the properties, but I'll show them to you here:
TargetControlID="mnuTBTNavigation"
VerticalSide ="top"
VerticalOffset = "10"
HorizontalOffset ="10"
HorizontalSide ="right"
ScrollEffectDuration =".1" />
The TargetControlID is the control that you will be making visible--in this case, the menu. The vertical and horizontal sides dictate where you want the menu to appear (see Figure 10). The offset is how far from the top/right you want the menu to appear, and the scroll effect duration is how long a delay you want (in this case, a tenth of a second). I hate to admit it, but that is all you do; Atlas takes care of the rest. As you scroll, the menu follows you, like a loyal puppy.

Figure 10. Always Visible Menu
It is almost magical how the menu chases after you as you scroll down to look at a bug's detail. It is magical how little you have to do, and it is damnable how hard some people make it seem. I love the fact that Microsoft has encapsulated all this into a control that I can just drag onto my form and--poof!--it works. Yes, over time, I'll want to understand (maybe) how they did it, but you know what? I have deadlines, and this is very cool functionality.
Creating a Trap for Cancel
One bit of coding that we all write a dozen (or a thousand) times is a "trap" for a Cancel button. The user clicks Cancel, and we want a popup that asks the user to confirm that s/he intended to cancel out of the work being done. This can be tricky to write in an ASP.NET application, as dialog boxes do require some JavaScript. Once again, however, Atlas not only makes this trivial, it does so in a way that is totally consistent with the rest of Atlas and integrates seamlessly with your existing application.
The TBTBug.aspx file has two controls, btnSave and btnCancel. To add a trap for btnCancel, all you need do is drag a ConfirmExtender into the cell with the two buttons:
<td colspan="2">
<AtlasToolkit:ConfirmButtonExtender
ID="ConfirmButtonExtender1" runat="server">
<AtlasToolkit:ConfirmButtonProperties ConfirmText="Are you sure you want to discard this?"
TargetControlID="btnCancel" />
</AtlasToolkit:ConfirmButtonExtender>
<asp:Button ID="btnSave" runat="server" Text="Save" BackColor="Lime" Font-Bold="True" OnClick="btnSave_Click" />
<asp:Button ID="btnCancel" runat="server" Text="Cancel" BackColor="Red" Font-Bold="True" ForeColor="Yellow" OnClick="btnCancel_Click" />
</td>
The effect is illustrated in Figure 11.

Figure 11. Cancel Confirmation
The ConfirmExtender takes one internal attribute of type ConfirmButtonProperties, which itself takes two attributes, one for the text to display in the message box, and the other to identify the button that triggers the confirmation.
For this to work, the control must be registered, but that is done for you when you drag the control onto the form. Sweet. In the version I'm using, the tag is set at CC1, but it's simple to change the tag to something you like; I changed it to AtlasToolkit
<%@ Register Assembly="AtlasControlToolkit" Namespace="AtlasControlToolkit" TagPrefix="AtlasToolkit" %>
so that when I use the control, I can write code like this: <AtlasToolkit:ConfirmButtonProperties, which reminds me of the source of the control. Not necessary, but nice.
There are a lot of useful controls that come with the Toolkit, and I strongly encourage you to take a look at the video tutorials available in the Atlas How Do I series, which will walk you through much of this material and beyond. You may want to subscribe to one or more of the RSS feeds that will keep you up-to-date on what Atlas controls are being released.
Above all, don't let anyone convince you that Atlas is difficult. The whole point is for it to be as easy as ASP.NET, allowing you to keep your focus on building your application, and not on building the plumbing.
Jesse Liberty
, Microsoft .NET MVP, is the best-selling author of O'Reilly Media's Programming ASP.NET, Programming C#, Programming VB2005 and over a dozen other books on web and object-oriented programming. He is president of Liberty Associates, Inc. where he provides contract programming, consulting and on-site training in .NET.
Tip/Trick: How to Register User Controls and Custom Controls in Web.config
I've been including this technique in my ASP.NET Tips/Tricks talks the last year, but given how many people are always surprised by its existence I thought it was worth a dedicated tip/trick post to raise the visibility of it (click here to read other posts in my ASP.NET Tips/Tricks series). Problem: In previous versions of ASP.NET developers imported and used both custom server controls and user controls on a page by adding <%@ Register %> directives to the top of pages like so: <%@ Register TagPrefix="scott" TagName="header" Src="Controls/Header.ascx" %> <%@ Register TagPrefix="scott" TagName="footer" Src="Controls/Footer.ascx" %> <%@ Register TagPrefix="ControlVendor" Assembly="ControlVendor" %>
<html> <body> <form id="form1" runat="server"> <scott:header ID="MyHeader" runat="server" /> </form> </body> </html> Note that the first two register directives above are for user-controls (implemented in .ascx files), while the last is for a custom control compiled into an assembly .dll file. Once registered developers could then declare these controls anywhere on the page using the tagprefix and tagnames configured. This works fine, but can be a pain to manage when you want to have controls used across lots of pages within your site (especially if you ever move your .ascx files and need to update all of the registration declarations. Solution: ASP.NET 2.0 makes control declarations much cleaner and easier to manage. Instead of duplicating them on all your pages, just declare them once within the new pages->controls section with the web.config file of your application: <?xml version="1.0"?>
<configuration>
<system.web> <pages> <controls> <add tagPrefix="scottgu" src="~/Controls/Header.ascx" tagName="header"/> <add tagPrefix="scottgu" src="~/Controls/Footer.ascx" tagName="footer"/> <add tagPrefix="ControlVendor" assembly="ControlVendorAssembly"/> </controls> </pages>
</system.web>
</configuration> You can declare both user controls and compiled custom controls this way. Both are fully supported by Visual Studio when you use this technique -- and both VS 2005 Web Site Projects and VS 2005 Web Application Projects support them (and show the controls in WYSIWYG mode in the designer as well as for field declarations in code-behind files). One thing to note above is the use of the "~" syntax with the user-controls. For those of you not familiar with this notation, the "~" keyword in ASP.NET means "resolve from the application root path", and provides a good way to avoid adding "..\" syntax all over your code. You will always want/need to use it when declaring user controls within web.config files since pages might be using the controls in different sub-directories - and so you always need to resolve paths from the application root to find the controls consistently. Once you register the controls within the web.config file, you can then just use the controls on any page, master-page or user control on your site like so (no registration directives required): <html> <body> <form id="form1" runat="server"> <scott:header ID="MyHeader" runat="server" /> </form> </body> </html> Hope this helps, Scott P.S. Special thanks to Phil Haack who blogged about this technique as well earlier this month (for those of you who don't know Phil, he helps build the very popular SubText blog engine and has a great blog).
More than just the basics with ASP.NET user controls
I was messing around with user controls, and this is like the 3rd time I’ve needed to look this up, so I figured I’d blog about it so I have it written down.
As you know, if you have some controls on an ASP.NET page, you could scoop those out and put those into a “User Control” – then drag that user control (the .ascx file) onto your .aspx page and voila. It’s a great way to isolate functionality and code.
Now, what if you need to pass the user control some data? Well, in the class of the user control, you can create public properties. For example:
private Unit _width = new Unit(400);
public Unit Width
{
get
{
return _width;
}
set
{
_width = value;
}
}
Then, when you create the control on your page, you’ll see that property available, like this:
<uc1:SectionHeader ID="sh1" runat="server" Width="400px">
</uc1:SectionHeader>
Now, what if you need to pass in something more? Perhaps you want to pass in a chunk of data. Or what if that chunk of data needs to be something even more complex, like ASP.NET controls? Well, there is this kind of obscure, poorly documented concept you can steal from the “templated controls” concept within web controls. You can have a user control, but also have “templates” that you can populate at runtime – with ANYthing that is valid within ASP.NET.
Let’s say you have this as your user control:
<div class="SectionHeader">
<asp:PlaceHolder ID="Title" runat="server"></asp:PlaceHolder><br />
<asp:PlaceHolder ID="Description" runat="server"></asp:PlaceHolder>
</div>
And let’s say we want to fill in Title and Description at runtime, by the caller – and I might want to put a bunch of things into that Description. Like, a calendar control, a gridview, just regular text - anything. What we need to do is create public ITemplate properties like this, in the user control class:
private ITemplate _title;
private ITemplate _description;
[PersistenceMode(PersistenceMode.InnerProperty), TemplateContainer(typeof(TemplateControl))]
public ITemplate TitleTemplate
{
get { return _title; }
set { _title = value; }
}
[PersistenceMode(PersistenceMode.InnerProperty), TemplateContainer(typeof(TemplateControl))]
public ITemplate DescriptionTemplate
{
get { return _description; }
set { _description = value; }
}
Now, we just need to add some code to the OnInit, so that it will take the controls and/or text that a user entered for the Title and Description templates – and it puts them into the respective PlaceHolders on the user control page:
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (_title != null)
{
_title.InstantiateIn(Title);
}
if (_description != null)
{
_description.InstantiateIn(Description);
}
}
Note that the Title and Description up there, are the references to the placeholders on the page. So this is where it assigns what the user passed in – to the placeholders on your user control.
Lastly, add the attribute ParseChildren to the UserControl class, like this:
[ParseChildren(true)]
public partial class SectionHeader : System.Web.UI.UserControl
{
This tells intellisense on the caller page, to look for child objects (which TitleTemplate and DescriptionTemplate will be “templates” that you put in between the opening and closing tag of the control). So ParseChildren will just make sure it acts correctly when you create the control on the page.
When you do all this, this is what this will look like (and Intellisense supports this too) on the consuming side:
<uc1:SectionHeader ID="sh1" runat="server" Width="400px">
<TitleTemplate>This is the title</TitleTemplate>
<DescriptionTemplate>
This is the description, and this has have controls in them
too. For example:
<asp:Calendar ID="cal1" runat="server">
</asp:Calendar>
</DescriptionTemplate>
</uc1:SectionHeader>
So this is quite a powerful and easy way to encapsulate and segregate UI functionality. Plus, it’s like a zillion times easier than manually rendering your own WebControl which is compiled in a .dll – which is nice, but there is no user interface, and you have to do all of your control creation programmatically. You have to figure that you are usually writing a custom control because you need to do something complicated - so that's the last place you want to be stuck without an interface!
Get selection
Page last changed 8 months ago This page is supposed to be in my frameset.
The script theoretically works in Opera 7.5, but any selection is removed before
the mousedown event occurs, so that this script won't ever catch a selection. Solved in Opera 8.
It does not work in Explorer 4 on Mac, Opera, Konqueror, Ice Browser,
Escape and Omniweb.
Netscape 4 on Linux doesn't support onMouseDown on the button.
See below for more browser compatibility details.
On this page I give the simple code you need for finding out what text the user
has selected with his mouse.
Example
First of all, select some text on this page and press the button or the link below:
Get selection
The script
The script that is executed when you click on the link or the button is extremely simple:
function getSel()
{
var txt = '';
var foundIn = '';
if (window.getSelection)
{
txt = window.getSelection();
foundIn = 'window.getSelection()';
}
else if (document.getSelection)
{
txt = document.getSelection();
foundIn = 'document.getSelection()';
}
else if (document.selection)
{
txt = document.selection.createRange().text;
foundIn = 'document.selection.createRange()';
}
else return;
document.forms[0].selectedtext.value = 'Found in: ' + foundIn + '\n' + txt;
}
So I try to find the selection by three methods: first window.getSelection(),
then document.getSelection(), then document.selection.createRange().text.
The resulting browser incompatibilities:
| Method |
Explorer 5 Win |
Explorer 6 Win |
Explorer 5.2 Mac |
Mozilla 1.75* |
Safari 1.3 |
Opera 8 |
Netscape 4 |
iCab 2.9.8 |
|
window.getSelection()
|
No |
No |
No |
Yes |
Yes |
No |
No |
No |
var txt = window.getSelection();
|
|
document.getSelection()
|
No |
No |
Yes |
Yes |
No |
Yes |
Yes |
Yes |
var txt = document.getSelection();
|
|
document.selection
|
Yes |
Yes |
No |
No |
No |
No |
No |
No |
var txt = document.selection.createRange().text
|
*: also tested in Firefox 1.0
Old browsers: use mousedown
Netscape 4 on Linux doesn't support onMouseDown on the button.
Whether you want to use a button or a link to get the selection, always use the mouseDown event handler
to call the script.
<input type="button" value="Get selection" onmousedown="getSel()" />
<a href="#" onmousedown="javascript:getSel(); return false"
class="page">Get selection</a>
Netscape and Explorer 4 each have their specific problems when you use an onClick. Of course, what's a problem in
one browser works fine in the other.
Netscape 4
Netscape 4 un-selects a text when you press the mouse button. Since an onClick event
fires after the mouse button has been released, you would lose the selected text. The onMouseDown event fires
just before Netscape 4 un-selects the text, so the script can still grab it. If you add the
return false
to the action, you cancel the unselecting of the text. I've done that so that Netscape 4 users can also
compare their selected text to the text in the textarea.
Furthermore, Netscape 4 does not see any selected texts inside form fields (try selecting text inside
the textarea).
Finally, the button does not work in Netscape 4 on Linux because it does not support the onMouseDown event
handler on buttons. The link works fine, though.
Explorer 4
Explorer 4 has the same problem as Netscape 4, but then with the button. When you click on a button, the text
is un-selected. Therefore, also use the mouseDown event handler here. Unfortunately adding return false does
not have any effect: the text is still un-selected.
Select (and copy) Form Element Script
Author:
Dynamic Drive
Description: Allow your
surfers to easily select the contents of your form elements- as we have done with our
script containers throughout Dynamic Drive- with this script. And just to outdo ourselves,
we've also embedded in this script the ability to concurrently copy the content to
clipboard (memory) as well (applicable only in IE 4+). Enjoy!
function HighlightAll(theField) { var tempval=eval("document."+theField) tempval.focus() tempval.select() if (document.all&©toclip==1){ therange=tempval.createTextRange() therange.execCommand("Copy") window.status="Contents highlighted and copied to clipboard!" setTimeout("window.status=''",1800) } }
When there is only one single-line text input field in a form, the user
agent should accept Enter in that field as
a request to submit the form.
This was evidently meant as a convenient way to submit simple
queries, but reducing the risk, on a complex form, of prematurely
submitting it while trying to fill it in.
Numerous browser implementers (e.g Netscape, Opera,
various versions of Lynx,...)
followed this advice, though it seems with slightly different
interpretations.
If you use only one "single-line text input field" (i.e
input type="text"), along with other kinds of form
widget such as buttons, selections etc., then these browsers will
submit the form.
Some (e.g Netscape) did this even if the form also contained
multi-line input field(s) i.e textarea element(s), but
some others will not.
Therefore, for widest accessibility of Enter=>Submit, you would
want to use no textarea elements in the same
form.
There's nothing more that the author needs to do in order to
make this possible, and nothing that the author can do,
within the context of HTML itself,
to help a browser that doesn't make this possible: the HTML4
specification makes no particular ruling about what Return or Enter
should do (and it looks as if recent versions of Lynx have
a different behaviour in this respect).
So, you really have to include a Submit function
(preferably a straightforward input type="submit")
to be sure that all bases are covered.
Note that a page can contain several forms: almost all
of the browsers tried were applying the rule to each form separately.
Activating ActiveX Controls
Users cannot directly interact with Microsoft ActiveX controls loaded by the APPLET, EMBED, or OBJECT elements. Users can interact with such controls after activating their user interfaces. This topic describes how Microsoft Internet Explorer handles ActiveX controls, shows how to load ActiveX controls so their interfaces are activated, and describes the impact of this behavior on accessibility tools and applications hosting the WebBrowser Control.
This topic contains the following sections. For an introduction to the user experience, please see Internet Explorer 6: ActiveX Update . For additional information regarding the platforms affected by this update, please see Internet Explorer ActiveX Update . Understanding Control ActivationInteractive controls are ActiveX controls that provide user interfaces. When a web page uses the APPLET, EMBED, or OBJECT elements to load an ActiveX control, the control's user interface is blocked until the user activates it. If a page uses these elements to load multiple controls, each interactive control must be individually activated. When a control is inactive, the following effects occur. Dynamic HTML (DHTML) events related to user interaction, such as onblur and onclick, are blocked. Appendix A lists the DHTML events that are blocked when a control is inactive. The control does not respond to window messages generated by the keyboard or mouse, such as WM_CLICK and WM_KEYPRESS, and so on. An overlay window, created on the control's OLE site, prevents keyboard and mouse messages from reaching the inactive control.
When an inactive control is created, Internet Explorer uses different techniques to prevent keyboard or mouse window messages from reaching the control. When the inactive control is a windowed control, such as the HTML Help Control, Internet Explorer uses the EnableWindow Function to disable the inactive control's window. When the user activates a windowed control, the same function activates the disabled window. When the inactive control is a windowless control, such as the Office Web Components, keyboard and mouse messages are filtered by the control's container. When a control is inactive, it does not respond to user input; however, it does perform operations that do not involve interaction. If, for example, you open a web page that uses Microsoft Windows Media Player to play a music file, the music plays after the page loads. You cannot interact with Windows Media Player until the control's user interface is activated, as shown in the following figure. 
Note While inactive controls do not respond to direct user interaction; they do respond to script commands. To activate an interactive control, either click it or use the TAB key to set focus on it and then press the SPACEBAR or the ENTER key. Interactive controls loaded from external script files immediately respond to user interaction and do not need to be activated. Some windowed controls use Windows API functions, such as GetKeyState and GetCursorPos, to determine the state of the keyboard and mouse and then respond to the function results. For these controls only, a prompt appears before the control is run in Internet Explorer. To run the control, the user needs to click the button in the message window before the page loads. After loading, the control will not require activation. At present, the following controls have this behavior, but the vendors are working on new controls that would not have this behavior. - Virtools (TM) Web Player from Virtools SA
- Macromedia Shockwave Player (TM) from Adobe Systems Inc.
- QuickTime (TM) from Apple Computer, Inc.
When loaded from external script files, these controls do not display a prompt. The following figure shows the prompt dialog. 
Loading Interactive Controls ExternallyTo create Web pages that load interactive controls that respond immediately to user input, use Microsoft JScript to load controls from external script files. You cannot write script elements inline with the main HTML page to load your control externally. If the script is written inline programmatically, for example with the writeln function, the loaded control will behave as if it was loaded by the HTML document itself and will require activation. To ensure a control is interactive when it is loaded, use one of the following techniques to load your control from an external file. The following example uses document.write to load a control dynamically. <!-- HTML File -->
<html>
<body leftmargin=0 topmargin=0 scroll=no>
<script src="docwrite.js"></script>
</body>
</html>// docwrite.js
document.write('<object classid="clsid:6BF52A52-394A-11d3-B153-00C04F79FAA6">');
document.write('<param name="URL" value="example.wmv">');
document.write('<param name="autoStart" value="-1"></object>');
External script files can also modify an element's outerHTML property to achieve the same effect, as shown in the following example. <!-- HTML File -->
<html>
<body>
<div>
<script src="embedControlOuterHTML.js"></script>
</div>
</body>
</html>// outerhtml.js
embedControlLocation.outerHTML = '<embed src="examplecontrol">'; The next example uses document.createElement to load an ActiveX control using the OBJECT element.
Important When using createElement to add an Object or Embed element to a web page, use care to create the element, initialize its attributes, and add it to the page's DOM before creating the ActiveX control to be loaded by the new element. For more information, please see the createElement documentation. <!-- HTML File -->
<html>
<body>
<div id="DivID">
<script src="createElementExplicit.js"></script>
</body>
</html>// createElementExplicit.js
var myObject = document.createElement('object');
DivID.appendChild(myObject);
myObject.width = "200";
myObject.height = "100";
myObject.classid= "clsid:6BF52A52-394A-11d3-B153-00C04F79FAA6";
myObject.URL = "example.wmv";
myObject.uiMode = "none" ;The next example uses innerHTML and a JScript function to load an ActiveX control while specifying parameter values. <!-- HTML File -->
<html>
<head>
<script src="external_script.js" language="JScript"></script>
</head>
<body>
<div id="EXAMPLE_DIV_ID">
This text will be replaced by the control
</div>
<script language="JScript">
CreateControl( "EXAMPLE_DIV_ID",
"clsid:6BF52A52-394A-11d3-B153-00C04F79FAA6",
"EXAMPLE_OBJECT_ID", "600", "400", "example.wmv",
"-1")
</script>
</body>
</html>// external_script.js
function CreateControl(DivID, CLSID, ObjectID,
WIDTH, HEIGHT, URL, AUTOSTART)
{
var d = document.getElementById(DivID);
d.innerHTML =
'<object classid=' + CLSID + ' id=' + ObjectID +
' width=' + WIDTH + ' height=' + HEIGHT +'>
<param name="URL" value=' + URL + '>
<param name="autoStart" value=' + AUTOSTART + '/>';
}Because the next example uses the writeln function to insert the script into the original HTML document, the resulting control requires activation. To load a control without requiring activation, use one of the previous examples. <!-- HTML File -->
<html>
<body>
<div id="embedControlLocation">
<script id="elementid" src="embedControl.js"></script>
</div>
</body>
</html>// embedControl.js
document.writeln('<script>');
document.write('document.writeln(\'');
document.write( '<object classid =
"clsid:6BF52A52-394A-11D3-B153-00C04F79FAA6"
width="100" height="100" />');
document.write('\');');
document.writeln('</script>');
Note To automatically activate ActiveX controls, Internet Explorer must be using a version of jscript.dll newer than September 30, 2003. Earlier versions of this DLL will require all controls to be activated, regardless of the mechanism used to load them from a web page. For the current version of jscript.dll, please see Windows Script Download  . Programmatically Determining Whether a Control is InactiveYou cannot use JScript functions or server-side scripts to determine whether or not a control is active. Application hosting the web browser control cannot determine whether or not a control is active. Controls can determine activation state via the DISPID_AMBIENT_UIDEAD ambient property by calling through IDispatch::Invoke. Controls that implement the IOleControl interface are notified when this property changes through IOleControl::OnAmbientPropertyChange. Accessibility ImpactWhen accessibility tools encounter ActiveX controls, they can use the object's IAccessible interface to obtain information about the control. Inactive controls can be activated with the IAccessible::accDoDefaultAction method. The following table describes the results when IAccessible methods are called on inactive controls. | Method | Description |
|---|
| IAccessible::accDoDefaultAction | Activates the control and will expose the ActiveX control or Java Applet within the MSAA tree. | | IAccessible::accHitTest | Returns CHILDID_SELF | | IAccessible::accLocation | Location of the underlying ActiveX control or Java Applet | | IAccessible::accNavigate | Returns E_NOTIMPL | | IAccessible::accSelect | Returns E_NOTIMPL | | IAccessible::get_accChild | Returns S_FALSE | | IAccessible::get_accChildCount | Returns 0 and S_OK | | IAccessible::get_accDefaultAction | Returns "Select this control" | | IAccessible::get_accDescription | Returns E_NOTIMPL | | IAccessible::get_accFocus | Returns E_NOTIMPL | | IAccessible::get_accHelp | Returns "This control is inactive. Select the control to activate and use it." | | IAccessible::get_accHelpTopic | No Change - Returns E_NOTIMPL | | IAccessible::get_accKeyboardShortcut | No Change - Delegates the object. If there is no object, the method returns E_NOTIMPL. | | IAccessible::get_accName | Returns "Inactive Control" | | IAccessible::get_accParent | No Change - Returns the closest accessible element in the parent chain. | | IAccessible::get_accRole | Returns ROLE_SYSTEM_PUSHBUTTON | | IAccessible::get_accSelection | Returns E_NOTIMPL | | IAccessible::get_accState | Returns current state of the object. This state always includes STATE_SYSTEM_FOCUSABLE | | IAccessible::get_accValue | Returns E_NOTIMPL | | IAccessible::put_accName | Returns E_NOTIMPL | | IAccessible::put_accValue | Returns E_NOTIMPL |
For information on activated controls, or controls that do not require activation please see the Active Accessibility SDK.
Note Accessibility tools should refresh after triggering the default action in order to properly display the ActiveX control's data and the data of its children, if any. WebBrowser Control ImpactBy default, custom applications hosting the WebBrowser Control do not block interactive ActiveX controls loaded by the APPLET, EMBED, or OBJECT elements. Inactive control blocking only applies to the following applications. - Windows Explorer
- Internet Explorer
- MSN Explorer
- AOL® Explorer
- AOL® 8.0
- AOL® 9.0
- CompuServe 2000
- AIM®
- NetCaptor
- Browse3D
- Macromedia Dreamweaver
- Macromedia Contribute
- Netscape® 8 (when using Internet Explorer as the rendering engine)
To match the behavior of Internet Explorer in your application, add the DOCHOSTUIFLAG_ENABLE_ACTIVEX_INACTIVATE_MODE flag to the dwFlags parameter of your DOCHOSTUIINFO structure, as shown in the following example.
HRESULT GetHostInfo(DOCHOSTUIINFO *pInfo)
{
...
pInfo->cbSize = sizeof(DOCHOSTUIINFO);
pInfo->dwFlags = { Other DOCHOSTUIFLAGs } |
DOCHOSTUIFLAG_ENABLE_ACTIVEX_INACTIVATE_MODE;
...
return S_OK;
}You can also enable interactive control blocking by adding your application's process name to the following registry key. - HKEY_LOCAL_MACHINE (or HKEY_CURRENT_USER)
- SOFTWARE
- Microsoft
- Internet Explorer
- Main
- FeatureControl
- FEATURE_ENABLE_ACTIVEX_INACTIVATE_MODE
- process_name.exe=(DWORD) 0x00000001
Note Because users can modify the registry, the DOCHOSTUIINFO flag is the preferred way to enable interactive control blocking. Applications can register to incorporate ActiveX control activation by default. For more information, please engage your Technical Account Manager or contact Microsoft Product Support. Appendix A: DHTML Events Blocked by Inactive ControlsThe following table lists the DTHML events that are blocked when ActiveX controls are inactive.
|
|  |
A nice and compact way to coerce to Boolean in JavaScript
JavaScript is always the strange beast as far as comparisons are concerned. There are cases where the automatic contextual casting is not quite convenient. For example, we like to reliably return booleans from some of our methods, not null, not undefined and not some random object. Being able to say that this function will return a boolean is a Good Thing that the users of the API will appreciate when debugging. Anyway, I used to do this to coerce something to Boolean: Dave Reed just showed me a much more compact way of doing that: Probably not the most readable thing in the world but I could get used to that, the same way I got used to | return something || null; |
when I want to coerce undefined into null as a return value.
Eliminating CSS Image Flicker with IE6
One challange that web designers and developers often wrestle with is an annoying "image flicker" issue that sometimes shows up when using CSS image references. For example, when using a CSS rule like so:
.someClassName { background:#AABBCC url(someBackGroundImage.gif) repeat-x; }
This can cause some browsers (including IE 6) to have an annoying flicker when rendering the image (especially when used with hover styles or for background images). In particular, this often shows up when building hierarchical and show/hide menus, and can degrade the UI experience for the site. There are two ways to fix the issue for clients:
1) Adjust your web-server's cache content settings for the static images being referenced from the CSS file. This unfortunately requires admin access on the machine with IIS 6 (although not with IIS7).
2) Use ASP.NET to define a handler that dynamically renders images with the appropriate cache content settings set. This does not require any special configuration on your web-server, and can be done by simply copying a .ashx handler file into your app.
Russ Helfand (who did the work on the ASP.NET CSS Adapter Toolkit) has a great blog post that details how to-do option #2. You can read all about his solution here.
This is a great tip/trick you can use for the Menu and TreeView CSS Adapters, as well as for any static element CSS rules you are using within your page.
Hope this helps,
Scott
Image flicker in Internet Explorer/Win causes IE to get bitchslapped quite often. To get rid of this effect Dean Edwards suggests to configure Apache to keep images cached. Otherwise you need to manually change caching preferences in IE which a lot of people don’t know how to do. Besides, when images flicker, it’s you who looks bad, not the users’ IE. Dean says he’s no server expert, and neither am I, but there’s a way to configure IIS in a similar fashion.
Cache-Control Extensions is a little-known feature that was rolled out with Internet Explorer 5.0. In a nutshell, it’s a proprietary extension to the Cache-Control header IE 5.x and 6.x understand. The two extensions are pre-check and post-check.

Internet Explorer applies the following logic to objects served with these extensions:
- Upon first request, the object is cached and is served from cache until the
post-check interval expires.
- Once the
post-check interval expires IE fetches the object from cache and checks for an updated one in the background. If a newer object is available it caches it. Upon every subsequent request this updated (and now cached) object is served until the pre-check interval expires.
- Once the
pre-check interval elapses the object is treated as expired. IE will first ask the HTTP server if the object has changed since it was requested by the browser. If it has, IE will load the updated object.
Note: I’m not referring to web pages. I refer to objects because we might be talking about images, web pages, style sheets, external JavaScript files, etc.
As MSDN states, the Refresh button will not trigger this logic because Refresh always sends the if-modified-since request to the server. Hyperlinks do trigger this logic.
How about an example? Suppose an HTTP server sends an image with the following header:
Cache-Control: post-check=3600,pre-check=43200
Both pre-check and post-check specify time intervals in seconds. We tell IE to cache the mentioned image for 12 hours (60 * 60 * 12 seconds). The first hour (60 * 60 seconds) IE will simply display the image from its local cache. However, after 60 minutes we want it to check for a newer one in the background, i.e. it will display the cached one and then do a background check. When 12 hours are up, IE checks for a modified image first.
Set Cache-Control Extensions In IIS
The only missing piece of the puzzle is where to set these extensions to get the ball rolling. You do it in the IIS Manager snap-in. You may choose to set them on a specific folder, such is a folder with images. You may also set them on your entire web app, but hardly ever would you want to cache every single page on your site.
Fire up the IIS Manager snap-in from Administrative Tools, pick a folder in your web app, right click, go to Properties, switch to the HTTP Headers tab, and click Add. Add cache-control extensions like this:

Click Ok to dismiss the dialog. Your HTTP Headers tab should have extensions listed.

Set Cache-Control Extensions Programmatically
I was in for a big surprise when I found documentation for HttpCachePolicy.AppendCacheExtension. You can accomplish what we’ve talked about in C# like this:
Response.Cache.AppendCacheExtension(
"post-check=900,pre-check=3600");
Now, if you want to serve images with this cache policy (which is a good idea) you need to assign them to the ASP.NET ISAPI extension in IIS because by default ASP.NET is not configured to pass them through its HTTP pipeline.
What I Don’t Know
What happens if either pre-check or post-check is missing? I don’t know. What happens if post-check is greater than pre-check? I don’t know either. I found no documentation on MSDN that talks about it.
Understanding SQL Server Full-Text Indexing
By Mike Gunderloy
December 13, 2004
Microsoft SQL Server supports T-SQL, an implementation of ANSI standard SQL.
T-SQL is designed to (among other things) search for matches in your
data. For example, if you've created a table with a column named Notes you could
construct these queries:
SELECT * FROM MyTable WHERE Notes = 'Deliver Tuesday'
SELECT * FROM MyTable WHERE Notes LIKE '%caution%'
But what if you're not looking for an exact match, either to the full text of
the column or a part of the column? That's when you need to go beyond the
standard SQL predicates and use SQL Server's full-text search capabilities. With
full-text searching, you can perform many other types of search:
- Two words near each other
- Any word derived from a particular root (for example run, ran, or
running)
- Multiple words with distinct weightings
- A word or phrase close to the search word or phrase
In this article, I'll show you how to set up and use full-text searching in
SQL
Server 2000, and give you a sneak peek of the changes that are coming in this
area when SQL Server 2005 ships next year.
Full-Text Indexing Architecture
You might be a bit surprised to learn that SQL Server doesn't handle its own
full-text indexing tasks. Any version of Windows that SQL Server will run on
includes an operating system component named the Microsoft Search Service. This
service provides indexing and searching capabilities to a variety of
applications, including SQL Server, Exchange, and SharePoint.
SQL Server uses an interface component, the SQL Server Handler, to communicate
with the Microsoft Search Service. The Handler extracts data from SQL Server
tables that have been enabled for full-text searching and passes it to the
search service for indexing. Another component, the full-text OLE DB provider,
gets invoked by SQL Server when you actually perform a full-text search. The
provider takes the portion of the search that needs to be satisfied by the
full-text index and passes it off to the Search Service for evaluation.
You need to be aware of one consequence of this architecture: because the
full-text indexes are not in your SQL Server database, they can't be backed up
from within SQL Server. Instead, you need to backup the disk files created by the
Search Service. You'll find these files located under Program Files\Microsoft SQL
Server\MSSQL\FTDATA.
Enabling Full-Text Indexing
As you can probably guess, there's a certain amount of overhead involved in
passing data back and forth between SQL Server and the Search Service. To speed
things up, SQL Server doesn't pass any data to the Search Service unless you
explicitly tell it to do so. After all, you might never want to do any full-text
searches, in which case it would be silly to spend time indexing your data for
them.
To get started, you need to add a full-text catalog to your database. The
easiest way to do this is to open SQL Server Enterprise Manager and expand the
node for your database to find the Full-Text Catalogs node (if that node isn't
present, check to make sure that the Microsoft Search Service is installed on the
server). Right-click on the node and select New Full-Text Catalog. SQL Server
will prompt you for a name and location for the catalog (and it will supply a
default location). Name the catalog anything you like and click OK to create
it.
Next you need to tell SQL Server what data to include in the catalog. Again,
you can do this in Enterprise Manager. Right-click on a table and select
Full-Text Index Table, Define Full-Text Indexing on a Table. This will launch the
SQL Server Full-Text Indexing Wizard. You need to make these choices to complete
the wizard:
- Select a unique index on the table
- Select the columns to index. You can optionally specify a language to use for
word breaking.
- Select the catalog to contain the index, or create a new catalog.
- Create a schedule to repopulate the index on a regular basis (this is also
optional).
When you finish the wizard, it will create the index for the table. But the
index won't have any entries in it yet. Right-click on the table again anfd
select Full-Text Index Table, Start Full Population to build the actual index
Performing a Full-Text Search
Now you're ready to actually do some searches. For these examples, I added a
full-text index to the ProductName column in the Northwind Products table. Four
T-SQL predicates are involved in full-text searching:
FREETEXT
FREETEXTTABLE
CONTAINS
CONTAINSTABLE
FREETEXT is the easiest of these to work with; it lets you
specify a search term but then tries to look at the meaning rather than the exact
term when finding matches. For instance, here's a query using
FREETEXT together with its results:
SELECT ProductName
FROM Products
WHERE FREETEXT (ProductName, 'spread' )
ProductName
----------------------------------------
Grandma's Boysenberry Spread
Vegie-spread
(2 row(s) affected)
As you can see, FREETEXT finds the word or words you give it
anywhere in the search column. FREETEXTTABLE works like
FREETEXT except that it returns its results in a Table object.
CONTAINS (and CONTAINSTABLE, which works the same
but delivers results in a table) offers a much more complex syntax for using a
full-text indexed column:
CONTAINS
( { column | * } , '< contains_search_condition >'
)
< contains_search_condition > ::=
{ < simple_term >
| < prefix_term >
| < generation_term >
| < proximity_term >
| < weighted_term >
}
| { ( < contains_search_condition > )
{ AND | AND NOT | OR } < contains_search_condition > [ ...n ]
}
< simple_term > ::=
word | " phrase "
< prefix term > ::=
{ "word * " | "phrase * " }
< generation_term > ::=
FORMSOF ( INFLECTIONAL , < simple_term > [ ,...n ] )
< proximity_term > ::=
{ < simple_term > | < prefix_term > }
{ { NEAR | ~ } { < simple_term > | < prefix_term > } } [ ...n ]
< weighted_term > ::=
ISABOUT
( { {
< simple_term >
| < prefix_term >
| < generation_term >
| < proximity_term >
}
[ WEIGHT ( weight_value ) ]
} [ ,...n ]
)
For instance, you can search for one word "near" another this way:
SELECT ProductName
FROM Products
WHERE CONTAINS(ProductName, '"laugh*" NEAR lager')
ProductName
----------------------------------------
Laughing Lumberjack Lager
(1 row(s) affected)
Note the use of "laugh*" to match any word starting with "laugh."
You can also supply a weighted list of terms to CONTAINS, and it
will prefer matches with a higher weight:
SELECT ProductName
FROM Products
WHERE CONTAINS(ProductName, 'ISABOUT (stout weight (.8),
ale weight (.4), lager weight (.2) )' )
ProductName
----------------------------------------
Laughing Lumberjack Lager
Steeleye Stout
Sasquatch Ale
Outback Lager
(4 row(s) affected)
Looking Forward to SQL Server 2005
SQL Server 2005 features quite a number of changes and improvements in
full-text
searching:
- A dedicated indexing service that works directly with SQL Serrver. This
speeds
up full-text operations and isolates SQL Server from changes to the search
service made by other applications.
- Data definition language (DDL) statements for creating and altering full-text
catalogs and indexes.
- Full-text queries against linked servers.
- Full-text queries against arbitrary sets of columns (instead of just one
column
or all columns).
- Specification of the language to be used for word-breaking in an index.
- Integrated backup and restore for full-text catalogs.
- Full-text indexing for XML data.
- Integration with SQL Profiler and logging of index operations.
If you were interested in full-text searching in SQL Server 2000 but ran into
brick walls, take another look when the new version comes out. Microsoft's
substantial work in this area means that full-text indexing and searching will
be better than ever.
Full-Text to the Rescue
Many SQL Server problems can be solved without ever looking at full-text
search. But it comes in very handy in one key scenario: when human beings are
supplying search terms from their own head, instead of from a list. You may need
to work at providing a good user interface for this facility, but if you have
people searching through a large corpus of text, you should definitely consider
full-text searching. The end result is likely to be a better application and
happier users.
Mike Gunderloy is the author of over 20 books and numerous articles on
development topics, and the lead developer for Larkware. Check out his latest book, Coder to Developer from Sybex. When
he's not writing code, Mike putters in the garden on his farm in eastern
Washington state.
Retrieving Scalar Data from a Stored Procedure
By Scott Mitchell
Introduction
Virtually all ASP.NET applications of interest work with database data at some level, and one of the most common databases
used in ASP.NET applications is Microsoft's own SQL Server database. With
relational databases like SQL, commands are issued through the SQL syntax, which includes SELECT,
INSERT, UPDATE, and DELETE statements, among others. One way to issue a command
to a database from an ASP.NET application is to craft the SQL query in the application itself. Such queries are often called
ad-hoc queries. The primary downside of ad-hoc queries is that they are hard to maintain - if you need to change
your query you need to edit the string in your application, recompile, and redeploy.
A better approach, in my opinion, is to use stored procedures. Stored procedures are pre-compiled functions that reside on
the database server that can be invoked by name. This is similar to compartmentalizing programmatic functionality into methods.
Stored procedures are not only more updateable than their ad-hoc counterpart, but also can be utilized by other applications.
For example, you might have both an ASP.NET application and a Web services application that is driven on data from the same
database. If you hard code your SQL queries in your source code, any changes will now require modifications in two
places (as well as two places that now require recompilation and redeployment). However, by using stored procedures there's
a single point that needs modification. (The debate between stored procedures and ad-hoc queries has been done in much greater
detail in other venues; see Rob Howard's blog entry
Don't use stored procedures yet? Must be suffering from NIHS (Not Invented Here Syndrome)
for a pro-stored procedures slant, and Frans Bouma's entry
Stored Procedures are Bad, M'Kay? for a look at
why stored procedures aren't the end-all answer.)
Stored procedures typically return resultsets, such as the results of a SELECT query. However, there are times
when you may be getting back just scalar data from a stored procedure. For example, you might have a stored procedure
that returns just the account balance for a particular customer, or one that returns the average age of all users in your
database. When calling a stored procedure that INSERTs a new record into a table with an IDENTITY
field, you may want to get back the ID for the newly inserted row.
There are a couple of ways to get back scalar data from a SQL Server stored procedure. In this article we'll look at these
various techniques along with how to fetch back the returned data in your ASP.NET code. Read on to learn more!
Returning Data with a SELECT Statement
Typically data is returned from a stored procedure using a SELECT statement, and typically the data returned
is a resultset, consisting of multiple fields and records. For example, a stored procedure might be created to get all
products in inventory, which might be accessed through the SQL query:
CREATE PROCEDURE store_GetInventory AS
SELECT InventoryID, ProductName, Price, UnitsOnStock
FROM store_Inventory
|
However, there's no reason why you can't return a simple scalar value. For example, if you were interested in the average
price of all items in inventory - i.e., just a simple number, like $11.92 - you could return this scalar data using a SELECT
statement:
CREATE PROCEDURE store_GetAverageInventoryPrice AS
SELECT AVG(Price) AS AveragePrice
FROM store_Inventory
|
Similarly, in stored procedures that insert a new record into a table that has an IDENTITY field, you can
get the ID value of the newly inserted record through the SCOPE_IDENTITY()
function. So, after INSERTing the new record you can simply return the value like so:
CREATE PROCEDURE store_AddNewInventoryItem
(
@ProductName nvarchar(50),
@Price money
) AS
-- INSERT the new record
INSERT INTO store_Inventory(ProductName, Price)
VALUES(@ProductName, @Price)
-- Now return the InventoryID of the newly inserted record
SELECT SCOPE_IDENTITY()
|
When returning scalar through a SELECT statement you can retrieve the data using the exact same technique
used to retrieve a resultset. That is, you can, if you want, use a DataReader, DataTable, or DataSet. The only thing to
keep in mind is that you're results will contain only one row with only one field. The following code would call
the store_GetAverageInventoryPrice and grab back the scalar result:
Dim myConnection as New SqlConnection(connection string)
Dim myCommand as New SqlCommand("store_GetAverageInventoryPrice", myConnection)
myCommand.CommandType = CommandType.StoredProcedure
Dim reader as SqlDataReader = myCommand.ExecuteReader()
'Read in the first record and grab the first column
Dim avgPrice as Decimal
If reader.Read() Then
avgPrice = Convert.ToDouble(reader("AveragePrice"))
End If
|
This is a bit of overkill, though, thanks to the DataCommand's ExecuteScalar() method. The
ExecuteScalar() method can be used in place of the ExecuteReader(), the difference being
ExecuteScalar() returns a single Object instance as opposed to a DataReader. Using
ExecuteScalar() the code would be simplified to:
Dim myConnection as New SqlConnection(connection string)
Dim myCommand as New SqlCommand("store_GetAverageInventoryPrice", myConnection)
myCommand.CommandType = CommandType.StoredProcedure
Dim avgPriceObject as Decimal = Convert.ToDecimal(myCommand.ExecuteScalar())
|
(The above omits a check to see if the result is NULL. If there were no rows in store_Inventory or
no rows with a non-NULL Price, the returned Object would be equal to DBNull.Value.
Ideally you would either add such a check to the above code or edit the stored procedure to use ISNULL to
convert any NULL result into a number (i.e., SELECT ISNULL(AVG(Price), 0.0) ...).)
While the SELECT method just discussed provides an easy way to return a scalar value from a stored procedure
it only works if the scalar value is the sole piece of data you want to return from the stored procedure. There
are times, however, where you want to return a full resultset from the stored procedure along with some scalar value.
The remaining two approaches we'll be looking at in this article address how to accomplish this feat.
Using Output Parameters
One way to retrieve scalar data in addition to a standard resultset from a stored procedure is to use one or more
output parameters. An output parameter is a parameter that is passed into the SQL stored procedure, but whose
value can be set in the stored procedure. This assigned parameter, then, is readable back from the application that
called the stored procedure.
To use an output parameter you need to indicate that the parameter is intended for output via the OUTPUT keyword.
The following snippet shows a stored procedure that returns the set of inventory items through a SELECT statement
and uses an output parameter to return the average price:
CREATE PROCEDURE store_GetInventoryWithAveragePrice
(
@AveragePrice money OUTPUT
)
AS
SET @AveragePrice = (SELECT AVG(Price) FROM store_Inventory)
SELECT InventoryID, ProductName, Price, UnitsOnStock
FROM store_Inventory
|
To access the value of an output parameter from your ASP.NET application you need to create a parameter object whose
Direction property is set to Output. After you call the stored procedure the output parameter's
value is accessible through the Value property, as the following code illustrates:
Dim myConnection as New SqlConnection(connection string)
Dim myCommand as New SqlCommand("store_GetInventoryWithAveragePrice", myConnection)
myCommand.CommandType = CommandType.StoredProcedure
'Create a SqlParameter object to hold the output parameter value
Dim avgPriceParam as New SqlParameter("@AveragePrice", SqlDbType.Money)
'IMPORTANT - must set Direction as Output
avgPriceParam.Direction = ParameterDirection.Output
'Finally, add the parameter to the Command's Parameters collection
myCommand.Parameters.Add(avgPriceParam)
'Call the sproc...
Dim reader as SqlDataReader = myCommand.ExecuteReader()
'Now you can grab the output parameter's value...
Dim avgPrice as Decimal = Convert.ToDecimal(avgPriceParam.Value)
|
(The same issue regarding NULLs applies here as in the previous example...)
You are not limited to a single output parameter; additionally, you can have stored procedures with both input
and output parameters.
Using a Return Value
The final technique I want to talk about for returning scalar values from a stored procedure is using return values. Whenever
a stored procedure finishes executing, it always returns a return value. This return value is, by default, 0. You can use
the RETURN statement yourself, however, to return a scalar integer value. For example, let's revisit
the store_AddNewInventoryItem, but modify it to return the ID of the newly inserted row as a return value.
CREATE PROCEDURE store_AddNewInventoryItem
(
@ProductName nvarchar(50),
@Price money
) AS
-- INSERT the new record
INSERT INTO store_Inventory(ProductName, Price)
VALUES(@ProductName, @Price)
-- Now return the InventoryID of the newly inserted record
RETURN SCOPE_IDENTITY()
|
Note that the SCOPE_IDENTITY() value is being return via a RETURN statement now, whereas in the
earlier example we used a SELECT.
To retrieve the return value from a stored procedure use the same technique as with output parameters, the only difference
being that you should use a Direction value of ReturnValue, as the following code snippet
illustrates:
Dim myConnection as New SqlConnection(connection string)
Dim myCommand as New SqlCommand("store_GetInventoryWithAveragePrice", myConnection)
myCommand.CommandType = CommandType.StoredProcedure
'Create a SqlParameter object to hold the output parameter value
Dim retValParam as New SqlParameter("@RETURN_VALUE", SqlDbType.Int)
'IMPORTANT - must set Direction as ReturnValue
retValParam.Direction = ParameterDirection.ReturnValue
'Finally, add the parameter to the Command's Parameters collection
myCommand.Parameters.Add(retValParam)
'Call the sproc...
Dim reader as SqlDataReader = myCommand.ExecuteReader()
'Now you can grab the output parameter's value...
Dim retValParam as Integer = Convert.ToInt32(retValParam.Value)
|
That's all there is to it! As I mentioned earlier, you can only return integer values through the stored procedure's
return type.
Conclusion
In this article we examined three ways to pass back scalar data from a stored procedure, along with the necessary code to
process the returned value. You can use a SELECT statement, output parameter, or return value (assuming you want
to pass back an integer value). When returning a scalar value via a SELECT statement you can read the resulting
value using the ExecuteScalar() method. For output parameters and return values you need to create a parameter
object with the proper Direction property value. Then, after you call the stored procedure, you can access the
retrieved value through the parameter's Value property.
Happy Programming!
By Scott Mitchell
You must precede all Unicode strings with a prefix N when you deal with Unicode string constants in SQL Server| Article ID | : | 239530 | | Last Review | : | December 1, 2005 | | Revision | : | 6.0 |
This article was previously published under Q239530 SUMMARY When dealing with Unicode string constants in SQL Server
you must precede all Unicode strings with a capital letter N, as documented in
the SQL Server Books Online topic "Using Unicode Data". The "N" prefix stands
for National Language in the SQL-92 standard, and must be uppercase. If you do
not prefix a Unicode string constant with N, SQL Server will convert it to the
non-Unicode code page of the current database before it uses the string.
MORE INFORMATION This notation is necessary to provide backward
compatibility with existing applications. For example, "SELECT 'Hello'" must
continue to return a non-Unicode string because many applications will expect
the behavior of SQL Server 6.5, which did not support Unicode data; the new
syntax "SELECT N'Hello'" has been added to allow the passing of Unicode strings
to and from SQL Server 7.0.
Any time you pass Unicode data to SQL
Server you must prefix the Unicode string with N. If your application is
Unicode-enabled and sends data to SQL Server 7.0 as Unicode string constants
without the N prefix, you may encounter a loss of character data. When SQL
Server converts a Unicode string without the N prefix from Unicode to the SQL
Server database's code page, any characters in the Unicode string that do not
exist in the SQL Server code page will be lost. Note that this translation is
not related to Autotranslation, OemToAnsi, or AutoAnsiToOem conversion, all of
which occur on the client at the ODBC, OLEDB, or DB-Library layer.
If
your application does not send Unicode data to SQL Server and the client's ANSI
code page matches the SQL Server code page, there is no need to prefix string
constants with N, and you will not experience data loss as a result of omitting
the prefix. However, SQL Server 7.0 allows you to select a Unicode collation
during installation that is distinct from the sort order, and in some cases
this can cause operations involving strings prefixed with N to have different
results from those that do not have the prefix. For example, suppose that when
you installed SQL Server 7.0, you selected a binary sort order (sort orders are
used when comparing non-Unicode strings), and selected General Unicode as the
Unicode collation (the Unicode collation is used for comparing Unicode
strings). The expression comparing two non-Unicode strings ("ABC" = "abc")
would return False since a capital letter "A" is not equivalent to a lower-case
"a" according to a binary sort order. In contrast, the expression (N'ABC' =
N'abc') would return True. Because the strings are prefixed with an N, they
will be converted to Unicode and the Unicode collation will be used to compare
them. Unlike the binary sort order, the General Unicode collation is case
insensitive and would regard the two strings as equivalent.
Note that
if one of two string constant operands is prefixed with an N and the other is
not, the non-Unicode string will be converted to Unicode and the Unicode
collation will apply when comparing them. This behavior is explained in the SQL
Server Books Online topic "Comparison Operators".
Server-Side Programming with Unicode
Updated:
17 July 2006 To make a database Unicode-aware involves defining Unicode-aware client interactions in addition to using the nchar, nvarchar, and nvarchar(max) data types to define Unicode storage. You can define Unicode-aware client interactions by performing the following on the database server side: -
Switch from non-Unicode data types to Unicode data types in table columns and in CONVERT() and CAST() operations.
-
Substitute using ASCII() and CHAR() functions with their Unicode equivalents, UNICODE() and NCHAR().
-
Define variables and parameters of stored procedures and triggers in Unicode.
-
Prefix Unicode character string constants with the letter N.
Using UNICODE(), NCHAR(), and Other Functions
The ASCII() function returns the non-Unicode character code of the character passed in. Therefore, use the counterpart UNICODE() function for Unicode strings where you would use the ASCII function on non-Unicode strings. The same is true of the CHAR function; NCHAR is its Unicode counterpart. Because the SOUNDEX() function is defined based on English phonetic rules, it is not meaningful on Unicode strings unless the string contains only the Latin characters A through Z and a through z. ASCII, CHAR, and SOUNDEX can be passed Unicode parameters, but these arguments are implicitly converted to non-Unicode strings. This could cause the possible loss of Unicode characters before processing, because these functions operate on non-Unicode strings by definition. Besides the UNICODE() and NCHAR() functions, the following string manipulation functions support Unicode wherever possible: CHARINDEX(), LEFT(), LEN(), UPPER(), LOWER(), LTRIM(), RTRIM(), PATINDEX(), REPLACE(), QUOTENAME(), REPLICATE(), REVERSE(), STUFF(), SUBSTRING(), UNICODE(). These functions accept Unicode arguments, respect the 2-byte character boundaries of Unicode strings, and use Unicode sorting rules for string comparisons when the input parameters are Unicode.
Defining Parameters in Stored Procedures
Defining parameters with a Unicode data type guarantees that client requests or input are implicitly converted to Unicode on the server and not corrupted in the process. If the parameter is specified as an OUTPUT parameter, a Unicode type also minimizes the chance of corruption on its way back to the client. In the following stored procedure, the variable is declared as a Unicode data type. CREATE PROCEDURE Product_Info
@name nvarchar(40)
AS
SELECT p.ListPrice, v.Name
FROM Production.Product p
INNER JOIN Purchasing.ProductVendor pv
ON p.ProductID = pv.ProductID
INNER JOIN Purchasing.Vendor v
ON pv.VendorID = v.VendorID
WHERE p.Name = @name;
Using the N Prefix
Unicode string constants that appear in code executed on the server, such as in stored procedures and triggers, must be preceded by the capital letter N. This is true even if the column being referenced is already defined as Unicode. Without the N prefix, the string is converted to the default code page of the database. This may not recognize certain characters. For example, the stored procedure created in the previous example can be executed on the server in the following way: EXECUTE Product_Info @name = N'Chain' The requirement to use the N prefix applies to both string constants that originate on the server and those sent from the client.
Understanding the WITH Clause
Submitted by JonathanGennick on 1 July 2003 - 12:00pm.
DB2 | Oracle | SQL
Recently I researched recursive SQL queries for an Oracle
Technology Network (OTN) article on Oracle's CONNECT BY
syntax. (By the way, that article is scheduled to appear on
OTN sometime in July) While researching that article,
someone at Oracle pointed me to the SELECT statement's WITH
clause. WITH was introduced in the SQL:1999 standard, and
made it's way into Oracle in Oracle9i Database Release 1.
As defined in the standard, WITH enables you to do two
things:
I was only dimly aware of WITH, and hadn't given it much
thought. That it could be used to write recursive queries
was a surprise to me, probably because Oracle hasn't
implemented that aspect of WITH. I decided to do some
investigating to find out just what WITH was all about.
Using WITH to Issue Recursive Queries
Perhaps the driving force behind the introduction of WITH
in the SQL standard was the need to issue recursive
queries. Oracle has long (since version 2) supported such
queries through its CONNECT BY syntax. For example, the
following CONNECT BY query retrieves the definition of an
automobile from a bill-of-mterials table:
SQL> SELECT assembly_id, assembly_name, parent_assembly
2 FROM bill_of_materials
3 START WITH assembly_id=100
4 CONNECT BY parent_assembly = PRIOR assembly_id;
ASSEMBLY_ID ASSEMBLY_NAME PARENT_ASSEMBLY
----------- ----------------------- ---------------
100 Automobile
110 Combustion Engine 100
111 Piston 110
112 Air Filter 110
113 Spark Plug 110
114 Block 110
115 Starter System 110
116 Alternator 115
117 Battery 115
118 Starter Motor 115
120 Body 100
121 Roof 120
122 Left Door 120
123 Right Door 120
130 Interior 100
If you look carefully at this output, you can see that an
automobile is made up of a combustion engine, which in turn
is made up of a piston, air filter, spark plug, block, and
starter system. The starter system is in turn composed of
an alternator, battery, and starter motor, and so forth.
CONNECT BY gives you the ability to query a table that is
recursively related to itself, and the results from a
CONNECT BY query are generated in the hierarchical order
shown here.
When you write a CONNECT BY query, you can think of the
START WITH clause as defining the set of root nodes to be
returned by the query. The CONNECT BY clause then defines
the "join" between parent and child rows. I explain
this in more detail in my OTN article.
Partly for grins, and partly to expand my horizons a bit, I
decided to look at how the preceding query could be
implemented using WITH instead of CONNECT BY. To do that, I
had to use a database supporting recursive WITH, so I
installed a copy of IBM DB2. After a good bit of
head-scratching and reading of IBM's example queries, I
came up with the following WITH-based solution to my
bill-of-materials query:
WITH recursiveBOM
(assembly_id, assembly_name, parent_assembly) AS
(SELECT parent.assembly_id,
parent.assembly_name,
parent.parent_assembly
FROM bill_of_materials parent
WHERE parent.assembly_id=100
UNION ALL
SELECT child.assembly_id,
child.assembly_name,
child.parent_assembly
FROM recursiveBOM parent, bill_of_materials child
WHERE child.parent_assembly = parent.assembly_id)
SELECT assembly_id, parent_assembly, assembly_name
FROM recursiveBOM;
Wow! This is inscrutable. It took me a long time
fathom this query. Let's take it a piece at a time:
WITH recursiveBOM
(assembly_id, assembly_name, parent_assembly) AS
The WITH keyword defines the name recursiveBOM for the
subquery that is to follow. Next comes a list of column
aliases to use when referencing the results of the
subquery. Oracle requires that you specify aliases in the
subquery's SELECT, something I'll talk more about later.
Next comes the first part of the named subquery:
(SELECT parent.assembly_id,
parent.assembly_name,
parent.parent_assembly
FROM bill_of_materials parent
WHERE parent.assembly_id=100
The named subquery is a UNION ALL of two queries. This,
the first query, defines the starting point for the
recursion. As in my CONNECT BY query, I want to know what
makes up assembly 100, an automobile.
Next up is the part that was most confusing to me:
UNION ALL
SELECT child.assembly_id,
child.assembly_name,
child.parent_assembly
FROM recursiveBOM parent, bill_of_materials child
WHERE child.parent_assembly = parent.assembly_id)
This is where things get recursive, and confusing. The
second query in the union joins bill_of_materials to the
results of the named subquery. Notice how the WHERE clause
strikingly resembles the CONNECT BY clause used in the
Oracle version of this recursive query.
The final part of the query is the main SELECT:
SELECT assembly_id, parent_assembly, assembly_name
FROM recursiveBOM;
This SELECT does nothing more than to retrieve the results
returned by named subquery. All the recursion happens in
the subquery. The important thing to notice here is that
the three column names in the main query's select-list
match the three aliases specified near the beginning of the
WITH clause. The aliases you specify for a named subquery
are the names you must use when retrieving the results of
that subquery.
How does WITH's recursion work? It's important to form a
clear mental-model of how a query such as this produces the
results that it does.
For more on mental models, see my article at: http://gennick.com/mental_models.html
My first attempt at understanding the preceding WITH query
was to think in terms of replacing the query *text*
recursively. When I came across the text "recursiveBOM", I
replaced it with the subquery text. The resulting mess
that I envisioned in my mind looked as follows, with
ellipses (...) indicating where further query text
expansion would occur:
WITH recursiveBOM
(assembly_id, assembly_name, parent_assembly) AS
(SELECT parent.assembly_id,
parent.assembly_name,
parent.parent_assembly
FROM bill_of_materials parent
WHERE parent.assembly_id=100
UNION ALL
SELECT child.assembly_id,
child.assembly_name,
child.parent_assembly
FROM (...) parent, bill_of_materials child
WHERE child.parent_assembly = parent.assembly_id)
SELECT assembly_id, parent_assembly, assembly_name
FROM (SELECT parent.assembly_id,
parent.assembly_name,
parent.parent_assembly
FROM bill_of_materials parent
WHERE parent.assembly_id=100
UNION ALL
SELECT child.assembly_id,
child.assembly_name,
child.parent_assembly
FROM (...) parent, bill_of_materials child
WHERE child.parent_assembly = parent.assembly_id);
The above is not the way to understand a recursive WITH
query. The recursion is not modeled in terms of the text.
You have to think in terms of subquery *results*.
Following is the mental model I'm currently using to
understand this recursive WITH query:
1. The parent query executes:
SELECT assembly_id, parent_assembly, assembly_name
FROM recursiveBOM;
This triggers execution of the named subquery.
2 The first query in the subquery's union executes, giving
us a seed row with which to begin the recursion:
SELECT parent.assembly_id,
parent.assembly_name,
parent.parent_assembly
FROM bill_of_materials parent
WHERE parent.assembly_id=100
The seed row in this case will be for "Automobile".
Let's refer to the seed row from here on out as the
"new results", new in the sense that we haven't finished
processing them yet.
3 The second query in the subquery's union executes:
SELECT child.assembly_id,
child.assembly_name,
child.parent_assembly
FROM recursiveBOM parent, bill_of_materials child
WHERE child.parent_assembly = parent.assembly_id
This is where things get interesting. Notice the
recursive reference to recursiveBOM. That plays into
Step 4.
4. The recursive reference to recursiveBOM is replaced by
the query results. And what are the query results?
They are:
Do you see the recursion here? It took me most of a day
to get to the point where I felt comfortable that I
understood this. Hopefully you'll catch on more quickly
than I did. By the way, the recursion stops when a join
fails to bring back any more new rows.
Oracle doesn't support the use of WITH to write recursive
queries, so if you're using only Oracle, everything I've
discussed so far is somewhat academic. However, WITH is
part of the ANSI/ISO SQL standard, and it's good to be
familiar with what the SQL standard calls for as opposed to
what Oracle implements. Who knows? Someday you may
encounter a recursive WITH in code written for another
database, and now you're equipped to understand it.
Factoring Out Subqueries
The factoring out of subqueries is one use of WITH that
Oracle does support, and that use has positive implications
for performance. Consider the following SQL query which
uses a non-correlated subquery to retrieve all parts with
inventories greater than the average:
SELECT part_number, (SELECT AVG(current_inventory)
FROM part)
FROM part
WHERE current_inventory > (SELECT AVG(current_inventory)
FROM part);
This query uses the same subquery twice, once to return the
average inventory along with each result row, and again to
determine the average inventory for use in the WHERE
clause. Using WITH, you can factor out the subquery:
WITH partavg AS (SELECT AVG(current_inventory) avg_inventory
FROM part)
SELECT part_number, (SELECT avg_inventory FROM partavg)
FROM part
WHERE current_inventory > (SELECT avg_inventory
FROM partavg);
The main query still contains two subqueries, but each of
those subqueries is a SELECT from the named subquery
partavg. This gives the optimizer enough information to
determine that the partavg subquery needs to be executed
only once.
Notice here that I've named the column returned by the
partavg query by specifying avg_inventory as a column alias
within the named subqueries SELECT clause. This is
different from the IBM DB2 approach you saw earlier.
To see how these queries executed, I connected to my
test database using SQL*Plus, issued the SQL*Plus command
SET AUTOTRACE TRACEONLY, and then executed each query.
Following are the two execution plans:
Execution Plan
---------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 0 FILTER
2 1 TABLE ACCESS (FULL) OF 'PART'
3 1 SORT (AGGREGATE)
4 3 TABLE ACCESS (FULL) OF 'PART'
Execution Plan
---------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 2 RECURSIVE EXECUTION OF 'SYS_LE_2_0'
2 0 TEMP TABLE TRANSFORMATION
3 2 FILTER
4 3 TABLE ACCESS (FULL) OF 'PART'
5 3 VIEW
6 5 TABLE ACCESS (FULL) OF 'SYS_TEMP_0FD9D660B_4B6F5C4'
It's interesting to notice that the first execution plan
does not indicate a double execution of the subquery. I'll
come back to this point shortly, because the execution
statistics make it rather obvious that the subquery is, in
fact, being executed twice.
The second execution plan is from the WITH version of the
query. You can see the effects of WITH in the plan. The
"RECURSIVE EXECUTION OF 'SYS_LE_2_0'" no doubt represents
execution of the named subquery. The results are stored in
a temporary table named SYS_TEMP_0FD9D660B_4B6F5C4, and
that table feeds into the processing of the main query.
How do I know that the subquery is executed twice the
first time, but only once when it's factored out using
WITH? The execution statistics provide a good clue. Look at
the following two sets of statistics, and focus on the
number of consistent gets in each set:
Statistics
---------------------------------------------------------
1 recursive calls
2 db block gets
9427 consistent gets
0 physical reads
53880 redo size
2290609 bytes sent via SQL*Net to client
572834 bytes received via SQL*Net from client
4371 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
65536 rows processed
Statistics
---------------------------------------------------------
69 recursive calls
10 db block gets
7141 consistent gets
2 physical reads
1264 redo size
2290603 bytes sent via SQL*Net to client
572834 bytes received via SQL*Net from client
4371 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
65536 rows processed
The first set of statistics is from the first query, which
specifies the same subquery redundantly. The execution of
that query against my test data required 9427 consistent
gets. (A "consistent get" represents the logical read of
a block from the database buffer cache) The second set of
statistics is from the WITH version of the query, and
required only 7141 consistent gets. Clearly the WITH
version of the query is doing less work, and I can only
attribute that to the fact that the subquery is being
executed once, not twice. It's a bit disappointing that the
execution plan doesn't make that crystal clear.
Correlated Subqueries
The previous example was easy, because it used a
non-correlated subquery, one that did not reference values
from the parent query. It's trivial to factor out a
non-correlated subqueries. But what if your subquery is
correlated, and references one or more values from the main
query. What then? It turns out you can still benefit from
WITH, but you may have to think just a bit harder.
The following query is a slight revision of the part query
shown earlier, but this time, rather than compare each
part's inventory to the average of all inventories, it
compares each part's inventory to the average of all
inventories of parts made by the same manufacturer. Thus,
the subqueries are correlated, referencing the
manufactured_by value from the parent row.
SELECT p1.part_number,
(SELECT AVG(p2.current_inventory)
FROM part p2
WHERE p2.manufactured_by
= p1.manufactured_by) avg_inventory
FROM part p1
WHERE p1.current_inventory
> (SELECT AVG(p2.current_inventory)
FROM part p2
WHERE p2.manufactured_by=p1.manufactured_by);
WITH clause subqueries can't be correlated. I can't factor
out these subqueries by copying them as-is into a WITH
clause. It isn't that simple this time. However, after
thinking about things a bit, I realized that I was after
the average part inventory for each manufacturer, and that
I could generate those values all at once using a simple
GROUP BY query:
SELECT manufactured_by,
AVG(current_inventory) avg_inventory
FROM part
GROUP BY manufactured_by;
This subquery became the basis for the WITH version of my
correlated part query. Rather than compute the average
inventory for a given manufacturer anew for each part,
I can generate all the manufacturer-specific part inventory
averages just once, and then reference those results from
the main query.
Here's what I came up with:
WITH partavg AS (SELECT manufactured_by,
AVG(current_inventory) avg_inventory
FROM part
GROUP BY manufactured_by)
SELECT part_number,
(SELECT avg_inventory
FROM partavg
WHERE part.manufactured_by
= partavg.manufactured_by) avg_inventory
FROM part
WHERE part.current_inventory
> (SELECT avg_inventory
FROM partavg
WHERE part.manufactured_by
= partavg.manufactured_by)
Notice that I didn't totally eliminate subqueries from my
main query. What I did eliminate from the main query is the
constant summarizing and averaging of data. The WITH
subquery computes part inventory averages for each
manufacturer just once, placing those averages into a
temporary table. The subqueries in the main query now
select from that much smaller, pre-aggregated temporary
table. The results are telling. I won't show the execution
plans, because they are similar to the previous two plans,
but look at the statistics:
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
16794 consistent gets
0 physical reads
0 redo size
2934767 bytes sent via SQL*Net to client
572834 bytes received via SQL*Net from client
4371 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
65536 rows processed
Statistics
----------------------------------------------------------
4 recursive calls
7 db block gets
7139 consistent gets
1 physical reads
520 redo size
2934767 bytes sent via SQL*Net to client
572834 bytes received via SQL*Net from client
4371 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)
65536 rows processed
The number of consistent gets issued by the first version
of the query, the one with the redundant, correlated
subqueries, is 16,794. Look at the improvement in the WITH
version of the query, which issued only 7139 consistent
gets. That's less than half the work! Using WITH in this
case leads to a dramatic decrease in the amount of logical
I/O, and decreasing logical I/O is one key to improving
performance, both for the database as a whole and for the
query in question.
WITH can be an important tool in your query-writing
arsenal. Anytime you find yourself using the same subquery
twice, give some thought to using WITH to factor out that
subquery, thus reducing the amount of logical I/O your
query must perform. You can also use WITH to factor out
non-redundant subqueries in order to make a main query more
readable. Be sure to weigh the benefits of using WITH,
against the lack of compatibility with older releases of
Oracle, because WITH is not available prior to Oracle9i.
Acknowledgments
I'd like to thank Jim Melton of Oracle Corporation, who is
also the editor of the ANSI/ISO SQL Standard, for his
long-suffering patience when it came to answering my many,
many questions about the recursive use of WITH. If my
explanation of recursive WITH execution has any clarity,
it's largely due to Jim's patient explanations.
App_Offline.htm and working around the "IE Friendly Errors" feature
I posted the slides+demos from my ASP.NET 2.0 Tips and Tricks talk online last week from the ASP.NET Connections Conference in Orlando. One of the new features I talked about was the "App_Offline.htm" feature in ASP.NET 2.0, which provides a super convenient way to bring down an ASP.NET application while you make changes to it (for example: updating a lot of content or making big changes to the site where you want to ensure that no users are accessing the application until all changes are done). The way app_offline.htm works is that you place this file in the root of the application. When ASP.NET sees it, it will shut-down the app-domain for the application (and not restart it for requests) and instead send back the contents of the app_offline.htm file in response to all new dynamic requests for the application. When you are done updating the site, just delete the file and it will come back online. One thing I pointed out in the talk that you want to keep an eye on is a feature of IE6 called "Show Friendly Http Errors". This can be configured in the Tools->Internet Options->Advanced tab within IE, and is on by default with IE6. When this is on, and a server returns a non HTTP-200 status code with less than 512 bytes of content, IE will not show the returned HTML and instead substitutes its own generic status code message (which personally I don't think is super friendly <g>). So if you use the app_offline.htm feature, you should make sure you have at least 512 bytes of content within it to make sure that your HTML (instead of IE's friendly status message) shows up to your users. If you don't want to have a lot of text show-up on the page, one trick you can use is to just add an html client-side comment with some bogus content to push it over 512 bytes. For example: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head> <title>Site Under Construction</title> </head> <body> <h1>Under Construction</h1>
|