April 24th 2010
Defining and consuming Master pages from reusable class libraries
One hole in the Asp.Net framework is this ability to create websites or web components and deploy them without any associated markup files. Even when using the aspnet_compile and aspnet_merge utilities (usually via the web deployment projects), files are still required as place holders to link the Asp.net classes to the associated resource URIs. Developers can work around this problem in a couple ways. They can write only custom controls that have no associated markup files. They can use a custom Virtual Path Provider to load the markup files from embedded resources in the component assemblies. Lastly, they can use virtual directories to load user controls and master pages from other applications in the same site.
I’ve used the virtual path provider model and found it to have a few problems. First it tends to be slow – when developing code the embedded resource markup files must be extracted and compiled before the page can render them. This slows development and hides compile time errors until run time. As the application grew, the number of embedded resources grew until each code change would result in an interminable wait while the page refreshed.
What we found was that once the look and feel of the content was relatively well defined, if we were willing to forgo the ease of use of the designer, we could write our code as custom controls without markup thereby avoiding this problem altogether, at least for user controls, but what about master pages?
Master pages are a little more challenging because the Asp.net framework is designed to find master page resources by URI, not by type. According to Microsoft:
Another solution would be to store the master pages in a virtual directory. Virtual directories are a convenient way to set up a site for local Web development work because they do not require a unique site identity. This means that a virtual directory requires fewer steps than it would to create a unique site to store your master pages. Virtual directories are maps to the physical location of files that a Web application can use. The files do not have to be under the root of the Web application. This approach enables you to maintain one set of master pages in one location that various Web applications residing on remote machines can use.
To use master pages in a virtual directory, a developer would navigate to the directory and add the master pages to the current Web application project.
…
If you are concerned about code in your master pages being visible to others reusing the pages, you can precompile the master pages’ code into a library. In this library, you can include code-behind pages as well as user or custom controls. Compiling master pages does not remove the declarative code for the master files or any server controls used, but you can compile the master files to remove the code for controls or code-behind pages used by the master pages.If you choose to compile the master pages into a library, you must use the “updatable” build option that allows for later modification of the markup.
Therefore, no matter what, we’ll still need to create a “dummy master” as a place holder in each consuming application that has no markup but inherits from the master defined in the control library.
Dan Wahlin has an excellent article detailing one approach. Mr. Wahlin uses aspnet_compile to create a class that has no markup. The developer can then write master pages with markup and rely on the framework to generate code from the markup that can be referenced by a consuming application. There is a simple tradeoff between using a markup file and doing everything in the code behind. The markup file is easier to work with and is more declarative in nature, but it requires an additional build step and removes some control over the namespace of the type generated. Without the markup file, we can simplify the build process and avoid any confusing type references. Note that in both cases, resources like cascading style sheets, javascript files, and images referenced by the master would have to be exposed via embedded resources.
The basic process is as follows:
- create a class library
- create a new class that inherits from System.Web.UI.MasterPage
- in the constructor for the new MasterPage class, add the ContentPlaceHolders defined in the master. Note: the ContentPlaceHolder names need to be all lower case although I’m not sure why
public Master() : base() { base.AppRelativeVirtualPath = "~/Master.Master"; base.ContentPlaceHolders.Add(NAME OF CONTENTPLACEHOLDER); } - instead of overriding CreateChildControls, override FrameworkInitialize.
The FrameworkInitialize method is used by the Asp.Net page lifecycle to build the Controls collection of a component using the component’s associated markup file. This method is defined in the TemplateControl Class and according to the MS documentation: “Do not override the FrameworkInitialize method”, although in this case, I believe the usage to be within the context of the framework - In order to put ContentPlaceHolders into the Controls collection, use the following code:
//define the container that will hold the ContentPlaceHolder - this could be the Form, the Head, a Panel inside the Form, etc.
var containerControl = SOME CONTROL THAT WILL CONTAIN THE CONTENTPLACEHOLDER;
//add the container to the controls collection
Controls.Add(containerControl);
//Instantiate the ContentPlaceHolder and set the ID so others can override it
var contentPlaceHolder = new ContentPlaceHolder();
contentPlaceHolder.ID = "YOUR CONTENT PLACEHOLDER NAME";
ITemplate template = null;
//Depending on the mode (design, render), instantiate the content in the template
if (base.ContentTemplates != null)
{
//if the ContentTemplate does not have content defined in the consuming page, you can prepopulate
//the content here
if (base.ContentTemplates[YOUR TEMPLATE NAME] == null)
{
base.ContentTemplates[YOUR TEMPLATE NAME] = new CONTROL THAT IMPLEMENTS IContainer(this);
}
template = (ITemplate)base.ContentTemplates[YOUR TEMPLATE NAME];
}
}
if (template != null)
{
template.InstantiateIn(contentPlaceHolder);
}
containerControl.Controls.Add(contentPlaceHolder);
Once you have created the master page component, create a Dummy Master in your consuming application. This master should have no content in the markup file and should inherit from the master defined in the class library. Pages in the consuming application can now reference the Dummy Master.
A sample demonstrating this technique can be found here.