Patrick Desjardins Blog
Patrick Desjardins picture from a conference

How to add CSS file or Javascript file by action of controller

Posted on: 2012-06-30

Would it be great to specify for a specific action what CSS or Javascript file to load? With Asp.Net MVC it's possible to do something custom pretty fast and useful with attribute. Attribute is something that the developer add at the top of the method (action). The syntax is simple. It uses the square bracket and between you have the name of the attribute and parameters.

Above is the result of how to use the Javascript attribute to get 2 Javascripts file loaded only when Index is called. We could improve by also let the developer add the tag over the controller class which would load the Javascript for all actions of this controller.

public class HomeController : Controller { 
  
  [JavaScript("MyFile1", "MyFile2")] 
  public ActionResult Index() { 
    ViewBag.Message = "Welcome to ASP.NET MVC!";
    return View(); 
  }

  public ActionResult About() { 
    return View();
  } 
} 

The first step is to create an attribute for each of the specific file you want. For example, one for CSS and one for Javascript. For simplicity, we will only do Javascript here.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 
public class JavaScript : Attribute { 
  public string[] FileNames { get; set; }

  public JavaScript(params string[] fileName) { 
    this.FileNames = fileName; 
  } 
} 

Then, we need to add to the master page (by default_Layout.cshtml) a code that will read those attributes and add the Javascript include tag in the header of the Html code.

<!DOCTYPE html> 
<html>
  <head> 
    <meta charset="utf-8" /> 
    <title>@ViewBag.Title</title> 
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" /> 
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script> 
    <script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")" type="text/javascript"></script> 
    @Html.GetJavascript(); 
</head> 

The GetJavascript() helper code will loop all attributes to find what has been defined at the controller side.

public static class JavascriptHelper { 
  public static MvcHtmlString GetJavascript(this HtmlHelper helper) { 
    IList<string> fileNames = new List<string>();

    //The class may have more than one Javascript file. Need to loop them all and also loop all entries 
    MemberInfo controllerInfo = helper.ViewContext.Controller.GetType(); 
    object[] listOfcontrollerAttributes = controllerInfo.GetCustomAttributes(typeof(JavaScript), true); 
    FillUpFileArray(fileNames, listOfcontrollerAttributes);

    //Method attributes. First get the method that has been called and loop all possible Javascript tag and entries MethodInfo method; 
    if (helper.ViewContext.HttpContext.Request.HttpMethod == "POST"){ 
      method = helper.ViewContext.Controller.GetType().GetMethods().FirstOrDefault(t => t.Name == helper.ViewContext.RouteData.GetRequiredString("action") && 
        t.GetCustomAttributes(typeof(JavaScript), true).Any() && 
        t.GetCustomAttributes(typeof(HttpPostAttribute), true).Any()); 
    } else{ 
      method = helper.ViewContext.Controller.GetType().GetMethods().FirstOrDefault(t => t.Name == helper.ViewContext.RouteData.GetRequiredString("action") && 
      t.GetCustomAttributes(typeof(JavaScript), true).Any() && 
      !t.GetCustomAttributes(typeof(HttpPostAttribute), true).Any()); 
    } 
    
    if(method!=null){ 
      object[] methodAttributes = method.GetCustomAttributes(typeof(JavaScript), true); 
      FillUpFileArray(fileNames, methodAttributes); 
    }

    //Create Html 
    if (fileNames.Any()) { 
      var sb = new StringBuilder(); 
      var url = new UrlHelper(helper.ViewContext.RequestContext); 
      const string JS_SCRIPT_FORMAT = "<script src=\\"{0}.js\\" type=\\"text/javascript\\"></script>"; 
      foreach (string fmt in fileNames.Select(name => string.Format(JS_SCRIPT_FORMAT, url.Content("~/Scripts/") + name))){ 
        sb.AppendLine(fmt); 
      } 
      return new MvcHtmlString(sb.ToString()); 
    }

    return new MvcHtmlString(string.Empty); 
  }

  private static void FillUpFileArray(ICollection<string> fileNames, IEnumerable<object> listOfcontrollerAttributes){ 
    if (listOfcontrollerAttributes != null){ 
      foreach (string name in listOfcontrollerAttributes.OfType<JavaScript>()
        .SelectMany(classAttributes => classAttributes.FileNames
        .Where(name => !string.IsNullOrEmpty(name) && !fileNames.Contains(name)))){ 
        fileNames.Add(name); 
      } 
    } 
  } 
} 

Here is the explication step by step:

 MemberInfo controllerInfo = helper.ViewContext.Controller.GetType(); 
 object[] listOfcontrollerAttributes = controllerInfo.GetCustomAttributes(typeof(JavaScript), true); 
 FillUpFileArray(fileNames, listOfcontrollerAttributes); 

The first section get everything from the controller that has been called the view to get all possibles attributes defined and loop through them. Every time it finds an attribute, it loops through the list of string that represent a Javascript file.

MethodInfo method; 
if (helper.ViewContext.HttpContext.Request.HttpMethod == "POST"){ 
  method = helper.ViewContext.Controller.GetType().GetMethods().FirstOrDefault(t => t.Name == helper.ViewContext.RouteData.GetRequiredString("action") && t.GetCustomAttributes(typeof(JavaScript), true).Any() && t.GetCustomAttributes(typeof(HttpPostAttribute), true).Any()); 
} else{ 
  method = helper.ViewContext.Controller.GetType().GetMethods().FirstOrDefault(t => t.Name == helper.ViewContext.RouteData.GetRequiredString("action") && t.GetCustomAttributes(typeof(JavaScript), true).Any() && !t.GetCustomAttributes(typeof(HttpPostAttribute), true).Any()); 
} 

if(method!=null){ 
  object[] methodAttributes = method.GetCustomAttributes(typeof(JavaScript), true); 
  FillUpFileArray(fileNames, methodAttributes); 
} 

It's almost the same with method, but this time, we need to get the good action method. A same method can be good for GET or HTTP so we need to figure out the good one. As you can see, we do not search explicitly for GET because action's method is implicitly GET.

if (fileNames.Any()) { 
  var sb = new StringBuilder(); 
  var url = new UrlHelper(helper.ViewContext.RequestContext); 
  const string JS_SCRIPT_FORMAT = "<script src=\\"{0}.js\\" type=\\"text/javascript\\"></script>"; 
  foreach (string fmt in fileNames.Select(name => string.Format(JS_SCRIPT_FORMAT, url.Content("~/Scripts/") + name))){ 
    sb.AppendLine(fmt); 
  } 
  return new MvcHtmlString(sb.ToString());
}

return new MvcHtmlString(string.Empty); 

At the end, we print the list of Javascript file by adding the extension and but referring to the good script folder.

Here is an example of a controller that works pretty well with this kind of scenario:

[JavaScript("Controller1", "Controller2")] 
public class HomeController : Controller {

  [JavaScript("MyFileAction1", "MyFileAction2")] 
  public ActionResult Index() { 
    ViewBag.Message = "Welcome to ASP.NET MVC!";
    return View(); 
  }

  [JavaScript("Integer", "Integer")] 
  [HttpGet] 
  public ActionResult Test(int i, int j) { 
    ViewBag.Message = "Welcome to ASP.NET MVC!" + i + " " + j;
    return View("Index"); 
  }

  [JavaScript("PostFile1")] 
  [HttpPost] 
  public ActionResult Test(string s) { 
    ViewBag.Message = "Welcome to ASP.NET MVC!" + s;
    return View("Index"); 
  }

  public ActionResult About() { return View(); } 
} 

Hope it helps you to get cleaner code!