Let's say I have a controller that uses attribute based routing to handle a requested url of /admin/product like so:
[Route("admin/[controller]")]
public class ProductController: Controller {
// GET: /admin/product
[Route("")]
public IActionResult Index() {
return View();
}
}
Now let's say that I'd like to keep my views organized in a folder structure that roughly reflects the url paths they are related to. So I'd like the view for this controller to be located here:
/Views/Admin/Product.cshtml
To go further, if I had a controller like this:
[Route("admin/marketing/[controller]")]
public class PromoCodeListController: Controller {
// GET: /admin/marketing/promocodelist
[Route("")]
public IActionResult Index() {
return View();
}
}
I would like the framework to automatically look for it's view here:
Views/Admin/Marketing/PromoCodeList.cshtml
Ideally the approach for informing the framework of the view location would work in a general fashion based on the attribute based route information regardless of how many url segments are involved (ie. how deeply nested it is).
How can I instruct the the Core MVC framework (I'm currently using RC1) to look for the controller's view in such a location?
You can expand the locations where the view engine looks for views by implementing a view location expander. Here is some sample code to demonstrate the approach:
public class ViewLocationExpander: IViewLocationExpander {
/// <summary>
/// Used to specify the locations that the view engine should search to
/// locate views.
/// </summary>
/// <param name="context"></param>
/// <param name="viewLocations"></param>
/// <returns></returns>
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations) {
//{2} is area, {1} is controller,{0} is the action
string[] locations = new string[] { "/Views/{2}/{1}/{0}.cshtml"};
return locations.Union(viewLocations); //Add mvc default locations after ours
}
public void PopulateValues(ViewLocationExpanderContext context) {
context.Values["customviewlocation"] = nameof(ViewLocationExpander);
}
}
Then in the ConfigureServices(IServiceCollection services)
method in the startup.cs file add the following code to register it with the IoC container. Do this right after services.AddMvc();
services.Configure<RazorViewEngineOptions>(options => {
options.ViewLocationExpanders.Add(new ViewLocationExpander());
});
Now you have a way to add any custom directory structure you want to the list of places the view engine looks for views, and partial views. Just add it to the locations
string[]
. Also, you can place a _ViewImports.cshtml
file in the same directory or any parent directory and it will be found and merged with your views located in this new directory structure.
Update:
One nice thing about this approach is that it provides more flexibility then the approach later introduced in ASP.NET Core 2 (Thanks @BrianMacKay for documenting the new approach). So for example this ViewLocationExpander approach allows for not only specifying a hierarchy of paths to search for views and areas but also for layouts and view components. Also you have access to the full ActionContext
to determine what an appropriate route might be. This provides alot of flexibility and power. So for example if you wanted to determine the appropriate view location by evaluating the path of the current request, you can get access to the path of the current request via context.ActionContext.HttpContext.Request.Path
.