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.

Please follow and like us:

10 Replies to “ASP.NET MVC and Tabs”

  1. Keith,

    Is the source available for download or do I just copy paste ?
    I'm assuming the Home / About actionlinks work the same as before
    and all we are dealing with is strictly the tabs using JQUERY.

    Thanks

  2. Dan,

    I haven't used the latest version of JQuery UI. I am sorry to say that I cannot give you a definitive answer. After a quick visit to the JQuery site, it does not appear that the tab widget directly supports ASP.NET MVC.

    Keith

  3. Hello again,

    I've modified your helper a little:

    // 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) || (Array.IndexOf(targetActionsLower, String.Concat(action, ".html")) > -1))))
    {
    css = cssClass;
    }

    Now user can use case invariant controller and action names and actions can have .html at the end.

  4. Perfect. I came across several of the other dozen ways to do this but all of them faltered because they only differentiated between different Controllers and didn't have the next next level of control by targetting Actions. Most of them were also needlessly complex involving alternate ViewEngines or inheritting base types such as ViewUserControl. I like this implementation because it a simple HtmlHelper which is exactly where I'd expect to find a gizmo for doing some simple UI logic in a View.

  5. Just what I was looking for. I ran into issues with using JQuery tabs on my master page, but I still wanted the look and feel of JQuery tabs that I was using on my views. Thanks.

  6. Nice catch, changing the case before searching the controllers and actions using Array.IndexOf() is a good idea. I forgot that it is case sensitive. I am guessing that you also have actions that return XML or JSON in addition to HTML views hence the need to add the .html URL extension to the helper.

    I added your changes for case sensitivity to the source listing. I omitted the changes for the .html URL extension. Most applications seem to implement different routing rules for handling formats based on URL extensions. Your changes in this regard are still available to reader in your comments.

    Thank you for the suggestions!

Leave a Reply

Your email address will not be published. Required fields are marked *