How to specify the view location in asp.net core mvc when using custom locations?

老子叫甜甜 提交于 2019-11-27 03:27:57

Great news... In ASP.NET Core 2.*, you don't need a custom ViewEngine or even ExpandViewLocations anymore.

Using the OdeToCode.AddFeatureFolders Package

This is the easiest way... K. Scott Allen has a nuget package for you at OdeToCode.AddFeatureFolders that is clean and includes optional support for areas. Github: https://github.com/OdeToCode/AddFeatureFolders

Install the package, and it's as simple as:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc()
                .AddFeatureFolders();

        ...
    }

    ...
}  

DIY

Use this if you need extremely fine control over your folder structure, or if you aren't allowed/don't want to take the dependency for whatever reason. This is also quite easy, although perhaps more cluttery than the nuget package above:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
         ...

         services.Configure<RazorViewEngineOptions>(o =>
         {
             // {2} is area, {1} is controller,{0} is the action    
             o.ViewLocationFormats.Clear(); 
             o.ViewLocationFormats.Add("/Controllers/{1}/Views/{0}" + RazorViewEngine.ViewExtension);
             o.ViewLocationFormats.Add("/Controllers/Shared/Views/{0}" + RazorViewEngine.ViewExtension);

             // Untested. You could remove this if you don't care about areas.
             o.AreaViewLocationFormats.Clear();
             o.AreaViewLocationFormats.Add("/Areas/{2}/Controllers/{1}/Views/{0}" + RazorViewEngine.ViewExtension);
             o.AreaViewLocationFormats.Add("/Areas/{2}/Controllers/Shared/Views/{0}" + RazorViewEngine.ViewExtension);
             o.AreaViewLocationFormats.Add("/Areas/Shared/Views/{0}" + RazorViewEngine.ViewExtension);
        });

        ...         
    }

...
}

And that's it! No special classes required.

Dealing with Resharper/Rider

Bonus tip: if you're using ReSharper, you might notice that in some places ReSharper can't find your views and gives you annoying warnings. To work around that, pull in the Resharper.Annotations package and in your startup.cs (or anywhere else really) add one of these attributes for each of your view locations:

[assembly: AspMvcViewLocationFormat("/Controllers/{1}/Views/{0}.cshtml")]
[assembly: AspMvcViewLocationFormat("/Controllers/Shared/Views/{0}.cshtml")]

[assembly: AspMvcViewLocationFormat("/Areas/{2}/Controllers/{1}/Views/{0}.cshtml")]
[assembly: AspMvcViewLocationFormat("/Controllers/Shared/Views/{0}.cshtml")]

Hopefully this spares some folks the hours of frustration I just lived through. :)

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.

In .net core you can specify the whole path to the view.

return View("~/Views/booking/checkout.cshtml", checkoutRequest);

Will Ray

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:

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!