Programming
Article
By Wyatt Barnett

MasterPages hate logic. Throw Interfaces at Them.

By Wyatt Barnett

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

Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account