Where to put view-specific javascript files in an ASP.NET MVC application?
asp.net Mvcasp.net Mvc Problem Overview
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."
asp.net Mvc Solutions
Solution 1 - asp.net Mvc
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.
Solution 2 - asp.net Mvc
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:
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);
}
}
}
Solution 3 - asp.net Mvc
You can invert davesw's suggestion and block only .cshtml
<httpHandlers>
<add path="*.cshtml" verb="*" type="System.Web.HttpNotFoundHandler"/>
</httpHandlers>
Solution 4 - asp.net Mvc
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
Solution 5 - asp.net Mvc
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>
Solution 6 - asp.net Mvc
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.