ASP.NET Core MVC 修改视图的默认路径及其实现原理

  • A+
所属分类:.NET技术
摘要

导语:在日常工作过程中你可能会遇到这样的一种需求,就是在访问同一个页面时PC端和移动端显示的内容和风格是不一样(类似两个不一样的主题),但是它们的后端代码又是差不多的,此时我们就希望能够使用同一套后端代码,然后由系统自动去判断到底是PC端访问还是移动端访问,如果是移动端访问就优先匹配移动端的视图,在没有匹配到的情况下才去匹配PC端的视图。

导语:在日常工作过程中你可能会遇到这样的一种需求,就是在访问同一个页面时PC端和移动端显示的内容和风格是不一样(类似两个不一样的主题),但是它们的后端代码又是差不多的,此时我们就希望能够使用同一套后端代码,然后由系统自动去判断到底是PC端访问还是移动端访问,如果是移动端访问就优先匹配移动端的视图,在没有匹配到的情况下才去匹配PC端的视图。

下面我们就来看下这个功能要如何实现,Demo的目录结构如下所示:

ASP.NET Core MVC 修改视图的默认路径及其实现原理

本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端视图如下所示:

ASP.NET Core MVC 修改视图的默认路径及其实现原理

其中移动端视图如下所示:

ASP.NET Core MVC 修改视图的默认路径及其实现原理

最后,我们分别使用 PC端和移动端 来访问相关页面,如下所示:

1、访问 /App/Home/Index 页面

使用PC端访问,运行结果如下:

ASP.NET Core MVC 修改视图的默认路径及其实现原理

使用移动端访问,运行结果如下:

ASP.NET Core MVC 修改视图的默认路径及其实现原理

此时没有对应的移动端视图,所以都返回PC端的视图内容。

2、访问 /App/Home/WeChat 页面 

使用PC端访问,运行结果如下:

ASP.NET Core MVC 修改视图的默认路径及其实现原理

使用移动端访问,运行结果如下: 

ASP.NET Core MVC 修改视图的默认路径及其实现原理

此时有对应的移动端视图,所以当使用移动端访问时返回的是移动端的视图内容,而使用PC端访问时返回的则是PC端的视图内容。

 

下面我们结合ASP.NET Core源码来分析下其实现原理:

ASP.NET Core源码下载地址:https://github.com/dotnet/aspnetcore

ASP.NET Core MVC 修改视图的默认路径及其实现原理

ASP.NET Core MVC 修改视图的默认路径及其实现原理

点击Source code下载,下载完成后,点击Release:

ASP.NET Core MVC 修改视图的默认路径及其实现原理

ASP.NET Core MVC 修改视图的默认路径及其实现原理

可以将这个extensions源码一起下载下来,下载完成后如下所示: 

ASP.NET Core MVC 修改视图的默认路径及其实现原理

解压后我们重点来关注 Razor视图引擎(RazorViewEngine.cs):

ASP.NET Core MVC 修改视图的默认路径及其实现原理

RazorViewEngine.cs 源码如下所示:

ASP.NET Core MVC 修改视图的默认路径及其实现原理ASP.NET Core MVC 修改视图的默认路径及其实现原理

// 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);         }     } }

ASP.NET Core中RazorViewEngine源码

我们从用于寻找视图的 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 类,如下所示:

ASP.NET Core MVC 修改视图的默认路径及其实现原理ASP.NET Core MVC 修改视图的默认路径及其实现原理

// 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;         }     } }

ASP.NET Core中ViewLocationCacheKey源码

我们重点来看下其中的 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 的初始值:

ASP.NET Core MVC 修改视图的默认路径及其实现原理

ASP.NET Core MVC 修改视图的默认路径及其实现原理

至此本文就全部介绍完了,如果觉得对您有所启发请记得点个赞哦!!! 

 

Demo源码:

链接:https://pan.baidu.com/s/1NpoPWhg5312KkvUg2dAjiw  提取码:0yyo

此文由博主精心撰写转载请保留此原文链接:https://www.cnblogs.com/xyh9039/p/15290967.html

版权声明:如有雷同纯属巧合,如有侵权请及时联系本人修改,谢谢!!!