自定义视图路径在ASP.NET Core MVC中的应用

ASP.NET Core MVC中,视图的默认存放位置是在Views文件夹下的一个子文件夹中,这个子文件夹的名称与控制器的名称相同,但是不包含"Controller"后缀。这种默认配置虽然简化了简单应用的开发,但在处理复杂的项目结构时就显得力不从心了。随着项目的增长,这种扁平化的结构会导致Views文件夹中出现大量的子文件夹,这不仅增加了管理的复杂性,还可能影响性能。

为了解决这个问题,ASP.NET Core MVC引入了ViewLocationExpander这一工具,它允许开发者指定自定义的视图搜索路径。通过这种方式,可以将视图放置在任何希望的位置,并且可以创建任意级别的层次结构或嵌套结构,从而为项目结构提供了更多的灵活性。

默认情况下,MVC期望视图存在于Views文件夹的子文件夹中。当调用"return View();"时,MVC会尝试通过搜索与控制器名称相同的子文件夹(去掉任何"Controller"后缀),并寻找一个与方法名称相同的文件,扩展名为".cshtml"。这种默认配置虽然简化了简单应用的开发,但在处理复杂的项目结构时就显得力不从心了。

问题

在默认配置下,如果需要为多个控制器中的一组视图应用不同的布局,就需要在每个视图中声明布局。如果布局位于相同的父文件夹下,MVC可以通过名称单独找到它们。但如果它们位于其他地方,就需要使用完整路径名,这意味着需要手动管理所有这些引用。

旧解决方案

传统上,有两种工具可以帮助解决这个问题。MVC支持Areas,它提供了第二级别的分组。可以将视图放在Areas下,从而形成一个两级结构。这在一定程度上解决了一些问题,但一直觉得Areas功能较弱,两级结构往往不够用。

另一种工具是自定义ViewEngines。这些工具帮助定义了多个搜索路径,但动态能力有限。

新工具

在MVC Core中,有了一个一流的工具来指定替代视图搜索路径——ViewLocationExpander。这个工具不仅将ViewPath工作从ViewEngine中提取出来,还可以高效地插入到页面生命周期中。这使可以在每个请求的基础上创建有针对性的搜索。有了这个功能,可以将视图放置在任何想要的多个目录中,而不必在每个请求中搜索它们。这为项目结构的构建提供了许多可能性——甚至可以完全消除Views文件夹和约定,而不必硬编码每个视图路径。

方法

本文的目标是简单地将Views文件夹移动到任何任意位置,同时仍然保持视图自动链接到控制器的约定方法。为此,将使用自定义属性在控制器上。

第一步是创建一个自定义属性。该属性简单地允许在控制器上放置一个名为ViewPath的单个字符串元值。这个简单的例子允许针对任何类,但可以针对更健壮的场景进行收紧。创建这个类在喜欢的任何地方。

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class ViewPathAttribute : Attribute { private string _viewPath; public ViewPathAttribute(string viewPath) { this._viewPath = viewPath; } public string ViewPath { get; set; } }

接下来,创建一个实现IViewLocationExpander接口的ViewLocationExpander。除了处理属性外,这里的示例展开器还做了一些额外的事情来展示可以用它做什么。在这个示例中,还重命名了Shared文件夹为_Shared,并添加了一个_Partials目录到搜索路径。可以在这里设置任何类型的结构或约定。

public class CustomViewLocationExpander : IViewLocationExpander { public void PopulateValues(ViewLocationExpanderContext context) { // 方法实现 } public virtual IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable viewLocations) { var descriptor = (context.ActionContext.ActionDescriptor as ControllerActionDescriptor); var viewPath = descriptor.ControllerTypeInfo.CustomAttributes?.FirstOrDefault(rc => rc.AttributeType == typeof(ViewPathAttribute))?.ConstructorArguments[0].Value.ToString(); var additionalLocations = new LinkedList(); if (viewPath != null) { additionalLocations.AddLast($"/{viewPath}/Views/{{1}}/{{0}}.cshtml"); additionalLocations.AddLast($"/{viewPath}/Views/{{0}}.cshtml"); additionalLocations.AddLast($"/{viewPath}/Views/_Shared/{{0}}.cshtml"); additionalLocations.AddLast($"/{viewPath}/Views/_Partials/{{0}}.cshtml"); } additionalLocations.AddLast("/Views/{1}/{0}.cshtml"); additionalLocations.AddLast("/Views/{0}.cshtml"); additionalLocations.AddLast("/Views/_Partials/{0}.cshtml"); return viewLocations.Concat(additionalLocations).Select(x => x.Replace("/Shared", "/_Shared")); } }

为了让MVC使用展开器,需要在启动时对其进行配置。为此,编辑Startup.cs文件的ConfigureServices方法。

public void ConfigureServices(IServiceCollection services) { services.Configure(options => { var expander = new CustomViewLocationExpander(); options.ViewLocationExpanders.Add(expander); }); }

在案例中,在控制器级别工作,所以需要为控制器分配视图路径。只需要用ViewPath属性装饰控制器和视图路径即可。路径基础是应用程序根目录。

[ViewPath("SiteHome")] public class HomeController : Controller { public IActionResult Index() { return View(); } }

测试

要测试这个示例,请创建一个新项目并按照上述步骤操作。在项目的根目录下创建一个名为"SiteHome"的目录,并将整个Views文件夹移动到该目录下。由于重命名了Shared,将"Shared"文件夹重命名为"_Shared"。运行应用程序,它将在新位置找到视图。

能够将视图放置在任何位置并创建任意级别的层次结构或嵌套结构,为项目结构提供了许多可能性。例如,在开发过程中,通常会保留一组临时的脚手架生成的控制器/视图集,用于测试和输入领域实体的测试数据。可以创建一个文件夹/Dev/Domains/Entities/Person/Views,并添加一个带有[ViewPath("Dev/Domains/Entities/Person")]的工作控制器(也在Dev分支中)。当完成它们时,可以简单地删除Dev文件夹,它们就会被清理掉,而不会受到影响。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485