Friday, February 27, 2009

ASP.NET MVC and Tabs

Recently I have been spending time creating a batch application for SQL Server Reporting Services. During the day, I work for a third-party administrator (TPA). Part of the business involves the payment of medical, vision and dental claims on the behalf of our clients. Periodically we need to print the check registers on the accounts from which the claims payments are made. A data driven subscription alone is not flexible enough. Running the report on demand for each date and client would take too long.

To make it easy for the accounting department to run these reports, I created a web application using ASP.NET MVC that allows the user to search for check runs by date and then select which registers to print from the search results. The selection is used to populate a parameter table followed by the application executing a SQL Agent job associated with a standard data driven subscription for the check register report. Now that the background is out of the way, on to the problem and solution.

The default ASP.NET MVC application layout is a simple master page with a "MainContent" place holder. The master page provides a menu located above the content place holder. The menu is a basic unordered list formatted to look like a series of tabs. Each tab contains an ActionLink helper that ties it to a specific controller and action. This works great except that there is no support for identifying and maintaining the currently selected tab.



JQuery UI was being used to implement all of the user interface elements. Because of this, I wanted the master page tabs to match the look and feel of the JQuery UI widgets. Since the application is MVC, it should be easy to set the tab styles based on which controller and action was accessed to produce the current view. To keep the view markup clean, I decided to create an HTML helper extension (TabExtensions.cs):
using System;
using System.Web.Mvc;

namespace DailyRegisters.Helpers
{
    public static class TabExtensions
    {
        public const String DEFAULT_CSS_CLASS = "ui-tabs-selected ui-state-active";

        public static string ActiveTabClass(this HtmlHelper helper, string targetController, string targetAction)
        {
            return ActiveTabClass(helper, targetController, targetAction == null ? null : new string[]{ targetAction });
        }

        public static string ActiveTabClass(this HtmlHelper helper, string targetController, string[] targetActions)
        {
            return ActiveTabClass(helper, targetController, targetActions, DEFAULT_CSS_CLASS);
        }

        public static string ActiveTabClass(this HtmlHelper helper, string targetController, string[] targetActions, string cssClass)
        {
            // CSS class
            string css = string.Empty;

            // Get the controller and action for the view
            string controller = helper.ViewContext.RouteData.GetRequiredString("controller").ToLower();
            string action = helper.ViewContext.RouteData.GetRequiredString("action").ToLower();
            string[] targetActionsLower = Array.ConvertAll(targetActions, delegate(string s) { return s.ToLower(); });

            // If the targets match what's in the view context, set the active tab class
            if ((targetController.ToLower().Equals(controller) && targetActions == null)
                || (targetController.ToLower().Equals(controller) && Array.IndexOf(targetActionsLower, action) > -1))
            {
                css = cssClass;
            }

            return css;
        }
    }
}
Since I was using JQuery, I wanted the helper to supply a default CSS style for the active tab. This value is defined by the DEFAULT_CSS_CLASS constant. The class also contains a number of overloaded methods that allow one to use the helper in a number of different ways:
  • A controller and a single action, returning the default CSS class for the active tab
  • A controller and an array of actions, returning the default CSS class for the active tab
  • A controller, array of actions and the CSS class to use for the active tab
Note: A value of null for the targetAction(s) will only use the controller to determine the active tab.

I then made the following modifications to my master page (Site.Master):
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="DailyRegisters.Views.Shared.Site" %>
<%@ Import Namespace="DailyRegisters.Helpers" %>

...

<div id="menucontainer" class="ui-tabs ui-widget ui-widget-content ui-corner-all">
    <ul id="menu" class="ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all">              
        <li class="ui-state-default ui-corner-top <%= Html.ActiveTabClass("Home", new string[]{"Index", "Search", "Print"}) %>"><%= Html.ActionLink("Home", "Index", "Home")%></li>
        <li class="ui-state-default ui-corner-top <%= Html.ActiveTabClass("Home", "About") %>"><%= Html.ActionLink("About", "About", "Home")%></li>
    </ul>
    <div id="main">
        <asp:ContentPlaceHolder ID="MainContent" runat="server" />
    </div>
    <div id="footer">
        Copyright © 2009 Generic Company, Inc.
    </div>
</div>
...
The "Home" tab will be active if the HomeController is accessed with the Index, Search or Print actions. The "About" tab will be active if the HomeController is accessed with the About action. The application's tabs now look like this:



There are probably a dozen other ways to do this, but for my purposes it is more than adequate. This solution was tested against the ASP.NET MVC release candidate.