Reputation: 1509
I've got a MVC 3 project with an area, let's call it MyArea. I'd like to place scripts and styles that are specific to MyArea under that area's subfolder, resulting in a project folder structure like this:
/Areas/MyArea
/Areas/MyArea/Controllers
/Areas/MyArea/Scripts <-------- I want these here
/Areas/MyArea/Styles <--------
/Areas/MyArea/ViewModels
/Areas/MyArea/Views
/Controllers
/Scripts
/Styles
/ViewModels
/Views
Fine, but now when I link to a style in the document/view it has to be written like this:
<link href="/Areas/MyArea/Styles/MyStyle.css" rel="stylesheet" type="text/css" />
I'd prefer to link it like this:
<link href="/MyArea/Styles/MyStyle.css" rel="stylesheet" type="text/css" />
This would be the same routing as for the area's countrollers and actions.
How can I achieve this routing?
Upvotes: 1
Views: 710
Reputation: 1509
Having researched the question mentioned by Behnam Esmaili, and read further, and further, and further :-) this is what I came up with.
I created a new IHttpModule that checks the path of each incoming request for /areaname/contentfolder/
, where areaname
is the name of any of the application's areas, and contentfolder
is any of any selected list of possible content folder names. I chose to hardcode a set of plausible content folder names, but you could have each area registration register all its content folder names somewhere and use that.
Note: The online Microsoft doc Walkthrough: Creating and Registering a Custom HTTP Module suggests you place the HTTPModule class in the App_Code folder. Don't! Classes in that folder are runtime compiled by ASP.Net, resulting in a binry copy of the class in the temp .Net folder, which in turn causes ambiguity when ASP.Net tries to load the HTTPModule class. Place the class in a different folder of your choice.
To find all area names, I chose to use AppDomain.CurrentDomain.GetAssemblies()
and find all subclasses of System.Web.Mvc.AreaRegistration
. Create an instance of each one and retrieve the value of its AreaName
property.
Full source code:
public class HTTPModuleAreaContent : IHttpModule
{
private List<string> allAreaNames = null;
public HTTPModuleAreaContent()
{
}
public String ModuleName
{
get { return "HTTPModuleAreaContent"; }
}
public void Init(HttpApplication application)
{
application.BeginRequest +=
(new EventHandler(this.BeginRequest));
}
private void GetAreaNames(HttpContext context)
{
allAreaNames = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(ass => ass.GetTypes())
.Where(t => t.IsSubclassOf(typeof(AreaRegistration)))
.Select(t => ((AreaRegistration)Activator.CreateInstance(t)).AreaName)
.ToList();
}
private void BeginRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext context = application.Context;
if (allAreaNames == null)
GetAreaNames(context);
string filePath = context.Request.FilePath.ToUpper();
string areaName = allAreaNames
.FirstOrDefault(an => filePath.StartsWith('/' + an + '/', StringComparison.OrdinalIgnoreCase));
if (string.IsNullOrEmpty(areaName))
return;
string areaNameUpper = areaName.ToUpper();
if (filePath.StartsWith('/' + areaNameUpper + "/STYLES/")
|| filePath.StartsWith('/' + areaNameUpper + "/SCRIPT/")
|| filePath.StartsWith('/' + areaNameUpper + "/SCRIPTS/")
|| filePath.StartsWith('/' + areaNameUpper + "/JS/")
|| filePath.StartsWith('/' + areaNameUpper + "/JAVASCRIPT/")
|| filePath.StartsWith('/' + areaNameUpper + "/JAVASCRIPTS/")
|| filePath.StartsWith('/' + areaNameUpper + "/CONTENT/")
|| filePath.StartsWith('/' + areaNameUpper + "/IMAGES/")
)
context.RewritePath("/Areas/" + context.Request.Path);
}
public void Dispose() { }
}
EDIT: Apparently, the above solution does not work for applications that ate not at the root of the domain. After some work I came up with the following solution instead:
public class HTTPModuleAreaContent : IHttpModule
{
private List<string> allAreaNames = null;
private HashSet<string> folderNamesToRewrite = new HashSet<string>();
public HTTPModuleAreaContent()
{
}
public String ModuleName
{
get { return "HTTPModuleAreaContent"; }
}
public void Init(HttpApplication application)
{
application.BeginRequest +=
(new EventHandler(this.BeginRequest));
folderNamesToRewrite.Add("STYLES");
folderNamesToRewrite.Add("SCRIPT");
folderNamesToRewrite.Add("SCRIPTS");
folderNamesToRewrite.Add("JS");
folderNamesToRewrite.Add("JAVASCRIPT");
folderNamesToRewrite.Add("JAVASCRIPTS");
folderNamesToRewrite.Add("CONTENT");
folderNamesToRewrite.Add("IMAGES");
}
private void GetAreaNames(HttpContext context)
{
allAreaNames = AppDomain.CurrentDomain.GetAssemblies().SelectMany(ass => ass.GetTypes()).Where(t => t.IsSubclassOf(typeof(AreaRegistration))).Select(t => ((AreaRegistration)Activator.CreateInstance(t)).AreaName).ToList();
}
private void BeginRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext context = application.Context;
if (allAreaNames == null)
GetAreaNames(context);
string filePath = context.Request.FilePath;
string areaName = allAreaNames.FirstOrDefault(an => Regex.IsMatch(filePath, '/' + an + '/', RegexOptions.IgnoreCase | RegexOptions.CultureInvariant));
if (string.IsNullOrEmpty(areaName))
return;
string areaNameUpper = areaName.ToUpperInvariant();
Regex regex = new Regex('/' + areaNameUpper + "/([^/]+)/", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
Match m = regex.Match(filePath);
if (m.Success && m.Groups.Count > 1)
{
string folderName = m.Groups[1].Value;
string folderNameUpper = folderName.ToUpperInvariant();
if (folderNamesToRewrite.Contains(folderNameUpper))
context.RewritePath(regex.Replace(context.Request.Path, string.Format("/Areas/{0}/{1}/", areaName, folderName), 1));
}
}
public void Dispose() { }
Upvotes: 3