46

假设我有一个控制器,它使用基于属性的路由来处理请求的 /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)在这样的位置查找控制器的视图?

4

8 回答 8

91

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

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

于 2017-10-24T03:40:51.213 回答
50

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

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

然后在ConfigureServices(IServiceCollection services)startup.cs 文件的方法中添加以下代码以将其注册到 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.

于 2016-04-21T14:33:39.700 回答
30

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

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

于 2017-01-13T21:16:47.183 回答
8

我正在使用核心 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} 只是视图名称的占位符。很好很简单。

于 2019-12-06T19:09:58.503 回答
4

你需要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}.

ConfigureServices二、在类的方法中注册引擎Startup.cs

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课程以使用AreaAttribute

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

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

示例项目结构

于 2016-04-20T16:33:02.393 回答
1

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

于 2019-06-08T22:34:14.920 回答
1

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

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

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

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

默认

- 默认的“路由”是 protocol://server:port/ ,例如http://localhost:607888/ 如果你没有任何带有显式路由的控制器,并且没有定义任何启动默认值,那将不起作用。这会:

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

控制器路由

如果你添加一个Special Controller : Controller和一个 Index() 方法,你的http://localhost:.../就会在那里。注意: 名称控制器 => 修复后控制器被省略,隐式命名约定

如果您更愿意在控制器上显式定义路由,请使用以下命令:

[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://<>:<>/Special => Method = Index(), View = /Views/Special/Index.cshtml

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

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

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

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

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

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

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

于 2020-03-18T09:35:59.103 回答
0

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

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

就我而言,我有一个名称与区域名称匹配的控制器。我使用自定义约定将控制器的名称更改为“Home”,以便可以{area}/{controller=Home}/{action=Index}/{id?}MapControllerRoute.

为什么我遇到这个 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
于 2020-04-07T17:54:26.243 回答