温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

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

发布时间:2021-09-24 15:16:11 来源:亿速云 阅读:122 作者:柒染 栏目:开发技术

这期内容当中小编将会给大家带来有关ASP.NET Core MVC修改视图的默认路径及其实现原理是怎样的,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。

导语:在日常工作过程中你可能会遇到这样的一种需求,就是在访问同一个页面时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 源码如下所示:

// 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 的初始值:

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

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

上述就是小编为大家分享的ASP.NET Core MVC修改视图的默认路径及其实现原理是怎样的了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注亿速云行业资讯频道。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI