MasterPages hate logic. Throw Interfaces at Them.

MasterPages are arguably the most useful new feature in ASP.NET 2.0. They solve one of the biggest issues with 1.1—providing a clean way of implementing a common look and feel across an application. In fact, they are so slick they end up encouraging some bad practices. Namely using the site’s MasterPage as the site’s base page class. Now, say this three times: master pages are not meant for logic but rather for look & feel.

Now, this is not to say using some sort of base page class is a bad idea. In fact, it is a very good idea. But a base page class still means a class that inherits from System.Web.UI.Page. The challenge becomes that now, with MasterPages and all, one ends up with a bit of a quandary: at the end of the day, this logic needs to get expressed in web controls. And the MasterPage, not the base page class, is directly aware of those controls. But the base page class is not, at least not unless one wants to start a messy game of calling Master.FindControl(). I argue, however, that the base page class need not be directly aware of these controls, but merely certain functions of these controls. A sort of contract, if you will. .NET conveniently has programmatic contracts, they are just known as Interfaces. And they are good.

Have I lost you yet? Ok, lets just dive into scenarios & code samples, eh?

The Scenario

Your project requires a common set of buttons be displayed on all pages. These buttons are to be Add, Edit, Delete and Exit. Now, some pages will not allow the use of one or all of these buttons. And the specifics of what happens when these buttons are clicked needs to be handled by each concrete page.

The Strategy

What we are going to do here is:

  1. Create an interface, ICommandStrip, which encapsulates the necessary functionality. Namely events for Adding, Deleting, Editing and Exiting. As well as a set of Boolean values for enabling or disabling commands.
  2. Create another interface, ICommandStripContainer, for the containing control to implement. Its sole purpose in life is to pass a reference to the concrete ICommandStrip
  3. A User Control which implements ICommandStrip
  4. An abstract base MasterPage class which implements ICommandStripContainer and implements some basic plumbing.
  5. A concrete MasterPage class which implements the abstract class in #4.
  6. A base Page class which provides a reference to our ICommandStripContainer and handles some basic plumbing for said container.
  7. An actual page that communicates with the command strip to prove it all works swimmingly.

So, first, ICommandStrip:


/// <summary>
/// Command strip interface contract.
/// </summary>
public interface ICommandStrip
{
	/// <summary>
	/// Fired when the Add command is enacted.
	/// </summary>
	event EventHandler AddClicked;
	/// <summary>
	/// Fired when the Edit command is enacted.
	/// </summary>
	event EventHandler EditClicked;
	/// <summary>
	/// Fired when the Delete command is enacted.
	/// </summary>
	event EventHandler DeleteClicked;
	/// <summary>
	/// Fired when the Exit command is enacted.
	/// </summary>
	event EventHandler ExitClicked;

	/// <summary>
	/// Is the Add commmand enabled?
	/// </summary>
	bool IsAddable { get; set; }
	/// <summary>
	/// Is the Delete command enabled?
	/// </summary>
	bool IsDeletable { get; set; }
	/// <summary>
	/// Is the Edit command enabled?
	/// </summary>
	bool IsEditable { get; set; }
	/// <summary>
	/// Is the Exit command enabled?
	/// </summary>
	bool IsExitable { get; set;}
}

Now ICommandStripContainer:


/// <summary>
/// Container interface for our Command Strip
/// </summary>
public interface ICommandStripContainer
{
	/// <summary>
	/// Gets a reference to the CommandStrip
	/// </summary>
	ICommandStrip CommandStrip { get;}
}

With me so far? Now we are starting to get to the good part. Lets see the user control:


<%@ Control Language="C#" AutoEventWireup="true" CodeFile="ButtonCommandStrip.ascx.cs" Inherits="ButtonCommandStrip" %>
<div style="background-color: #e2e2e2; padding:5px; text-align: right;"> 
    <asp:Button runat="Server" ID="AddButton" OnClick="AddButtonClicked" Text="Add" />
    <asp:Button runat="Server" ID="EditButton" OnClick="EditButtonClicked" Text="Edit" />
    <asp:Button runat="Server" ID="DeleteButton" OnClick="DeleteButtonClicked" Text="Delete" />
    <asp:Button runat="Server" ID="ExitButton" OnClick="ExitButtonClicked" Text="Exit" />
</div>

And Codebehind:


public partial class ButtonCommandStrip : System.Web.UI.UserControl, ICommandStrip
{
    public ButtonCommandStrip()
    {
        PreRender += new EventHandler(bindButtonAbilities);
    }

    private void bindButtonAbilities(object sender, EventArgs e)
    {
        AddButton.Enabled = IsAddable;
        EditButton.Enabled = IsEditable;
        DeleteButton.Enabled = IsDeletable;
        ExitButton.Enabled = IsExitable;        
    }

    #region ICommandStrip Members

    private event EventHandler addClicked;
    public event EventHandler AddClicked
    {
        add { addClicked += value; }
        remove { addClicked -= value; }
    }

    private event EventHandler editClicked;
    public event EventHandler EditClicked
    {
        add { editClicked += value; }
        remove { editClicked -= value; }
    }

    private event EventHandler deleteClicked;
    public event EventHandler DeleteClicked
    {
        add { deleteClicked += value; }
        remove { deleteClicked -= value; }
    }

    private event EventHandler exitClicked;
    public event EventHandler ExitClicked
    {
        add { exitClicked += value; }
        remove { deleteClicked -= value; }
    }

    public bool IsAddable
    {
        get
        {
            if (ViewState["IsAddable"]==null)
            {
                return true;
            }
            return (bool)ViewState["IsAddable"];
        }
        set
        {
            ViewState["IsAddable"] = value;
        }
    }

    public bool IsDeletable
    {
        get
        {
            if (ViewState["IsDeletable"] == null)
            {
                return true;
            }
            return (bool)ViewState["IsDeletable"];
        }
        set
        {
            ViewState["IsDeletable"] = value;
        }
    }

    public bool IsEditable
    {
        get
        {
            if (ViewState["IsEditable"] == null)
            {
                return true;
            }
            return (bool)ViewState["IsEditable"];
        }
        set
        {
            ViewState["IsEditable"] = value;
        }
    }

    public bool IsExitable
    {
        get
        {
            if (ViewState["IsExitable"] == null)
            {
                return true;
            }
            return (bool)ViewState["IsExitable"];
        }
        set
        {
            ViewState["IsExitable"] = value;
        }
    }

    #endregion

    protected void AddButtonClicked(object sender, EventArgs e)
    {
        if (addClicked != null)
        {
            addClicked(this, e);
        }
    }
    protected void EditButtonClicked(object sender, EventArgs e)
    {
        if (editClicked != null)
        {
            editClicked(this, e);
        }
    }
    protected void DeleteButtonClicked(object sender, EventArgs e)
    {
        if (deleteClicked != null)
        {
            deleteClicked(this, e);
        }
    }
    protected void ExitButtonClicked(object sender, EventArgs e)
    {
        if (exitClicked != null)
        {
            exitClicked(this, e);
        }
    }
}

Nothing too fancy there. It just implements ICommandStrip, as well as some plumbing to enable or disable buttons as appropriate.

Now back to App_Code for the master page base class:


public abstract class SiteMasterPageBase : MasterPage, ICommandStripContainer
{
	/// <summary>
	/// Internal mapping property to get a reference to the concrete ICommandStrip control
	/// </summary>
	protected abstract ICommandStrip CommandStripControl { get; }

	/// <summary>
	/// Returns a reference to the CommandStripControl for use by the SitePage.
	/// </summary>
	public ICommandStrip CommandStrip
	{
		get { return CommandStripControl; }
	}
}

The only voodoo going on here is that it is an abstract class. Why, you ask? Pretty simple really—you can have multiple master pages for a site. And if you maintain the same back-end interfaces, you can switch between them on the fly. Or at least in the PreInit event. But then the trick becomes wiring the master pages up to the controls they must manipulate; hence the abstract CommandStripControl property. Its purpose is to act as a bridge between the concrete MasterPage class and the normal interface.

Just to prove it is easy, lets have a looksee at our site’s master page:


<%@ Master Language="C#" AutoEventWireup="true" CodeFile="Site.master.cs" Inherits="Site" %>
<%@ Register Src="ButtonCommandStrip.ascx" TagName="ButtonCommandStrip" TagPrefix="SpUc" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>SpBlog Test Page</title>
</head>
<body>
    <form id="form1" runat="server">
        <h1>Master Page Command Strip Demo</h1>
        <SpUc:ButtonCommandStrip id="CommandStripCtl" runat="server" />
        <asp:contentplaceholder id="MainContentPlaceHolder" runat="server" />
    </form>
</body>
</html>

And the codebehind, which does nothing save tell the MasterPage how to find the ICommandStrip:


public partial class Site : SiteMasterPageBase
{
    protected override ICommandStrip CommandStripControl
    {
        get { return this.CommandStripCtl; }
    }
}

Working hand-in-hand (or Interface-to-Interface, lol) with this SiteMasterPageBase class is the SitePageBase class:


public class SitePageBase : Page
{
	public SitePageBase()
	{
		//Wire up the command strip. 
		//Note that Init is the earliest we can hook up the events.
		//They are not avaliable before the MasterPage has loaded its controls.
		Init+=new EventHandler(BindCommandStrip);
	}

	/// <summary>
	/// Gets a reference to the Master CommandStripContainer
	/// </summary>
	protected ICommandStripContainer CommandStripContainer
	{
		get
		{
			return (ICommandStripContainer)Master;
		}
	}

	#region commandstrip utility

	/// <summary>
	/// Binds the command strip events
	/// </summary>
	/// <param name="sender">sender</param>
	/// <param name="e">empty event args</param>
	private void BindCommandStrip(object sender, EventArgs e)
	{
		if (CommandStripContainer != null)
		{
			CommandStripContainer.CommandStrip.AddClicked += new EventHandler(AddClicked);
			CommandStripContainer.CommandStrip.DeleteClicked += new EventHandler(DeleteClicked);
			CommandStripContainer.CommandStrip.EditClicked += new EventHandler(EditClicked);
			CommandStripContainer.CommandStrip.ExitClicked += new EventHandler(ExitClicked);
		}
	}

	#region virtual event handler stubs

	protected virtual void ExitClicked(object sender, EventArgs e)
	{}

	protected virtual void EditClicked(object sender, EventArgs e)
	{}

	protected virtual void DeleteClicked(object sender, EventArgs e)
	{}

	protected virtual void AddClicked(object sender, EventArgs e)
	{ }
	#endregion

	#endregion
}

Nothing too fancy here either. Some code to wire up the events, and some virtual stub methods for easy overriding later. Do note the CommandStripContainer method—it is the bridge back to the MasterPage’s ICommandStrip control.

Now for the good part—a simple example of a concrete implementation. First the ASPX file:


<%@ Page Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeFile="CommandStripPage.aspx.cs" Inherits="CommandStripPage" Title="Untitled Page" %>
<asp:Content ID="MainContentPlaceHolder" ContentPlaceHolderID="MainContentPlaceHolder" Runat="Server">
<p>Last Command: <asp:Label runat="Server" ID="WhatWasClicked" Text="Nothing Yet" ForeColor="red" Font-Bold="true" /></p>
<asp:CheckBox ID="AddableCheckbox" Text="Addable" runat="Server" Checked="true" /><br />
<asp:CheckBox ID="EditableCheckbox" Text="Editable" runat="Server" Checked="true" /><br />
<asp:CheckBox ID="DeletableCheckbox" Text="Deletable" runat="Server" Checked="true" /><br />
<asp:CheckBox ID="ExitableCheckbox" Text="Exitable" runat="Server" Checked="true" /><br />
<asp:Button runat="Server" ID="ApplyChangesButton" Text="Apply Ability Changes" OnClick="ApplyChanges" />
</asp:Content>

And now the C# bits:


public partial class CommandStripPage : SitePageBase
{
    protected override void EditClicked(object sender, EventArgs e)
    {
        WhatWasClicked.Text = "EDIT was clicked!";
    }

    protected override void DeleteClicked(object sender, EventArgs e)
    {
        WhatWasClicked.Text = "DELETE was clicked!";
    }

    protected override void AddClicked(object sender, EventArgs e)
    {
        WhatWasClicked.Text = "ADD was clicked!";
    }

    protected override void ExitClicked(object sender, EventArgs e)
    {
        WhatWasClicked.Text = "EXIT was clicked!";
    }

    protected void ApplyChanges(object sender, EventArgs e)
    {
        CommandStripContainer.CommandStrip.IsAddable = AddableCheckbox.Checked;
        CommandStripContainer.CommandStrip.IsDeletable = DeletableCheckbox.Checked;
        CommandStripContainer.CommandStrip.IsEditable = EditableCheckbox.Checked;
        CommandStripContainer.CommandStrip.IsExitable = ExitableCheckbox.Checked;
    }
}

As you can see, there is nothing particularly fancy here either. Just handling some events like a normal ASP.NET page. But the trick is that all those events get wired up by the SiteMasterPageBase & SitePageBase without this page caring a bit. And when you need to swap out control structures, you will only need to swap them out in one place because your pages are only sharing defined interfaces rather than classes.

Enjoy, happy coding and don’t forget to kick it if you like it.

[A few editorial notes & shout outz: the inspiration for this demo came out of this thread on SitePoint’s ASP.NET forums. And I did borrow most of the interface from Pufa’s post. His enum is a bit more elegant than my bools, but I got lazy.]

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://www.ilovecode.com ses5909

    This is an excellent post. You broke it down very well. Thanks.

  • Hima_.net

    Good article .
    You have been kicked

  • Steve

    Excellent! Great explanation and the code works perfectly.

  • Rui da Luz

    I was trying to solve the problem of having a page that “implements” a master page and, in the other hand, inherits from a custom base page class. The dificulty was that we can’t access properties in the master page through the custom base page class. Well, your article really opened may mind. Using an interface to perform this access to the master page works fine.

    To do this i create a master base page that implements ITeste named BaseMaster (in VB) and i made the property implemented from the interface MustOverride:

    Public MustInherit Class BaseMaster

    Inherits MasterPage

    Implements ITeste

    #Region “ITeste Implementation”

    MustOverride Property MenuHiddenz() As Boolean Implements ITeste.MenuHiddenz

    #End Region

    Public Interface ITeste

    Property MenuHiddenz() As Boolean

    End Interface

    Then in the page that “implements” BaseMaster:

    Public Overrides Property MenuHiddenz() As Boolean
    Get
    Return Not Me.MainMenu.Visible
    End Get
    Set(ByVal value As Boolean)
    Me.MainMenu.Visible = Not value
    End Set
    End Property

    Finally, in the custom base page class i can access the master page:

    DirectCast(Me.Master, ITeste).MenuHiddenz = True

    or in C#

    (ITeste)this.Master.MenuHiddenz = true;

    Thank’s a lot Wyatt

  • wwb_99

    Glad you found it useful Rui. I actually came up with this pattern while trying to solve a similar problem. Now, there is probably an official name, but official design patterns never were my thing.

  • tomandlis

    When I right click my web project I don’t have the option to add an App_code folder. Its missing and I can’t add it :-(

    All I can see is

    App_globalresources
    App_localresources
    App_data
    App_browsers
    Theme

    Argh, I think your code sample above will work for me, but I’m stuck on step 1.

  • tomandlis

    I should add that I migrated this from a vs 2003 web app. I suppose that has something to do with the missing option for app_code

  • wwb_99

    Sounds like it converted into a web application project. Which is no matter; just put the stuff from app_code in a separate class library and reference it.

    Note the library will need to reference System.Web.

  • pandojc

    Thanks for the post. I was struggling with how to reconcile master pages with a custom page base… this was the answer I was looking for!

  • seth

    (sorry the code in my first post wasn’t formatted properly)

    I agree that interfaces are good, but that solution seems overly complicated to me.

    The base page does not need to be involved at all and the button.click event handlers should be in the code behind for the master page.

    If I were going to use interfaces to solve the example here’s what I would do:

    Interface Definition

    Public Interface IAddable
        Sub Add()
    End Interface
     
    Public Interface IEditable
        Sub Edit()
    End Interface
     
    Public Interface IDeletable
        Sub Delete()
    End Interface
     
    Public Interface IExitable
        Sub [Exit]()
    End Interface
     
    Public Interface ICommandButtonContainer
        ReadOnly Property AddButton() As Button
        ReadOnly Property EditButton() As Button
        ReadOnly Property DeleteButton() As Button
        ReadOnly Property ExitButton() As Button
    End Interface

    Code Behind for Master Page

    Partial Class MasterPage
        Inherits System.Web.UI.MasterPage
        Implements ICommandButtonContainer
     
        Public ReadOnly Property AddButton() As System.Web.UI.WebControls.Button Implements ICommandButtonContainer.AddButton
            Get
                ‘return a reference to the button
                Return btnAdd
            End Get
        End Property
     
        Public ReadOnly Property DeleteButton() As System.Web.UI.WebControls.Button Implements ICommandButtonContainer.DeleteButton
            Get
                Return btnDelete
            End Get
        End Property
     
        Public ReadOnly Property EditButton() As System.Web.UI.WebControls.Button Implements ICommandButtonContainer.EditButton
            Get
                Return btnEdit
            End Get
        End Property
     
        Public ReadOnly Property ExitButton() As System.Web.UI.WebControls.Button Implements ICommandButtonContainer.ExitButton
            Get
                Return btnExit
            End Get
        End Property
     
        Protected Sub btnAdd_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnAdd.Click
            ‘if the child page implements IAddable then call it’s Add() method
            If TypeOf Page Is IAddable Then
                CType(Page, IAddable).Add()
            End If
        End Sub
     
        Protected Sub btnEdit_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnEdit.Click
            If TypeOf Page Is IEditable Then
                CType(Page, IEditable).Edit()
            End If
        End Sub
     
        Protected Sub btnDelete_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnDelete.Click
            If TypeOf Page Is IDeletable Then
                CType(Page, IDeletable).Delete()
            End If
        End Sub
     
        Protected Sub btnExit_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnExit.Click
            If TypeOf Page Is IExitable Then
                CType(Page, IExitable).Exit()
            End If
        End Sub
    End Class

    Code behind for Page

    Partial Class _Default
        Inherits Web.UI.Page
        Implements IAddable, IDeletable, IEditable, IExitable
     
        Public Sub Add() Implements IAddable.Add
            WhatWasClicked.Text = “Add”
        End Sub
     
        Public Sub Delete() Implements IDeletable.Delete
            WhatWasClicked.Text = “Delete”
        End Sub
     
        Public Sub Edit() Implements IEditable.Edit
            WhatWasClicked.Text = “Edit”
        End Sub
     
        Public Sub [Exit]() Implements IExitable.Exit
            WhatWasClicked.Text = “Exit”
        End Sub
     
        Protected Sub btnApplyChanges_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnApplyChanges.Click
            If TypeOf Master Is ICommandButtonContainer Then
                Dim container As ICommandButtonContainer = Master
                container.AddButton.Enabled = AddableCheckbox.Checked
                container.DeleteButton.Enabled = DeletableCheckbox.Checked
                container.EditButton.Enabled = EditableCheckbox.Checked
                container.ExitButton.Enabled = ExitableCheckbox.Checked
            End If
        End Sub
    End Class