Click here to Skip to main content
Click here to Skip to main content

NoSpamEmailHyperlink: 2. Properties & Rendering

, 22 Oct 2003
Rate this:
Please Sign up or sign in to vote.
A look at ASP.NET custom control properties and rendering to HTML.

Transformation from ASP.NET properties to HTML

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 &lt;).

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)
    {
        // If inner text is a literal, we can
        // pick up the text at any point and
        // populate our Text property
        
        Text = ((LiteralControl)obj).Text;
    }
    else 
    {
        // If inner text is databound, we need to 
        // pick up the text at render time, so 
        // we'll add the control as a child
        //
        // NoSpamEmailHyperlinkBuilder.AppendSubBuilder
        // will throw out any other child controls at
        // parse-time, no other condition should exist        

        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)
        {
            // This allows codeblocks to be added in the
            // inner text of the control
            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)
{
    // The only controls that should be left at this
    // point should be DataBoundLiteralControls.  In theory,
    // if there are any then there should be exactly one 
    // and nothing else.
    string displayText = null;

    if (Controls.Count > 0 && Controls[0] is DataBoundLiteralControl)
    {
        displayText = ((DataBoundLiteralControl)Controls[0]).Text;
    }
    else
    {
        // If there is some text, use it.  If not then display the
        // encoded email address or the hyperlink will not be
        // visible
        displayText = (Text.Length == 0)
            ? Email
            : Text;
    }

    // If the EncodeInText flag is set and the email address
    // is somewhere in the Text, encode it.
    if (EncodeInText && Email.Length > 0)
    {
        int idx = displayText.IndexOf(Email);
        if (idx > -1) displayText = BrowserNeedsHide ? 
            HideText : Encode(displayText, Email);
    }

    // Html encode any text that remains.
    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
    {
        // If the Browser is Netscape (v4.x or less), we cannot change
        // the innerHTML at run time, so we'll just hide it

        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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Paul Riley
Web Developer
United Kingdom United Kingdom
Paul lives in the heart of En a backwater village in the middle of England. Since writing his first Hello World on an Oric 1 in 1980, Paul has become a programming addict, got married and lost most of his hair (these events may or may not be related in any number of ways).
 
Since writing the above, Paul got divorced and moved to London. His hair never grew back.
 
Paul's ambition in life is to be the scary old guy whose house kids dare not approach except at halloween.

Comments and Discussions

 
QuestionWhat about image url? Pinmemberforeverlegend19-Mar-05 23:33 
Generaloptions in grouping of RadioButtons in a Repeater and getting the value clicked on the Radiobutton by user!Please help urgent Pinmemberfredango16-Mar-05 22:26 
GeneralIE Pinmemberokigan23-Oct-03 8:49 
GeneralRe: IE PinmemberPaul Riley23-Oct-03 11:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.141022.2 | Last Updated 23 Oct 2003
Article Copyright 2003 by Paul Riley
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid