Kjell Rilbe
Kjell Rilbe

Reputation: 1509

Route MVC area content the same way as controllers and actions?

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

Answers (1)

Kjell Rilbe
Kjell Rilbe

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

Related Questions