Introduction
Master pages are a very cool feature in ASP.NET 2.0. They allow you to easily create a consistent look and feel throughout your website. They also provide an easy way to set/change the master pages at runtime. This is achieved by setting the MasterPageFile
property of a page in the PreInit
event. This, however, requires that the master pages have ContentPlaceHolders
which the Content
controls on the page reference through the ContentPlaceHolderID
property. This article focuses on creating ContentPlaceHolders
, Contents
and linking them together programmatically.
Background
When I first used ASP.NET 2.0, one of the first features I used were master pages. I soon learned how to switch between such at runtime. Some time later, I was working on a small project, where the goal was to create as many things dynamically as possible (kind of a web application framework). I had a thought about using master pages that are stored in a DB (as metadata) and then later building them dynamically. I soon found out that this task is not as easy as it looks. It requires more than just the knowledge of how to create controls through code. I did some research on how to properly do this and I wanted to share this technique, hence this article was born.
Using the Code
The sample provided contains two pages and a master page. Its only purpose is to demonstrate how things are done. On the start page (Default.aspx), you are to enter a number, press a button and let the magic begin. Despite the visually not very attractive result on the next page (only some text can be seen), the magic still does happen in the background. Let's see what happens. For this, you should have some knowledge about the ASP.NET page lifecycle.
The most important step is to create the content on the page. You will be surprised to find that no creation of Content
classes is necessary. Instead you should use the AddContentTemplate
method of the Page
class. Chances are you never heard of this method, it is because it has the [EditorBrowsable(EditorBrowsableState.Never)]
attribute applied and thus isn't visible for IntelliSense. MSDN has some information about it, you can read it here. You need two parameters, the first being the name of the ContentPlaceHolder
it will be linked to (you can only set the ContentPlaceHolderID
attribute in the markup), the other an ITemplate
. If you have never worked with templated controls, you should find this a bit complicated, but it really isn't. However explaining this still goes beyond the reach of this article. In this example, all the content is the same, but that's not obligatory, then again easier to implement and satisfactory for demonstration purposes.
protected void Page_PreInit(object sender, EventArgs e)
{
for (int i = 1; i < num + 1; i++)
base.AddContentTemplate("ContentPlaceHolder" + i.ToString(),
new CompiledTemplateBuilder(new BuildTemplateMethod(this.Build)));
}
Using constructors in ASP.NET is a very uncommon scenario, but in this case there's no other place to put the code needed, because the master page's Init
event is already too late and there's no PreInit
event. You just need to add the names of the ContentPlaceHolder
controls to be created later to the ContentPlaceHolders
collection of the Master
class. Note that you must use the names in lowercase.
public MasterPage()
{
object cphnum = HttpContext.Current.Session["cphnum"];
if (cphnum == null || !(cphnum is int))
return;
num = (int)cphnum;
for (int i = 1; i < num + 1; i++)
base.ContentPlaceHolders.Add("contentplaceholder" + i.ToString());
}
Finally you should instantiate the ContentPlaceHolders
and add them to the control hierarchy of the page. This is also achieved in a for
loop, so that the required number of controls gets created. For this purpose, you must use the ContentTemplates
collection the master pages base, which as of type is an ITemplate
.
protected void Page_Init(object sender, EventArgs e)
{
for (int i = 1; i < num + 1; i++)
{
ContentPlaceHolder cph = new ContentPlaceHolder();
cph.ID = "ContentPlaceHolder" + i.ToString();
PlaceHolder1.Controls.Add(cph);
((ITemplate)base.ContentTemplates["ContentPlaceHolder" +
i.ToString()]).InstantiateIn(cph);
}
}
Now that you know the basics, you can easily extend the sample to be more useful than its current form.
Nesting master pages
Creating nesting master pages with this technique is also supported, however a slight modification of the code is needed. Theoretically a nested master page is nothing more then both a master page with it's own ContentPlaceHolders
and a regular page with Contents
to fill in it's parent's placeholders. Note that content needs to exist on the content page before the master can use it to populate it's placeholder, however the init events fire in opposite order, so the main master's one fires before the nested one's, resulting in a NullReferenceException (or when checked for, in empty content). And the constructor is too early to use for this, because then it would ruin the synergy between the nested master and the content page, for similiar reasons. So another event must be used called FrameworkInitialize(). This fires in the needed time window, and also in the correct order so it allows for deep nesting. A demo is also available for this scenario, see the list of downloads above.
History
- 12.02.2007 - Initial posting of the article
- 16.04.2007 - Added VB.NET demo
- 07.05.2012 - Added a section about nesting