Writing modular controls in ASP.NET Web Forms

crenshaw_dev

Michael Crenshaw

Posted on May 3, 2019

Writing modular controls in ASP.NET Web Forms

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>


Enter fullscreen mode Exit fullscreen mode

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 &raquo;</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 &raquo;</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 &raquo;</a>
        </p>
    </div>
</div>


Enter fullscreen mode Exit fullscreen mode

That code renders the three columns visible on the homepage:

Screenshot of Visual Studio 2017 boilerplate ASP.NET Web Forms application 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...

Screenshot of the new project dialog

Choose the Web Forms type of Web Application.

Web application type dialog

(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.)

Imperial guard from Star wars saying

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

Screenshot showing how to create a 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

Screenshot showing how to create a new 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>


Enter fullscreen mode Exit fullscreen mode

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);
            }
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

There's a lot going on, so I'll break down the key parts.



[ParseChildren(typeof(Control), DefaultProperty = "Content", ChildrenAsProperties = true)]


Enter fullscreen mode Exit fullscreen mode

The three arguments of the ParseChildren attribute instruct ASP.NET to

  1. parse nested content as Control objects (typeof(Control))
  2. if no section is specified (we'll look at how that's done below), parse controls into the Content property
  3. parse children into properties, as defined below


public HomepageCard()
{
    Title = new ControlCollection(this);
    Content = new ControlCollection(this);
    Footer = new ControlCollection(this);
}


Enter fullscreen mode Exit fullscreen mode

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; }


Enter fullscreen mode Exit fullscreen mode

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);
    }
}


Enter fullscreen mode Exit fullscreen mode

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" %>


Enter fullscreen mode Exit fullscreen mode

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 &raquo;</a>
        </asp:PlaceHolder>
    </Footer>
</ex:HomepageCard>


Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
crenshaw_dev
Michael Crenshaw

Posted on May 3, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related