Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A DRY Approach to Using Fancybox in an MVC Project

0.00/5 (No votes)
12 Apr 2012 1  
A DRY approach to using Fancybox in an MVC project

Typically, when an MVC View is opened inside a Fancybox, you need to display a page that is somewhat simpler than the other Views on your site. So, while you might have a standard layout file that you use when creating Views, which has a standard header and footer, for example, you have to make the up-front decision that the Views to be rendered inside light boxes will use a different layout with less decoration (after all, you don't want your header and footer repeating inside the light box).

So, how do you handle the inevitability that someone out there using your site will be super-paranoid about security and has decided to switch JavaScript off? For him, that fancybox link will still navigate the browser to the Controller method in question, but as you're probably returning a PartialView _ViewStart.cshtml won't be called and your user will experience a fairly bland page that looks out of place compared with everything else on your site. And, what's worse, if you haven't developed with that user in mind and have coded the View to only work inside a light box, dependent on the user's ability to close the light box manually, or making the navigation within the View entirely dependent on JavaScript, the user is now stuck and can only get back to the rest of your site by using the browser's Back button.

My solution to the problem is to use progressive enhancement. Only enable things that rely on JavaScript using JavaScript itself, and have something meaningful behind the default (non-JavaScript) behaviour. In the past, my solution was something along these lines:

  1. Create a PartialView that renders the content I want to display inside the fancybox.
  2. Create a full View that simply wraps my PartialView with a proper Layout.
  3. Create two Controller methods, one that returns my full ViewResult and another that returns the PartialView result.
    • It's important to name these methods something like "ShowMyView" and "ShowMyPartialView"
  4. Render an ActionLink that, by default, navigates to the ControllerMethod that returns the full ViewResult.
  5. In JavaScript, write some client code that transforms the href on the ActionLink at document load from "ShowMyView" to "ShowMyPartialView" and attaches the fancybox.

As you can imagine, I soon got a little tired with all this messing about just to accommodate that 1 in a million user who either has turned off JavaScript or is somehow still using Netscape Navigator.

So, my solution was to encapsulate all of that logic in the following HtmlHelper extension method and ActionFilterAttribute.

In brief, the HtmlHelper extension method allows you to create a link that will have the fancybox JavaScript applied to it at document load (the method creates that script so that you don't have to, although you will need to include jQuery in your project and probably have it referenced in the head on your Layout View).

You won't need to create a separate PartialView and View any more, just create the View as you would if it was to be rendered as a normal page on your site; nor will you need to code two Controller methods - just the one. That's because the FancyboxActionAttribute that you'll need to use to decorate your one and only Controller method, will look into the request to see if the request was originated by a fancybox link and use an alternative layout file (provided in the attribute constructor as a path). So, all you need to ensure is that you have an alternative Layout file that renders a minimal view inside a fancybox. If the attribute doesn't detect that the request came from a fancybox link, it will just return the View in its standard format.

public static class HtmlHelperExtensions
{
    /// <summary>
    /// Use this extension method to generate a fancybox link that will
    /// work in conjunction with the <see cref="FancyboxActionAttribute"/>
    /// DataAnnotation to gracefully degrade views to render properly when JavaScript
    /// is not enabled
    /// </summary>
    /// <param name="htmlHelper"></param>
    /// <param name="linkText"></param>
    /// <param name="actionName"></param>
    /// <param name="controllerName"></param>
    /// <param name="routeValues"></param>
    /// <param name="htmlAttributes"></param>
    /// <param name="fancyboxOptions">
    /// An anonymous object that will be interrogated by Reflection 
    /// to contruct a JavaScript object to be passed to fancybox</param>
    /// <returns></returns>
    public static MvcHtmlString FancyboxLink(this HtmlHelper htmlHelper, 
           string linkText, string actionName, string controllerName, 
           object routeValues = null, object htmlAttributes = null, 
           object fancyboxOptions = null)
    {
        MvcHtmlString actionLink = htmlHelper.ActionLink(linkText, 
          actionName, controllerName, routeValues, htmlAttributes);

        string link = actionLink.ToString();
        string href = "", preHref = "", 
               postHref = "", script = "", 
               fancyboxOptionsString = "";
        Guid id = Guid.NewGuid();
        int locationQuestionMark = link.IndexOf("?");
        int locationHref = link.ToLower().IndexOf("href=\"");
        int locationEndHref = link.IndexOf
        ("\"", locationHref + "href=\"".Length);

        preHref = link.Substring(0, locationHref);
        preHref += " data-fancybox=\"" + id.ToString() + "\"";
        postHref = link.Substring(locationEndHref);
        href = link.Substring(locationHref, locationEndHref - locationHref);

        if (locationQuestionMark > 0)
        {
            href += "&fancybox=false";
        }
        else
        {
            href += "?fancybox=false";
        }

        link = preHref + " " + href + " " + postHref;

        StringBuilder fancyboxOptionsStringBuilder = new StringBuilder();
        if (fancyboxOptions != null)
        {
            PropertyInfo[] members = fancyboxOptions.GetType().GetProperties();
            for (int i = 0; i < members.Length; i++)
            {
                PropertyInfo prop = members[i];

                string propName = prop.Name;
                object propValue = prop.GetValue(fancyboxOptions, null);
                string propValueString = "";

                if (propValue is int || propValue is bool)
                {
                    propValueString = propValue.ToString().ToLower();
                }
                else if (propValue is string)
                {
                    if (propValue.ToString().ToLower().StartsWith("function"))
                    {
                        propValueString = propValue.ToString();
                    }
                    else
                    {
                        propValueString = "'" + 
                        propValue.ToString() + "'";
                    }
                }

                if (i != members.Length - 1)
                {
                    propValueString += ",";
                }

                fancyboxOptionsStringBuilder.AppendLine(propName + ":" + propValueString);
            }
        }

        fancyboxOptionsString = "{" + 
        fancyboxOptionsStringBuilder.ToString() + "}";

        StringBuilder scriptBuilder = new StringBuilder();
        scriptBuilder.AppendLine
        ("<script language="\""javascript\">");
        scriptBuilder.AppendLine
        ("$(function(){");
        scriptBuilder.AppendLine
        ("var href = $('a[data-fancybox=\"" + 
        id.ToString() + "\"]').attr('href');");
        scriptBuilder.AppendLine
        ("href = href.replace('fancybox=false', 'fancybox=true');");
        scriptBuilder.AppendLine("$('a[data-fancybox=\"" + id.ToString() + "\"]').attr('href', href);");
        scriptBuilder.AppendLine
        ("$('a[data-fancybox=\"" + id.ToString() + 
         "\"]').fancybox(" + fancyboxOptionsString + ");");
        scriptBuilder.AppendLine("});");
        scriptBuilder.AppendLine("</script>");

        script = scriptBuilder.ToString();

        return new MvcHtmlString(link + script);
    }
}

/// <summary>
/// <para>
/// Use this DataAnnotation in conjunction with the 
/// <see cref="FancyboxLink"/>
/// extension method.
/// Typically when a view is opened inside a fancybox the
/// rendered view needs to use a simpler layout from the rest of the views in the 
/// site so that such items as the header and footer are not repeated inside the lightbox.
/// However, if Javascript is disabled the link would usually return the full view in its
/// normal state, without the context of the lightbox to frame it. 
/// This is generally undesired behaiour.
/// </para>
/// <para>
/// This attribute, when applied to a controller method, will detect if the action
/// was called from a fancybox link generated with the above mentioned extension method.
/// If JavaScript is enabled it will take the provided 
/// <paramref name="LayoutPath"/> 
/// to an alternative Layout for the view
/// and render a normal view inside the lightbox using that layout.
/// </para>
/// <para>
/// Alternatively, if Javascript is disabled the view 
/// will be returned in its original state
/// with the layout defined in its design.
/// </para>
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class FancyboxActionAttribute : ActionFilterAttribute
{
    public string LayoutPath { get; set; }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="LayoutPath">
    /// The path to the alternative Layout file to use
    /// for the view when rendered within the lightbox
    /// </param>
    public FancyboxActionAttribute(string LayoutPath)
    {
        this.LayoutPath = LayoutPath;
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        if (filterContext.RequestContext.HttpContext.Request["fancybox"] != null)
        {
            string returnInFancyBoxString = 
              filterContext.RequestContext.HttpContext.Request["fancybox"].ToString();
            bool returnInFancyBox = bool.Parse(returnInFancyBoxString);

            ActionResult result = filterContext.Result;
            if (result is ViewResult && returnInFancyBox)
            {
                ViewResult viewResult = result as ViewResult;
                viewResult.MasterName = LayoutPath;
            }
        }
    }
}

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here