AspAlliance.com LogoASPAlliance: Articles, reviews, and samples for .NET Developers
URL:
http://aspalliance.com/articleViewer.aspx?aId=13&pId=-1
Page Templating for Web Applications
page
by J. Ambrose Little
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 30115/ 46

Introduction

Originally Published: 2 October 2002

I've seen quite a few different templating schemes around since I started looking for them, but most had two flaws. One is the requirement to either use a UserControl for every page or to load the controls into a template user control, which in both cases overcomplicates things and makes client-side scripting more obscure due to ASP.NET's unique Id system. The other is that most do not support view state properly, which is a consequence of changing the location of controls in the control tree after they're initially parsed into the tree.

Neither of these seemed acceptable to me, so I kept toying with the various schemes trying to arrive at what I thought to be the best of both worlds. The purpose of this article is to share that with you. The key to solving the view state problem lies in intercepting (overriding) ASP.NET's AddParsedSubObject method, which adds the controls parsed from the ASPX page to the control tree.

 

The Code

using System;

using System.Web.UI;
using System.Web.UI.WebControls;

namespace TemplateApp.Templates
{
/// <summary>
/// Base template for site
/// </summary>
public class PageTemplate : Page
{
// HTML title
private System.Web.UI.LiteralControl _title =
new System.Web.UI.LiteralControl();
// Stylesheet file location
private System.Web.UI.LiteralControl _styleSheet =
new System.Web.UI.LiteralControl();

/// <summary>
/// Common site web user control.
/// </summary>
protected TemplateApp.Components.TopBar TopBar;

/// <summary>
/// Temporary control collection to hold parsed controls.
/// </summary>
protected ControlCollection ControlBin;

/// <summary>
/// Placeholder for the HTML HEAD section.
/// </summary>
protected PlaceHolder Head = new PlaceHolder();
/// <summary>
/// Placeholder for the body/content section of the form;
/// all controls on inheritors will be added to this.
/// </summary>
protected PlaceHolder Body = new PlaceHolder();
/// <summary>
/// Standard form for the page
/// </summary>
protected System.Web.UI.HtmlControls.HtmlForm MainForm =
new System.Web.UI.HtmlControls.HtmlForm();

/// <summary>
/// Gets or sets HTML title for the page
/// </summary>
protected string Title
{
get
{
return this._title.Text;
}
set
{
this._title.Text = "My Site Title Prefix: " + value;
ViewState["PageTitle"] = this._title.Text;
}
}

/// <summary>
/// Gets or sets stylesheet location
/// </summary>
protected string StyleSheet
{
get
{
return this._styleSheet.Text;
}
set
{
this._styleSheet.Text = value;
ViewState["StyleSheet"] = this._styleSheet.Text;
}
}

/// <summary>
/// Receives the objects parsed from the ASPX page and
/// adds them to a private control collection.
/// </summary>
/// <param name="obj">Parsed object.</param>
protected override void AddParsedSubObject(Object obj)
{
// if this is the first time here,
// instantiate our control collection
if (ControlBin == null)
ControlBin = new ControlCollection(this);

// add the control to our holding bin collection
this.ControlBin.Add((System.Web.UI.Control)obj);
}

/// <summary>
/// Page init; calls private method LoadControls,
/// which adds IDs to template controls, loads the
/// TopBar control, and defines the template page layout
/// </summary>
/// <param name="e">Page init event arguments</param>
protected override void OnInit(EventArgs e)
{
this.LoadControls();
base.OnInit(e);
}

/// <summary>
/// Adds IDs to template controls, loads the TopBar control,
/// and defines the template page layout
/// </summary>
private void LoadControls()
{
// Load TopBar control; this is just an example of how to
// use a user control w/ the template; actual control
// not included in sample
this.TopBar = (TemplateApp.Components.TopBar)this.LoadControl(
this.Request.ApplicationPath + "/Components/TopBar.ascx");
// Add IDs to template controls
this.Head.ID = "HEAD";
this.TopBar.ID = "TopBar";
this.MainForm.ID = "MainForm";
this.Body.ID = "BODY";

// /////////////////
// Begin page layout
// /////////////////
// Add standard HTML, title, & stylesheet tags
AddStaticText(
"<!DOCTYPE HTML PUBLIC " +
"\"-//W3C//DTD HTML 4.0 Transitional//EN\" >" +
"<html>\n" +
"<head>\n" +
"<title>");
this.Controls.Add(this._title);
AddStaticText("</title>\n" +
"<link type=\"text/css\" rel=\"stylesheet\" href=\"");
this.Controls.Add(this._styleSheet);
AddStaticText("\">\n");
// Add Head to the HEAD section
this.Controls.Add(this.Head);
// Close Head section; open body section
AddStaticText(
"</head>\n" +
"<body>\n");
// Add the TopBar control to top of form
this.MainForm.Controls.Add(this.TopBar);
// Add a central div for body to form
this.MainForm.Controls.Add(
new LiteralControl("<div id=\"bodyDiv\">"));
// Load all of the controls from the child page into
// the Body PlaceHolder on the template
for(int i = 0; i < ControlBin.Count; i++)
{
switch (ControlBin[i].GetType().FullName)
{
case "System.Web.UI.WebControls.Image":
((Image)ControlBin[i]).ImageUrl =
this.Request.ApplicationPath + "/" +
((Image)ControlBin[i]).ImageUrl;
break;
case "System.Web.UI.WebControls.ImageButton":
((ImageButton)ControlBin[i]).ImageUrl =
this.Request.ApplicationPath + "/" +
((ImageButton)ControlBin[i]).ImageUrl;
break;
case "System.Web.UI.WebControls.HyperLink":
((HyperLink)ControlBin[i]).NavigateUrl =
this.Request.ApplicationPath + "/" +
((HyperLink)ControlBin[i]).NavigateUrl;
break;
}
this.Body.Controls.Add(ControlBin[i]);
}
// Add body placeholder to form
this.MainForm.Controls.Add(this.Body);
// Add close body div and add standard footer to form
this.MainForm.Controls.Add(new LiteralControl(
"</div>" +
"<div class=\"templateFooter\"><nobr>" +
"Copyright &copy; 2002 My Company</nobr></div>"));
// Add form to page
this.Controls.Add(this.MainForm);
// close standard body & HTML tags
AddStaticText(
"</body>\n" +
"</html>\n");
}

// Helper method to add literal controls to page
private void AddStaticText(string output)
{
this.Controls.Add(new LiteralControl(output));
}

/// <summary>
/// Loads view state for page.
/// </summary>
/// <param name="savedState">Saved state.</param>
protected override void LoadViewState(object savedState)
{
base.LoadViewState (savedState);
// Initialize Page Title & Stylesheet
this._title.Text = Convert.ToString(ViewState["PageTitle"]);
if (this._title.Text == string.Empty)
this._title.Text = "My Default Title Here";
this._styleSheet.Text = Convert.ToString(ViewState["StyleSheet"]);
if (this._styleSheet.Text == string.Empty)
this._styleSheet.Text = "myDefaultStyleSheet.css";
}


}


}



 

Code Walk-Through

Open New Window For Code Reference

The next crucial step is to override the Page.Init method and call the custom initialization procedure, LoadControls. This is where most of the work gets done, where the control tree is initialized for the page.

I've included a mythical "TopBar" control to illustrate that if you have any standard site navigation, images, etc., you should load them into web user controls (as you probably would even if you weren't using a templating scheme). These controls will be placed in the basic page layout for your site that you create in the LoadControls method.

So, the first thing I do in the LoadControls method is load up my TopBar control. I then assign all of my PlaceHolder controls' Id property (on the off chance you need to Find them by their Id at some point). Then comes the page layout. I've created a little helper method, AddStaticText, that I use to add literal controls where static text is needed. (In case you don't know, each contiguous bit of HTML/Text that you put on an ASPX/ASCX page is parsed into a LiteralControl, so we're just using the same method that ASP.NET does to add text like this to the control tree.)

You'll note that I'm using LiteralControl controls for the <TITLE /> and stylesheet declarations that use the Title and StyleSheet properties for their values. This of course assumes that you'll be using a stylesheet file. If you are not, you can simply remove the code for the StyleSheet property. The same goes for any of the other default controls on this template--there is no requirement to create all of the same place holders that I have--feel free to change them around all you like.

The next thing I do is add my "Head" PlaceHolder to the control collection. This of course is there to make it easy to add client-side script functions in what I believe to be the proper place. If you wanted to go all out, you could go ahead and create a "Script" PlaceHolder that writes out the <SCRIPT /> tags for you.

After opening the body, I go ahead and add my TopBar control. If I had a more complex layout, such as a left and right bar containing ads, navigation, or what have you, I would likely build the layout here with a table that has the least amount of formatting and content possible here because modifying UI on user controls is easier than modifying UI in code.

Next, I add a "body" DIV with a specific Id ("bodyDiv") so that I can give the "body" specific styles by creating a CSS Id declaration. You could do this with a CSS class as well, depending on preference. Then, to the Body PlaceHolder, I add all of the parsed controls from the child ASPX. Again, this place holder is arbitrary; I simply used it to expose another place to add controls to programmatically if I, or any user of this template, so desire.

After this, I add the Body PlaceHolder to the main form. This is a key step; in order for controls in the ASPX to participate in postback and raise corresponding events, they must be part of the server-side form. So you could have skipped adding the parsed controls to the Body PlaceHolder and added them directly to the form, but as I said, I used the Body PlaceHolder to enable consumers of the class to know exactly where the parsed controls are added in the control tree and to expose it for them to add more controls via code. The key thing to remember is that for a control to participate in postback and/or raise any of its server-side events, it MUST be part of the server-side form.

Key Note:This brings up an important consideration. Some of the other templating schemes I've seen do not create a server-side form for you. In cases where you're producing mostly static content that does not require server-side, object-oriented postback handling, having a server-side form is largely unnecessary; however, if most of your pages will need postback handling (such as in a web application in the strict sense) and given that ASP.NET limits you to one and only one server-side form, it makes sense to create that one server-side form in your base page class (your template class) so that you do not have to declare it on every page and so that all parts of your page template (such as your various navigation controls and the controls parsed from the ASPX) can participate in object-oriented postback event handling and view state.

Keep in mind that this is not the same thing as placing a server-side form in a user control. Microsoft rightly recommends against that because it limits postback participation to those server controls in your user control. In this template, however, all (or as many as you want) server controls will be added to the server-side form and can thus participate in postback.

Finally, I add a nifty footer including my copyright statement. (This would also be a good place to add a link to a privacy policy.) I add the form to the Page.Controls collection (a vital but simple step), and I close the BODY and HTML tags for the page.

 

Drawbacks
 
The are only three drawbacks of using this scheme that I am aware of. The first is that it can complicate things if you have more than one form, as far as the UI is concerned, that might require a separate "default button." The good news is that there are at least a few workarounds for this, and if interest is expressed, I will expand in a part two of this article on how to do that.

The second drawback is that ASPX Intellisense is not enabled for most of the server controls when you remove the HTML and BODY tags from the ASPX (which you should if you're using this scheme). There are a number of workarounds for this. You can read about a creative one at Paul Wilson's Page Templates: Designer Issues. I've found that temporarily adding the BODY tags enables Intellisense. You can them just remove them or comment them out when you put the page into production, although I've found that leaving them there doesn't appear to hurt anything. Hopefully, the next release of VS.NET will account for this and provide Intellisense on such ASPX pages, as they do now for ASCX pages that also do not have HTML or BODY tags.

The third is that you cannot use ASP-style code blocks (e.g., <% ... %>) on the page.  For whatever reason, ASP.NET marks control collections as read-only as soon as you add a literal with code blocks, so further modification cannot be made to the tree.  I have asked around extensively and have yet to find a solution to this.  A workaround is to simply not use code blocks.  There are a few instances where using code blocks might make sense, but generally I think it's a bad habit and holdover from the ASP model that should be avoided.

 

Summary
 
To summarize, this templating scheme provides you strongly-typed properties, such as page title, stylesheet, and the server-side form, as well as several PlaceHolder controls to enable the page developer to programmatically add controls to various places on the page's control tree. It automatically adds all controls to the server-side form to enable them to participate in ASP.NET's object-oriented postback processing and view state. By intercepting the AddParsedSubObject method and adding the objects to a temporary control collection, it enables view state for all controls added to the tree in the template.

This scheme also allows the developer to create as many sub-templates from this template as desired by creating similar template classes that derive from this one. It has been tested on a major intranet project and is being used on this site. On these, performance impact is minimal, and usability for the page developers on my team has proven to be good.

Overall, for postback-intensive applications, I think this scheme or some variation of it is the best solution for page templating. The only difficulties I have found with it are accounting for multiple default buttons and providing full design-time support, both of which have workable remedies.



©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-03-19 6:42:26 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search