ASP.NET Core MVC 修改視圖的默認路徑及其實現原理解析
本章將和大傢分享如何在ASP.NET Core MVC中修改視圖的默認路徑,以及它的實現原理。
導語:在日常工作過程中你可能會遇到這樣的一種需求,就是在訪問同一個頁面時PC端和移動端顯示的內容和風格是不一樣(類似兩個不一樣的主題),但是它們的後端代碼又是差不多的,此時我們就希望能夠使用同一套後端代碼,然後由系統自動去判斷到底是PC端訪問還是移動端訪問,如果是移動端訪問就優先匹配移動端的視圖,在沒有匹配到的情況下才去匹配PC端的視圖。
下面我們就來看下這個功能要如何實現,Demo的目錄結構如下所示:
本Demo的Web項目為ASP.NET Core Web 應用程序(目標框架為.NET Core 3.1) MVC項目。
首先需要去擴展視圖的默認路徑,如下所示:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Razor; namespace NETCoreViewLocationExpander.ViewLocationExtend { /// <summary> /// 視圖默認路徑擴展 /// </summary> public class TemplateViewLocationExpander : IViewLocationExpander { /// <summary> /// 擴展視圖默認路徑(PS:並非每次請求都會執行該方法) /// </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值在前--優先查找weChatAreaViewLocationFormats(即優先查找移動端目錄) return weChatAreaViewLocationFormats.Union(viewLocations); } else if (template == TemplateEnum.WeChat.ToString()) { string[] weChatViewLocationFormats = { "/WeChatViews/{1}/{0}.cshtml", "/WeChatViews/Shared/{0}.cshtml" }; //weChatViewLocationFormats值在前--優先查找weChatViewLocationFormats(即優先查找移動端目錄) return weChatViewLocationFormats.Union(viewLocations); } return viewLocations; } /// <summary> /// 往ViewLocationExpanderContext.Values裡面添加鍵值對(PS:每次請求都會執行該方法) /// </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 = //區域名稱 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)) //訪問的是區域 { template = TemplateEnum.WeChatArea.ToString(); } else { template = TemplateEnum.WeChat.ToString(); } } context.Values["template"] = template; //context.Values會參與ViewLookupCache緩存Key(cacheKey)的生成 } /// <summary> /// 判斷是否是移動端 /// </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()); //視圖默認路徑擴展 }); } // 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中還準備瞭兩套視圖:
其中PC端視圖如下所示:
其中移動端視圖如下所示:
最後,我們分別使用PC端和移動端 來訪問相關頁面,如下所示:
1、訪問 /App/Home/Index 頁面
使用PC端訪問,運行結果如下:
使用移動端訪問,運行結果如下:
此時沒有對應的移動端視圖,所以都返回PC端的視圖內容。
2、訪問 /App/Home/WeChat 頁面
使用PC端訪問,運行結果如下:
使用移動端訪問,運行結果如下:
此時有對應的移動端視圖,所以當使用移動端訪問時返回的是移動端的視圖內容,而使用PC端訪問時返回的則是PC端的視圖內容。
下面我們結合ASP.NET Core源碼來分析下其實現原理:
ASP.NET Core源碼下載地址:https://github.com/dotnet/aspnetcore
點擊Source code下載,下載完成後,點擊Release:
可以將這個extensions源碼一起下載下來,下載完成後如下所示:
解壓後我們重點來關註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; }
從此處可以看出,每次查找視圖的時候都會調用 ViewLocationExpander.PopulateValues 方法,並且最終的這個 expanderValues 會參與ViewLookupCache 緩存key(cacheKey)的生成。
此外還可以看出,如果從 ViewLookupCache 這個緩存中能找到數據的話,它就直接返回瞭,不會再去調用ViewLocationExpander.ExpandViewLocations 方法。
這也就解釋瞭為什麼我們Demo中是在 PopulateValues 方法裡面去設置context.Values[“template”] 的值,而不是直接在 ExpandViewLocations 方法裡面去設置這個值。
下面我們接著找到用於生成 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; } } }
我們重點來看下其中的 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 字典中 鍵/值對的數目不同或者其中任意一個值不同,那麼這個 cacheKey 就是不同的。
我們繼續往下分析, 從上文中我們知道,如果從ViewLookupCache 緩存中沒有找到數據,那麼它就會執行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); }
仔細觀察之後你就會發現:
1、首先它是通過GetViewLocationFormats 方法獲取初始的 viewLocations視圖位置集合。
2、接著它會按順序依次調用所有的ViewLocationExpander.ExpandViewLocations 方法,經過一系列聚合操作後得到最終的viewLocations 視圖位置集合。
3、然後遍歷 viewLocations 視圖位置集合,按順序依次去指定的路徑中查找對應的視圖,隻要找到符合條件的第一個視圖就結束循環,不再往下查找,最後設置緩存返回結果。
4、視圖位置字符串(例如:“/Areas/{2}/WeChatViews/{1}/{0}.cshtml”)中的占位符含義:“{0}” 表示視圖名稱,“{1}” 表示控制器名稱,“{2}” 表示區域名稱。
下面我們繼續找到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; } }
從此處可以看出,它是通過判斷 區域名稱和控制器名稱 是否都不為空,以此來判斷客戶端訪問的到底是區域還是非區域。
文章最後我們通過調試來看下AreaViewLocationFormats 和ViewLocationFormats 的初始值:
至此本文就全部介紹完瞭,如果覺得對您有所啟發請記得點個贊哦!!!
Demo源碼:
鏈接: https://pan.baidu.com/s/1gn4JQTzn7hQLgfAtaUPDLg
提取碼: mjgr
到此這篇關於ASP.NET Core MVC 修改視圖的默認路徑及其實現原理的文章就介紹到這瞭,更多相關ASP.NET Core MVC 視圖路徑內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Mybatis如何解決sql中like通配符模糊匹配問題
- c# 如何對網絡信息進行相關設置(ip,dns,網關等)
- Unity實現動物識別的示例代碼
- Unity實現菜品識別的示例代碼
- .NET實現API版本控制