千家信息网

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

发表于:2025-01-31 作者:千家信息网编辑
千家信息网最后更新 2025年01月31日,这期内容当中小编将会给大家带来有关ASP.NET Core MVC修改视图的默认路径及其实现原理是怎样的,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。导语:在日常工
千家信息网最后更新 2025年01月31日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{    ///     /// 视图默认路径扩展    ///     public class TemplateViewLocationExpander : IViewLocationExpander    {        ///         /// 扩展视图默认路径(PS:并非每次请求都会执行该方法)        ///         ///         ///         ///         public IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable 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;        }        ///         /// 往ViewLocationExpanderContext.Values里面添加键值对(PS:每次请求都会执行该方法)        ///         ///         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)的生成        }        ///         /// 判断是否是移动端        ///         ///         ///         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;        }    }    ///     /// 模板枚举    ///     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(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{    ///     /// Default implementation of .    ///     ///     /// For ViewResults returned from controllers, views should be located in    ///     /// by default. For the controllers in an area, views should exist in    /// .    ///     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;        ///         /// Initializes a new instance of the .        ///         public RazorViewEngine(            IRazorPageFactoryProvider pageFactory,            IRazorPageActivator pageActivator,            HtmlEncoder htmlEncoder,            IOptions 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();            _diagnosticListener = diagnosticListener;            ViewLookupCache = new MemoryCache(new MemoryCacheOptions());        }        ///         /// A cache for results of view lookups.        ///         protected IMemoryCache ViewLookupCache { get; }        ///         /// Gets the case-normalized route value for the specified route .        ///         /// The .        /// The route key to lookup.        /// The value corresponding to the key.        ///         /// The casing of a route value in  is determined by the client.        /// This making constructing paths for view locations in a case sensitive file system unreliable. Using the        ///  to get route values        /// produces consistently cased results.        ///         public static string GetNormalizedRouteValue(ActionContext context, string key)            => NormalizedRouteValue.GetNormalizedRouteValue(context, key);        ///         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());            }            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);            }        }        ///         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());            }            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);            }        }        ///         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());            }            var cacheResult = LocatePageFromViewLocations(context, viewName, isMainPage);            return CreateViewEngineResult(cacheResult, viewName);        }        ///         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());            }            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();                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 expanderValues = null;            var expanders = _options.ViewLocationExpanders;            // Read interface .Count once rather than per iteration            var expandersCount = expanders.Count;            if (expandersCount > 0)            {                expanderValues = new Dictionary(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;        }        ///         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 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();            var expirationTokens = new HashSet();            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 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();                return new ViewLocationCacheResult(                    new ViewLocationCacheItem(factoryResult.RazorPageFactory, relativePath),                    viewStartPages);            }            return null;        }        private IReadOnlyList GetViewStartPages(            string path,            HashSet expirationTokens)        {            var viewStartPages = new List();            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 方法开始阅读:

/// 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());    }    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 expanderValues = null;    var expanders = _options.ViewLocationExpanders;    // Read interface .Count once rather than per iteration    var expandersCount = expanders.Count;    if (expandersCount > 0)    {        expanderValues = new Dictionary(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{    ///     /// Key for entries in .    ///     internal readonly struct ViewLocationCacheKey : IEquatable    {        ///         /// Initializes a new instance of .        ///         /// The view name or path.        /// Determines if the page being found is the main page for an action.        public ViewLocationCacheKey(            string viewName,            bool isMainPage)            : this(                  viewName,                  controllerName: null,                  areaName: null,                  pageName: null,                  isMainPage: isMainPage,                  values: null)        {        }        ///         /// Initializes a new instance of .        ///         /// The view name.        /// The controller name.        /// The area name.        /// The page name.        /// Determines if the page being found is the main page for an action.        /// Values from  instances.        public ViewLocationCacheKey(            string viewName,            string controllerName,            string areaName,            string pageName,            bool isMainPage,            IReadOnlyDictionary values)        {            ViewName = viewName;            ControllerName = controllerName;            AreaName = areaName;            PageName = pageName;            IsMainPage = isMainPage;            ViewLocationExpanderValues = values;        }        ///         /// Gets the view name.        ///         public string ViewName { get; }        ///         /// Gets the controller name.        ///         public string ControllerName { get; }        ///         /// Gets the area name.        ///         public string AreaName { get; }        ///         /// Gets the page name.        ///         public string PageName { get; }        ///         /// Determines if the page being found is the main page for an action.        ///         public bool IsMainPage { get; }        ///         /// Gets the values populated by  instances.        ///         public IReadOnlyDictionary ViewLocationExpanderValues { get; }        ///         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;        }        ///         public override bool Equals(object obj)        {            if (obj is ViewLocationCacheKey)            {                return Equals((ViewLocationCacheKey)obj);            }            return false;        }        ///         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 方法,如下所示:

/// 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();    var expirationTokens = new HashSet();    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 testsinternal IEnumerable 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修改视图的默认路径及其实现原理是怎样的了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注行业资讯频道。

0