97

What is the best place (which folder, etc) to put view-specific javascript files in an ASP.NET MVC application?

To keep my project organized, I'd really love to be able to put them side-by-side with the view's .aspx files, but I haven't found a good way to reference them when doing that without exposing the ~/Views/Action/ folder structure. Is it really a bad thing to let details of that folder structure leak?

The alternative is to put them in the ~/Scripts or ~/Content folders, but is a minor irritation because now I have to worry about filename clashes. It's an irritation I can get over, though, if it is "the right thing."

Erv Walter
  • 13,737
  • 8
  • 44
  • 57
  • 2
    I found sections useful for this. See: http://stackoverflow.com/questions/4311783/asp-net-mvc-3-razor-include-js-file-in-head-tag – Frison Alexander Apr 11 '13 at 13:25
  • 1
    This sounds like a crazy question, but an extremely useful scenario is when you nest a page's javascript file under the .cshtml. (For example, with [NestIn](http://visualstudiogallery.msdn.microsoft.com/9d6ef0ce-2bef-4a82-9a84-7718caa5bb45)). It helps not having to bounce around solution explorer. – David Sherret Jan 17 '14 at 00:56

6 Answers6

126

Old question, but I wanted to put my answer incase anyone else comes looking for it.

I too wanted my view specific js/css files under the views folder, and here's how I did it:

In the web.config folder in the root of /Views you need to modify two sections to enable the webserver to serve the files:

    <system.web>
        <httpHandlers>
            <add path="*.js" verb="GET,HEAD" type="System.Web.StaticFileHandler" />
            <add path="*.css" verb="GET,HEAD" type="System.Web.StaticFileHandler" />
            <add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
        </httpHandlers>
        <!-- other content here -->
    </system.web>

    <system.webServer>
        <handlers>
            <remove name="BlockViewHandler"/>
            <add name="JavaScript" path="*.js" verb="GET,HEAD" type="System.Web.StaticFileHandler" />
            <add name="CSS" path="*.css" verb="GET,HEAD" type="System.Web.StaticFileHandler" />
            <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
        </handlers>
        <!-- other content here -->
    </system.webServer>

Then from your view file you can reference the urls like you expect:

@Url.Content("~/Views/<ControllerName>/somefile.css")

This will allow serving of .js and .css files, and will forbid serving of anything else.

davesw
  • 1,850
  • 1
  • 14
  • 11
  • 1
    When i do this i get the error that httpHandlers cannot be used in pipeline mode. It wants me to switch to classic mode on the server. What is the the correct way of doing this when one does not want the server to use classic mode? – Bjørn Jul 17 '14 at 06:21
  • 1
    @BjørnØyvindHalvorsen You can delete one or the other handler section or turn off configuration validation in your web.config. [See here](http://stackoverflow.com/questions/4209999/an-asp-net-setting-has-been-detected-that-does-not-apply-in-integrated-managed-p) – davesw Apr 21 '15 at 12:51
  • 2
    Only the mods to the section was required for it to work, mods was NOT required. – joedotnot Dec 06 '17 at 15:52
  • @joedotnot Correct, only one section is needed, but which one depends on your webserver configuration. Currently most people will be need the system.webServer section, not the older system.web section. – davesw Dec 07 '17 at 16:29
  • It doesn't work in my case! It just renders the path inside the @Url.Content as plain text inside the view html page. – Sotiris Zegiannis Mar 07 '19 at 11:43
  • @SotirisZegiannis: I'm not quite sure what you mean, but my path to Url.Content was just an example. You need to replace with the name of your actual controller. The path to Url.Content should be a real path (like other uses of Url.Content). – davesw Mar 08 '19 at 12:50
  • Could you explain why the BlockViewHandler is needed for this to work? Removing that line causes the static files to 404 and I was wondering why. – Slight Sep 11 '19 at 19:41
  • @Slight By default asp.net has a BlockViewHandler than prevents serving files from view folders as a security precaution. That's why you get a 404 without the override. The override allows specific extensions via a whitelist in the view folders only. – davesw Sep 12 '19 at 20:16
6

One way of achieving this is to supply your own ActionInvoker. Using the code included below, you can add to your controller's constructor:

ActionInvoker = new JavaScriptActionInvoker();

Now, whenever you place a .js file next to your view:

enter image description here

You can access it directly:

http://yourdomain.com/YourController/Index.js

Below is the source:

namespace JavaScriptViews {
    public class JavaScriptActionDescriptor : ActionDescriptor
    {
        private string actionName;
        private ControllerDescriptor controllerDescriptor;

        public JavaScriptActionDescriptor(string actionName, ControllerDescriptor controllerDescriptor)
        {
            this.actionName = actionName;
            this.controllerDescriptor = controllerDescriptor;
        }

        public override object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters)
        {
            return new ViewResult();
        }

        public override ParameterDescriptor[] GetParameters()
        {
            return new ParameterDescriptor[0];
        }

        public override string ActionName
        {
            get { return actionName; }
        }

        public override ControllerDescriptor ControllerDescriptor
        {
            get { return controllerDescriptor; }
        }
    }

    public class JavaScriptActionInvoker : ControllerActionInvoker
    {
        protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
        {
            var action = base.FindAction(controllerContext, controllerDescriptor, actionName);
            if (action != null)
            {
                return action;
            } 

            if (actionName.EndsWith(".js"))
            {
                return new JavaScriptActionDescriptor(actionName, controllerDescriptor);
            }

            else 
                return null;
        }
    }

    public class JavaScriptView : IView
    {
        private string fileName;

        public JavaScriptView(string fileName)
        {
            this.fileName = fileName;
        }

        public void Render(ViewContext viewContext, TextWriter writer)
        {
            var file = File.ReadAllText(viewContext.HttpContext.Server.MapPath(fileName));
            writer.Write(file);
        }
    }


    public class JavaScriptViewEngine : VirtualPathProviderViewEngine
    {
        public JavaScriptViewEngine()
            : this(null)
        {
        }

        public JavaScriptViewEngine(IViewPageActivator viewPageActivator)
            : base()
        {
            AreaViewLocationFormats = new[]
            {
                "~/Areas/{2}/Views/{1}/{0}.js",
                "~/Areas/{2}/Views/Shared/{0}.js"
            };
            AreaMasterLocationFormats = new[]
            {
                "~/Areas/{2}/Views/{1}/{0}.js",
                "~/Areas/{2}/Views/Shared/{0}.js"
            };
            AreaPartialViewLocationFormats = new []
            {
                "~/Areas/{2}/Views/{1}/{0}.js",
                "~/Areas/{2}/Views/Shared/{0}.js"
            };
            ViewLocationFormats = new[]
            {
                "~/Views/{1}/{0}.js",
                "~/Views/Shared/{0}.js"
            };
            MasterLocationFormats = new[]
            {
                "~/Views/{1}/{0}.js",
                "~/Views/Shared/{0}.js"
            };
            PartialViewLocationFormats = new[]
            {
                "~/Views/{1}/{0}.js",
                "~/Views/Shared/{0}.js"
            };
            FileExtensions = new[]
            {
                "js"
            };
        }

        public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
        {
            if (viewName.EndsWith(".js"))
                viewName = viewName.ChopEnd(".js");
            return base.FindView(controllerContext, viewName, masterName, useCache);
        }


        protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
        {
            return new JavaScriptView(partialPath);
        }

        protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
        {
            return new JavaScriptView(viewPath);
        }
    }
}
Kirk Woll
  • 76,112
  • 22
  • 180
  • 195
  • This seems a good solution, however, but does it affect the call time to actions? – Leandro Soares Jan 13 '16 at 10:17
  • It may well work, but what the? i want to write less code, not more. – joedotnot Dec 08 '17 at 02:49
  • 1
    @joedotnot you write more code once, and less code forevermore. The mantra of a programmer, no? :) – Kirk Woll Dec 08 '17 at 05:41
  • @KirkWoll. no disagreement there. Just disappointed that for what should be a "simple feature", it did not come out of the box. So i preferred to opt for davesw's answer (the accepted answer). But thanks for sharing your code, it may be useful for others. – joedotnot Dec 08 '17 at 15:51
  • @KirkWoll I'm new to MVC and I am trying to implement your solution in an MVC5 site. I am not sure where to place or "use" the "ActionInvoker = new JavaScriptActionInvoker()" ?? – Drew Jan 10 '19 at 13:52
3

You can invert davesw's suggestion and block only .cshtml

<httpHandlers>
    <add path="*.cshtml" verb="*" type="System.Web.HttpNotFoundHandler"/>
</httpHandlers>
  • Perfect! :) A nice short solution! And it works! :) I have no idea why this is not the default setting, because it is much better to be able to keep scripts relating to views together with the actual views. Thanks, Vadym. – BruceHill Dec 20 '12 at 07:10
  • 24
    I would be cautious with this approach, even though it seems nice and clean. If in the future, this app includes view engines other than Razor (ex WebForms, Spark, etc), they will silently be public. Also affecting files like `Site.Master`. Whitelisting seems the safer approach – arserbin3 Sep 24 '13 at 22:28
  • I agree with @arserbin3 that white-listing seems safer; at the same time, I cant feel a possibility of an enterprise application's View Engine change from one to another. There is no perfect automation tool for doing so. Conversion needs to be done by hand. Once I did so for a large Web Application; converted WebForm viewengine to Razor, and i remember the days of nightmare, for couple of months, things were not working here and there... Cant think about doing such thing again :). If I have to do such giant change anyway, then, I believe such web.config setting change wont be forgotten. – Emran Hussain Sep 04 '15 at 15:58
1

I know this is a rather old topic, but I have a few things I would like to add. I tried davesw's answer but it was throwing a 500 error when trying to load the script files, so I had to add this to the web.config:

<validation validateIntegratedModeConfiguration="false" />

to system.webServer. Here is what I have, and I was able to get it to work:

<system.webServer>
  <handlers>
    <remove name="BlockViewHandler"/>
    <add name="JavaScript" path="*.js" verb="GET,HEAD" type="System.Web.StaticFileHandler" />
    <add name="CSS" path="*.css" verb="GET,HEAD" type="System.Web.StaticFileHandler" />
    <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
  </handlers>
  <validation validateIntegratedModeConfiguration="false" />
</system.webServer>
<system.web>
  <compilation>
    <assemblies>
      <add assembly="System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    </assemblies>
  </compilation>
  <httpHandlers>
      <add path="*.js" verb="GET,HEAD" type="System.Web.StaticFileHandler" />
      <add path="*.css" verb="GET,HEAD" type="System.Web.StaticFileHandler" />
      <add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
  </httpHandlers>
</system.web>

Here is more information on validation: https://www.iis.net/configreference/system.webserver/validation

dh6984
  • 108
  • 12
0

add this code in web.config file inside system.web tag

<handlers>
    <remove name="BlockViewHandler"/>
    <add name="JavaScript" path="*.js" verb="GET,HEAD" type="System.Web.StaticFileHandler" />
    <add name="CSS" path="*.css" verb="GET,HEAD" type="System.Web.StaticFileHandler" />
     <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
</handlers>
Peter Isaac
  • 352
  • 2
  • 12
0

I also wanted to place js files related to a view in the same folder as the view.

I wasn't able to get the other solutions in this thread to work, not that they are broken but I am too new to MVC to get them working.

Using information given here and several other stacks I came up with a solution that:

  • Allows the javascript file to be placed in the same directory as the view it is associated with.
  • Script URL's don't give away the underlying physical site structure
  • Script URL's don't have to end with a trailing slash (/)
  • Doesn't interfere with static resources, eg: /Scripts/someFile.js still works
  • Doesn't require runAllManagedModulesForAllRequests to be enabled.

Note: I am also using HTTP Attribute Routing. It's possible that the route's used in my soultion could be modified to work without enabling this.

Given the following example directory/file structure:

Controllers
-- Example
   -- ExampleController.vb

Views
-- Example
   -- Test.vbhtml
   -- Test.js

Using the configuration steps given below, combined with the example structure above, the test view URL would be accessed via: /Example/Test and the javascript file would be referenced via: /Example/Scripts/test.js

Step 1 - Enable Attribute Routing:

Edit your /App_start/RouteConfig.vb file and add routes.MapMvcAttributeRoutes() just above the existing routes.MapRoute:

Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Web
Imports System.Web.Mvc
Imports System.Web.Routing

Public Module RouteConfig
    Public Sub RegisterRoutes(ByVal routes As RouteCollection)
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}")

        ' Enable HTTP atribute routing
        routes.MapMvcAttributeRoutes()

        routes.MapRoute(
            name:="Default",
            url:="{controller}/{action}/{id}",
            defaults:=New With {.controller = "Home", .action = "Index", .id = UrlParameter.Optional}
        )
    End Sub
End Module

Step 2 -Configure your site to treat, and process, /{controller}/Scripts/*.js as an MVC path and not a static resource

Edit your /Web.config file, adding the following to the system.webServer --> handlers section of the file:

<add name="ApiURIs-ISAPI-Integrated-4.0" path="*/scripts/*.js" verb="GET" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />

Here it is again with context:

  <system.webServer>
    <modules>
      <remove name="TelemetryCorrelationHttpModule"/>
      <add name="TelemetryCorrelationHttpModule" type="Microsoft.AspNet.TelemetryCorrelation.TelemetryCorrelationHttpModule, Microsoft.AspNet.TelemetryCorrelation" preCondition="managedHandler"/>
      <remove name="ApplicationInsightsWebTracking"/>
      <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" preCondition="managedHandler"/>
    </modules>
    <validation validateIntegratedModeConfiguration="false"/>
    <handlers>
      <remove name="ExtensionlessUrlHandler-Integrated-4.0"/>
      <remove name="OPTIONSVerbHandler"/>
      <remove name="TRACEVerbHandler"/>
      <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0"/>
      <add name="ApiURIs-ISAPI-Integrated-4.0" path="*/scripts/*.js" verb="GET" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
    </handlers>
  </system.webServer>  

Step 3 - Add the following scripts action result to your Controller file

  • Be sure to edit the route path to match the {controller} name for the controller, for this example it's: <Route("Example/Scripts/{filename}")>
  • You will need to copy this into each of your Controller files. If you wanted, there is probably a way to do this as a single, one-time, route configuration somehow.

        ' /Example/Scripts/*.js
        <Route("Example/Scripts/{filename}")>
        Function Scripts(filename As String) As ActionResult
            ' ControllerName could be hardcoded but doing it this way allows for copy/pasting this code block into other controllers without having to edit
            Dim ControllerName As String = System.Web.HttpContext.Current.Request.RequestContext.RouteData.Values("controller").ToString()
    
            ' the real file path
            Dim filePath As String = Server.MapPath("~/Views/" & ControllerName & "/" & filename)
    
            ' send the file contents back
            Return Content(System.IO.File.ReadAllText(filePath), "text/javascript")
        End Function
    

For context, this is my ExampleController.vb file:

Imports System.Web.Mvc

Namespace myAppName
    Public Class ExampleController
        Inherits Controller

        ' /Example/Test
        Function Test() As ActionResult
            Return View()
        End Function


        ' /Example/Scripts/*.js
        <Route("Example/Scripts/{filename}")>
        Function Scripts(filename As String) As ActionResult
            ' ControllerName could be hardcoded but doing it this way allows for copy/pasting this code block into other controllers without having to edit
            Dim ControllerName As String = System.Web.HttpContext.Current.Request.RequestContext.RouteData.Values("controller").ToString()

            ' the real file path
            Dim filePath As String = Server.MapPath("~/Views/" & ControllerName & "/" & filename)

            ' send the file contents back
            Return Content(System.IO.File.ReadAllText(filePath), "text/javascript")
        End Function


    End Class
End Namespace

Final Notes There is nothing special about the test.vbhtml view / test.js javascript files and are not shown here.

I keep my CSS in the view file but you could easily add to this solution so that you can reference your CSS files in a similar way.

Drew
  • 4,215
  • 3
  • 26
  • 40