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

本章将和大家分享如何在ASP.NET Core MVC中修改视图的默认路径,以及它的实现原理。

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

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

本Demo的Web项目为ASP.NET Core Web 应用程序(目标框架为.NET Core 3.1) MVC项目。

首先需要去扩展视图的默认路径,如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor;

namespace NETCoreViewLocationExpander.ViewLocationExtend
{
    /// <summary>
    /// 视图默认路径扩展
    /// </summary>
    public class TemplateViewLocationExpander : IViewLocationExpander
    {
        /// <summary>
        /// 扩展视图默认路径(PS:并非每次请求都会执行该方法)
        /// </summary>
        /// <param name="context"></param>
        /// <param name="viewLocations"></param>
        /// <returns></returns>
        public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
        {
            var template = context.Values["template"] ?? TemplateEnum.Default.ToString();
            if (template == TemplateEnum.WeChatArea.ToString())
            {
                string[] weChatAreaViewLocationFormats = {
                    "/Areas/{2}/WeChatViews/{1}/{0}.cshtml",
                    "/Areas/{2}/WeChatViews/Shared/{0}.cshtml",
                    "/WeChatViews/Shared/{0}.cshtml"
                };
                //weChatAreaViewLocationFormats值在前--优先查找weChatAreaViewLocationFormats(即优先查找移动端目录)
                return weChatAreaViewLocationFormats.Union(viewLocations);
            }
            else if (template == TemplateEnum.WeChat.ToString())
            {
                string[] weChatViewLocationFormats = {
                    "/WeChatViews/{1}/{0}.cshtml",
                    "/WeChatViews/Shared/{0}.cshtml"
                };
                //weChatViewLocationFormats值在前--优先查找weChatViewLocationFormats(即优先查找移动端目录)
                return weChatViewLocationFormats.Union(viewLocations);
            }

            return viewLocations;
        }

        /// <summary>
        /// 往ViewLocationExpanderContext.Values里面添加键值对(PS:每次请求都会执行该方法)
        /// </summary>
        /// <param name="context"></param>
        public void PopulateValues(ViewLocationExpanderContext context)
        {
            var userAgent = context.ActionContext.HttpContext.Request.Headers["User-Agent"].ToString();
            var isMobile = IsMobile(userAgent);
            var template = TemplateEnum.Default.ToString();
            if (isMobile)
            {
                var areaName = //区域名称
                    context.ActionContext.RouteData.Values.ContainsKey("area")
                    ? context.ActionContext.RouteData.Values["area"].ToString()
                    : "";
                var controllerName = //控制器名称
                    context.ActionContext.RouteData.Values.ContainsKey("controller")
                    ? context.ActionContext.RouteData.Values["controller"].ToString()
                    : "";
                if (!string.IsNullOrEmpty(areaName) &&
                    !string.IsNullOrEmpty(controllerName)) //访问的是区域
                {
                    template = TemplateEnum.WeChatArea.ToString();
                }
                else
                {
                    template = TemplateEnum.WeChat.ToString();
                }
            }

            context.Values["template"] = template; //context.Values会参与ViewLookupCache缓存Key(cacheKey)的生成
        }

        /// <summary>
        /// 判断是否是移动端
        /// </summary>
        /// <param name="userAgent"></param>
        /// <returns></returns>
        protected bool IsMobile(string userAgent)
        {
            userAgent = userAgent.ToLower();
            if (userAgent == "" ||
                userAgent.IndexOf("mobile") > -1 ||
                userAgent.IndexOf("mobi") > -1 ||
                userAgent.IndexOf("nokia") > -1 ||
                userAgent.IndexOf("samsung") > -1 ||
                userAgent.IndexOf("sonyericsson") > -1 ||
                userAgent.IndexOf("mot") > -1 ||
                userAgent.IndexOf("blackberry") > -1 ||
                userAgent.IndexOf("lg") > -1 ||
                userAgent.IndexOf("htc") > -1 ||
                userAgent.IndexOf("j2me") > -1 ||
                userAgent.IndexOf("ucweb") > -1 ||
                userAgent.IndexOf("opera mini") > -1 ||
                userAgent.IndexOf("android") > -1 ||
                userAgent.IndexOf("transcoder") > -1)
            {
                return true;
            }

            return false;
        }
    }

    /// <summary>
    /// 模板枚举
    /// </summary>
    public enum TemplateEnum
    {
        Default = 1,
        WeChat = 2,
        WeChatArea = 3
    }
}

接着修改Startup.cs类,如下所示:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using NETCoreViewLocationExpander.ViewLocationExtend;

namespace NETCoreViewLocationExpander
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();

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

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "areas",
                    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

此外,Demo中还准备了两套视图:

其中PC端视图如下所示:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

RazorViewEngine.cs 源码如下所示:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Mvc.Razor
{
    /// <summary>
    /// Default implementation of <see cref="IRazorViewEngine"/>.
    /// </summary>
    /// <remarks>
    /// For <c>ViewResults</c> returned from controllers, views should be located in
    /// <see cref="RazorViewEngineOptions.ViewLocationFormats"/>
    /// by default. For the controllers in an area, views should exist in
    /// <see cref="RazorViewEngineOptions.AreaViewLocationFormats"/>.
    /// </remarks>
    public class RazorViewEngine : IRazorViewEngine
    {
        public static readonly string ViewExtension = ".cshtml";

        private const string AreaKey = "area";
        private const string ControllerKey = "controller";
        private const string PageKey = "page";

        private static readonly TimeSpan _cacheExpirationDuration = TimeSpan.FromMinutes(20);

        private readonly IRazorPageFactoryProvider _pageFactory;
        private readonly IRazorPageActivator _pageActivator;
        private readonly HtmlEncoder _htmlEncoder;
        private readonly ILogger _logger;
        private readonly RazorViewEngineOptions _options;
        private readonly DiagnosticListener _diagnosticListener;

        /// <summary>
        /// Initializes a new instance of the <see cref="RazorViewEngine" />.
        /// </summary>
        public RazorViewEngine(
            IRazorPageFactoryProvider pageFactory,
            IRazorPageActivator pageActivator,
            HtmlEncoder htmlEncoder,
            IOptions<RazorViewEngineOptions> optionsAccessor,
            ILoggerFactory loggerFactory,
            DiagnosticListener diagnosticListener)
        {
            _options = optionsAccessor.Value;

            if (_options.ViewLocationFormats.Count == 0)
            {
                throw new ArgumentException(
                    Resources.FormatViewLocationFormatsIsRequired(nameof(RazorViewEngineOptions.ViewLocationFormats)),
                    nameof(optionsAccessor));
            }

            if (_options.AreaViewLocationFormats.Count == 0)
            {
                throw new ArgumentException(
                    Resources.FormatViewLocationFormatsIsRequired(nameof(RazorViewEngineOptions.AreaViewLocationFormats)),
                    nameof(optionsAccessor));
            }

            _pageFactory = pageFactory;
            _pageActivator = pageActivator;
            _htmlEncoder = htmlEncoder;
            _logger = loggerFactory.CreateLogger<RazorViewEngine>();
            _diagnosticListener = diagnosticListener;
            ViewLookupCache = new MemoryCache(new MemoryCacheOptions());
        }

        /// <summary>
        /// A cache for results of view lookups.
        /// </summary>
        protected IMemoryCache ViewLookupCache { get; }

        /// <summary>
        /// Gets the case-normalized route value for the specified route <paramref name="key"/>.
        /// </summary>
        /// <param name="context">The <see cref="ActionContext"/>.</param>
        /// <param name="key">The route key to lookup.</param>
        /// <returns>The value corresponding to the key.</returns>
        /// <remarks>
        /// The casing of a route value in <see cref="ActionContext.RouteData"/> is determined by the client.
        /// This making constructing paths for view locations in a case sensitive file system unreliable. Using the
        /// <see cref="Abstractions.ActionDescriptor.RouteValues"/> to get route values
        /// produces consistently cased results.
        /// </remarks>
        public static string GetNormalizedRouteValue(ActionContext context, string key)
            => NormalizedRouteValue.GetNormalizedRouteValue(context, key);

        /// <inheritdoc />
        public RazorPageResult FindPage(ActionContext context, string pageName)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (string.IsNullOrEmpty(pageName))
            {
                throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
            }

            if (IsApplicationRelativePath(pageName) || IsRelativePath(pageName))
            {
                // A path; not a name this method can handle.
                return new RazorPageResult(pageName, Enumerable.Empty<string>());
            }

            var cacheResult = LocatePageFromViewLocations(context, pageName, isMainPage: false);
            if (cacheResult.Success)
            {
                var razorPage = cacheResult.ViewEntry.PageFactory();
                return new RazorPageResult(pageName, razorPage);
            }
            else
            {
                return new RazorPageResult(pageName, cacheResult.SearchedLocations);
            }
        }

        /// <inheritdoc />
        public RazorPageResult GetPage(string executingFilePath, string pagePath)
        {
            if (string.IsNullOrEmpty(pagePath))
            {
                throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pagePath));
            }

            if (!(IsApplicationRelativePath(pagePath) || IsRelativePath(pagePath)))
            {
                // Not a path this method can handle.
                return new RazorPageResult(pagePath, Enumerable.Empty<string>());
            }

            var cacheResult = LocatePageFromPath(executingFilePath, pagePath, isMainPage: false);
            if (cacheResult.Success)
            {
                var razorPage = cacheResult.ViewEntry.PageFactory();
                return new RazorPageResult(pagePath, razorPage);
            }
            else
            {
                return new RazorPageResult(pagePath, cacheResult.SearchedLocations);
            }
        }

        /// <inheritdoc />
        public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (string.IsNullOrEmpty(viewName))
            {
                throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName));
            }

            if (IsApplicationRelativePath(viewName) || IsRelativePath(viewName))
            {
                // A path; not a name this method can handle.
                return ViewEngineResult.NotFound(viewName, Enumerable.Empty<string>());
            }

            var cacheResult = LocatePageFromViewLocations(context, viewName, isMainPage);
            return CreateViewEngineResult(cacheResult, viewName);
        }

        /// <inheritdoc />
        public ViewEngineResult GetView(string executingFilePath, string viewPath, bool isMainPage)
        {
            if (string.IsNullOrEmpty(viewPath))
            {
                throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewPath));
            }

            if (!(IsApplicationRelativePath(viewPath) || IsRelativePath(viewPath)))
            {
                // Not a path this method can handle.
                return ViewEngineResult.NotFound(viewPath, Enumerable.Empty<string>());
            }

            var cacheResult = LocatePageFromPath(executingFilePath, viewPath, isMainPage);
            return CreateViewEngineResult(cacheResult, viewPath);
        }

        private ViewLocationCacheResult LocatePageFromPath(string executingFilePath, string pagePath, bool isMainPage)
        {
            var applicationRelativePath = GetAbsolutePath(executingFilePath, pagePath);
            var cacheKey = new ViewLocationCacheKey(applicationRelativePath, isMainPage);
            if (!ViewLookupCache.TryGetValue(cacheKey, out ViewLocationCacheResult cacheResult))
            {
                var expirationTokens = new HashSet<IChangeToken>();
                cacheResult = CreateCacheResult(expirationTokens, applicationRelativePath, isMainPage);

                var cacheEntryOptions = new MemoryCacheEntryOptions();
                cacheEntryOptions.SetSlidingExpiration(_cacheExpirationDuration);
                foreach (var expirationToken in expirationTokens)
                {
                    cacheEntryOptions.AddExpirationToken(expirationToken);
                }

                // No views were found at the specified location. Create a not found result.
                if (cacheResult == null)
                {
                    cacheResult = new ViewLocationCacheResult(new[] { applicationRelativePath });
                }

                cacheResult = ViewLookupCache.Set(
                    cacheKey,
                    cacheResult,
                    cacheEntryOptions);
            }

            return cacheResult;
        }

        private ViewLocationCacheResult LocatePageFromViewLocations(
            ActionContext actionContext,
            string pageName,
            bool isMainPage)
        {
            var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey);
            var areaName = GetNormalizedRouteValue(actionContext, AreaKey);
            string razorPageName = null;
            if (actionContext.ActionDescriptor.RouteValues.ContainsKey(PageKey))
            {
                // Only calculate the Razor Page name if "page" is registered in RouteValues.
                razorPageName = GetNormalizedRouteValue(actionContext, PageKey);
            }

            var expanderContext = new ViewLocationExpanderContext(
                actionContext,
                pageName,
                controllerName,
                areaName,
                razorPageName,
                isMainPage);
            Dictionary<string, string> expanderValues = null;

            var expanders = _options.ViewLocationExpanders;
            // Read interface .Count once rather than per iteration
            var expandersCount = expanders.Count;
            if (expandersCount > 0)
            {
                expanderValues = new Dictionary<string, string>(StringComparer.Ordinal);
                expanderContext.Values = expanderValues;

                // Perf: Avoid allocations
                for (var i = 0; i < expandersCount; i++)
                {
                    expanders[i].PopulateValues(expanderContext);
                }
            }

            var cacheKey = new ViewLocationCacheKey(
                expanderContext.ViewName,
                expanderContext.ControllerName,
                expanderContext.AreaName,
                expanderContext.PageName,
                expanderContext.IsMainPage,
                expanderValues);

            if (!ViewLookupCache.TryGetValue(cacheKey, out ViewLocationCacheResult cacheResult))
            {
                _logger.ViewLookupCacheMiss(cacheKey.ViewName, cacheKey.ControllerName);
                cacheResult = OnCacheMiss(expanderContext, cacheKey);
            }
            else
            {
                _logger.ViewLookupCacheHit(cacheKey.ViewName, cacheKey.ControllerName);
            }

            return cacheResult;
        }

        /// <inheritdoc />
        public string GetAbsolutePath(string executingFilePath, string pagePath)
        {
            if (string.IsNullOrEmpty(pagePath))
            {
                // Path is not valid; no change required.
                return pagePath;
            }

            if (IsApplicationRelativePath(pagePath))
            {
                // An absolute path already; no change required.
                return pagePath;
            }

            if (!IsRelativePath(pagePath))
            {
                // A page name; no change required.
                return pagePath;
            }

            if (string.IsNullOrEmpty(executingFilePath))
            {
                // Given a relative path i.e. not yet application-relative (starting with "~/" or "/"), interpret
                // path relative to currently-executing view, if any.
                // Not yet executing a view. Start in app root.
                var absolutePath = "/" + pagePath;
                return ViewEnginePath.ResolvePath(absolutePath);
            }

            return ViewEnginePath.CombinePath(executingFilePath, pagePath);
        }

        // internal for tests
        internal IEnumerable<string> GetViewLocationFormats(ViewLocationExpanderContext context)
        {
            if (!string.IsNullOrEmpty(context.AreaName) &&
                !string.IsNullOrEmpty(context.ControllerName))
            {
                return _options.AreaViewLocationFormats;
            }
            else if (!string.IsNullOrEmpty(context.ControllerName))
            {
                return _options.ViewLocationFormats;
            }
            else if (!string.IsNullOrEmpty(context.AreaName) &&
                !string.IsNullOrEmpty(context.PageName))
            {
                return _options.AreaPageViewLocationFormats;
            }
            else if (!string.IsNullOrEmpty(context.PageName))
            {
                return _options.PageViewLocationFormats;
            }
            else
            {
                // If we don't match one of these conditions, we'll just treat it like regular controller/action
                // and use those search paths. This is what we did in 1.0.0 without giving much thought to it.
                return _options.ViewLocationFormats;
            }
        }

        private ViewLocationCacheResult OnCacheMiss(
            ViewLocationExpanderContext expanderContext,
            ViewLocationCacheKey cacheKey)
        {
            var viewLocations = GetViewLocationFormats(expanderContext);

            var expanders = _options.ViewLocationExpanders;
            // Read interface .Count once rather than per iteration
            var expandersCount = expanders.Count;
            for (var i = 0; i < expandersCount; i++)
            {
                viewLocations = expanders[i].ExpandViewLocations(expanderContext, viewLocations);
            }

            ViewLocationCacheResult cacheResult = null;
            var searchedLocations = new List<string>();
            var expirationTokens = new HashSet<IChangeToken>();
            foreach (var location in viewLocations)
            {
                var path = string.Format(
                    CultureInfo.InvariantCulture,
                    location,
                    expanderContext.ViewName,
                    expanderContext.ControllerName,
                    expanderContext.AreaName);

                path = ViewEnginePath.ResolvePath(path);

                cacheResult = CreateCacheResult(expirationTokens, path, expanderContext.IsMainPage);
                if (cacheResult != null)
                {
                    break;
                }

                searchedLocations.Add(path);
            }

            // No views were found at the specified location. Create a not found result.
            if (cacheResult == null)
            {
                cacheResult = new ViewLocationCacheResult(searchedLocations);
            }

            var cacheEntryOptions = new MemoryCacheEntryOptions();
            cacheEntryOptions.SetSlidingExpiration(_cacheExpirationDuration);
            foreach (var expirationToken in expirationTokens)
            {
                cacheEntryOptions.AddExpirationToken(expirationToken);
            }

            return ViewLookupCache.Set(cacheKey, cacheResult, cacheEntryOptions);
        }

        // Internal for unit testing
        internal ViewLocationCacheResult CreateCacheResult(
            HashSet<IChangeToken> expirationTokens,
            string relativePath,
            bool isMainPage)
        {
            var factoryResult = _pageFactory.CreateFactory(relativePath);
            var viewDescriptor = factoryResult.ViewDescriptor;
            if (viewDescriptor?.ExpirationTokens != null)
            {
                var viewExpirationTokens = viewDescriptor.ExpirationTokens;
                // Read interface .Count once rather than per iteration
                var viewExpirationTokensCount = viewExpirationTokens.Count;
                for (var i = 0; i < viewExpirationTokensCount; i++)
                {
                    expirationTokens.Add(viewExpirationTokens[i]);
                }
            }

            if (factoryResult.Success)
            {
                // Only need to lookup _ViewStarts for the main page.
                var viewStartPages = isMainPage ?
                    GetViewStartPages(viewDescriptor.RelativePath, expirationTokens) :
                    Array.Empty<ViewLocationCacheItem>();

                return new ViewLocationCacheResult(
                    new ViewLocationCacheItem(factoryResult.RazorPageFactory, relativePath),
                    viewStartPages);
            }

            return null;
        }

        private IReadOnlyList<ViewLocationCacheItem> GetViewStartPages(
            string path,
            HashSet<IChangeToken> expirationTokens)
        {
            var viewStartPages = new List<ViewLocationCacheItem>();

            foreach (var filePath in RazorFileHierarchy.GetViewStartPaths(path))
            {
                var result = _pageFactory.CreateFactory(filePath);
                var viewDescriptor = result.ViewDescriptor;
                if (viewDescriptor?.ExpirationTokens != null)
                {
                    for (var i = 0; i < viewDescriptor.ExpirationTokens.Count; i++)
                    {
                        expirationTokens.Add(viewDescriptor.ExpirationTokens[i]);
                    }
                }

                if (result.Success)
                {
                    // Populate the viewStartPages list so that _ViewStarts appear in the order the need to be
                    // executed (closest last, furthest first). This is the reverse order in which
                    // ViewHierarchyUtility.GetViewStartLocations returns _ViewStarts.
                    viewStartPages.Insert(0, new ViewLocationCacheItem(result.RazorPageFactory, filePath));
                }
            }

            return viewStartPages;
        }

        private ViewEngineResult CreateViewEngineResult(ViewLocationCacheResult result, string viewName)
        {
            if (!result.Success)
            {
                return ViewEngineResult.NotFound(viewName, result.SearchedLocations);
            }

            var page = result.ViewEntry.PageFactory();

            var viewStarts = new IRazorPage[result.ViewStartEntries.Count];
            for (var i = 0; i < viewStarts.Length; i++)
            {
                var viewStartItem = result.ViewStartEntries[i];
                viewStarts[i] = viewStartItem.PageFactory();
            }

            var view = new RazorView(this, _pageActivator, viewStarts, page, _htmlEncoder, _diagnosticListener);
            return ViewEngineResult.Found(viewName, view);
        }

        private static bool IsApplicationRelativePath(string name)
        {
            Debug.Assert(!string.IsNullOrEmpty(name));
            return name[0] == '~' || name[0] == '/';
        }

        private static bool IsRelativePath(string name)
        {
            Debug.Assert(!string.IsNullOrEmpty(name));

            // Though ./ViewName looks like a relative path, framework searches for that view using view locations.
            return name.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase);
        }
    }
}

我们从用于寻找视图的 FindView 方法开始阅读:

/// <inheritdoc />
public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (string.IsNullOrEmpty(viewName))
    {
        throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName));
    }

    if (IsApplicationRelativePath(viewName) || IsRelativePath(viewName))
    {
        // A path; not a name this method can handle.
        return ViewEngineResult.NotFound(viewName, Enumerable.Empty<string>());
    }

    var cacheResult = LocatePageFromViewLocations(context, viewName, isMainPage);
    return CreateViewEngineResult(cacheResult, viewName);
}

接着定位找到LocatePageFromViewLocations 方法:

private ViewLocationCacheResult LocatePageFromViewLocations(
    ActionContext actionContext,
    string pageName,
    bool isMainPage)
{
    var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey);
    var areaName = GetNormalizedRouteValue(actionContext, AreaKey);
    string razorPageName = null;
    if (actionContext.ActionDescriptor.RouteValues.ContainsKey(PageKey))
    {
        // Only calculate the Razor Page name if "page" is registered in RouteValues.
        razorPageName = GetNormalizedRouteValue(actionContext, PageKey);
    }

    var expanderContext = new ViewLocationExpanderContext(
        actionContext,
        pageName,
        controllerName,
        areaName,
        razorPageName,
        isMainPage);
    Dictionary<string, string> expanderValues = null;

    var expanders = _options.ViewLocationExpanders;
    // Read interface .Count once rather than per iteration
    var expandersCount = expanders.Count;
    if (expandersCount > 0)
    {
        expanderValues = new Dictionary<string, string>(StringComparer.Ordinal);
        expanderContext.Values = expanderValues;

        // Perf: Avoid allocations
        for (var i = 0; i < expandersCount; i++)
        {
            expanders[i].PopulateValues(expanderContext);
        }
    }

    var cacheKey = new ViewLocationCacheKey(
        expanderContext.ViewName,
        expanderContext.ControllerName,
        expanderContext.AreaName,
        expanderContext.PageName,
        expanderContext.IsMainPage,
        expanderValues);

    if (!ViewLookupCache.TryGetValue(cacheKey, out ViewLocationCacheResult cacheResult))
    {
        _logger.ViewLookupCacheMiss(cacheKey.ViewName, cacheKey.ControllerName);
        cacheResult = OnCacheMiss(expanderContext, cacheKey);
    }
    else
    {
        _logger.ViewLookupCacheHit(cacheKey.ViewName, cacheKey.ControllerName);
    }

    return cacheResult;
}

从此处可以看出,每次查找视图的时候都会调用 ViewLocationExpander.PopulateValues 方法,并且最终的这个 expanderValues 会参与ViewLookupCache 缓存key(cacheKey)的生成。

此外还可以看出,如果从 ViewLookupCache 这个缓存中能找到数据的话,它就直接返回了,不会再去调用ViewLocationExpander.ExpandViewLocations 方法。

这也就解释了为什么我们Demo中是在 PopulateValues 方法里面去设置context.Values["template"] 的值,而不是直接在 ExpandViewLocations 方法里面去设置这个值。

下面我们接着找到用于生成 cacheKey 的ViewLocationCacheKey 类,如下所示:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Mvc.Razor
{
    /// <summary>
    /// Key for entries in <see cref="RazorViewEngine.ViewLookupCache"/>.
    /// </summary>
    internal readonly struct ViewLocationCacheKey : IEquatable<ViewLocationCacheKey>
    {
        /// <summary>
        /// Initializes a new instance of <see cref="ViewLocationCacheKey"/>.
        /// </summary>
        /// <param name="viewName">The view name or path.</param>
        /// <param name="isMainPage">Determines if the page being found is the main page for an action.</param>
        public ViewLocationCacheKey(
            string viewName,
            bool isMainPage)
            : this(
                  viewName,
                  controllerName: null,
                  areaName: null,
                  pageName: null,
                  isMainPage: isMainPage,
                  values: null)
        {
        }

        /// <summary>
        /// Initializes a new instance of <see cref="ViewLocationCacheKey"/>.
        /// </summary>
        /// <param name="viewName">The view name.</param>
        /// <param name="controllerName">The controller name.</param>
        /// <param name="areaName">The area name.</param>
        /// <param name="pageName">The page name.</param>
        /// <param name="isMainPage">Determines if the page being found is the main page for an action.</param>
        /// <param name="values">Values from <see cref="IViewLocationExpander"/> instances.</param>
        public ViewLocationCacheKey(
            string viewName,
            string controllerName,
            string areaName,
            string pageName,
            bool isMainPage,
            IReadOnlyDictionary<string, string> values)
        {
            ViewName = viewName;
            ControllerName = controllerName;
            AreaName = areaName;
            PageName = pageName;
            IsMainPage = isMainPage;
            ViewLocationExpanderValues = values;
        }

        /// <summary>
        /// Gets the view name.
        /// </summary>
        public string ViewName { get; }

        /// <summary>
        /// Gets the controller name.
        /// </summary>
        public string ControllerName { get; }

        /// <summary>
        /// Gets the area name.
        /// </summary>
        public string AreaName { get; }

        /// <summary>
        /// Gets the page name.
        /// </summary>
        public string PageName { get; }

        /// <summary>
        /// Determines if the page being found is the main page for an action.
        /// </summary>
        public bool IsMainPage { get; }

        /// <summary>
        /// Gets the values populated by <see cref="IViewLocationExpander"/> instances.
        /// </summary>
        public IReadOnlyDictionary<string, string> ViewLocationExpanderValues { get; }

        /// <inheritdoc />
        public bool Equals(ViewLocationCacheKey y)
        {
            if (IsMainPage != y.IsMainPage ||
                !string.Equals(ViewName, y.ViewName, StringComparison.Ordinal) ||
                !string.Equals(ControllerName, y.ControllerName, StringComparison.Ordinal) ||
                !string.Equals(AreaName, y.AreaName, StringComparison.Ordinal) ||
                !string.Equals(PageName, y.PageName, StringComparison.Ordinal))
            {
                return false;
            }

            if (ReferenceEquals(ViewLocationExpanderValues, y.ViewLocationExpanderValues))
            {
                return true;
            }

            if (ViewLocationExpanderValues == null ||
                y.ViewLocationExpanderValues == null ||
                (ViewLocationExpanderValues.Count != y.ViewLocationExpanderValues.Count))
            {
                return false;
            }

            foreach (var item in ViewLocationExpanderValues)
            {
                if (!y.ViewLocationExpanderValues.TryGetValue(item.Key, out var yValue) ||
                    !string.Equals(item.Value, yValue, StringComparison.Ordinal))
                {
                    return false;
                }
            }

            return true;
        }

        /// <inheritdoc />
        public override bool Equals(object obj)
        {
            if (obj is ViewLocationCacheKey)
            {
                return Equals((ViewLocationCacheKey)obj);
            }

            return false;
        }

        /// <inheritdoc />
        public override int GetHashCode()
        {
            var hashCodeCombiner = HashCodeCombiner.Start();
            hashCodeCombiner.Add(IsMainPage ? 1 : 0);
            hashCodeCombiner.Add(ViewName, StringComparer.Ordinal);
            hashCodeCombiner.Add(ControllerName, StringComparer.Ordinal);
            hashCodeCombiner.Add(AreaName, StringComparer.Ordinal);
            hashCodeCombiner.Add(PageName, StringComparer.Ordinal);

            if (ViewLocationExpanderValues != null)
            {
                foreach (var item in ViewLocationExpanderValues)
                {
                    hashCodeCombiner.Add(item.Key, StringComparer.Ordinal);
                    hashCodeCombiner.Add(item.Value, StringComparer.Ordinal);
                }
            }

            return hashCodeCombiner;
        }
    }
}

我们重点来看下其中的 Equals 方法,如下所示:

/// <inheritdoc />
public bool Equals(ViewLocationCacheKey y)
{
    if (IsMainPage != y.IsMainPage ||
        !string.Equals(ViewName, y.ViewName, StringComparison.Ordinal) ||
        !string.Equals(ControllerName, y.ControllerName, StringComparison.Ordinal) ||
        !string.Equals(AreaName, y.AreaName, StringComparison.Ordinal) ||
        !string.Equals(PageName, y.PageName, StringComparison.Ordinal))
    {
        return false;
    }

    if (ReferenceEquals(ViewLocationExpanderValues, y.ViewLocationExpanderValues))
    {
        return true;
    }

    if (ViewLocationExpanderValues == null ||
        y.ViewLocationExpanderValues == null ||
        (ViewLocationExpanderValues.Count != y.ViewLocationExpanderValues.Count))
    {
        return false;
    }

    foreach (var item in ViewLocationExpanderValues)
    {
        if (!y.ViewLocationExpanderValues.TryGetValue(item.Key, out var yValue) ||
            !string.Equals(item.Value, yValue, StringComparison.Ordinal))
        {
            return false;
        }
    }

    return true;
}

从此处可以看出,如果 expanderValues 字典中 键/值对的数目不同或者其中任意一个值不同,那么这个 cacheKey 就是不同的。

我们继续往下分析, 从上文中我们知道,如果从ViewLookupCache 缓存中没有找到数据,那么它就会执行OnCacheMiss 方法。

我们找到OnCacheMiss 方法,如下所示:

private ViewLocationCacheResult OnCacheMiss(
    ViewLocationExpanderContext expanderContext,
    ViewLocationCacheKey cacheKey)
{
    var viewLocations = GetViewLocationFormats(expanderContext);

    var expanders = _options.ViewLocationExpanders;
    // Read interface .Count once rather than per iteration
    var expandersCount = expanders.Count;
    for (var i = 0; i < expandersCount; i++)
    {
        viewLocations = expanders[i].ExpandViewLocations(expanderContext, viewLocations);
    }

    ViewLocationCacheResult cacheResult = null;
    var searchedLocations = new List<string>();
    var expirationTokens = new HashSet<IChangeToken>();
    foreach (var location in viewLocations)
    {
        var path = string.Format(
            CultureInfo.InvariantCulture,
            location,
            expanderContext.ViewName,
            expanderContext.ControllerName,
            expanderContext.AreaName);

        path = ViewEnginePath.ResolvePath(path);

        cacheResult = CreateCacheResult(expirationTokens, path, expanderContext.IsMainPage);
        if (cacheResult != null)
        {
            break;
        }

        searchedLocations.Add(path);
    }

    // No views were found at the specified location. Create a not found result.
    if (cacheResult == null)
    {
        cacheResult = new ViewLocationCacheResult(searchedLocations);
    }

    var cacheEntryOptions = new MemoryCacheEntryOptions();
    cacheEntryOptions.SetSlidingExpiration(_cacheExpirationDuration);
    foreach (var expirationToken in expirationTokens)
    {
        cacheEntryOptions.AddExpirationToken(expirationToken);
    }

    return ViewLookupCache.Set(cacheKey, cacheResult, cacheEntryOptions);
}

仔细观察之后你就会发现:

1、首先它是通过GetViewLocationFormats 方法获取初始的 viewLocations视图位置集合。

2、接着它会按顺序依次调用所有的ViewLocationExpander.ExpandViewLocations 方法,经过一系列聚合操作后得到最终的viewLocations 视图位置集合。

3、然后遍历 viewLocations 视图位置集合,按顺序依次去指定的路径中查找对应的视图,只要找到符合条件的第一个视图就结束循环,不再往下查找,最后设置缓存返回结果。

4、视图位置字符串(例如:“/Areas/{2}/WeChatViews/{1}/{0}.cshtml”)中的占位符含义:“{0}” 表示视图名称,“{1}” 表示控制器名称,“{2}” 表示区域名称。

下面我们继续找到GetViewLocationFormats 方法,如下所示:

// internal for tests
internal IEnumerable<string> GetViewLocationFormats(ViewLocationExpanderContext context)
{
    if (!string.IsNullOrEmpty(context.AreaName) &&
        !string.IsNullOrEmpty(context.ControllerName))
    {
        return _options.AreaViewLocationFormats;
    }
    else if (!string.IsNullOrEmpty(context.ControllerName))
    {
        return _options.ViewLocationFormats;
    }
    else if (!string.IsNullOrEmpty(context.AreaName) &&
        !string.IsNullOrEmpty(context.PageName))
    {
        return _options.AreaPageViewLocationFormats;
    }
    else if (!string.IsNullOrEmpty(context.PageName))
    {
        return _options.PageViewLocationFormats;
    }
    else
    {
        // If we don't match one of these conditions, we'll just treat it like regular controller/action
        // and use those search paths. This is what we did in 1.0.0 without giving much thought to it.
        return _options.ViewLocationFormats;
    }
}

从此处可以看出,它是通过判断 区域名称和控制器名称 是否都不为空,以此来判断客户端访问的到底是区域还是非区域。

文章最后我们通过调试来看下AreaViewLocationFormats 和ViewLocationFormats 的初始值:

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

Demo源码:

链接: https://pan.baidu.com/s/1gn4JQTzn7hQLgfAtaUPDLg

提取码: mjgr

到此这篇关于ASP.NET Core MVC 修改视图的默认路径及其实现原理的文章就介绍到这了,更多相关ASP.NET Core MVC 视图路径内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • ASP.NET Core MVC基础学习之局部视图(Partial Views)

    1.什么是局部视图 局部视图是在其他视图中呈现的视图.通过执行局部视图生成的HTML输出呈现在调用视图中.与视图一样,局部视图使用 .cshtml 文件扩展名.当希望在不同视图之间共享网页的可重用部分时,就可以使用局部视图. 2.什么时候使用局部视图 局部视图是将大视图分成小组件的有效方法.通用的布局元素应在 _Layout.cshtml 中指定,非布局可重用内容可以封装成局部视图. 如果一个由几个逻辑部分组成的复杂页面,那么将每个逻辑部分作为局部视图是很有用.布局视图与普通视图之间没有语义差别

  • ASP.NET Core MVC通过IViewLocationExpander扩展视图搜索路径的实现

    IViewLocationExpander API ExpandViewLocations Razor视图路径,视图引擎会搜索该路径. PopulateValues 每次调用都会填充路由 项目目录如下所示 创建区域扩展器,其实我并不需要多区域,我目前只需要达到一个区域中有多个文件夹进行存放我的视图. 所以我通过实现IViewLocationExpander进行扩展添加我自定义视图路径规则即可正如下代码片段 public class MyViewLocationExpander : IViewLo

  • asp.net core mvc权限控制:在视图中控制操作权限

    在asp.net core mvc中提供了权限验证框架,前面的文章中已经介绍了如何进行权限控制配置,权限配置好后,权限验证逻辑自动就会执行,但是在某些情况下,我们可能需要在代码里或者视图中通过手工方式判断权限,我们现在就来介绍下具体的操作方法. 如果在控制器方法里想要判断当前用户是否具有某个权限,可以直接使用HttpContext.User.HasClaim(string cliamtype,string cliamvalue)方法进行判断,该方法返回bool类型,返回true表示具有权限,否则

  • ASP.NET Core MVC学习之视图组件(View Component)

    1.视图组件介绍 视图组件是 ASP.NET Core MVC 的新特性,类似于局部视图,但它更强大.视图组件不使用模型绑定,并且仅依赖于调用它时所提供的数据. 视图组件特点: 呈块状,而不是整个响应 包括在控制器和视图之间发现的相同的关注点和可测试性优点 可以拥有参数和业务逻辑 通常从布局页面调用 视图组件可以用在任何需要重复逻辑且对局部视图来说过于复杂的情况,例如: 动态导航菜单 标签云(需要查询数据库) 登录面板 购物车 最近发表的文章 典型博客上的侧边栏内容 将在每个页面上呈现的登录面板

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

    本章将和大家分享如何在ASP.NET Core MVC中修改视图的默认路径,以及它的实现原理. 导语:在日常工作过程中你可能会遇到这样的一种需求,就是在访问同一个页面时PC端和移动端显示的内容和风格是不一样(类似两个不一样的主题),但是它们的后端代码又是差不多的,此时我们就希望能够使用同一套后端代码,然后由系统自动去判断到底是PC端访问还是移动端访问,如果是移动端访问就优先匹配移动端的视图,在没有匹配到的情况下才去匹配PC端的视图. 下面我们就来看下这个功能要如何实现,Demo的目录结构如下所示

  • ASP.NET Core MVC在视图中使用依赖注入

    ASP.NET Core 支持在试图中使用依赖注入.这将有助于提供视图专用的服务,比如本地化或者仅用于填充视图元素的数据.应尽量保持控制器和视图之间的关注点分离.视图所显示的大部分数据应该从控制器传入. 使用 @inject 指令将服务注入到视图,语法 @inject <type> <name>,例如: @model MVCTest.Models.Operation @using MVCTest.Services @inject BaseInfoServices BaseInfoS

  • ASP.NET Core MVC 过滤器的使用方法介绍

    过滤器的作用是在 Action 方法执行前或执行后做一些加工处理.使用过滤器可以避免Action方法的重复代码,例如,您可以使用异常过滤器合并异常处理的代码. 过滤器如何工作? 过滤器在 MVC Action 调用管道中运行,有时称为过滤器管道.MVC选择要执行的Action方法后,才会执行过滤器管道: 实现 过滤器同时支持同步和异步两种不同的接口定义.您可以根据执行的任务类型,选择同步或异步实现. 同步过滤器定义OnStageExecuting和OnStageExecuted方法,会在管道特定

  • 详解ASP.NET Core MVC 源码学习:Routing 路由

    前言 最近打算抽时间看一下 ASP.NET Core MVC 的源码,特此把自己学习到的内容记录下来,也算是做个笔记吧. 路由作为 MVC 的基本部分,所以在学习 MVC 的其他源码之前还是先学习一下路由系统,ASP.NET Core 的路由系统相对于以前的 Mvc 变化很大,它重新整合了 Web Api 和 MVC. 路由源码地址 :Routing-dev_jb51.rar 路由(Routing)功能介绍 路由是 MVC 的一个重要组成部分,它主要负责将接收到的 Http 请求映射到具体的一个

  • ASP.NET Core MVC中过滤器工作原理介绍

    过滤器的作用是在 Action 方法执行前或执行后做一些加工处理.使用过滤器可以避免Action方法的重复代码,例如,您可以使用异常过滤器合并异常处理的代码. 过滤器如何工作? 过滤器在 MVC Action 调用管道中运行,有时称为过滤器管道.MVC选择要执行的Action方法后,才会执行过滤器管道: 实现 过滤器同时支持同步和异步两种不同的接口定义.您可以根据执行的任务类型,选择同步或异步实现. 同步过滤器定义OnStageExecuting和OnStageExecuted方法,会在管道特定

  • ASP.NET Core MVC中的视图(Views)

    目录 1.什么是视图 2.创建视图 3.控制器指定视图 4.给视图传递数据 1.弱类型数据 2.动态视图 5.更多视图特性 ASP.NET Core MVC 控制器可以使用视图返回格式化的结果. 1.什么是视图 在 MVC 中,视图封装了用户与应用交互呈现细节.视图是具有生成要发送到客户端内容的,包含嵌入代码的HTML模板.视图使用使用 Razor 语法,该语法允许以最少的代码或复杂度与 HTML 进行交互. ASP.NET Core MVC 视图默认以 .cshtml 文件保存在应用程序的 V

  • 详解ASP.NET Core MVC四种枚举绑定方式

    前言 本节我们来讲讲在ASP.NET Core MVC又为我们提供了哪些方便,之前我们探讨过在ASP.NET MVC中下拉框绑定方式,这节我们来再来重点看看枚举绑定的方式,充分实现你所能想到的场景,满满的干货,你值得拥有. 探讨枚举绑定方式 我们首先给出要绑定的枚举类. public enum Language { JavaScript, Java, C, Python, SQL, Oracle } 枚举绑定方式一(@Html.DropDownList) 接下来我们废话少说直接进入主题. 复制代

随机推荐