在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"。运行应用程序,它将在新位置找到视图。
能够将视图放置在任何位置并创建任意级别的层次结构或嵌套结构,为项目结构提供了许多可能性。例如,在开发过程中,通常会保留一组临时的脚手架生成的控制器/视图集,用于测试和输入领域实体的测试数据。可以创建一个文件夹