Michael Crenshaw
Posted on May 3, 2019
Most sites have common components, like headers and footers. Some components exhibit only slight variations across the site.
You can copy and paste the common parts of the markup. But that can lead to headaches when it's time to make a change to each copy of the component.
ASP.NET provides a way to be more modular, with nested components.
A familiar example of nesting is with the built-in Repeater control.
<asp:Repeater ID="ExampleRepeater" runat="server">
<HeaderTemplate></HeaderTemplate>
<ItemTemplate></ItemTemplate>
<FooterTemplate></FooterTemplate>
</asp:Repeater>
The Repeater
has common behavior that's applied to the content in the Template
tags.
Take a look at this markup from the boilerplate ASP.NET Web Form site that comes with Visual Studio 2017:
<div class="row">
<div class="col-md-4">
<h2>Getting started</h2>
<p>
ASP.NET Web Forms lets you build dynamic websites using a familiar drag-and-drop, event-driven model.
A design surface and hundreds of controls and components let you rapidly build sophisticated, powerful UI-driven sites with data access.
</p>
<p>
<a class="btn btn-default" href="https://go.microsoft.com/fwlink/?LinkId=301948">Learn more »</a>
</p>
</div>
<div class="col-md-4">
<h2>Get more libraries</h2>
<p>
NuGet is a free Visual Studio extension that makes it easy to add, remove, and update libraries and tools in Visual Studio projects.
</p>
<p>
<a class="btn btn-default" href="https://go.microsoft.com/fwlink/?LinkId=301949">Learn more »</a>
</p>
</div>
<div class="col-md-4">
<h2>Web Hosting</h2>
<p>
You can easily find a web hosting company that offers the right mix of features and price for your applications.
</p>
<p>
<a class="btn btn-default" href="https://go.microsoft.com/fwlink/?LinkId=301950">Learn more »</a>
</p>
</div>
</div>
That code renders the three columns visible on the homepage:
Let's consolidate the common parts of the markup into a modular user control.
Setting up the project
If you know how to set up an ASP.NET Web Forms project in Visual Studio 2017, or are doing so in a different IDE, feel free to skip to the next section.
First, create a new project. I'm using the C# flavor of an ASP.NET Web Application project.
File > New > Project...
Choose the Web Forms type of Web Application.
(I know Web Forms is passé, but many sites still use it. As far as I can tell, it's still a perfectly acceptable way to build a site.)
If you launch the site in a browser, you'll see the default homepage as shown above.
We're going to create a new user control to hold the common "column" code.
Setting up the modular control
I'm creating a new "Controls" folder, just to stay organized.
Solution Explorer > Right-click ModularControls project > Add > New Folder
Now create the control. I'm calling it a "homepage card," though you'd probably only ever bother to create a modular control for something that's used on multiple pages.
Solution Explorer > Right-Click Controls folder > Add > Web Forms User Control
Copy and paste the code for any of the columns to your new control. Then replace the contents of the header and paragraph tags with appropriately-named <asp:PlaceHolder>
controls.
<div class="col-md-4">
<h2>
<asp:PlaceHolder ID="TitlePlaceHolder" runat="server"></asp:PlaceHolder>
</h2>
<p>
<asp:PlaceHolder ID="ContentPlaceHolder" runat="server"></asp:PlaceHolder>
</p>
<p>
<asp:PlaceHolder ID="FooterPlaceHolder" runat="server"></asp:PlaceHolder>
</p>
</div>
Then set up the control's code-behind something like this:
using System;
using System.Web.UI;
namespace ModularControls.Controls
{
[ParseChildren(typeof(Control), DefaultProperty = "Content", ChildrenAsProperties = true)]
public partial class HomepageCard : UserControl
{
public HomepageCard()
{
Title = new ControlCollection(this);
Content = new ControlCollection(this);
Footer = new ControlCollection(this);
}
[PersistenceMode(PersistenceMode.InnerProperty)]
public ControlCollection Title { get; private set; }
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
public ControlCollection Content { get; private set; }
[PersistenceMode(PersistenceMode.InnerProperty)]
public ControlCollection Footer { get; private set; }
protected void Page_Load(object sender, EventArgs e)
{
foreach (Control control in Title)
{
TitlePlaceHolder.Controls.Add(control);
}
foreach (Control control in Content)
{
ContentPlaceHolder.Controls.Add(control);
}
foreach (Control control in Footer)
{
FooterPlaceHolder.Controls.Add(control);
}
}
}
}
There's a lot going on, so I'll break down the key parts.
[ParseChildren(typeof(Control), DefaultProperty = "Content", ChildrenAsProperties = true)]
The three arguments of the ParseChildren
attribute instruct ASP.NET to
- parse nested content as
Control
objects (typeof(Control)
) - if no section is specified (we'll look at how that's done below), parse controls into the
Content
property - parse children into properties, as defined below
public HomepageCard()
{
Title = new ControlCollection(this);
Content = new ControlCollection(this);
Footer = new ControlCollection(this);
}
The default constructor news up the control collections so they're ready to accept controls parsed by ASP.NET. The this
argument specifies the "owner" control of these control collections.
[PersistenceMode(PersistenceMode.InnerProperty)]
public ControlCollection Title { get; private set; }
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
public ControlCollection Content { get; private set; }
[PersistenceMode(PersistenceMode.InnerProperty)]
public ControlCollection Footer { get; private set; }
The PersistentMode
attributes tell ASP.NET how the contents are persisted into the rendered page. Note that the Content
property gets marked as the InnerDefaultProperty
.
Each ControlCollection
needs the setter explicitly set to private
. If you skip this step, you'll get difficult-to-debug runtime errors.
protected void Page_Load(object sender, EventArgs e)
{
foreach (Control control in Title)
{
TitlePlaceHolder.Controls.Add(control);
}
foreach (Control control in Content)
{
ContentPlaceHolder.Controls.Add(control);
}
foreach (Control control in Footer)
{
FooterPlaceHolder.Controls.Add(control);
}
}
The Load
event handler loops through the parsed controls and drops them into the appropriate placeholders.
Using the modular control
First, register the control in Default.aspx.
<%@ Register Src="~/Controls/HomepageCard.ascx" TagPrefix="ex" TagName="HomepageCard" %>
Then replace the first column with the following code.
<ex:HomepageCard runat="server">
<Title>
<asp:PlaceHolder runat="server">
Getting started
</asp:PlaceHolder>
</Title>
<Content>
<asp:PlaceHolder runat="server">
ASP.NET Web Forms lets you build dynamic websites using a familiar drag-and-drop, event-driven model.
A design surface and hundreds of controls and components let you rapidly build sophisticated, powerful
UI-driven sites with data access.
</asp:PlaceHolder>
</Content>
<Footer>
<asp:PlaceHolder runat="server">
<a class="btn btn-default" href="https://go.microsoft.com/fwlink/?LinkId=301948">Learn more »</a>
</asp:PlaceHolder>
</Footer>
</ex:HomepageCard>
The Title, Content, and Footer tags tell ASP.NET where to place the contents.
The <asp:PlaceHolder>
controls allow ASP.NET to parse the markup as controls.
It's a bit verbose. But once the verbose part is written, it never has to be changed. Common parts of the controls can be changed in HomepageCard.ascx.
If you rebuild and reload the home page, it should look precisely the same as before you wrote the modular control. That's the idea! This pattern doesn't change the rendered content - it just makes it more modular and maintainable.
Improvements?
When researching this technique, I found very little documentation. Since the first version of ASP.NET Web Forms was released in 2002, it's possible most of the guides for this technique are in printed form.
If you know tweaks to improve this pattern, please let me know! I'd love to drop the PlaceHolder
tags to tighten up the markup. And I'd really love to drop all the properties and placeholder population from the code-behind in favor of some more expressive syntax. But I'm not sure either is possible.
I hope this is a helpful pattern! If so, please comment and let me know how you used it!
Posted on May 3, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.