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:
- Create a
PartialView
that renders the content I want to display inside the fancybox.
- Create a full View that simply wraps my
PartialView
with a proper Layout
.
- 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
"
- Render an
ActionLink
that, by default, navigates to the ControllerMethod
that returns the full ViewResult
.
- 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
{
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);
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class FancyboxActionAttribute : ActionFilterAttribute
{
public string LayoutPath { get; set; }
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;
}
}
}
}