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

Posted

技术标签:

【中文标题】使用自定义位置时如何在 asp.net core mvc 中指定视图位置?【英文标题】:How to specify the view location in asp.net core mvc when using custom locations? 【发布时间】:2016-04-20 14:40:37 【问题描述】:

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

[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)在这样的位置寻找控制器的视图?

【问题讨论】:

【参考方案1】:

好消息...在 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

如果您需要对您的文件夹结构进行非常精细的控制,或者如果您因任何原因不允许/不想使用依赖项,请使用此选项。这也很容易,虽然可能比上面的 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")]

希望这能让一些人免于我刚刚经历的沮丧时光。 :)

【讨论】:

你是我的英雄。我来这个问题是因为我想实现基于功能的文件夹结构,而你已经做到了。太棒了! @JohnHargrove 谢谢你的客气话,你照亮了艰难的一天 :) 我喜欢关于 ReSharper 的额外提示!我对ViewPartialView 的所有呼叫都以红色突出显示。 老兄。这是一个好消息。 @IvanMontilla 如果你想共享一个视图,你必须把它放在共享下(在这个例子中是/Controllers/Shared/Views)。不幸的是,它将在所有控制器中可用,但这是我知道的唯一方法。除了使用区域之外,这将是疯狂的过度杀伤。【参考方案2】:

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

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

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

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

【讨论】:

这是一个很好的解决方案,但是这并不能解决在操作或控制器上查找具有路由属性的视图的问题。 View 方法似乎仍然使用控制器的名称而不是路由名称来定位视图。 @Xipooo,好点子。我提供的示例是一个好的开始,但要使用路由,您可以设置 locations 数组以包含 /Views + context.ActionContext.HttpContext.Request.Path + index.cshtml.cshtml 这允许我使用通过 .net 标准 1.6 添加到 bin 文件夹的视图。谢谢你很好的解决方案。 您能解释一下为什么在PopulateValues 方法中这样做context.Values["customviewlocation"] = nameof(ViewLocationExpander) 吗?这是必要的还是优化性能还是有其他目的? 我在最初处理这个问题时遇到了同样的问题,我花了很长时间才得到好的答案。请参阅此处:***.com/questions/36802661/… 简短的回答是,它提供了合并到用于位置值的缓存键中的附加数据。【参考方案3】:

在 .net core 中,您可以指定视图的完整路径。

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

【讨论】:

完全正确,但我一直在寻找一种解决方案,让框架自动在自定义位置查找视图,而无需以这种方式指定它。不过对于想要手动指定视图位置的人来说,这是一个很好的解决方案。 是的,我更喜欢你的回答而不是接受的回答。我正要使用它,直到我意识到它对于我需要的东西来说太过分了。我发布这个是因为我能够解决我的问题而无需添加任何自定义代码。也许有人会觉得这很方便。感谢您的回答!问候【参考方案4】:

我使用的是核心 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 只是视图名称的占位符。 漂亮又简单。

【讨论】:

【参考方案5】:

您将需要一个自定义的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.cs类的ConfigureServices方法中注册引擎:

public void ConfigureServices(IServiceCollection services)

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

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

第三,在 Configure 方法中为 MVC 路由添加区域路由:

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 类更改为使用AreaAttribute

[Area("admin")]
public class ProductController : Controller

    public IActionResult Index()
    
        return View();
    

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

【讨论】:

将 - 感谢您提供此解决方案。我可以确认它有效。经过额外的研究,我想出了另一种我认为通过注入 ExpandViewLocations 类更简单的方法。我会将您的解决方案作为答案,因为它确实有效,而且您的彻底回应值得称赞。 这个答案对当前的 ASP.Net Core 项目不再有效,因为 AreaViewLocationFormats 不再存在。我认为现在已将其移至RazorViewEngineOptions【参考方案6】:

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

【讨论】:

【参考方案7】:

虽然上面的回答可能是正确的,但我想添加一些更“基本”的东西:

-MVC .NET 中有(很多)隐式路由行为

-你也可以让一切都变得明确

那么,.NET MVC 是如何工作的?

默认

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

app.UseMvc(routes =>
        
            routes.MapRoute(
                name: "default",
                template: "controller=Special/action=Index");
        );

控制器路由

如果您使用 Index() 方法添加 SpecialController : Controller,您的 http://localhost:.../ 将在那里。 注意:NameController => 后期修复 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");
    

所以,我们有:

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

返回视图(“索引”); //显式视图名称,隐式路径和扩展 => 方法 = Special1Controller.Index(),视图 = /Views/Special/Index.cshtml

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

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

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

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

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

我的偏好是明确的。为什么?我喜欢显式映射和关注点分离。类名和方法名可以有一个命名约定,而不会干扰您的请求命名约定。 例如。假设我的类/方法是 camelCase,我的查询是小写的,那么这会很好地工作:http://..:../whatever/something 和 ControllerX.someThing(请记住,Windows 有点不区分大小写,Linux 是已知的!现代 .netcore Docker 组件可能会结束在Linux平台上!) 我也不喜欢 X000 行代码的“大单体”类。通过给它们显式相同的 http 查询路由,拆分您的控制器而不是您的查询可以完美地工作。 底线:了解它的工作原理,并明智地选择策略!

【讨论】:

【参考方案8】:

根据问题,我认为在路线中使用区域时,我认为值得一提。

我将此答案的大部分归功于@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

【讨论】:

以上是关于使用自定义位置时如何在 asp.net core mvc 中指定视图位置?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 asp.net core 2.1 中使用自定义消息设置状态代码?

如何在 asp.net core 中编写自定义 actionResult

如何使用 AuthorizationHandlerContext 在 ASP.NET Core 2 自定义基于策略的授权中访问当前的 HttpContext

如何在 ASP.NET Core 中实现自定义模型验证?

如何在 ASP.NET Core 控制器中返回自定义 HTTP 响应?

如何从 ASP.NET Core 2.0 中的自定义中间件请求身份验证