Title: ASP.NET Common Web Page Class Library - Part 4
Author: Eric Woodruff
Email: Eric@EWoodruff.us
Environment: Visual Studio .NET, IIS, ASP.NET, C#, VB.NET
Keywords: page, template, render, link, regular expression, validation summary
Level: Intermediate
Description: A utility class containing some useful features for ASP.NET applications
Section ASP.NET
SubSection General
Table of Contents
Introduction
This is the fourth in a series of articles on a class library for
ASP.NET applications that I have developed. It contains a set of common,
reusable page classes that can be utilized in web applications as-is to
provide a consistent look, feel, and set of features. New classes can also
be derived from them to extend their capabilities. The features are all
fairly modular and may be extracted and placed into your own classes too.
For a complete list of articles in the series along with a demonstration
application and the code for the classes, see Part
1 [^ ].
This article describes the only non-page derived class in the library,
PageUtils, along with the remaining methods of the
BasePage class that are somewhat similar in nature to those
contained in it. PageUtils contains a set of utility functions
that you may find useful in any ASP.NET application. Each of the features
is described below. The class itself is sealed and all public properties
and methods are static. As such, the constructor is declared private as
there is no need to instantiate the class.
HTML Encoding
The first method presented is HtmlEncode, which can be
called to encode an object for output to an HTML page. It encodes any HTML
special characters as literals instead of letting the browser interpret
them. In addition, it replaces multiple spaces, tabs, and line breaks with
their HTML equivalents thus preserving the layout of the specified text.
The size of expanded tab characters can be altered using the
TabSize property. Set it to the number of non-breaking spaces
that should replace the tab character. The default is four.
If the object is null
(Nothing), results in an empty string, or is a
single space, a non-breaking space is returned. In conjunction with the
above-described behavior, this is useful for displaying database fields
that contain HTML special characters, formatting, or nulls such as those
with the text or memo data type.
As an added bonus, if the encodeLinks parameter is true,
URLs, UNCs, and e-mail addresses are converted to hyperlinks whenever
possible using the EncodeLinks method (see below). If false,
they are not converted and will be rendered as normal text. As shown below,
the code is fairly simple and requires little in the way of additional
explanation.
public static string HtmlEncode(Object objText, bool encodeLinks)
{
StringBuilder sb;
string text;
if(objText != null)
{
text = objText.ToString();
if(text.Length != 0)
{
// Create tab expansion string if not done already
if(expandTabs == null)
expandTabs = new String(' ',
PageUtils.TabSize).Replace(" ", " ");
// Encode the string
sb = new StringBuilder(
HttpUtility.HtmlEncode(text), 256);
sb.Replace(" ", " "); // Two spaces
sb.Replace("\t", expandTabs);
sb.Replace("\r", "");
sb.Replace("\n", "<br>");
text = sb.ToString();
if(text.Length > 1)
{
if(!encodeLinks)
return text;
// Try to convert URLs, UNCs, and e-mail
// addresses to links.
return PageUtils.EncodeLinks(text);
}
if(text.Length == 1 && text[0] != ' ')
return text;
}
}
return " ";
}
Link Encoding
The second method presented is EncodeLinks. This method is
called by HtmlEncode but can also be called directly by your
code. It takes the passed string and finds all URLs, UNCs, and e-mail
addresses and converts them to clickable hyperlinks suitable for rendering
in an HTML page. For UNC paths, it will include any text up to the first
whitespace character. If the path contains spaces, you can enclose the
entire path in angle brackets (i.e., <\\Server\Folder\Name With
Spaces>) and the encoder will include all text between the angle
brackets in the hyperlink. The angle brackets will not appear in the
encoded hyperlink.
public static string EncodeLinks(string text)
{
// We'll create these on first use and keep them around
// for subsequent calls to save resources.
if(reURL == null)
{
reURL = new Regex(@"(((file|news|(ht|f|nn)tp(s?))://)|" +
@"(www\.))+[\w()*\-!_%]+.[\w()*\-/.!_#%]+[\w()*\-/" +
@".!_#%]*((\?\w+(\=[\w()*\-/.!_#%]*)?)(((&|&(?" +
@"!\w+;))(\w+(\=[\w()*\-/.!_#%]*)?))+)?)?",
RegexOptions.IgnoreCase);
reUNC = new Regex(@"(\\{2}\w+(\\((&.{2,8};|" +
@"[\w\-\.,@?^=%&:/~\+#\$])*[\w\-\@?^=%&/~\+#\$])?)" +
@"*)|((\<|\<)\\{2}\w+(\\((&.{2,8};|" +
@"[\w\-\.,@?^=%&:/~\+#\$ ])*)?)*(\>|\>))",
RegexOptions.IgnoreCase);
reEMail = new Regex(@"([a-zA-Z0-9_\-])([a-zA-Z0-9_\-\." +
@"]*)@(\[((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]" +
@"[0-9]|[0-9])\.){3}|((([a-zA-Z0-9\-]+)\.)+))(" +
@"[a-zA-Z]{2,}|(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|" +
@"[1-9][0-9]|[0-9])\])", RegexOptions.IgnoreCase);
reTSUNC = new Regex(
@"\.?((&\#\d{1,3}|&\w{2,8});((&\#\d{1,3}|&" +
@"\w{2,8}))?)+\w*$");
urlMatchEvaluator = new MatchEvaluator(OnUrlMatch);
uncMatchEvaluator = new MatchEvaluator(OnUncMatch);
}
// Do the replacements
text = reURL.Replace(text, urlMatchEvaluator);
text = reUNC.Replace(text, uncMatchEvaluator);
text = reEMail.Replace(text,
@"<a href='mailto:$&'>$&</a>");
return text;
}
As you can see, the method uses regular expressions to search for and
replace each URL, UNC, and e-mail address. The expressions used should
catch just about all variations of each type. The regular expression
objects are created on first use and are kept around for subsequent calls
to save a little time. For URLs and UNCs, the following match evaluators
handle the actual work of the replacement:
// Replace a URL with a link to the URL. This checks for a
// missing protocol and adds it if necessary.
private static string OnUrlMatch(Match match)
{
StringBuilder sb = new StringBuilder("<a href='", 256);
string url = match.Value;
// Use default HTTP protocol if one wasn't specified
if(url.IndexOf("://") == -1)
sb.Append("http://");
sb.Append(url);
sb.Append("' target='_BLANK'>");
sb.Append(url);
sb.Append("</a>");
return sb.ToString();
}
// Replace a UNC with a link to the UNC. This strips off any
// containing brackets (plain or encoded) and flips the slashes.
private static string OnUncMatch(Match match)
{
StringBuilder sb = new StringBuilder("<a href='file:", 256);
string unc = match.Value;
// Strip brackets if found. If it has encoded brackets,
// strip them too.
if(unc[0] == '<')
unc = unc.Substring(1, unc.Length - 2);
else
if(unc.StartsWith("<"))
unc = unc.Substring(4, unc.Length - 8);
// Move trailing special characters outside the link
Match m = reTSUNC.Match(unc);
if(m.Success == true)
unc = reTSUNC.Replace(unc, "");
sb.Append(unc);
sb.Append("' target='_BLANK'>");
// Replace backslashes with forward slashes
sb.Replace('\\', '/');
sb.Append(unc);
sb.Append("</a>");
if(m.Success == true)
sb.Append(m.Value);
return sb.ToString();
}
A regular expression match evaluator is like a callback. Each time the
regular expression finds a match, it calls the evaluator. Its job is to
take the found text and modify it in any way necessary and then return it
to the regular expression so that it can be used to replace the original
text. In these two cases, the match evaluators add the anchor tag and
ensure that the links are formatted appropriately.
Converting Validation Messages to Hyperlinks
In my applications, I have come to favor the validation summary control
to contain all validation error messages generated by the page. It keeps
them all in one location and does not adversely affect the layout of the
controls in the form when they are made visible. The drawback is that on a
form with a large number of controls and validation conditions, it can
sometimes be difficult to match each message to its control, especially if
the form is long enough to require scrolling around to find it. As such, I
have added functionality to the BasePage class to
automatically convert all validation control error messages that are set to
appear in a validation summary control to clickable hyperlinks that will
take you directly to the offending field by giving it the focus.
protected virtual void ConvertValMsgsToLinks()
{
BaseValidator bv;
foreach(IValidator val in this.Validators)
{
bv = val as BaseValidator;
if(bv != null && bv.Visible == true &&
bv.ControlToValidate.Length > 0 &&
bv.Display == ValidatorDisplay.None)
bv.ErrorMessage = MakeMsgLink(bv.ControlToValidate,
bv.ErrorMessage, this.MsgLinkCssClass);
}
}
A call to ConvertValMsgsToLinks is done as the very first
step in the overridden Render method. It iterates over the
page's Validators collection. The validator control must be
visible, must have its ControlToValidate property set to a
control ID, and must have its Display property set to
None indicating that it will appear in a validation summary
control. If all of the necessary conditions are met, a call is placed to
the MakeMsgLink method to convert the error message to a
hyperlink.
Note that since this occurs within the rendering step, changes to the
error messages are not retained. If the page posts back, the error messages
are restored from view state and will be in their non-hyperlink form. When
the page renders during the postback, the messages will be converted to
hyperlinks again provided that they still meet the necessary conditions. I
chose this approach so that it is transparent to users of the class, is
non-intrusive, and will not break any code that expects the messages to be
in their non-hyperlink form. Derived classes can override this method to
extend or suppress this behavior.
Note: If extracting the above method for use in your own
classes, be sure to override the page's Render method and call
it. If not, the links will not be converted.
public string MakeMsgLink(string id, string msg, string cssClass)
{
string newClass;
// Don't bother if it's null, empty, or already in the form
// of a link.
if(msg == null || msg.Length == 0 || msg.StartsWith("<a "))
return msg;
StringBuilder sb = new StringBuilder(512);
// Add the anchor tag and the optional CSS class
sb.Append("<a ");
newClass = (cssClass == null) ?
this.MsgLinkCssClass : cssClass;
if(newClass != null && newClass.Length > 0)
{
sb.Append("class='");
sb.Append(newClass);
sb.Append("' ");
}
// An HREF is included that does nothing so that we can use
// the hover style to do stuff like underline the link when
// the mouse is over it. OnClick performs the action and
// returns false so that we don't trigger IE's
// OnBeforeUnload event which may be tied to data change
// checking code.
// NOTE: OnPreRender registers the script containing the
// function. Tell the function to use the "Find Control"
// method to locate the ID. That way, it works for controls
// embedded in data grids.
sb.Append("href='javascript:return false;' " +
"onclick='javascript: return BP_funSetFocus(\"");
sb.Append(id);
sb.Append("\", true);'>");
sb.Append(msg);
sb.Append("</a>");
return sb.ToString();
}
The MakeMsgLink method will convert the passed text into a
hyperlink that transfers focus to the control with the specified ID. The
Set Focus script, described in part one of
this series, controls setting the focus to the control. As such, the
specified ID can be an exact match or the ending part of an ID (see
part one for details). An optional CSS class name
can be specified that will be applied to the hyperlink. If
null, it uses the one defined by the
MsgLinkCssClass property. By default, it is set to the value
of the BasePage.MsgLinkCssName constant which is currently set
to the style name ErrorMsgLink. The class name should appear in
the stylesheet associated with the application. As noted, a dummy
href is added to the link so that you can add a
hover style to the CSS class. For example, in my applications,
the error messages display as normal text and show an underline as the
mouse passes over them.
Conclusion
Although small, the PageUtils class contains some very
helpful features. The validation message link feature of
BasePage can also make the validation summary control more
user friendly. Hopefully, you will find this class and the others in the
library, or parts of them, as useful as I have.
Revision History
| 04/02/2006 |
|
Changes in this release:
- Reworked the URL encoding regular expression in PageUtils so
that it includes a few more protocols, includes all valid URL
characters, handles URLs with parameters, and does not include
special characters after the URL.
- Breaking Change: Property and method names have been
modified to conform to the .NET naming conventions with regard to casing
(
PageUtils.HtmlEncode, BasePage.MsgLinkCssClass,
and BasePage.MsgLinkCssName).
|
| 11/26/2004 |
|
Made some changes to the URL and UNC link encoding regular
expressions to make them more accurate. |
| |
| 12/01/2003 |
|
Initial release |