ASP.NET Core MVC 修改視圖的默認(rèn)路徑及其實(shí)現(xiàn)原理解析
本章將和大家分享如何在ASP.NET Core MVC中修改視圖的默認(rèn)路徑,以及它的實(shí)現(xiàn)原理。
導(dǎo)語(yǔ):在日常工作過程中你可能會(huì)遇到這樣的一種需求,就是在訪問同一個(gè)頁(yè)面時(shí)PC端和移動(dòng)端顯示的內(nèi)容和風(fēng)格是不一樣(類似兩個(gè)不一樣的主題),但是它們的后端代碼又是差不多的,此時(shí)我們就希望能夠使用同一套后端代碼,然后由系統(tǒng)自動(dòng)去判斷到底是PC端訪問還是移動(dòng)端訪問,如果是移動(dòng)端訪問就優(yōu)先匹配移動(dòng)端的視圖,在沒有匹配到的情況下才去匹配PC端的視圖。
下面我們就來看下這個(gè)功能要如何實(shí)現(xiàn),Demo的目錄結(jié)構(gòu)如下所示:

本Demo的Web項(xiàng)目為ASP.NET Core Web 應(yīng)用程序(目標(biāo)框架為.NET Core 3.1) MVC項(xiàng)目。
首先需要去擴(kuò)展視圖的默認(rèn)路徑,如下所示:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor;
namespace NETCoreViewLocationExpander.ViewLocationExtend
{
/// <summary>
/// 視圖默認(rèn)路徑擴(kuò)展
/// </summary>
public class TemplateViewLocationExpander : IViewLocationExpander
{
/// <summary>
/// 擴(kuò)展視圖默認(rèn)路徑(PS:并非每次請(qǐng)求都會(huì)執(zhí)行該方法)
/// </summary>
/// <param name="context"></param>
/// <param name="viewLocations"></param>
/// <returns></returns>
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
var template = context.Values["template"] ?? TemplateEnum.Default.ToString();
if (template == TemplateEnum.WeChatArea.ToString())
{
string[] weChatAreaViewLocationFormats = {
"/Areas/{2}/WeChatViews/{1}/{0}.cshtml",
"/Areas/{2}/WeChatViews/Shared/{0}.cshtml",
"/WeChatViews/Shared/{0}.cshtml"
};
//weChatAreaViewLocationFormats值在前--優(yōu)先查找weChatAreaViewLocationFormats(即優(yōu)先查找移動(dòng)端目錄)
return weChatAreaViewLocationFormats.Union(viewLocations);
}
else if (template == TemplateEnum.WeChat.ToString())
{
string[] weChatViewLocationFormats = {
"/WeChatViews/{1}/{0}.cshtml",
"/WeChatViews/Shared/{0}.cshtml"
};
//weChatViewLocationFormats值在前--優(yōu)先查找weChatViewLocationFormats(即優(yōu)先查找移動(dòng)端目錄)
return weChatViewLocationFormats.Union(viewLocations);
}
return viewLocations;
}
/// <summary>
/// 往ViewLocationExpanderContext.Values里面添加鍵值對(duì)(PS:每次請(qǐng)求都會(huì)執(zhí)行該方法)
/// </summary>
/// <param name="context"></param>
public void PopulateValues(ViewLocationExpanderContext context)
{
var userAgent = context.ActionContext.HttpContext.Request.Headers["User-Agent"].ToString();
var isMobile = IsMobile(userAgent);
var template = TemplateEnum.Default.ToString();
if (isMobile)
{
var areaName = //區(qū)域名稱
context.ActionContext.RouteData.Values.ContainsKey("area")
? context.ActionContext.RouteData.Values["area"].ToString()
: "";
var controllerName = //控制器名稱
context.ActionContext.RouteData.Values.ContainsKey("controller")
? context.ActionContext.RouteData.Values["controller"].ToString()
: "";
if (!string.IsNullOrEmpty(areaName) &&
!string.IsNullOrEmpty(controllerName)) //訪問的是區(qū)域
{
template = TemplateEnum.WeChatArea.ToString();
}
else
{
template = TemplateEnum.WeChat.ToString();
}
}
context.Values["template"] = template; //context.Values會(huì)參與ViewLookupCache緩存Key(cacheKey)的生成
}
/// <summary>
/// 判斷是否是移動(dòng)端
/// </summary>
/// <param name="userAgent"></param>
/// <returns></returns>
protected bool IsMobile(string userAgent)
{
userAgent = userAgent.ToLower();
if (userAgent == "" ||
userAgent.IndexOf("mobile") > -1 ||
userAgent.IndexOf("mobi") > -1 ||
userAgent.IndexOf("nokia") > -1 ||
userAgent.IndexOf("samsung") > -1 ||
userAgent.IndexOf("sonyericsson") > -1 ||
userAgent.IndexOf("mot") > -1 ||
userAgent.IndexOf("blackberry") > -1 ||
userAgent.IndexOf("lg") > -1 ||
userAgent.IndexOf("htc") > -1 ||
userAgent.IndexOf("j2me") > -1 ||
userAgent.IndexOf("ucweb") > -1 ||
userAgent.IndexOf("opera mini") > -1 ||
userAgent.IndexOf("android") > -1 ||
userAgent.IndexOf("transcoder") > -1)
{
return true;
}
return false;
}
}
/// <summary>
/// 模板枚舉
/// </summary>
public enum TemplateEnum
{
Default = 1,
WeChat = 2,
WeChatArea = 3
}
}
接著修改Startup.cs類,如下所示:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NETCoreViewLocationExpander.ViewLocationExtend;
namespace NETCoreViewLocationExpander
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new TemplateViewLocationExpander()); //視圖默認(rèn)路徑擴(kuò)展
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
此外,Demo中還準(zhǔn)備了兩套視圖:
其中PC端視圖如下所示:

其中移動(dòng)端視圖如下所示:

最后,我們分別使用PC端和移動(dòng)端 來訪問相關(guān)頁(yè)面,如下所示:
1、訪問 /App/Home/Index 頁(yè)面
使用PC端訪問,運(yùn)行結(jié)果如下:

使用移動(dòng)端訪問,運(yùn)行結(jié)果如下:

此時(shí)沒有對(duì)應(yīng)的移動(dòng)端視圖,所以都返回PC端的視圖內(nèi)容。
2、訪問 /App/Home/WeChat 頁(yè)面
使用PC端訪問,運(yùn)行結(jié)果如下:

使用移動(dòng)端訪問,運(yùn)行結(jié)果如下:

此時(shí)有對(duì)應(yīng)的移動(dòng)端視圖,所以當(dāng)使用移動(dòng)端訪問時(shí)返回的是移動(dòng)端的視圖內(nèi)容,而使用PC端訪問時(shí)返回的則是PC端的視圖內(nèi)容。
下面我們結(jié)合ASP.NET Core源碼來分析下其實(shí)現(xiàn)原理:
ASP.NET Core源碼下載地址:https://github.com/dotnet/aspnetcore


點(diǎn)擊Source code下載,下載完成后,點(diǎn)擊Release:


可以將這個(gè)extensions源碼一起下載下來,下載完成后如下所示:

解壓后我們重點(diǎn)來關(guān)注Razor視圖引擎(RazorViewEngine.cs):

RazorViewEngine.cs 源碼如下所示:
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Razor
{
/// <summary>
/// Default implementation of <see cref="IRazorViewEngine"/>.
/// </summary>
/// <remarks>
/// For <c>ViewResults</c> returned from controllers, views should be located in
/// <see cref="RazorViewEngineOptions.ViewLocationFormats"/>
/// by default. For the controllers in an area, views should exist in
/// <see cref="RazorViewEngineOptions.AreaViewLocationFormats"/>.
/// </remarks>
public class RazorViewEngine : IRazorViewEngine
{
public static readonly string ViewExtension = ".cshtml";
private const string AreaKey = "area";
private const string ControllerKey = "controller";
private const string PageKey = "page";
private static readonly TimeSpan _cacheExpirationDuration = TimeSpan.FromMinutes(20);
private readonly IRazorPageFactoryProvider _pageFactory;
private readonly IRazorPageActivator _pageActivator;
private readonly HtmlEncoder _htmlEncoder;
private readonly ILogger _logger;
private readonly RazorViewEngineOptions _options;
private readonly DiagnosticListener _diagnosticListener;
/// <summary>
/// Initializes a new instance of the <see cref="RazorViewEngine" />.
/// </summary>
public RazorViewEngine(
IRazorPageFactoryProvider pageFactory,
IRazorPageActivator pageActivator,
HtmlEncoder htmlEncoder,
IOptions<RazorViewEngineOptions> optionsAccessor,
ILoggerFactory loggerFactory,
DiagnosticListener diagnosticListener)
{
_options = optionsAccessor.Value;
if (_options.ViewLocationFormats.Count == 0)
{
throw new ArgumentException(
Resources.FormatViewLocationFormatsIsRequired(nameof(RazorViewEngineOptions.ViewLocationFormats)),
nameof(optionsAccessor));
}
if (_options.AreaViewLocationFormats.Count == 0)
{
throw new ArgumentException(
Resources.FormatViewLocationFormatsIsRequired(nameof(RazorViewEngineOptions.AreaViewLocationFormats)),
nameof(optionsAccessor));
}
_pageFactory = pageFactory;
_pageActivator = pageActivator;
_htmlEncoder = htmlEncoder;
_logger = loggerFactory.CreateLogger<RazorViewEngine>();
_diagnosticListener = diagnosticListener;
ViewLookupCache = new MemoryCache(new MemoryCacheOptions());
}
/// <summary>
/// A cache for results of view lookups.
/// </summary>
protected IMemoryCache ViewLookupCache { get; }
/// <summary>
/// Gets the case-normalized route value for the specified route <paramref name="key"/>.
/// </summary>
/// <param name="context">The <see cref="ActionContext"/>.</param>
/// <param name="key">The route key to lookup.</param>
/// <returns>The value corresponding to the key.</returns>
/// <remarks>
/// The casing of a route value in <see cref="ActionContext.RouteData"/> is determined by the client.
/// This making constructing paths for view locations in a case sensitive file system unreliable. Using the
/// <see cref="Abstractions.ActionDescriptor.RouteValues"/> to get route values
/// produces consistently cased results.
/// </remarks>
public static string GetNormalizedRouteValue(ActionContext context, string key)
=> NormalizedRouteValue.GetNormalizedRouteValue(context, key);
/// <inheritdoc />
public RazorPageResult FindPage(ActionContext context, string pageName)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(pageName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
}
if (IsApplicationRelativePath(pageName) || IsRelativePath(pageName))
{
// A path; not a name this method can handle.
return new RazorPageResult(pageName, Enumerable.Empty<string>());
}
var cacheResult = LocatePageFromViewLocations(context, pageName, isMainPage: false);
if (cacheResult.Success)
{
var razorPage = cacheResult.ViewEntry.PageFactory();
return new RazorPageResult(pageName, razorPage);
}
else
{
return new RazorPageResult(pageName, cacheResult.SearchedLocations);
}
}
/// <inheritdoc />
public RazorPageResult GetPage(string executingFilePath, string pagePath)
{
if (string.IsNullOrEmpty(pagePath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pagePath));
}
if (!(IsApplicationRelativePath(pagePath) || IsRelativePath(pagePath)))
{
// Not a path this method can handle.
return new RazorPageResult(pagePath, Enumerable.Empty<string>());
}
var cacheResult = LocatePageFromPath(executingFilePath, pagePath, isMainPage: false);
if (cacheResult.Success)
{
var razorPage = cacheResult.ViewEntry.PageFactory();
return new RazorPageResult(pagePath, razorPage);
}
else
{
return new RazorPageResult(pagePath, cacheResult.SearchedLocations);
}
}
/// <inheritdoc />
public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName));
}
if (IsApplicationRelativePath(viewName) || IsRelativePath(viewName))
{
// A path; not a name this method can handle.
return ViewEngineResult.NotFound(viewName, Enumerable.Empty<string>());
}
var cacheResult = LocatePageFromViewLocations(context, viewName, isMainPage);
return CreateViewEngineResult(cacheResult, viewName);
}
/// <inheritdoc />
public ViewEngineResult GetView(string executingFilePath, string viewPath, bool isMainPage)
{
if (string.IsNullOrEmpty(viewPath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewPath));
}
if (!(IsApplicationRelativePath(viewPath) || IsRelativePath(viewPath)))
{
// Not a path this method can handle.
return ViewEngineResult.NotFound(viewPath, Enumerable.Empty<string>());
}
var cacheResult = LocatePageFromPath(executingFilePath, viewPath, isMainPage);
return CreateViewEngineResult(cacheResult, viewPath);
}
private ViewLocationCacheResult LocatePageFromPath(string executingFilePath, string pagePath, bool isMainPage)
{
var applicationRelativePath = GetAbsolutePath(executingFilePath, pagePath);
var cacheKey = new ViewLocationCacheKey(applicationRelativePath, isMainPage);
if (!ViewLookupCache.TryGetValue(cacheKey, out ViewLocationCacheResult cacheResult))
{
var expirationTokens = new HashSet<IChangeToken>();
cacheResult = CreateCacheResult(expirationTokens, applicationRelativePath, isMainPage);
var cacheEntryOptions = new MemoryCacheEntryOptions();
cacheEntryOptions.SetSlidingExpiration(_cacheExpirationDuration);
foreach (var expirationToken in expirationTokens)
{
cacheEntryOptions.AddExpirationToken(expirationToken);
}
// No views were found at the specified location. Create a not found result.
if (cacheResult == null)
{
cacheResult = new ViewLocationCacheResult(new[] { applicationRelativePath });
}
cacheResult = ViewLookupCache.Set(
cacheKey,
cacheResult,
cacheEntryOptions);
}
return cacheResult;
}
private ViewLocationCacheResult LocatePageFromViewLocations(
ActionContext actionContext,
string pageName,
bool isMainPage)
{
var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey);
var areaName = GetNormalizedRouteValue(actionContext, AreaKey);
string razorPageName = null;
if (actionContext.ActionDescriptor.RouteValues.ContainsKey(PageKey))
{
// Only calculate the Razor Page name if "page" is registered in RouteValues.
razorPageName = GetNormalizedRouteValue(actionContext, PageKey);
}
var expanderContext = new ViewLocationExpanderContext(
actionContext,
pageName,
controllerName,
areaName,
razorPageName,
isMainPage);
Dictionary<string, string> expanderValues = null;
var expanders = _options.ViewLocationExpanders;
// Read interface .Count once rather than per iteration
var expandersCount = expanders.Count;
if (expandersCount > 0)
{
expanderValues = new Dictionary<string, string>(StringComparer.Ordinal);
expanderContext.Values = expanderValues;
// Perf: Avoid allocations
for (var i = 0; i < expandersCount; i++)
{
expanders[i].PopulateValues(expanderContext);
}
}
var cacheKey = new ViewLocationCacheKey(
expanderContext.ViewName,
expanderContext.ControllerName,
expanderContext.AreaName,
expanderContext.PageName,
expanderContext.IsMainPage,
expanderValues);
if (!ViewLookupCache.TryGetValue(cacheKey, out ViewLocationCacheResult cacheResult))
{
_logger.ViewLookupCacheMiss(cacheKey.ViewName, cacheKey.ControllerName);
cacheResult = OnCacheMiss(expanderContext, cacheKey);
}
else
{
_logger.ViewLookupCacheHit(cacheKey.ViewName, cacheKey.ControllerName);
}
return cacheResult;
}
/// <inheritdoc />
public string GetAbsolutePath(string executingFilePath, string pagePath)
{
if (string.IsNullOrEmpty(pagePath))
{
// Path is not valid; no change required.
return pagePath;
}
if (IsApplicationRelativePath(pagePath))
{
// An absolute path already; no change required.
return pagePath;
}
if (!IsRelativePath(pagePath))
{
// A page name; no change required.
return pagePath;
}
if (string.IsNullOrEmpty(executingFilePath))
{
// Given a relative path i.e. not yet application-relative (starting with "~/" or "/"), interpret
// path relative to currently-executing view, if any.
// Not yet executing a view. Start in app root.
var absolutePath = "/" + pagePath;
return ViewEnginePath.ResolvePath(absolutePath);
}
return ViewEnginePath.CombinePath(executingFilePath, pagePath);
}
// internal for tests
internal IEnumerable<string> GetViewLocationFormats(ViewLocationExpanderContext context)
{
if (!string.IsNullOrEmpty(context.AreaName) &&
!string.IsNullOrEmpty(context.ControllerName))
{
return _options.AreaViewLocationFormats;
}
else if (!string.IsNullOrEmpty(context.ControllerName))
{
return _options.ViewLocationFormats;
}
else if (!string.IsNullOrEmpty(context.AreaName) &&
!string.IsNullOrEmpty(context.PageName))
{
return _options.AreaPageViewLocationFormats;
}
else if (!string.IsNullOrEmpty(context.PageName))
{
return _options.PageViewLocationFormats;
}
else
{
// If we don't match one of these conditions, we'll just treat it like regular controller/action
// and use those search paths. This is what we did in 1.0.0 without giving much thought to it.
return _options.ViewLocationFormats;
}
}
private ViewLocationCacheResult OnCacheMiss(
ViewLocationExpanderContext expanderContext,
ViewLocationCacheKey cacheKey)
{
var viewLocations = GetViewLocationFormats(expanderContext);
var expanders = _options.ViewLocationExpanders;
// Read interface .Count once rather than per iteration
var expandersCount = expanders.Count;
for (var i = 0; i < expandersCount; i++)
{
viewLocations = expanders[i].ExpandViewLocations(expanderContext, viewLocations);
}
ViewLocationCacheResult cacheResult = null;
var searchedLocations = new List<string>();
var expirationTokens = new HashSet<IChangeToken>();
foreach (var location in viewLocations)
{
var path = string.Format(
CultureInfo.InvariantCulture,
location,
expanderContext.ViewName,
expanderContext.ControllerName,
expanderContext.AreaName);
path = ViewEnginePath.ResolvePath(path);
cacheResult = CreateCacheResult(expirationTokens, path, expanderContext.IsMainPage);
if (cacheResult != null)
{
break;
}
searchedLocations.Add(path);
}
// No views were found at the specified location. Create a not found result.
if (cacheResult == null)
{
cacheResult = new ViewLocationCacheResult(searchedLocations);
}
var cacheEntryOptions = new MemoryCacheEntryOptions();
cacheEntryOptions.SetSlidingExpiration(_cacheExpirationDuration);
foreach (var expirationToken in expirationTokens)
{
cacheEntryOptions.AddExpirationToken(expirationToken);
}
return ViewLookupCache.Set(cacheKey, cacheResult, cacheEntryOptions);
}
// Internal for unit testing
internal ViewLocationCacheResult CreateCacheResult(
HashSet<IChangeToken> expirationTokens,
string relativePath,
bool isMainPage)
{
var factoryResult = _pageFactory.CreateFactory(relativePath);
var viewDescriptor = factoryResult.ViewDescriptor;
if (viewDescriptor?.ExpirationTokens != null)
{
var viewExpirationTokens = viewDescriptor.ExpirationTokens;
// Read interface .Count once rather than per iteration
var viewExpirationTokensCount = viewExpirationTokens.Count;
for (var i = 0; i < viewExpirationTokensCount; i++)
{
expirationTokens.Add(viewExpirationTokens[i]);
}
}
if (factoryResult.Success)
{
// Only need to lookup _ViewStarts for the main page.
var viewStartPages = isMainPage ?
GetViewStartPages(viewDescriptor.RelativePath, expirationTokens) :
Array.Empty<ViewLocationCacheItem>();
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 filePath in RazorFileHierarchy.GetViewStartPaths(path))
{
var result = _pageFactory.CreateFactory(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, filePath));
}
}
return viewStartPages;
}
private ViewEngineResult CreateViewEngineResult(ViewLocationCacheResult result, string viewName)
{
if (!result.Success)
{
return ViewEngineResult.NotFound(viewName, result.SearchedLocations);
}
var page = result.ViewEntry.PageFactory();
var viewStarts = new IRazorPage[result.ViewStartEntries.Count];
for (var i = 0; i < viewStarts.Length; i++)
{
var viewStartItem = result.ViewStartEntries[i];
viewStarts[i] = viewStartItem.PageFactory();
}
var view = new RazorView(this, _pageActivator, viewStarts, page, _htmlEncoder, _diagnosticListener);
return ViewEngineResult.Found(viewName, view);
}
private static bool IsApplicationRelativePath(string name)
{
Debug.Assert(!string.IsNullOrEmpty(name));
return name[0] == '~' || name[0] == '/';
}
private static bool IsRelativePath(string name)
{
Debug.Assert(!string.IsNullOrEmpty(name));
// Though ./ViewName looks like a relative path, framework searches for that view using view locations.
return name.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase);
}
}
}
我們從用于尋找視圖的 FindView 方法開始閱讀:
/// <inheritdoc />
public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName));
}
if (IsApplicationRelativePath(viewName) || IsRelativePath(viewName))
{
// A path; not a name this method can handle.
return ViewEngineResult.NotFound(viewName, Enumerable.Empty<string>());
}
var cacheResult = LocatePageFromViewLocations(context, viewName, isMainPage);
return CreateViewEngineResult(cacheResult, viewName);
}
接著定位找到LocatePageFromViewLocations 方法:
private ViewLocationCacheResult LocatePageFromViewLocations(
ActionContext actionContext,
string pageName,
bool isMainPage)
{
var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey);
var areaName = GetNormalizedRouteValue(actionContext, AreaKey);
string razorPageName = null;
if (actionContext.ActionDescriptor.RouteValues.ContainsKey(PageKey))
{
// Only calculate the Razor Page name if "page" is registered in RouteValues.
razorPageName = GetNormalizedRouteValue(actionContext, PageKey);
}
var expanderContext = new ViewLocationExpanderContext(
actionContext,
pageName,
controllerName,
areaName,
razorPageName,
isMainPage);
Dictionary<string, string> expanderValues = null;
var expanders = _options.ViewLocationExpanders;
// Read interface .Count once rather than per iteration
var expandersCount = expanders.Count;
if (expandersCount > 0)
{
expanderValues = new Dictionary<string, string>(StringComparer.Ordinal);
expanderContext.Values = expanderValues;
// Perf: Avoid allocations
for (var i = 0; i < expandersCount; i++)
{
expanders[i].PopulateValues(expanderContext);
}
}
var cacheKey = new ViewLocationCacheKey(
expanderContext.ViewName,
expanderContext.ControllerName,
expanderContext.AreaName,
expanderContext.PageName,
expanderContext.IsMainPage,
expanderValues);
if (!ViewLookupCache.TryGetValue(cacheKey, out ViewLocationCacheResult cacheResult))
{
_logger.ViewLookupCacheMiss(cacheKey.ViewName, cacheKey.ControllerName);
cacheResult = OnCacheMiss(expanderContext, cacheKey);
}
else
{
_logger.ViewLookupCacheHit(cacheKey.ViewName, cacheKey.ControllerName);
}
return cacheResult;
}
從此處可以看出,每次查找視圖的時(shí)候都會(huì)調(diào)用 ViewLocationExpander.PopulateValues 方法,并且最終的這個(gè) expanderValues 會(huì)參與ViewLookupCache 緩存key(cacheKey)的生成。
此外還可以看出,如果從 ViewLookupCache 這個(gè)緩存中能找到數(shù)據(jù)的話,它就直接返回了,不會(huì)再去調(diào)用ViewLocationExpander.ExpandViewLocations 方法。
這也就解釋了為什么我們Demo中是在 PopulateValues 方法里面去設(shè)置context.Values["template"] 的值,而不是直接在 ExpandViewLocations 方法里面去設(shè)置這個(gè)值。
下面我們接著找到用于生成 cacheKey 的ViewLocationCacheKey 類,如下所示:
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.Razor
{
/// <summary>
/// Key for entries in <see cref="RazorViewEngine.ViewLookupCache"/>.
/// </summary>
internal readonly struct ViewLocationCacheKey : IEquatable<ViewLocationCacheKey>
{
/// <summary>
/// Initializes a new instance of <see cref="ViewLocationCacheKey"/>.
/// </summary>
/// <param name="viewName">The view name or path.</param>
/// <param name="isMainPage">Determines if the page being found is the main page for an action.</param>
public ViewLocationCacheKey(
string viewName,
bool isMainPage)
: this(
viewName,
controllerName: null,
areaName: null,
pageName: null,
isMainPage: isMainPage,
values: null)
{
}
/// <summary>
/// Initializes a new instance of <see cref="ViewLocationCacheKey"/>.
/// </summary>
/// <param name="viewName">The view name.</param>
/// <param name="controllerName">The controller name.</param>
/// <param name="areaName">The area name.</param>
/// <param name="pageName">The page name.</param>
/// <param name="isMainPage">Determines if the page being found is the main page for an action.</param>
/// <param name="values">Values from <see cref="IViewLocationExpander"/> instances.</param>
public ViewLocationCacheKey(
string viewName,
string controllerName,
string areaName,
string pageName,
bool isMainPage,
IReadOnlyDictionary<string, string> values)
{
ViewName = viewName;
ControllerName = controllerName;
AreaName = areaName;
PageName = pageName;
IsMainPage = isMainPage;
ViewLocationExpanderValues = values;
}
/// <summary>
/// Gets the view name.
/// </summary>
public string ViewName { get; }
/// <summary>
/// Gets the controller name.
/// </summary>
public string ControllerName { get; }
/// <summary>
/// Gets the area name.
/// </summary>
public string AreaName { get; }
/// <summary>
/// Gets the page name.
/// </summary>
public string PageName { get; }
/// <summary>
/// Determines if the page being found is the main page for an action.
/// </summary>
public bool IsMainPage { get; }
/// <summary>
/// Gets the values populated by <see cref="IViewLocationExpander"/> instances.
/// </summary>
public IReadOnlyDictionary<string, string> ViewLocationExpanderValues { get; }
/// <inheritdoc />
public bool Equals(ViewLocationCacheKey y)
{
if (IsMainPage != y.IsMainPage ||
!string.Equals(ViewName, y.ViewName, StringComparison.Ordinal) ||
!string.Equals(ControllerName, y.ControllerName, StringComparison.Ordinal) ||
!string.Equals(AreaName, y.AreaName, StringComparison.Ordinal) ||
!string.Equals(PageName, y.PageName, StringComparison.Ordinal))
{
return false;
}
if (ReferenceEquals(ViewLocationExpanderValues, y.ViewLocationExpanderValues))
{
return true;
}
if (ViewLocationExpanderValues == null ||
y.ViewLocationExpanderValues == null ||
(ViewLocationExpanderValues.Count != y.ViewLocationExpanderValues.Count))
{
return false;
}
foreach (var item in ViewLocationExpanderValues)
{
if (!y.ViewLocationExpanderValues.TryGetValue(item.Key, out var yValue) ||
!string.Equals(item.Value, yValue, StringComparison.Ordinal))
{
return false;
}
}
return true;
}
/// <inheritdoc />
public override bool Equals(object obj)
{
if (obj is ViewLocationCacheKey)
{
return Equals((ViewLocationCacheKey)obj);
}
return false;
}
/// <inheritdoc />
public override int GetHashCode()
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(IsMainPage ? 1 : 0);
hashCodeCombiner.Add(ViewName, StringComparer.Ordinal);
hashCodeCombiner.Add(ControllerName, StringComparer.Ordinal);
hashCodeCombiner.Add(AreaName, StringComparer.Ordinal);
hashCodeCombiner.Add(PageName, StringComparer.Ordinal);
if (ViewLocationExpanderValues != null)
{
foreach (var item in ViewLocationExpanderValues)
{
hashCodeCombiner.Add(item.Key, StringComparer.Ordinal);
hashCodeCombiner.Add(item.Value, StringComparer.Ordinal);
}
}
return hashCodeCombiner;
}
}
}
我們重點(diǎn)來看下其中的 Equals 方法,如下所示:
/// <inheritdoc />
public bool Equals(ViewLocationCacheKey y)
{
if (IsMainPage != y.IsMainPage ||
!string.Equals(ViewName, y.ViewName, StringComparison.Ordinal) ||
!string.Equals(ControllerName, y.ControllerName, StringComparison.Ordinal) ||
!string.Equals(AreaName, y.AreaName, StringComparison.Ordinal) ||
!string.Equals(PageName, y.PageName, StringComparison.Ordinal))
{
return false;
}
if (ReferenceEquals(ViewLocationExpanderValues, y.ViewLocationExpanderValues))
{
return true;
}
if (ViewLocationExpanderValues == null ||
y.ViewLocationExpanderValues == null ||
(ViewLocationExpanderValues.Count != y.ViewLocationExpanderValues.Count))
{
return false;
}
foreach (var item in ViewLocationExpanderValues)
{
if (!y.ViewLocationExpanderValues.TryGetValue(item.Key, out var yValue) ||
!string.Equals(item.Value, yValue, StringComparison.Ordinal))
{
return false;
}
}
return true;
}
從此處可以看出,如果 expanderValues 字典中 鍵/值對(duì)的數(shù)目不同或者其中任意一個(gè)值不同,那么這個(gè) cacheKey 就是不同的。
我們繼續(xù)往下分析, 從上文中我們知道,如果從ViewLookupCache 緩存中沒有找到數(shù)據(jù),那么它就會(huì)執(zhí)行OnCacheMiss 方法。
我們找到OnCacheMiss 方法,如下所示:
private ViewLocationCacheResult OnCacheMiss(
ViewLocationExpanderContext expanderContext,
ViewLocationCacheKey cacheKey)
{
var viewLocations = GetViewLocationFormats(expanderContext);
var expanders = _options.ViewLocationExpanders;
// Read interface .Count once rather than per iteration
var expandersCount = expanders.Count;
for (var i = 0; i < expandersCount; i++)
{
viewLocations = expanders[i].ExpandViewLocations(expanderContext, viewLocations);
}
ViewLocationCacheResult cacheResult = null;
var searchedLocations = new List<string>();
var expirationTokens = new HashSet<IChangeToken>();
foreach (var location in viewLocations)
{
var path = string.Format(
CultureInfo.InvariantCulture,
location,
expanderContext.ViewName,
expanderContext.ControllerName,
expanderContext.AreaName);
path = ViewEnginePath.ResolvePath(path);
cacheResult = CreateCacheResult(expirationTokens, path, expanderContext.IsMainPage);
if (cacheResult != null)
{
break;
}
searchedLocations.Add(path);
}
// No views were found at the specified location. Create a not found result.
if (cacheResult == null)
{
cacheResult = new ViewLocationCacheResult(searchedLocations);
}
var cacheEntryOptions = new MemoryCacheEntryOptions();
cacheEntryOptions.SetSlidingExpiration(_cacheExpirationDuration);
foreach (var expirationToken in expirationTokens)
{
cacheEntryOptions.AddExpirationToken(expirationToken);
}
return ViewLookupCache.Set(cacheKey, cacheResult, cacheEntryOptions);
}
仔細(xì)觀察之后你就會(huì)發(fā)現(xiàn):
1、首先它是通過GetViewLocationFormats 方法獲取初始的 viewLocations視圖位置集合。
2、接著它會(huì)按順序依次調(diào)用所有的ViewLocationExpander.ExpandViewLocations 方法,經(jīng)過一系列聚合操作后得到最終的viewLocations 視圖位置集合。
3、然后遍歷 viewLocations 視圖位置集合,按順序依次去指定的路徑中查找對(duì)應(yīng)的視圖,只要找到符合條件的第一個(gè)視圖就結(jié)束循環(huán),不再往下查找,最后設(shè)置緩存返回結(jié)果。
4、視圖位置字符串(例如:“/Areas/{2}/WeChatViews/{1}/{0}.cshtml”)中的占位符含義:“{0}” 表示視圖名稱,“{1}” 表示控制器名稱,“{2}” 表示區(qū)域名稱。
下面我們繼續(xù)找到GetViewLocationFormats 方法,如下所示:
// internal for tests
internal IEnumerable<string> GetViewLocationFormats(ViewLocationExpanderContext context)
{
if (!string.IsNullOrEmpty(context.AreaName) &&
!string.IsNullOrEmpty(context.ControllerName))
{
return _options.AreaViewLocationFormats;
}
else if (!string.IsNullOrEmpty(context.ControllerName))
{
return _options.ViewLocationFormats;
}
else if (!string.IsNullOrEmpty(context.AreaName) &&
!string.IsNullOrEmpty(context.PageName))
{
return _options.AreaPageViewLocationFormats;
}
else if (!string.IsNullOrEmpty(context.PageName))
{
return _options.PageViewLocationFormats;
}
else
{
// If we don't match one of these conditions, we'll just treat it like regular controller/action
// and use those search paths. This is what we did in 1.0.0 without giving much thought to it.
return _options.ViewLocationFormats;
}
}
從此處可以看出,它是通過判斷 區(qū)域名稱和控制器名稱 是否都不為空,以此來判斷客戶端訪問的到底是區(qū)域還是非區(qū)域。
文章最后我們通過調(diào)試來看下AreaViewLocationFormats 和ViewLocationFormats 的初始值:


至此本文就全部介紹完了,如果覺得對(duì)您有所啟發(fā)請(qǐng)記得點(diǎn)個(gè)贊哦!??!
Demo源碼:
鏈接: https://pan.baidu.com/s/1gn4JQTzn7hQLgfAtaUPDLg
提取碼: mjgr
到此這篇關(guān)于ASP.NET Core MVC 修改視圖的默認(rèn)路徑及其實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)ASP.NET Core MVC 視圖路徑內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ASP.NET操作Word的IIS權(quán)限設(shè)置
檢索 COM 類工廠中 CLSID 為 {00024500-0000-0000-C000-000000000046} 的組件時(shí)失敗,原因是出現(xiàn)以下錯(cuò)誤: 80070005。2011-02-02
ASP.NET Core項(xiàng)目中集成TypeScript
這篇文章介紹了ASP.NET Core項(xiàng)目中集成TypeScript的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
Entity?Framework使用Code?First的實(shí)體繼承模式
本文詳細(xì)講解了Entity?Framework使用Code?First的實(shí)體繼承模式,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03
.net?程序通過?crontab?無(wú)法啟動(dòng)手動(dòng)執(zhí)行腳本啟動(dòng)的方法
.net 網(wǎng)關(guān)程序需要設(shè)置定時(shí)重啟,按照日常操作先把正在運(yùn)行的 PID kill 掉后,再執(zhí)行啟動(dòng)服務(wù)。通過腳本無(wú)法啟動(dòng),試著把 .net 程序?qū)懗煞?wù)后,發(fā)現(xiàn)是可以正常重啟的,本文給大家介紹下.net 程序通過 crontab 無(wú)法啟動(dòng)手動(dòng)執(zhí)行腳本啟動(dòng),感興趣的朋友一起看看吧2021-12-12
詳解.NET Core+Docker 開發(fā)微服務(wù)
這篇文章給大家分享了.NET Core+Docker 開發(fā)微服務(wù)的相關(guān)知識(shí)點(diǎn)內(nèi)容,有興趣的朋友們參考下。2018-09-09
.net后臺(tái)代碼調(diào)用前臺(tái)JS的兩種方式
這篇文章主要介紹了.net后臺(tái)代碼調(diào)用前臺(tái)JS的兩種方式,需要的朋友可以參考下2014-03-03
asp.net實(shí)現(xiàn)三層架構(gòu)的例子
這篇文章主要介紹了asp.net實(shí)現(xiàn)三層架構(gòu)的例子,十分的簡(jiǎn)單實(shí)用,有需要的小伙伴可以參考下。2015-07-07

