Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Letting your (custom) web controls have their say on the Page HEAD

0.00/5 (No votes)
7 Nov 2004 2  
Addresses the problem of web controls that need additions to the page HEAD element such as depending on an external style sheet, JavaScript or XML file.

Introduction

Web controls are a good thing to have, whether they are Web Controls, User controls or Custom Web Controls; and from now on, I will refer to them collectively as web controls. But ultimately, it all comes down to appearance and functionality.

This article is then about those times when you find out that the cow has five legs (in a figurative manner of course). Five legs? What does that have to do with ASP.NET? Well, that is the topic of the next section.

Before we continue and even though I classify the article at requiring Beginner's level of proficiency, I assume that Interfaces, HTML and basic ASP.NET are no mystery to you.

Background

In my previous life (as a web site developer), I did quite a bit of web control development in PHP. No, PHP does not have web controls as such - and that is what I found attractive from .NET to begin with - but you can make the equivalent of controls. When I began developing .NET web controls - and not that I have done many - I suddenly found myself confronted with one problem that had obviously not gone away. And that problem is what do you do when a web control's appearance or behavior depends on something else.

From now on, when I refer to the page HEAD section/element, I refer exclusively to the contents of the <HEAD> element in a properly formatted HTML document. It is therefore not a template header that makes all pages look the same at the top.

So, what is that something else? In my case, the dilemma always arose when my web control's behavior depended on some JavaScript code, or when its appearance depended on certain styles (from cascaded style sheets).

A control does not need JavaScript (or any other supported scripting language) for behaving as expected, but sometimes it does. Most of the times, you can get away with putting the script inline, but other times you also need to render the same control on the the page without duplicating the code. In this case, one typically relies on putting this "satellite code" in a separate .js file and specify it in a link header.

Likewise, a control sometimes needs quite some 'make up' in the form of styles. You can put it in the tag's style attribute and the job is done. A situation arises when the style is much more elaborate that it actually requires a CSS class in which case you simply use the tag's class attribute. In the former case, there is nothing else to worry about, in the latter however, you need that the CSS class be defined either as a block of text in the page's HEAD in the form of a STYLE element, or as a reference to an external cascaded stylesheet file with the LINK element. Either way, this goes in the HEAD section of the page where the control itself has no access. The same applies when you have multiple instances of the same web control within the page.

If you ask me, I would say there should be a programmatic way to let a control specify these things just like there is the Response.AddHeaders, only you never know when somebody (yuk!) writes an HTML page without the HEAD section, though tools normally add this automatically.

If on the other hand your control only needs to register in-line scripts that are rendered somewhere within the BODY element of the page you are better off using any of the Page class' RegisterClientScriptBlock, RegisterStartupScript, RegisterOnSubmitStatement or any combination thereof. Exploring these are left to the curiousity of the reader as that was not part of the problem domain.

That being said, it is time to get to how I solved the problem. This is not the only way to solve the problem, may not even be the best, but so far with all the searching I have done, I am yet to find something that addresses the problem, and so this idea materialized into C# code.

My Solution

The solution I describe here relies on the following:

  • Putting a Web Literal control within the page's HEAD section.
  • Defining the Literal control in the code behind.
  • Querying the child controls to check whether any of them needs "to have their say" in the HEAD section, i.e. need some JavaScript or style sheet file reference.
  • Have the controls with such special need to implement the IPageHeaderSubscriber interface.

I am sure somebody will come up with another variation on the theme - one of the nice things of these forums - or even better have something like that become part of the Framework. As for myself, I am glad that I no longer have to face the same recurring issue again, now I simply use this - with the small hassles of items 1 and 2 - and the job is done.

Understanding the code

I am not going to describe every little piece of code in this section, but I am sure reading through my code is not going to be an unpleasant experience. Craftsmanship is not only about writing code that works, but also documenting it properly, because some months down the line, you are not going to remember many things (it is a fact). I am a strong believer in documentation and coding standards, to say the least. But OK! Let's get going and see some snippets!.

It is very simple, really! The whole thing consists of two things as depicted below..

  • The IPageHeaderSubscriber that "needy" controls should implement.
  • The PageHeaderSubscriber class that performs the work.

The interface defines a set of properties that the control's implementing class must implement. It does not describe capabilities but rather the needs, or to put it more politely, the requests of the control. These properties are queried -one by one- by the PageHeaderSubscriber instance during processing of the child controls, if it gets true as a result, it then proceeds to invoke the associated method to obtain the information the child control is requesting to be placed in the page HEAD .

/****************************************************************
 *                 same control
 ****************************************************************/
/// The key is used to avoid duplicates, for example when multiple
/// instances of the same control appear on the same page.
string PageSubscriberKey { get; }
/****************************************************************
 *      Properties - These are queried IF the key above is not
 *                   the same as any other control keys.
 ****************************************************************/        
/// Gets whether the control depends on some file such
/// as a configuration file that if changed should cause
/// the web server to invalidate the current instances of
/// the page in the cache. <span class="code-SummaryComment"><see cref="GetFileDependencies"/>
</span>

The PageHeaderSubscriber class is the one that does all the work for you. It consists of a simple constructor where you specify a reference to the instance of the current page (where the controls are to be rendered), and a reference to the instance of the Literal control that will act as our place holder in the HEAD section (more on that later).

/// Constructor. This overload explicitely indicates which
/// literal will hold the information we gather.
/// <span class="code-SummaryComment"><param name="page">Page object we will handle </param>
</span>

And then it also has a simple method that takes the name of the ASP.NET web form that represents these page where the controls are used. This name is a string and is exactly the same you use in ID attribute of the <Form> tag in the ".aspx" page. Why the name has to be given rather than derive it, has already been explained earlier in this article.

public void Process(string formID) { ... }

As I mentioned earlier, this method should be called during the Page_Load to do the processing. At this point, it already knows the ID/Name of the form, and the instance of the Page object given in the constructor. Its first task is then to find the instance of the HtmlForm that corresponds to this form ID. It does so by invoking the FindControl() method of the page.

Once it finds the instance of the form control, it gets the list of children. The list of child controls is obtained by using the Controls property which returns a ControlsCollection value. We then go through each of the children using an iterator and check if the child implements IPageHeaderSubscriber; if it doesn't, we skip it, otherwise we invoke the interface properties and methods as explained earlier. One thing to note though, is that we check for the key returned by the control. This key helps to eliminate duplicates, it can also help to differentiate aesthetically different versions of the same control, for example two instances that use a different stylesheet.

if (en.Current is IPageHeaderSubscriber)
{
    ... some code omitted ...
    // Query each of the things we might need
    IPageHeaderSubscriber ctrl = (IPageHeaderSubscriber)en.Current;
    if (!keys.Contains(ctrl.PageSubscriberKey))
    {
        if (ctrl.HasFileDependency)
            depFiles.AddRange(ctrl.RegisterFileDependencies());
        if (ctrl.NeedsStyle)
            pgStyles.Add(ctrl.RegisterStyleBlock());
        if (ctrl.NeedsStyleSheet)
            pgStyleSheets.AddRange(ctrl.RegisterStyleSheets());
        if (ctrl.NeedsScript)
            pgScripts.Add(ctrl.RegisterScriptFiles());
        keys.Add(ctrl.PageSubscriberKey);
    }

When the child implements IPageHaderSubscriber, we go through each of the interface properties. The code snippet above shows the data collection part that we do for each of the qualifying children. Then the actual processing is performed as shown in the following code snippet.

if (this.pageHeaderLiteral != null)
{
    this.pageHeaderLiteral.Text = "";
    string headExtras  = "";

    // Files that are required. Handled directly by the Page object
    if (depFiles.Count > 0)
        DependentFileCollector(depFiles);

    // These appear within <head><style>...<style><head>
    if (pgStyles.Count > 0)
    {
        headExtras += ContentCollector(pgStyles, "style");
    }

    // These within a <link type='text/css' .. />
    if (pgStyleSheets.Count > 0)
    {
        headExtras += StyleSheetCollector(pgStyleSheets);
    }

    // And these within <script ..> elements
    if (pgScripts.Count > 0)
        headExtras += ScriptCollector(pgScripts);

    this.pageHeaderLiteral.Text = headExtras;
}

The beginning of the code above shows where the Literal control we talked about comes to some use. This is the instance which was provided to us at the constructor. Inquisitive readers may ask why am I checking whether the instance is null at this point rather than before doing the data collection. The answer is simple too, there is an extra constructor for lazy programmers that omits the instance of this Literal control. That means that in such cases, we also search for this control during the processing phase. This Literal control is expected to have the name /ID PageHeaderSubscriber, so if you don't provide it in the constructor, we look for this, but the control must exist, otherwise no further processing will be done.

The code above is pretty straightforward and self-explanatory, except perhaps for the addition of file dependencies. The processing phase of file dependencies actually converts the provided virtual/relative paths into physical paths using the MapPath() method, because that is what the Page.AddFileDependencies() method expects. This is the only item that does not appear in the HEAD section, because it is handled by the Page object.

The last part of the processing simply involves pasting up all the collected and transformed data into something we can stuff into the Literal control that is placed (by you) in the ASPX document's HEAD section.

Using the code

Needless to say, you would only use this in ASP.NET pages where you need this functionality, otherwise you need not bother with it. So, here is what you would do to have your ASPX page support "demanding child controls."

  • Having the control(s) implement the interface if they need to.
  • Placing a Literal ASP control within the HEAD section of the ASPX page.
  • Declaring the Literal control in the code of the page (or its code-behind).
  • Instantiating the handler and invoking the processing method.

The first has already been dealt with. The second involves putting the control as shown in the snippet of the .aspx file below. This shows only the relevant parts with the only exception of the SideBoxSimple custom web control that I will mention later. From this snippet, you only need to remember the values you use in the ID attribute of the Literal and Form elements.

<%@ Register TagPrefix="cc1" Namespace="Coralys.WebControls.SideBoxSimple" 
                Assembly="Coralys.WebControls.SideBoxSimple" %>
<HTML>
    <HEAD>
        <asp:Literal ID="PageHeaderSubscriber" 
                        Runat="server"></asp:Literal>  
    <HEAD>
    <BODY>
        <form ID="Form1" method="post" runat="server">
    ... Rest of the ASPX (BODY, etc. here) ...
        </form>
    </BODY>
</HTML>

The third part onwards is shown below where the Literal we added is declared (note its type). The variable name of the Literal control can be anything, but the ID shown in the code snippet above is not. The ID (and I feel I must stress that) is fixed.

public class WebForm1 : System.Web.UI.Page
{
    protected System.Web.UI.WebControls.Literal PageHeaderSubscriber;
    protected Coralys.WebControls.SideBoxSimple.SideBoxSimple SideBoxSimple1;

    private void Page_Load(object sender, System.EventArgs e)
    {
        // Put user code to initialize the page here
        Coralys.Web.PageHeaderSubscriber phsub = 
                new Coralys.Web.PageHeaderSubscriber(this.Page, 
                this.PageHeaderSubscriber);
        phsub.Process("Form1");
    }
}

Easy does it! The Literal control is declared and I am also showing the declaration of a custom web control that happens to implement IPageHeaderSubscriber. This custom control will be the subject of another article.

Then in the Page_Load event, the first thing I do is instantiate our handler, give it the required parameters for it to do its job later. Then, immediately we call the Process() method and Klaar Is Kees like they say in The Netherlands. Not much to it, huh?.

Points of Interest

Sure! While we don't have a guarantee that any of the child controls has been created in the Page_Init event, I was a bit surprised to see that in Page_Load, there was no way to programmatically find out the name (ID, UniqueID) of the ASP.NET form implementing the web page. While debugging it, you can actually see it is "known" by rummaging through the Quick Watch window, but sometimes what you see is not what you can get, querying the properties in question programmatically (and on the Command Window) yielded a null value. That is the reason why the form name has to be given as a parameter.

The solution is by no means complete, but it accomplishes what I need at this moment given my time constraints. This package does not go recursively into grand-children, it restricts itself to the first level children. It also does not offer a way to produce inline (Java)scripts in the HEAD section like it does with Styles, but that is easy to implement. I personally avoid depending on JavaScript and therefore, it was not a priority of mine to make this an all-things for all-people kind of solution.

As one of my ex-colleagues pointed out, the Whidbey release will have an HtmlHead control that might address this issue, but until Whidbey becomes mainstream the solution of this article (or variations of it) will have to do, you want a solution today, right?.

History

  • 04-Nov-2004 DEGT v1.0.1 Renamed interface methods as Register*() to conform to the Page class' syntax. Added information regarding registering script blocks outside the HEAD element in the Background section. Use key property to avoid duplicates.
  • 03-Nov-2004 DEGT v1.0 Initial version.

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