使用自定义位置时如何在 asp.net core mvc 中指定视图位置?

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

假设我有一个控制器,它使用基于属性的路由来处理请求的 url of /admin/product,如下所示:

[Route("admin/[controller]")]        
public class ProductController: Controller {

    // GET: /admin/product
    [Route("")]
    public IActionResult Index() {

        return View();
    }
}

现在假设我想将我的视图组织在文件夹结构中,该文件夹结构大致反映了它们相关的 url 路径。所以我希望这个控制器的视图位于此处:

/Views/Admin/Product.cshtml

更进一步,如果我有这样的控制器:

[Route("admin/marketing/[controller]")]        
public class PromoCodeListController: Controller {

    // GET: /admin/marketing/promocodelist
    [Route("")]
    public IActionResult Index() {

        return View();
    }
}

我希望框架在此处自动查找它的视图:

Views/Admin/Marketing/PromoCodeList.cshtml

理想情况下,无论涉及多少 url 段(即嵌套有多深),通知视图位置框架的方法将基于基于属性的路由信息​​以一般方式工作.

如何指示 Core MVC 框架(我目前使用的是 RC1)在这样的位置查找控制器的视图?

您将需要一个自定义的 RazorviewEngine

一、引擎:

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);
}

这将创建一个额外的区域格式,匹配 {areaName}/{controller}/{view} 的用例。

其次,在Startup.csclass的ConfigureServices方法中注册引擎:

public void ConfigureServices(IServiceCollection services)
{
    // Add custom engine (must be BEFORE services.AddMvc() call)
    services.AddSingleton<IRazorViewEngine, CustomEngine>();

    // Add framework services.
    services.AddMvc();
}

第三,将区域路由添加到您的 MVC 路由中,在 Configure 方法中:

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?}");
});

最后,将 ProductController class 更改为使用 AreaAttribute:

[Area("admin")]
public class ProductController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

现在,您的应用程序结构可能如下所示:

您可以通过实施视图位置扩展器来扩展视图引擎查找视图的位置。下面是一些示例代码来演示该方法:

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);
    }
}

然后在startup.cs文件中的ConfigureServices(IServiceCollection services)方法中添加如下代码,将其注册到IoC容器中。在 services.AddMvc();

之后立即执行此操作
services.Configure<RazorViewEngineOptions>(options => {
        options.ViewLocationExpanders.Add(new ViewLocationExpander());
    });

现在您可以将您想要的任何自定义目录结构添加到视图引擎查找视图和部分视图的位置列表中。只需将其添加到 locations string[]。此外,您可以将 _ViewImports.cshtml 文件放在同一目录或任何父目录中,它将被发现并与位于这个新目录结构中的视图合并。

更新:
这种方法的一个好处是它提供了比后来在 ASP.NET Core 2 中引入的方法更多的灵活性(感谢@BrianMacKay 记录了新方法)。因此,例如,这种 ViewLocationExpander 方法不仅允许指定路径层次结构来搜索视图和区域,还允许指定布局和视图组件。您还可以访问完整的 ActionContext 以确定合适的路线。这提供了很多灵活性和功能。因此,例如,如果您想通过评估当前请求的路径来确定合适的视图位置,您可以通过 context.ActionContext.HttpContext.Request.Path 访问当前请求的路径。

在 .net 核心中,您可以指定视图的整个路径。

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

好消息...在 ASP.NET Core 2 及更高版本中,您不再需要自定义 ViewEngine 甚至 ExpandViewLocations。

使用 OdeToCode.AddFeatureFolders 包

这是最简单的方法...K. Scott Allen 在 OdeToCode.AddFeatureFolders 为您提供了一个 nuget 包,它很干净并且包括对区域的可选支持。 Github: https://github.com/OdeToCode/AddFeatureFolders

安装包,就这么简单:

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

        ...
    }

    ...
}  

DIY

如果您需要对文件夹结构进行极其精细的控制,或者如果您不想allowed/don出于任何原因不想依赖它,请使用它。这也很简单,尽管可能比上面的 nuget 包更混乱:

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);
        });

        ...         
    }

...
}

就是这样!不需要特殊的 类。

处理Resharper/Rider

额外提示:如果您使用的是 ReSharper,您可能会注意到在某些地方 ReSharper 无法找到您的视图并向您发出烦人的警告。要解决此问题,请拉入 Resharper.Annotations 包并在您的 startup.cs (或其他任何地方)为每个视图位置添加以下属性之一:

[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")]

希望这能让一些人免于我刚刚经历的挫败感。 :)

所以在挖掘之后,我想我在不同的 Whosebug 上发现了这个问题。 我遇到了同样的问题,从非区域部分复制到 ViewImports 文件后,链接开始按预期运行。
如此处所示:Asp.Net core 2.0 MVC anchor tag helper not working
另一个解决方案是在视图级别复制:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

我使用的是核心 3.1,只是在 Startup.cs 的 ConfigureServices 方法中执行此操作。

 services.AddControllersWithViews().AddRazorOptions(
     options => {// Add custom location to view search location
         options.ViewLocationFormats.Add("/Views/Shared/YourLocation/{0}.cshtml");                    
     });

{0} 只是视图名称的占位符。 漂亮又简单。

虽然其他答案可能是正确的,但我想添加一些更“基本”的内容:

  • MVC .NET 中有(很多)隐式路由行为
  • 您也可以将所有内容都明确化

那么,这对 .NET MVC 有何作用?

默认

  • 默认的“路由”是 protocol://server:port/ ,例如http://localhost:607888/ 如果您没有任何带有显式路由的控制器,并且没有定义任何启动默认值,那将无法工作。 这将:

    app.UseMvc(路线 => { routes.MapRoute( 名称:“默认”, 模板:“{controller=Special}/{action=Index}”); });

控制器路由

并且如果您使用 Index() 方法添加 class SpecialController : Controller,您的 http:/ /localhost:.../ 将在那里。 注意:NameController => post 修复 Controller 被遗漏,隐式命名约定

如果您更愿意在控制器上明确定义路线,请使用:

[Route("Special")]//explicit route
public class SpecialController : Controller
{ ....

=> http://localhost:<port>/Special will end up on this controller

为了将 http 请求映射到控制器方法,您还可以添加显式 [Route(...)] 方法信息:

// GET: explicit route page
[HttpGet("MySpecialIndex")]
public ActionResult Index(){...}

=> http://localhost:<port>/Special/MySpecialIndex will end up on SpecialController.Index()

查看路线

现在假设您的 Views 文件夹是这样的:

Views\
   Special1\
          Index1.cshtml
   Special\
          Index.cshtml

控制器如何“找到”通往视图的方式? 这里的例子是

[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");
    }

所以,我们有:

return View(); => 一切都是隐含的,将方法名称作为视图,将控制器路径作为视图路径等。 http://<>:<>/Special => Method = Index(), View = /Views/Special/Index.cshtml

return View("Index"); //显式视图名称、隐式路径和扩展 => 方法 = Special1Controller.Index(),视图 = /Views/Special/Index.cshtml

return View("Views/Special1/Index1.cshtml"); // 方法隐式,视图显式 => http://<>:<>/Special, Method = Special1Controller.Index(), View = /Views/Special1/Index1.cshtml

如果将显式映射组合到方法和视图中: => http://<>:<>/Special/MySpecialIndex, 方法 = Special1Controller.Index(), 视图 = /Views/Special1/Index1.cshtml

那么最后,你为什么要隐含一切? 优点是管理较少,容易出错,并且您在文件夹的命名和设置中强制执行一些干净的管理 缺点是很多魔法正在发生,每个人都需要理解。

那你为什么要把一切都说清楚? 优点:这对“每个人”来说更具可读性。无需了解所有隐含规则。更灵活地显式更改路线和地图。 controller 和 route 路径冲突的机会也少了一些。

最后:当然你可以混合显式和隐式路由。

我的偏好是一切都明确。为什么?我喜欢显式映射和关注点分离。 class 名称和方法名称可以有命名约定,而不会干扰您的请求命名约定。 例如。假设我的 classes/methods 是驼峰式的,我的查询是小写的,那么它会很好地工作:http://..:../whatever/something and ControllerX.someThing(请记住,Windows 是一种不区分大小写的,Linux 绝不是!现代 .netcore Docker 组件最终可能会出现在 Linux 平台上!) 我也不喜欢 X000 行代码的“大单体”classes。通过为它们提供明确的相同 http 查询路由,拆分您的控制器而不是您的查询可以完美地工作。 底线:了解其运作方式,明智地选择策略!

根据问题,我认为值得一提的是在路线中使用区域时如何做到这一点。

我把这个答案的大部分归功于@Mike 的回答。

就我而言,我有一个名称与区域名称匹配的控制器。我使用自定义约定将控制器的名称更改为 "Home",以便我可以在 MapControllerRoute 中创建默认路由 {area}/{controller=Home}/{action=Index}/{id?}

为什么我提出这个 SO 问题是因为现在 Razor 没有搜索我原来控制器的名称视图文件夹,因此找不到我的视图。

我只需要将这段代码添加到 ConfigureServices(这里的区别在于 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