File: Infrastructure\PageViewLocationExpander.cs
Web Access
Project: src\src\Mvc\Mvc.RazorPages\src\Microsoft.AspNetCore.Mvc.RazorPages.csproj (Microsoft.AspNetCore.Mvc.RazorPages)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.Mvc.Razor;
 
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
 
/// <summary>
/// An <see cref="IViewLocationExpander"/> for PageView.
/// </summary>
public class PageViewLocationExpander : IViewLocationExpander
{
    /// <inheritdoc/>
    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
    {
        if ((context.ActionContext.ActionDescriptor is PageActionDescriptor) && !string.IsNullOrEmpty(context.PageName))
        {
            return ExpandPageHierarchy();
        }
 
        // Not a page - just act natural.
        return viewLocations;
 
        IEnumerable<string> ExpandPageHierarchy()
        {
            foreach (var location in viewLocations)
            {
                // For pages, we only handle the 'page' token when it's surrounded by slashes.
                //
                // Explanation:
                //      We need the ability to 'collapse' the segment which requires us to understand slashes.
                //      Imagine a path like /{1}/{0} - we might end up with //{0} if we don't do *something* with
                //      the slashes. Instead of picking on (leading or trailing), we choose both. This seems
                //      less arbitrary.
                //
                //
                // So given a Page like /Account/Manage/Index using /Pages as the root, and the default set of
                // search paths, this will produce the expanded paths:
                //
                //  /Pages/Account/Manage/{0}.cshtml
                //  /Pages/Account/{0}.cshtml
                //  /Pages/{0}.cshtml
                //  /Views/Shared/{0}.cshtml
 
                if (!location.Contains("/{1}/"))
                {
                    // If the location doesn't have the 'page' replacement token just return it as-is.
                    yield return location;
                    continue;
                }
 
                // For locations with the 'page' token - expand them into an ascending directory search,
                // but only up to the pages root.
                //
                // This is easy because the 'page' token already trims the root directory.
                var end = context.PageName.Length;
 
                while (end > 0 && (end = context.PageName.LastIndexOf('/', end - 1)) != -1)
                {
                    // PageName always starts with `/`
                    yield return location.Replace("/{1}/", context.PageName.Substring(0, end + 1));
                }
            }
        }
    }
 
    /// <inheritdoc/>
    public void PopulateValues(ViewLocationExpanderContext context)
    {
        // The value we care about - 'page' is already part of the system. We don't need to add it manually.
    }
}