MasterPages hate logic. Throw Interfaces at Them.

By | | .NET

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.]

Written By:

Wyatt Barnett

Wyatt leads the in-house development team for a major industry trade association. When not slinging obscene amounts of C# and SQL at a few exceedingly large monitors, he is most often spotted staring at HDTV and other forms of entertainment in local watering holes.

 

{ 10 comments }

seth March 29, 2008 at 4:44 am

(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

pandojc December 8, 2006 at 4:39 am

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!

wwb_99 October 23, 2006 at 10:00 pm

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.

tomandlis October 23, 2006 at 9:49 pm

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

tomandlis October 23, 2006 at 9:45 pm

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.

wwb_99 October 3, 2006 at 11:29 pm

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.

Rui da Luz October 3, 2006 at 9:14 pm

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

Steve October 1, 2006 at 9:20 am

Excellent! Great explanation and the code works perfectly.

Hima_.net September 5, 2006 at 8:57 pm

Good article .
You have been kicked

ses5909 August 31, 2006 at 4:44 pm

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

Comments on this entry are closed.