Let\'s say I have a controller that uses attribute based routing to handle a requested url of /admin/product like so:
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
.
Though anwers above may be correct, I'd like to add something that is a little bit more "basic":
-There is (a lot of) implicit routing behaviour in MVC .NET
-You can make everything explicit also
So, how does that work for .NET MVC?
Default
-The default "route" is protocol://server:port/ , e.g. http://localhost:607888/ If you dont have any controller with a explicit route, and dont define any startup defaults, that wont work. This will:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Special}/{action=Index}");
});
Controller Routes
And if you add a class SpecialController : Controller with a Index() method, your http://localhost:.../ will and up there. Note: NameController => post fix Controller is left out, implicit naming convention
If you rather define your routes explicit on the controllers, use this:
[Route("Special")]//explicit route
public class SpecialController : Controller
{ ....
=> http://localhost:<port>/Special will end up on this controller
For mapping http requests to controller methods, you can also add explicit [Route(...)] information to your Methods:
// GET: explicit route page
[HttpGet("MySpecialIndex")]
public ActionResult Index(){...}
=> http://localhost:<port>/Special/MySpecialIndex will end up on SpecialController.Index()
View routes
Now suppose your Views folder is like this:
Views\
Special1\
Index1.cshtml
Special\
Index.cshtml
How does the Controller "finds" its way to the views? The example here is
[Route("Special")]//explicit route
public class Special1Controller : Controller
{
// GET: Default route page
[HttpGet]
public ActionResult Index()
{
//
// Implicit path, implicit view name: Special1<Controller> -> View = Views/Special/Index.cshtml
//
//return View();
//
// Implicit path, explicit view name, implicit extention
// Special <Controller> -> View = Views/Special/Index.cshtml
//
//return View("Index");
//
// Everything explcit
//
return View("Views/Special1/Index1.cshtml");
}
So, we have:
return View(); => everything implicit, take Method name as view, controller path as view path etc. http://<>:<>/Special => Method = Index(), View = /Views/Special/Index.cshtml
return View("Index"); //Explicit view name, implicit paths and extention => Method = Special1Controller.Index(), View = /Views/Special/Index.cshtml
return View("Views/Special1/Index1.cshtml"); // method implicit, view explicit => http://<>:<>/Special, Method = Special1Controller.Index(), View = /Views/Special1/Index1.cshtml
And if you combine explicit mapping to methods and views: => http://<>:<>/Special/MySpecialIndex, Method = Special1Controller.Index(), View = /Views/Special1/Index1.cshtml
Then finally, why would you make everything implicit? The pros are less administration that is error prone, and you force some clean administration in your naming and setup of folders The con is a lot of magic is going on, that everybody needs to understand.
Then why would you make everything explicit? Pros: This is more readable for "everyone". No need to know all implicit rules. And more flexibility for changing routes and maps explcitly. The chance on conflicts between controllers and route paths is also a little less.
Finally: of course you can mix explicit and implicit routing.
My preference would be everything explicit. Why? I like explicit mappings and separation of concerns. class names and method names can have a naming convention, without interference with your request naming conventions. E.g. suppose my classes/methods are camelCase, my queries lowercase, then that would work nicely: http://..:../whatever/something and ControllerX.someThing (Keep in mind, Windows is kindof case insensitive, Linux by know means is! And modern .netcore Docker components may end up on a Linux platform!) I also dont like "big monolitic" classes with X000 lines of code. Splitting your controllers but not your queries works perfectly, by giving them explicit the same http query routes. Bottom line: know how it works, and choose a strategy whisely!
According to the question, I think it is worth mentioning how to do so when you use areas in your routes.
I credit most of this answer to @Mike's answer.
In my case, I have a controller with a name that matches the area name. I use a custom convention to alter the controller's name to "Home" so that I can create a default route {area}/{controller=Home}/{action=Index}/{id?}
in MapControllerRoute
.
Why I landed on this SO question was because now Razor wasn't searching my original controller's name view folders, therefore not finding my view.
I simply had to add this bit of code to ConfigureServices
(the difference here is the use of AreaViewLocationFormats
):
services.AddMvc().AddRazorOptions(options =>
options.AreaViewLocationFormats.Add("/Areas/{2}/Views/{2}/{0}" + RazorViewEngine.ViewExtension));
// as already noted, {0} = action name, {1} = controller name, {2} = area name
You're going to need a custom RazorviewEngine
for this one.
First, the engine:
public class CustomEngine : RazorViewEngine
{
private readonly string[] _customAreaFormats = new string[]
{
"/Views/{2}/{1}/{0}.cshtml"
};
public CustomEngine(
IRazorPageFactory pageFactory,
IRazorViewFactory viewFactory,
IOptions<RazorViewEngineOptions> optionsAccessor,
IViewLocationCache viewLocationCache)
: base(pageFactory, viewFactory, optionsAccessor, viewLocationCache)
{
}
public override IEnumerable<string> AreaViewLocationFormats =>
_customAreaFormats.Concat(base.AreaViewLocationFormats);
}
This will create an additional area format, which matches the use case of {areaName}/{controller}/{view}
.
Second, register the engine in the ConfigureServices
method of the Startup.cs
class:
public void ConfigureServices(IServiceCollection services)
{
// Add custom engine (must be BEFORE services.AddMvc() call)
services.AddSingleton<IRazorViewEngine, CustomEngine>();
// Add framework services.
services.AddMvc();
}
Thirdly, add area routing to your MVC routes, in the Configure
method:
app.UseMvc(routes =>
{
// add area routes
routes.MapRoute(name: "areaRoute",
template: "{area:exists}/{controller}/{action}",
defaults: new { controller = "Home", action = "Index" });
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Lastly, change your ProductController
class to use the AreaAttribute
:
[Area("admin")]
public class ProductController : Controller
{
public IActionResult Index()
{
return View();
}
}
Now, your application structure can look like this:
In .net core you can specify the whole path to the view.
return View("~/Views/booking/checkout.cshtml", checkoutRequest);
So after digging, I think I found the issue on a different stackoverflow.
I had the same issue, and upon copying in the ViewImports file from the non area section, the links started to function as anticipated.
As seen here: Asp.Net core 2.0 MVC anchor tag helper not working
The other solution was to copy at the view level:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers