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
.
string PageSubscriberKey { get; }
</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).
</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 ...
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 = "";
if (depFiles.Count > 0)
DependentFileCollector(depFiles);
if (pgStyles.Count > 0)
{
headExtras += ContentCollector(pgStyles, "style");
}
if (pgStyleSheets.Count > 0)
{
headExtras += StyleSheetCollector(pgStyleSheets);
}
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)
{
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.