导语:在日常工作过程中你可能会遇到这样的一种需求,就是在访问同一个页面时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:并非每次FindView都会执行该方法)
        /// </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:每次FindView都会执行该方法)
        /// </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.AreaName ?? "";
                //var areaName =
                //    context.ActionContext.RouteData.Values.ContainsKey("area")
                //    ? context.ActionContext.RouteData.Values["area"].ToString()
                //    : "";

                //控制器名称
                var controllerName = context.ControllerName ?? "";
                //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;
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<MvcViewOptions>(configureOptions =>
            {
                var viewEngines = configureOptions.ViewEngines; //视图引擎
                //可在此处扩展视图引擎
            });

            services.Configure<RazorViewEngineOptions>(configureOptions =>
            {
                configureOptions.ViewLocationExpanders.Add(new TemplateViewLocationExpander()); //视图默认路径扩展
            });
        }

        //该方法用于配置HTTP请求管道
        // 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);
        }
    }
}
ASP.NET Core中RazorViewEngine源码

相关文章: