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






4.40/5 (5 votes)
Nov 3, 2004
11 min read

48960

326
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'sHEAD
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. <see cref="GetFileDependencies"/>
bool HasFileDependency { get; }
/// Returns true if the control needs to define some
/// Styles in the head section. <see cref="GetStyle"/>
bool NeedsStyle { get; }
/// Returns true if the control needs an external
/// Cascading Style Sheet file. <see cref="RegisterStyleSheets"/>
bool NeedsStyleSheet { get; }
/// Returns true if the control needs an external
/// (java)script defined in the Head section. <see cref="RegisterScriptFiles"/>
bool NeedsScript { get; }
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.
/// <param name="page">Page object we will handle </param>
/// <param name="headLiteral">Literal control place holder </param>
public PageHeaderSubscriber(System.Web.UI.Page page,
System.Web.UI.WebControls.Literal headLiteral)
{ ... }
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 theHEAD
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.