NET Core 2.0 自定义
ASP.NET Core 2.0 自定义 _ViewStart 和 _ViewImports 的目录位置
在 ASP.NET Core 里扩展 Razor 查找视图目录不是什么新鲜和困难的事情,但 _ViewStart
和 _ViewImports
这2个视图比较特殊,如果想让 Razor 在我们指定的目录中查找它们,则需要耗费一点额外的精力。本文将提供一种方法做到这一点。注意,文本仅适用于 ASP.NET Core 2.0+, 因为 Razor 在 2.0 版本里的内部实现有较大重构,因此这里提供的方法并不适用于 ASP.NET Core 1.x
为了全面描述 ASP.NET Core 2.0 中扩展 Razor 查找视图目录的能力,我们还是由浅入深,从最简单的扩展方式着手吧。
准备工作
首先,我们可以创建一个新的 ASP.NET Core 项目用于演示。
mkdir CustomizedViewLocation
cd CustomizedViewLocation
dotnet new web # 创建一个空的 ASP.NET Core 应用
接下来稍微调整下 Startup.cs 文件的内容,引入 MVC:
// Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace CustomizedViewLocation
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMvcWithDefaultRoute();
}
}
}
好了我们的演示项目已经搭好了架子。
我们的目标
在我们的示例项目中,我们希望我们的目录组织方式是按照功能模块组织的,即同一个功能模块的所有 Controller 和 View 都放在同一个目录下。对于多个功能模块共享、通用的内容,比如 _Layout
, _Footer
, _ViewStart
和 _ViewImports
则单独放在根目录下的一个叫 Shared 的子目录中。
最简单的方式: ViewLocationFormats
假设我们现在有2个功能模块 Home 和 About,分别需要 HomeController
和它的 Index
view,以及 AboutMeController
和它的 Index
view. 因为一个 Controller 可能会包含多个 view,因此我选择为每一个功能模块目录下再增加一个 Views
目录,集中这个功能模块下的所有 View. 整个目录结构看起来是这样的:
从目录结构中我们可以发现我们的视图目录为 /{controller}/Views/{viewName}.cshtml
, 比如 HomeController
的 Index
视图所在的位置就是 /Home/Views/Index.cshtml
,这跟 MVC 默认的视图位置 /Views/{Controller}/{viewName}.cshtml
很相似(/Views/Home/Index.cshtml
),共同的特点是路径中的 Controller 部分和 View 部分是动态的,其它的都是固定不变的。其实 MVC 默认的寻找视图位置的方式一点都不高端,类似于这样:
string controllerName = "Home"; // “我”知道当前 Controller 是 Home
string viewName = "Index"; // "我“知道当前需要解析的 View 的名字
// 把 viewName 和 controllerName 带入一个代表视图路径的格式化字符串得到最终的视图路径。
string viewPath = string.Format("/Views/{1}/{0}.cshtml", viewName, controllerName);
// 根据 viewPath 找到视图文件做后续处理
如果我们可以构建另一个格式字符串,其中 {0}
代表 View 名称, {1}
代表 Controller 名称,然后替换掉默认的 /Views/{1}/{0}.cshtml
,那我们就可以让 Razor 到我们设定的路径去检索视图。而要做到这点非常容易,利用 ViewLocationFormats
,代码如下:
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
IMvcBuilder mvcBuilder = services.AddMvc();
mvcBuilder.AddRazorOptions(options => options.ViewLocationFormats.Add("/{1}/Views/{0}.cshtml"));
}
收工,就这么简单。顺便说一句,还有一个参数 {2}
,代表 Area 名称。
这种做法是不是已经很完美了呢?No, No, No. 谁能看出来这种做法有什么缺点?
这种做法有2个缺点。
- 所有的功能模块目录必须在根目录下创建,无法建立层级目录关系。且看下面的目录结构截图:
注意 Reports 目录,因为我们有种类繁多的报表,因此我们希望可以把各种报表分门别类放入各自的目录。但是这么做之后,我们之前设置的 ViewLocationFormats
就无效了。例如我们访问 URL /EmployeeReport/Index
, Razor 会试图寻找 /EmployeeReport/Views/Index.cshtml
,但其真正的位置是 /Reports/AdHocReports/EmployeeReport/Views/Index.cshtml
。前面还有好几层目录呢~
- 因为所有的 View 文件不再位于同一个父级目录之下,因此
_ViewStart.cshtml
和_ViewImports.cshtml
的作用将受到极大限制。原因后面细表。
下面我们来分别解决这2个问题。
最灵活的方式: IViewLocationExpander
有时候,我们的视图目录除了 controller 名称 和 view 名称2个变量外,还涉及到别的动态部分,比如上面的 Reports 相关 Controller,视图路径有更深的目录结构,而 controller 名称仅代表末级的目录。此时,我们需要一种更灵活的方式来处理: IViewLocationExpander
,通过实现 IViewLocationExpander
,我们可以得到一个 ViewLocationExpanderContext
,然后据此更灵活地创建 view location formats。
对于我们要解决的目录层次问题,我们首先需要观察,然后会发现目录层次结构和 Controller 类型的命名空间是有对应关系的。例如如下定义:
using Microsoft.AspNetCore.Mvc;
namespace CustomizedViewLocation.Reports.AdHocReports.EmployeeReport
{
public class EmployeeReportController : Controller
{
public IActionResult Index() => View();
}
}
观察 EmployeeReportController
的命名空间 CustomizedViewLocation.Reports.AdHocReports.EmployeeReport
以及 Index 视图对应的目录 /Reports/AdHocReports/EmployeeReport/Views/Index.cshtml
可以发现如下对应关系:
命名空间 | 视图路径 | ViewLocationFormat |
---|---|---|
CustomizedViewLocation | 项目根路径 | / |
Reports.AdHocReports | Reports/AdHocReports | 把整个命名空间以“.”为分割点掐头去尾,然后把“.”替换为“/” |
EmployeeReport | EmployeeReport | Controller 名称 |
Views | 固定目录 | |
Index.cshtml | 视图名称.cshtml |
所以我们 IViewLocationExpander
的实现类型主要是获取和处理 Controller 的命名空间。且看下面的代码。
// NamespaceViewLocationExpander.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
namespace CustomizedViewLocation
{
public class NamespaceViewLocationExpander : IViewLocationExpander
{
private const string VIEWS_FOLDER_NAME = "Views";
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
ControllerActionDescriptor cad = context.ActionContext.ActionDescriptor as ControllerActionDescriptor;
string controllerNamespace = cad.ControllerTypeInfo.Namespace;
int firstDotIndex = controllerNamespace.IndexOf('.');
int lastDotIndex = controllerNamespace.LastIndexOf('.');
if (firstDotIndex < 0)
return viewLocations;
string viewLocation;
if (firstDotIndex == lastDotIndex)
{
// controller folder is the first level sub folder of root folder
viewLocation = "/{1}/Views/{0}.cshtml";
}
else
{
string viewPath = controllerNamespace.Substring(firstDotIndex + 1, lastDotIndex - firstDotIndex - 1).Replace(".", "/");
viewLocation = $"/{viewPath}/{{1}}/Views/{{0}}.cshtml";
}
if (viewLocations.Any(l => l.Equals(viewLocation, StringComparison.InvariantCultureIgnoreCase)))
return viewLocations;
if (viewLocations is List<string> locations)
{
locations.Add(viewLocation);
return locations;
}
// it turns out the viewLocations from ASP.NET Core is List<string>, so the code path should not go here.
List<string> newViewLocations = viewLocations.ToList();
newViewLocations.Add(viewLocation);
return newViewLocations;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
}
}
}
上面对命名空间的处理略显繁琐。其实你可以不用管,重点是我们可以得到 ViewLocationExpanderContext
,并据此构建新的 view location format 然后与现有的 viewLocations
合并并返回给 ASP.NET Core。
细心的同学可能还注意到一个空的方法 PopulateValues
,这玩意儿有什么用?具体作用可以参照这个 StackOverflow 的问题,基本上来说,一旦某个 Controller 及其某个 View 找到视图位置之后,这个对应关系就会缓存下来,以后就不会再调用 ExpandViewLocations
方法了。但是,如果你有这种情况,就是同一个 Controller, 同一个视图名称但是还应该依据某些特别条件去找不同的视图位置,那么就可以利用 PopulateValues
方法填充一些特定的 Value, 这些 Value 会参与到缓存键的创建, 从而控制到视图位置缓存的创建。
下一步,把我们的 NamespaceViewLocationExpander
注册一下:
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
IMvcBuilder mvcBuilder = services.AddMvc();
mvcBuilder.AddRazorOptions(options =>
{
// options.ViewLocationFormats.Add("/{1}/Views/{0}.cshtml"); we don't need this any more if we make use of NamespaceViewLocationExpander
options.ViewLocationExpanders.Add(new NamespaceViewLocationExpander());
});
}
另外,有了 NamespaceViewLocationExpander
, 我们就不需要前面对 ViewLocationFormats
的追加了,因为那种情况作为一种特例已经在 NamespaceViewLocationExpander
中处理了。
至此,目录分层的问题解决了。
_ViewStart.cshtml 和 _ViewImports 的起效机制与调整
对这2个特别的视图,我们并不陌生,通常在 _ViewStart.cshtml 里面设置 Layout 视图,然后每个视图就自动地启用了那个 Layout 视图,在 _ViewImports.cshtml 里引入的命名空间和 TagHelper 也会自动包含在所有视图里。它们为什么会起作用呢?
_ViewImports 的秘密藏在 RazorTemplateEngine 类 和 MvcRazorTemplateEngine 类中。
MvcRazorTemplateEngine 类指明了 "_ViewImports.cshtml" 作为默认的名字。
// MvcRazorTemplateEngine.cs 部分代码
// 完整代码: https://github.com/aspnet/Razor/blob/rel/2.0.0/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/MvcRazorTemplateEngine.cs
public class MvcRazorTemplateEngine : RazorTemplateEngine
{
public MvcRazorTemplateEngine(RazorEngine engine, RazorProject project)
: base(engine, project)
{
Options.ImportsFileName = "_ViewImports.cshtml";
Options.DefaultImports = GetDefaultImports();
}
}
RazorTemplateEngine 类则表明了 Razor 是如何去寻找 _ViewImports.cshtml 文件的。
// RazorTemplateEngine.cs 部分代码
// 完整代码:https://github.com/aspnet/Razor/blob/rel/2.0.0/src/Microsoft.AspNetCore.Razor.Language/RazorTemplateEngine.cs
public class RazorTemplateEngine
{
public virtual IEnumerable<RazorProjectItem> GetImportItems(RazorProjectItem projectItem)
{
var importsFileName = Options.ImportsFileName;
if (!string.IsNullOrEmpty(importsFileName))
{
return Project.FindHierarchicalItems(projectItem.FilePath, importsFileName);
}
return Enumerable.Empty<RazorProjectItem>();
}
}
FindHierarchicalItems
方法会返回一个路径集合,其中包括从视图当前目录一路到根目录的每一级目录下的 _ViewImports.cshtml 路径。换句话说,如果从根目录开始,到视图所在目录的每一层目录都有 _ViewImports.cshtml 文件的话,那么它们都会起作用。这也是为什么通常我们在 根目录下的 Views 目录里放一个 _ViewImports.cshtml 文件就会被所有视图文件所引用,因为 Views 目录是是所有视图文件的父/祖父目录。那么如果我们的 _ViewImports.cshtml 文件不在视图的目录层次结构中呢?
在这个 DI 为王的 ASP.NET Core 世界里,RazorTemplateEngine 也被注册为 DI 里的服务,因此我目前的做法继承 MvcRazorTemplateEngine
类,微调 GetImportItems
方法的逻辑,加入我们的特定路径,然后注册到 DI 取代原来的实现类型。代码如下:
// ModuleRazorTemplateEngine.cs
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language;
namespace CustomizedViewLocation
{
public class ModuleRazorTemplateEngine : MvcRazorTemplateEngine
{
public ModuleRazorTemplateEngine(RazorEngine engine, RazorProject project) : base(engine, project)
{
}
public override IEnumerable<RazorProjectItem> GetImportItems(RazorProjectItem projectItem)
{
IEnumerable<RazorProjectItem> importItems = base.GetImportItems(projectItem);
return importItems.Append(Project.GetItem($"/Shared/Views/{Options.ImportsFileName}"));
}
}
}
然后在 Startup 类里把它注册到 DI 取代默认的实现类型。
// Startup.cs
// using Microsoft.AspNetCore.Razor.Language;
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<RazorTemplateEngine, ModuleRazorTemplateEngine>();
IMvcBuilder mvcBuilder = services.AddMvc();
// 其它代码省略
}
下面是 _ViewStart.cshtml 的问题了。不幸的是,Razor 对 _ViewStart.cshtml 的处理并没有那么“灵活”,看代码就知道了。
// RazorViewEngine.cs 部分代码
// 完整代码:https://github.com/aspnet/Mvc/blob/rel/2.0.0/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs
public class RazorViewEngine : IRazorViewEngine
{
private const string ViewStartFileName = "_ViewStart.cshtml";
internal ViewLocationCacheResult CreateCacheResult(
HashSet<IChangeToken> expirationTokens,
string relativePath,
bool isMainPage)
{
var factoryResult = _pageFactory.CreateFactory(relativePath);
var viewDescriptor = factoryResult.ViewDescriptor;
if (viewDescriptor?.ExpirationTokens != null)
{
for (var i = 0; i < viewDescriptor.ExpirationTokens.Count; i++)
{
expirationTokens.Add(viewDescriptor.ExpirationTokens[i]);
}
}
if (factoryResult.Success)
{
// Only need to lookup _ViewStarts for the main page.
var viewStartPages = isMainPage ?
GetViewStartPages(viewDescriptor.RelativePath, expirationTokens) :
Array.Empty<ViewLocationCacheItem>();
if (viewDescriptor.IsPrecompiled)
{
_logger.PrecompiledViewFound(relativePath);
}
return new ViewLocationCacheResult(
new ViewLocationCacheItem(factoryResult.RazorPageFactory, relativePath),
viewStartPages);
}
return null;
}
private IReadOnlyList<ViewLocationCacheItem> GetViewStartPages(
string path,
HashSet<IChangeToken> expirationTokens)
{
var viewStartPages = new List<ViewLocationCacheItem>();
foreach (var viewStartProjectItem in _razorProject.FindHierarchicalItems(path, ViewStartFileName))
{
var result = _pageFactory.CreateFactory(viewStartProjectItem.FilePath);
var viewDescriptor = result.ViewDescriptor;
if (viewDescriptor?.ExpirationTokens != null)
{
for (var i = 0; i < viewDescriptor.ExpirationTokens.Count; i++)
{
expirationTokens.Add(viewDescriptor.ExpirationTokens[i]);
}
}
if (result.Success)
{
// Populate the viewStartPages list so that _ViewStarts appear in the order the need to be
// executed (closest last, furthest first). This is the reverse order in which
// ViewHierarchyUtility.GetViewStartLocations returns _ViewStarts.
viewStartPages.Insert(0, new ViewLocationCacheItem(result.RazorPageFactory, viewStartProjectItem.FilePath));
}
}
return viewStartPages;
}
}
上面的代码里 GetViewStartPages
方法是个 private
,没有什么机会让我们加入自己的逻辑。看了又看,好像只能从 _razorProject.FindHierarchicalItems(path, ViewStartFileName)
这里着手。这个方法同样在处理 _ViewImports.cshtml时用到过,因此和 _ViewImports.cshtml 一样,从根目录到视图当前目录之间的每一层目录的 _ViewStarts.cshtml 都会被引入。如果我们可以调整一下 FindHierarchicalItems
方法,除了完成它原本的逻辑之外,再加入我们对我们 /Shared/Views
目录的引用就好了。而 FindHierarchicalItems
这个方法是在 Microsoft.AspNetCore.Razor.Language.RazorProject 类型里定义的,而且是个 virtual
方法,而且它是注册在 DI 里的,不过在 DI 中的实现类型是 Microsoft.AspNetCore.Mvc.Razor.Internal.FileProviderRazorProject。我们所要做的就是创建一个继承自 FileProviderRazorProject
的类型,然后调整 FindHierarchicalItems
方法。
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Razor.Language;
namespace CustomizedViewLocation
{
public class ModuleBasedRazorProject : FileProviderRazorProject
{
public ModuleBasedRazorProject(IRazorViewEngineFileProviderAccessor accessor)
: base(accessor)
{
}
public override IEnumerable<RazorProjectItem> FindHierarchicalItems(string basePath, string path, string fileName)
{
IEnumerable<RazorProjectItem> items = base.FindHierarchicalItems(basePath, path, fileName);
// the items are in the order of closest first, furthest last, therefore we append our item to be the last item.
return items.Append(GetItem("/Shared/Views/" + fileName));
}
}
}
完成之后再注册到 DI。
// Startup.cs
// using Microsoft.AspNetCore.Razor.Language;
public void ConfigureServices(IServiceCollection services)
{
// services.AddSingleton<RazorTemplateEngine, ModuleRazorTemplateEngine>(); // we don't need this any more if we make use of ModuleBasedRazorProject
services.AddSingleton<RazorProject, ModuleBasedRazorProject>();
IMvcBuilder mvcBuilder = services.AddMvc();
// 其它代码省略
}
有了 ModuleBasedRazorProject
我们甚至可以去掉之前我们写的 ModuleRazorTemplateEngine
类型了,因为 Razor 采用相同的逻辑 —— 使用 RazorProject
的 FindHierarchicalItems
方法 —— 来构建应用 _ViewImports.cshtml 和 _ViewStart.cshtml 的目录层次结构。所以最终,我们只需要一个类型来解决问题 —— ModuleBasedRazorProject
。
回顾这整个思考和尝试的过程,很有意思,最终解决方案是自定义一个 RazorProject
。是啊,毕竟我们的需求只是一个不同目录结构的 Razor Project,所以去实现一个我们自己的 RazorProject
类型真是再自然不过的了。
文本中的示例代码在这里
NET Core 2.0 自定义的更多相关文章
- ASP.NET Core 2.0 自定义 _ViewStart 和 _ViewImports 的目录位置
在 ASP.NET Core 里扩展 Razor 查找视图目录不是什么新鲜和困难的事情,但 _ViewStart 和 _ViewImports 这2个视图比较特殊,如果想让 Razor 在我们指定的目 ...
- EntityFramework Core 2.0自定义标量函数两种方式
前言 上一节我们讲完原始查询如何防止SQL注入问题同时并提供了几种方式.本节我们继续来讲讲EF Core 2.0中的新特性自定义标量函数. 自定义标量函数两种方式 在EF Core 2.0中我们可以将 ...
- C#_.net core 3.0自定义读取.csv文件数据_解决首行不是标题的问题_Linqtocsv改进
linqtocsv文件有不太好的地方就是:无法设置标题的行数,默认首行就是标题,这不是很尴尬吗? 并不是所有的csv文件严格写的首行是标题,下面全是数据,我接受的任务就是读取很多.csv报表数据, ...
- ASP.NET Core 1.0 静态文件、路由、自定义中间件、身份验证简介
概述 ASP.NET Core 1.0是ASP.NET的一个重要的重新设计. 例如,在ASP.NET Core中,使用Middleware编写请求管道. ASP.NET Core中间件对HttpCon ...
- .net core 1.0 Web MVC 自定义认证过程
通过官方的介绍可知,若要本地开始部署搭建一个基于.net core 1.0的Web应用,需要下载dotnet SDK,或在Visual Studio IDE之上安装相关插件以布置开发环境.为了使开发环 ...
- asp.net core 2.0 web api基于JWT自定义策略授权
JWT(json web token)是一种基于json的身份验证机制,流程如下: 通过登录,来获取Token,再在之后每次请求的Header中追加Authorization为Token的凭据,服务端 ...
- 并发编程概述 委托(delegate) 事件(event) .net core 2.0 event bus 一个简单的基于内存事件总线实现 .net core 基于NPOI 的excel导出类,支持自定义导出哪些字段 基于Ace Admin 的菜单栏实现 第五节:SignalR大杂烩(与MVC融合、全局的几个配置、跨域的应用、C/S程序充当Client和Server)
并发编程概述 前言 说实话,在我软件开发的头两年几乎不考虑并发编程,请求与响应把业务逻辑尽快完成一个星期的任务能两天完成绝不拖三天(剩下时间各种浪),根本不会考虑性能问题(能接受范围内).但随着工 ...
- .net core 3.0 Signalr - 07 业务实现-服务端 自定义管理组、用户、连接
Hub的管理 重写OnConnectedAsync 从连接信息中获取UserId.Groups,ConnectId,并实现这三者的关系,存放于redis中 代码请查看 using CTS.Signal ...
- .Net Core 3.0 以及其前版本编写自定义主机,以允许本机程式(转载)
像所有的托管代码一样,.NET Core 应用程序也由主机执行. 主机负责启动运行时(包括 JIT 和垃圾回收器等组件)和调用托管的入口点. 托管 .NET Core 运行时是高级方案,在大多数情况下 ...
随机推荐
- 安装.NET Core 3.0预览版后VS项目目标框架中不显示的解决方法
下载了微软在GitHub上的cSharpSamples项目后发现其中一些项目使用框架为.NET Core3.0,就下载了.NET Core3.0,但发现项目依然不可用,编译时提示如下 当前 .net ...
- swift的泛型貌似还差点意思
protocol Container { typealias ItemType mutating func append(item: ItemType) mutating func removelas ...
- TF-IFD算法及python实现关键字提取
TF-IDF算法: TF:词频(Term Frequency),即在分词后,某一个词在文档中出现的频率. IDF:逆文档频率(Inverse Document Frequency).在词频的基础上给每 ...
- Cache系列:spring-cache简单三步快速应用ehcache3.x-jcache缓存(spring4.x)
前言:本项目基于spring4.x构建,使用ehcache3.5.2和JCache(jsr107规范) 一.依赖 除了ehcache和cache-api外,注意引用spring-context-sup ...
- bzoj 4453 cys就是要拿英魂! —— 后缀数组+单调栈+set
题目:https://www.lydsy.com/JudgeOnline/problem.php?id=4453 这种问题...一般先把询问离线,排序: 区间对后缀排名的影响在于一些排名大而位置靠后的 ...
- HAOI2006受欢迎的牛——缩点
题目:http://poj.org/problem?id=2186 本题是缩点模板题,将强连通分量缩成一个点,从而形成一个有向无环图,当仅有一个出度为0的点时答案即此点的大小,否则无解. 代码如下: ...
- Attribute meta-data#android.support.VERSION@value value=(25.4.0) from AndroidManifest.xml:25:13-35 is also present at AndroidManifest.xml:28:13-35 value=(26.1.0).
Android Studio 编译项目的时候报错 Merging Errors: Error: Attribute meta-data#android.support.VERSION@value va ...
- CF-828B
B. Black Square time limit per test 1 second memory limit per test 256 megabytes input standard inpu ...
- CodeForces 485A Factory (抽屉原理)
A. Factory time limit per test 1 second memory limit per test 256 megabytes input standard input out ...
- Coding-Job:从研发到生产的容器化融合实践
大家好,我是来自 CODING 的全栈开发工程师,我有幸在 CODING 参与了 Coding-Job 这个容器化的编排平台的研发.大家对 CODING 可能比较了解, Coding.net 是一个一 ...