
Introduction
This is the second in a series of six articles, following the design, development and practical use of a fully functional ASP.NET custom control.
The full list of articles is as follows:
These articles are not intended to be a comprehensive look at custom control development (there are 700+ page books that barely cover it), but they do cover a significant number of fundamentals, some of which are poorly documented elsewhere.
The intent is to do so in the context of a single fully reusable and customizable control (as opposed to many contrived examples) with some awareness that few people will want many parts of the overall article but many people will want few parts of it.
This article examines properties and ViewState
handling, different property PersistenceMode
values and simple control rendering functionality. It is intended for those who are relatively new to writing custom controls but have learned the very basics already.
It assumes at least a basic knowledge of C#, WebControl
-derived classes and .NET Framework attributes.
Note: Downloads are available in the first article of the series.
Property Handling
The NoSpamEmailHyperlink
is based loosely on the functionality of the Hyperlink
control, but does not derive from Hyperlink
, primarily because .Email
is a more accurate name for the property we are using to build the hyperlink than .Href
would be.
It derives directly from System.Web.UI.WebControls.WebControl
, inheriting all that the base class has to offer in terms of formatting properties.
It also adds five public properties to allow the page developer to customize the handling at the control level:
.Text | The text do be displayed as the inner text of the hyperlink. Defaults to the email address. |
.Email | The address to which the hyperlink should point. |
.ScrambleSeed | Defines an integer seed for encoding. Defaults to 23. |
.EncodeInText | Encodes the email address where it exists in .Text . Defaults to true |
.HideText | The string to be used to hide addresses where the browser cannot handle the decode process. Defaults to Hidden |
Generic Property Handling
Most control properties will be handled as attributes of the control. For example, a Button
control may be defined as follows:
<asp:Button id="subCmd" runat="server" Text="Submit"></asp:Button>
The Text
attribute here is used to populate a .Text
property in the control. The NoSpamEmailHyperlink
control contains four such properties: .Email
, .ScrambleSeed
, .EncodeInText
and .HideText
.
The code for these properties does not vary much.
[
Bindable(true),
Category("Data"),
DefaultValue(""),
Description("Email address for hyperlink")
]
public virtual string Email
{
get
{
return (ViewState["Email"] is string)
? (string) ViewState["Email"]
: String.Empty;
}
set
{
ViewState["Email"] = value;
}
}
There are two significant points to address here.
Attributes
Each property has Bindable
, Category
, DefaultValue
and Description
attributes.
Note that "attribute" in this context is not the same as an HTML/XML attribute, it is a .NET Framework Attribute that appears in square brackets before a property, class or method. Mentioning the two together is unavoidable when discussing web controls, though the two are completely unrelated.
None of these attributes have any effect on the way the control is rendered at runtime. They are simply information for a WYSIWYG designer (such as Visual Studio .NET) to use when handling properties in the property page.
Bindable(true)
informs the designer that the property should be included in the DataBindings dialog, accessed usually by clicking the ellipses (...) button next to the (DataBindings)
pseudo-property. It never hurts to include this attribute as a matter of course, until you have a good reason for excluding it.
CategoryAttribute
helps the property page in a designer to group properties of a similar nature. Category("Data")
groups the properties alongside (DataBindings)
which is the most useful location in many cases. If you wish to use a different category then that is not a problem, but if you have no preference then use Data
because the default category is Misc
, which degrades the apparent importance of your properties.
The DefaultValueAttribute
for a WebControl
tells the property page which value to show if the property is "Reset" (Right-click, Reset in Visual Studio .NET) or which value, when entered, should stop the property from being persisted in the HTML. This should always be the same value returned in the code where the ViewState
value (see below) is null
.
Be aware that persistence in Web Form controls is handled very differently from persistence in Windows Forms controls.
DescriptionAttribute
defines some text that can be picked up by a designer to display alongside a property. In Visual Studio .NET, for example, the description is shown below the property page and can be shown or hidden using a splitter.
ViewState
When requested, the property checks a related ViewState
value and if it is null
or of an incorrect type (only possible where a request is forged) then it returns the default value. If the ViewState
value is valid for the property then it is cast to the correct type and returned.
ViewState
is an object of type StateBag
. When the page is being rendered, all of the values in the ViewState
of each control are optimized and combined into a single "hidden" input field.
This allows the value to be retained on postback, though if this is not required by the page developer, it can be switched off using the .EnableViewState
property without damaging the functionality of the control.
Note that if you wish to include a property of a type other than the very basic types (string
, bool
, int
, long
or anything else that is a keyword in C#) or an array of those types, it is necessary to create a TypeConverter
class to tell StateBag
how to optimize it. The details of this are beyond the scope of this article.
PersistenceModeAttribute
The above properties do not use the PersistenceModeAttribute
because they are allowed to default to PersistenceMode.Attribute
. There are three other kinds of PersistenceMode
, commonly applied to properties in a control: .InnerProperty
, .DefaultInnerProperty
and .EncodedDefaultInnerProperty
.
There are some common misconceptions about the use of this attribute that can easily confuse the first-time web control developer. Addressing them here will help explain the process we have to go through to make the .Text
property handle the inner text of the control.
Like the Attributes applied to attribute-style properties, the PersistenceModeAttribute
has no effect at run-time. It only tells the designer where to get or set the value of the property in the HTML.
PersistenceMode.EncodedDefaultInnerProperty
In reality, all attributes can be defined as inner properties or as attribute properties. For example, any of the following are equally valid definitions for the same Hyperlink
control:
<asp:HyperLink Runat="server" id="hl" Text="CodeProject"
NavigateUrl="http://www.codeproject.com"></asp:HyperLink>
<asp:HyperLink Runat="server" ID="hl" Text="CodeProject">
<NavigateUrl>http://www.codeproject.com</NavigateUrl>
</asp:HyperLink>
<asp:HyperLink Runat="server" ID="hl" NavigateUrl="http://www.codeproject.com">
CodeProject
</asp:HyperLink>
By default, the designer will use the last definition, because the .NavigateUrl
property is not marked with a PersistenceModeAttribute
(and thus defaults to PersistenceMode.Attribute
) while the .Text
property is marked as PersistenceMode.EncodedDefaultInnerProperty
.
Note that in VS.NET 2002 and 2003, there is one exception to this: when you use the DataBindings dialog to bind an inner property, the designer will persist it as an attribute property.
If the .Text
property was marked as PersistenceMode.InnerProperty
then it would be persisted in the same way but surrounded by <Text>...</Text>
tags. The Default
tells the designer not to use the tags while the Encoded
tells the designer to HTML-encode the property while persisting (i.e. <
becomes <
).
The NoSpamEmailHyperlink
control uses the same strategy:
[
Bindable(true),
Category("Data"),
DefaultValue(""),
PersistenceMode(PersistenceMode.EncodedInnerDefaultProperty)
]
public virtual string Text
{
get
{
return (ViewState["Text"] is string)
? (string) ViewState["Text"]
: String.Empty;
}
set
{
ViewState["Text"] = value;
}
}
Note that the same Attributes are used for the inner property as for our HTML-attribute style properties above and that the .Text
property is still persisted across round trips using ViewState
.
The only difference here is that we have added the PersistenceModeAttribute
, but remember that this change is only informing the designer how to handle updates in the property table. It does not tell the control how to handle such inner content at run time. On its own, a PersistenceMode.InnerDefaultProperty
attribute will cause the control to act very strangely. We need to make some further changes to handle the functionality of this.
ParseChildrenAttribute
First we need to tell the control not to parse child objects before we get chance to process them. We do this by overriding the default ParseChildrenAttribute
on the control.
[
// ...
ParseChildren(false),
// ...
]
public class NoSpamEmailHyperlink : System.Web.UI.WebControls.WebControl
{
}
AddParsedSubObject
We also need to override the .AddParsedSubObject()
method to handle the literal content, which is parsed as a control.
protected override void AddParsedSubObject(object obj)
{
if (obj is LiteralControl)
{
Text = ((LiteralControl)obj).Text;
}
else
{
base.AddParsedSubObject(obj);
}
}
The .AddParsedSubObject()
method is called by the page-parser at design time and at run time to add child controls. The only types of child controls we are interested in are:
LiteralControl
- when the control's inner text is static.
DataBoundLiteralControl
- when the control's inner text includes databinding code blocks ('<@# ... @>
') at run time.
DesignerDataBoundLiteralControl
- when the control's inner text includes databinding code blocks at design time
ControlBuilder
We could reject the rest here by throwing an exception if the control types fail to match any of those listed above. However, it is more efficient to do so by providing a custom ControlBuilder
and overriding the .AppendSubBuilder()
method. This is executed as each control is created by the parser and thus saves the effort of creating numerous controls where one will throw an exception.
public class NoSpamEmailHyperlinkBuilder : ControlBuilder
{
public override void AppendSubBuilder(ControlBuilder subBuilder)
{
if (subBuilder.ControlType == null)
{
base.AppendSubBuilder(subBuilder);
}
else
{
throw new InvalidOperationException(
String.Format(
"Control {0} may not contain {1}",
ControlType.FullName,
subBuilder.ControlType.FullName
)
);
}
}
}
This method is never called when the control's content is literal text, only when there are databinding code blocks and / or sub controls.
The databinding code block builders (those where subBuilder.ControlType == null
) should be handled as normal. Any other controls should throw an exception, which is caught by the parser and turned into an error marker at design time or a parsing error at run time.
While we are building a custom designer, we should also override the .AllowWhiteSpaceLiterals
property.
public class NoSpamEmailHyperlinkBuilder : ControlBuilder
{
public override bool AllowWhitespaceLiterals()
{
return false;
}
}
By default, this returns true
. That means that a literal containing only whitespace (spaces, tabs, new lines) will be accepted as it appears. Having overridden this to return false
, a literal containing only whitespace will be considered null
.
For the NoSpamEmailHyperlink
, this ensures that the control is always visible, except at run time where both the .Email
and .Text
properties are empty.
There are a number of other things we can do by overriding properties in the ControlBuilder
but for the NoSpamEmailHyperlinkBuilder
, this is sufficient. Look at other classes in the .NET Framework to see how many ways custom ControlBuilder
classes can be utilized.
Tying the Loose Ends
Finally, we attach our new NoSpamEmailHyperlinkBuilder
class to the NoSpamEmailHyperlink
control by adding a ControlBuilderAttribute
.
[
// ...
ControlBuilder(typeof(NoSpamEmailHyperlinkBuilder)),
// ...
]
public class NoSpamEmailHyperlink : System.Web.UI.WebControls.WebControl
{
}
Rendering the Control
The code required to actually render the control is now quite simple.
TagKey
The .TagKey
property is overridden in any control that is to render an HTML object other than the default <SPAN>
. As we are developing a hyperlink control, our primary tag type is <A>
.
protected override HtmlTextWriterTag TagKey
{
get
{
return HtmlTextWriterTag.A;
}
}
AddAttributesToRender
If the .Email
property is set, we need to add an href
attribute to the tag. We build this attribute by encoding the email address and prepending the mailto:
clause.
Regardless of this, we also need to render any formatting attributes, so it is important to call the base
implementation as well.
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
if (Email.Length > 0)
{
writer.AddAttribute(
HtmlTextWriterAttribute.Href,
"mailto:" + Encode(Email)
);
}
base.AddAttributesToRender (writer);
}
RenderContents
To render the contents of the control (i.e. the text between the <A>...</A>
tags), first we need to establish whether we have any child controls remaining.
If there are children of type DataBoundLiteralControl
then there should be only one. By this point in the lifespan of the control, the databinding has been dealt with and we can simply retrieve the .Text
property of the NoSpamEmailHyperlink
. Meanwhile, child controls of type DesignerDataBoundLiteralControl
have been used to populate the text seen in the designer, so we can ignore these.
Thus, if the child control is not of type DataBoundLiteralControl
then we can process the .Text
of this control directly. If the .Text
property has been set, this should be used to render the control text. If the .Text
property is empty, the .Email
property should be used in its place.
Once we have the contents of the string to be rendered, we can Encode
the email address (if necessary and requested), HtmlEncode
it and then write it to the page between the start and end tags.
protected override void RenderContents(HtmlTextWriter writer)
{
string displayText = null;
if (Controls.Count > 0 && Controls[0] is DataBoundLiteralControl)
{
displayText = ((DataBoundLiteralControl)Controls[0]).Text;
}
else
{
displayText = (Text.Length == 0)
? Email
: Text;
}
if (EncodeInText && Email.Length > 0)
{
int idx = displayText.IndexOf(Email);
if (idx > -1) displayText = BrowserNeedsHide ?
HideText : Encode(displayText, Email);
}
writer.Write(HttpUtility.HtmlEncode(displayText));
}
The BrowserNeedsHide
property simply checks the Page.Request.Browser
for Netscape versions 4.x or below, where decoding the email address in the link's innerHTML
property is not possible.
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
protected virtual bool BrowserNeedsHide
{
get
{
HttpBrowserCapabilities bc = Page.Request.Browser;
Version bv = new Version(bc.Version);
return (bc.Browser.ToLower().IndexOf("netscape") > -1
&& bv.Major < 5);
}
}
We do not want the address to be encoded for these browsers where we cannot decode them at the client, but we also do not want them shown in case a smart email harvester developer realizes that they only need to identify themselves as Netscape 4.5 to discover our otherwise hidden addresses. So, rather than encoding the address, we use the contents of the .HideText
property to replace the email address.
Of course, the href
attribute is encoded and decoded as normal in these cases, so there is no negative effect on functionality.
Conclusion
If we assume for now that the .Encode()
method does nothing and simply returns the string it is given, a NoSpamEmailHyperlink
control with these settings:
<cpspam:NoSpamEmailHyperlink id="nseh" runat="server"
Email="pdriley@santt.com" ScrambleSeed="181">
Paul Riley (pdriley@santt.com)
</cpspam:NoSpamEmailHyperlink>
will render the following HTML.
Netscape 4.x or earlier
<a id="nseh" href="mailto:pdriley@santt.com">
Paul Riley ([Hidden])
</a>
Other Browsers
<a id="nseh" href="mailto:pdriley@santt.com">
Paul Riley (pdriley@santt.com)
</a>
This is exactly the format we are looking for in an email hyperlink control. The only thing missing is the encode and decode functionality. The next article looks to fix that oversight.
For now we have examined a number of methods for persisting properties and implemented both the most simple and the most complex of them.
We have looked into a simple implementation of a ControlBuilder
class and considered property manipulation as part of the rendering process.
This should be enough to make a good start into the world of custom controls but in many cases, we need more. Next we shall take a look at JavaScript registration functions and how they can be used to manipulate any number of instances of your control on a single page without repetitive code.
Revision History
- 1.0 12-Oct-2003 - Created.
- 1.1 23-Oct-2003 - Added the
.HideText
property.