Implement view search for pages
The View Engine now needs to know about pages :(. This isn't ideal but the view engine needs to know what set of search paths to use. This was already hardcoded for controllers vs controllers + areas. It felt right to further hardcode instead of introduce a wierd abstraction that we only use. Additionally pages use a view location expander to implement an ascending directory search.
This commit is contained in:
parent
c56b64fc41
commit
a8eb5bee70
|
|
@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
|||
viewName,
|
||||
controllerName: null,
|
||||
areaName: null,
|
||||
pageName: null,
|
||||
isMainPage: isMainPage,
|
||||
values: null)
|
||||
{
|
||||
|
|
@ -35,18 +36,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
|||
/// <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;
|
||||
}
|
||||
|
|
@ -66,6 +70,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
|||
/// </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>
|
||||
|
|
@ -82,7 +91,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
|||
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(AreaName, y.AreaName, StringComparison.Ordinal) ||
|
||||
!string.Equals(PageName, y.PageName, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -131,6 +141,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
|||
hashCodeCombiner.Add(ViewName, StringComparer.Ordinal);
|
||||
hashCodeCombiner.Add(ControllerName, StringComparer.Ordinal);
|
||||
hashCodeCombiner.Add(AreaName, StringComparer.Ordinal);
|
||||
hashCodeCombiner.Add(PageName, StringComparer.Ordinal);
|
||||
|
||||
if (ViewLocationExpanderValues != null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using System.Globalization;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
|
|
@ -33,8 +32,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
public static readonly string ViewExtension = ".cshtml";
|
||||
private const string ViewStartFileName = "_ViewStart.cshtml";
|
||||
|
||||
private const string ControllerKey = "controller";
|
||||
private const string AreaKey = "area";
|
||||
private const string ControllerKey = "controller";
|
||||
private const string PageKey = "page";
|
||||
|
||||
private const string ParentDirectoryToken = "..";
|
||||
private static readonly TimeSpan _cacheExpirationDuration = TimeSpan.FromMinutes(20);
|
||||
private static readonly char[] _pathSeparators = new[] { '/', '\\' };
|
||||
|
|
@ -272,11 +273,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
{
|
||||
var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey);
|
||||
var areaName = GetNormalizedRouteValue(actionContext, AreaKey);
|
||||
var razorPageName = GetNormalizedRouteValue(actionContext, PageKey);
|
||||
var expanderContext = new ViewLocationExpanderContext(
|
||||
actionContext,
|
||||
pageName,
|
||||
controllerName,
|
||||
areaName,
|
||||
razorPageName,
|
||||
isMainPage);
|
||||
Dictionary<string, string> expanderValues = null;
|
||||
|
||||
|
|
@ -296,6 +299,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
expanderContext.ViewName,
|
||||
expanderContext.ControllerName,
|
||||
expanderContext.AreaName,
|
||||
expanderContext.PageName,
|
||||
expanderContext.IsMainPage,
|
||||
expanderValues);
|
||||
|
||||
|
|
@ -396,14 +400,35 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
return builder.ToString();
|
||||
}
|
||||
|
||||
// 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.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)
|
||||
{
|
||||
// Only use the area view location formats if we have an area token.
|
||||
IEnumerable<string> viewLocations = !string.IsNullOrEmpty(expanderContext.AreaName) ?
|
||||
_options.AreaViewLocationFormats :
|
||||
_options.ViewLocationFormats;
|
||||
var viewLocations = GetViewLocationFormats(expanderContext);
|
||||
|
||||
for (var i = 0; i < _options.ViewLocationExpanders.Count; i++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -92,6 +92,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
/// </remarks>
|
||||
public IList<string> AreaViewLocationFormats { get; } = new List<string>();
|
||||
|
||||
public IList<string> PageViewLocationFormats { get; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="MetadataReference" /> instances that should be included in Razor compilation, along with
|
||||
/// those discovered by <see cref="MetadataReferenceFeatureProvider" />s.
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
/// <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>
|
||||
public ViewLocationExpanderContext(
|
||||
ActionContext actionContext,
|
||||
string viewName,
|
||||
string controllerName,
|
||||
string areaName,
|
||||
string pageName,
|
||||
bool isMainPage)
|
||||
{
|
||||
if (actionContext == null)
|
||||
|
|
@ -40,9 +42,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
ViewName = viewName;
|
||||
ControllerName = controllerName;
|
||||
AreaName = areaName;
|
||||
PageName = pageName;
|
||||
IsMainPage = isMainPage;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Mvc.ActionContext"/> for the current executing action.
|
||||
/// </summary>
|
||||
|
|
@ -58,6 +61,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
/// </summary>
|
||||
public string ControllerName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the page name. This will be the value of the <c>page</c> route value when rendering a Page from the
|
||||
/// Razor Pages framework. This value will be <c>null</c> if rendering a view as the result of a controller.
|
||||
/// </summary>
|
||||
public string PageName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the area name.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions;
|
|||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
|
||||
|
|
@ -71,6 +72,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
// Options
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IConfigureOptions<RazorPagesOptions>, RazorPagesOptionsSetup>());
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IConfigureOptions<RazorViewEngineOptions>, RazorPagesRazorViewEngineOptionsSetup>());
|
||||
|
||||
// Action description and invocation
|
||||
services.TryAddEnumerable(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
// 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.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||
{
|
||||
public class PageViewLocationExpander : IViewLocationExpander
|
||||
{
|
||||
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
|
||||
{
|
||||
if (string.IsNullOrEmpty(context.PageName))
|
||||
{
|
||||
// Not a page - just act natural.
|
||||
return viewLocations;
|
||||
}
|
||||
|
||||
return ExpandPageHierarchy();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// 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.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||
{
|
||||
public class RazorPagesRazorViewEngineOptionsSetup : IConfigureOptions<RazorViewEngineOptions>
|
||||
{
|
||||
private readonly IOptions<RazorPagesOptions> _pagesOptions;
|
||||
|
||||
public RazorPagesRazorViewEngineOptionsSetup(IOptions<RazorPagesOptions> pagesOptions)
|
||||
{
|
||||
_pagesOptions = pagesOptions;
|
||||
}
|
||||
|
||||
public void Configure(RazorViewEngineOptions options)
|
||||
{
|
||||
Debug.Assert(_pagesOptions.Value.RootDirectory.Length > 0);
|
||||
|
||||
if (_pagesOptions.Value.RootDirectory == "/")
|
||||
{
|
||||
options.PageViewLocationFormats.Add("/{1}/{0}" + RazorViewEngine.ViewExtension);
|
||||
}
|
||||
else
|
||||
{
|
||||
options.PageViewLocationFormats.Add(_pagesOptions.Value.RootDirectory + "/{1}/{0}" + RazorViewEngine.ViewExtension);
|
||||
}
|
||||
|
||||
options.PageViewLocationFormats.Add("/Views/Shared/{0}" + RazorViewEngine.ViewExtension);
|
||||
|
||||
options.ViewLocationExpanders.Add(new PageViewLocationExpander());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
// 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.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
{
|
||||
public class RazorPagesViewSearchTest : IClassFixture<MvcTestFixture<RazorPagesWebSite.Startup>>
|
||||
{
|
||||
public RazorPagesViewSearchTest(MvcTestFixture<RazorPagesWebSite.Startup> fixture)
|
||||
{
|
||||
Client = fixture.Client;
|
||||
}
|
||||
|
||||
public HttpClient Client { get; }
|
||||
|
||||
[Fact]
|
||||
public async Task Page_CanFindPartial_InCurrentDirectory()
|
||||
{
|
||||
// Arrange & Act
|
||||
var content = await Client.GetStringAsync("http://localhost/Pages/ViewSearch?partial=_Sibling");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Hello from sibling", content.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Page_CanFindPartial_InParentDirectory()
|
||||
{
|
||||
// Arrange & Act
|
||||
var content = await Client.GetStringAsync("http://localhost/Pages/ViewSearch?partial=_Parent");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Hello from parent", content.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Page_CanFindPartial_InRootDirectory()
|
||||
{
|
||||
// Arrange & Act
|
||||
var content = await Client.GetStringAsync("http://localhost/Pages/ViewSearch?partial=_Root");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Hello from root", content.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Page_CanFindPartial_InViewsSharedDirectory()
|
||||
{
|
||||
// Arrange & Act
|
||||
var content = await Client.GetStringAsync("http://localhost/Pages/ViewSearch?partial=_Shared");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Hello from shared", content.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -137,6 +137,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
"testView",
|
||||
"test-controller",
|
||||
"",
|
||||
null,
|
||||
false);
|
||||
var languageViewLocationExpander = new LanguageViewLocationExpander(format);
|
||||
viewLocationExpanderContext.Values = new Dictionary<string, string>();
|
||||
|
|
@ -161,6 +162,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
"testView",
|
||||
"test-controller",
|
||||
"test-area",
|
||||
null,
|
||||
false);
|
||||
var languageViewLocationExpander = new LanguageViewLocationExpander();
|
||||
viewLocationExpanderContext.Values = new Dictionary<string, string>();
|
||||
|
|
@ -188,6 +190,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
"testView",
|
||||
"test-controller",
|
||||
"test-area",
|
||||
null,
|
||||
false);
|
||||
var languageViewLocationExpander = new LanguageViewLocationExpander();
|
||||
viewLocationExpanderContext.Values = new Dictionary<string, string>();
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
|||
{"controller", "bar"},
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, object> _pageTestContext = new Dictionary<string, object>()
|
||||
{
|
||||
{"page", "/Accounts/Index"},
|
||||
};
|
||||
|
||||
public static IEnumerable<string[]> AbsoluteViewPathData
|
||||
{
|
||||
get
|
||||
|
|
@ -781,6 +786,43 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
|||
pageFactory.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindView_CachesValuesIfViewWasFound_ForPages()
|
||||
{
|
||||
// Arrange
|
||||
var page = Mock.Of<IRazorPage>();
|
||||
var pageFactory = new Mock<IRazorPageFactoryProvider>();
|
||||
pageFactory
|
||||
.Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml"))
|
||||
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]))
|
||||
.Verifiable();
|
||||
|
||||
var viewEngine = CreateViewEngine(pageFactory.Object);
|
||||
var context = GetActionContext(_pageTestContext);
|
||||
|
||||
// Act 1
|
||||
var result1 = viewEngine.FindView(context, "baz", isMainPage: false);
|
||||
|
||||
// Assert 1
|
||||
Assert.True(result1.Success);
|
||||
var view1 = Assert.IsType<RazorView>(result1.View);
|
||||
Assert.Same(page, view1.RazorPage);
|
||||
pageFactory.Verify();
|
||||
|
||||
// Act 2
|
||||
pageFactory
|
||||
.Setup(p => p.CreateFactory(It.IsAny<string>()))
|
||||
.Throws(new Exception("Shouldn't be called"));
|
||||
|
||||
var result2 = viewEngine.FindView(context, "baz", isMainPage: false);
|
||||
|
||||
// Assert 2
|
||||
Assert.True(result2.Success);
|
||||
var view2 = Assert.IsType<RazorView>(result2.View);
|
||||
Assert.Same(page, view2.RazorPage);
|
||||
pageFactory.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindView_InvokesPageFactoryIfChangeTokenExpired()
|
||||
{
|
||||
|
|
@ -1608,6 +1650,108 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetViewLocationFormats_ForControllerWithoutArea_ReturnsDefaultSet()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new string[] { "expected", };
|
||||
|
||||
var viewEngine = new TestableRazorViewEngine(
|
||||
Mock.Of<IRazorPageFactoryProvider>(),
|
||||
GetOptionsAccessor(viewLocationFormats: expected));
|
||||
|
||||
var context = new ViewLocationExpanderContext(
|
||||
new ActionContext(),
|
||||
"Index.cshtml",
|
||||
controllerName: "Home",
|
||||
areaName: null,
|
||||
pageName: "ignored",
|
||||
isMainPage: true);
|
||||
|
||||
// Act
|
||||
var actual = viewEngine.GetViewLocationFormats(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetViewLocationFormats_ForControllerWithArea_ReturnsAreaSet()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new string[] { "expected", };
|
||||
|
||||
var viewEngine = new TestableRazorViewEngine(
|
||||
Mock.Of<IRazorPageFactoryProvider>(),
|
||||
GetOptionsAccessor(areaViewLocationFormats: expected));
|
||||
|
||||
var context = new ViewLocationExpanderContext(
|
||||
new ActionContext(),
|
||||
"Index.cshtml",
|
||||
controllerName: "Home",
|
||||
areaName: "Admin",
|
||||
pageName: "ignored",
|
||||
isMainPage: true);
|
||||
|
||||
// Act
|
||||
var actual = viewEngine.GetViewLocationFormats(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetViewLocationFormats_ForPage_ReturnsPageSet()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new string[] { "expected", };
|
||||
|
||||
var viewEngine = new TestableRazorViewEngine(
|
||||
Mock.Of<IRazorPageFactoryProvider>(),
|
||||
GetOptionsAccessor(pageViewLocationFormats: expected));
|
||||
|
||||
var context = new ViewLocationExpanderContext(
|
||||
new ActionContext(),
|
||||
"Index.cshtml",
|
||||
controllerName: null,
|
||||
areaName: null,
|
||||
pageName: "/Some/Page",
|
||||
isMainPage: true);
|
||||
|
||||
// Act
|
||||
var actual = viewEngine.GetViewLocationFormats(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
// This isn't a real case we expect to hit in an app, just making sure we have a reasonable default
|
||||
// for a weird configuration. In this case we preserve what we did in 1.0.0.
|
||||
[Fact]
|
||||
public void GetViewLocationFormats_NoRouteValues_ReturnsDefaultSet()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new string[] { "expected", };
|
||||
|
||||
var viewEngine = new TestableRazorViewEngine(
|
||||
Mock.Of<IRazorPageFactoryProvider>(),
|
||||
GetOptionsAccessor(viewLocationFormats: expected));
|
||||
|
||||
var context = new ViewLocationExpanderContext(
|
||||
new ActionContext(),
|
||||
"Index.cshtml",
|
||||
controllerName: null,
|
||||
areaName: null,
|
||||
pageName: null,
|
||||
isMainPage: true);
|
||||
|
||||
// Act
|
||||
var actual = viewEngine.GetViewLocationFormats(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
// Return RazorViewEngine with a page factory provider that is always successful.
|
||||
private RazorViewEngine CreateSuccessfulViewEngine()
|
||||
{
|
||||
|
|
@ -1635,14 +1779,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
|||
private static IOptions<RazorViewEngineOptions> GetOptionsAccessor(
|
||||
IEnumerable<IViewLocationExpander> expanders = null,
|
||||
IEnumerable<string> viewLocationFormats = null,
|
||||
IEnumerable<string> areaViewLocationFormats = null)
|
||||
IEnumerable<string> areaViewLocationFormats = null,
|
||||
IEnumerable<string> pageViewLocationFormats = null)
|
||||
{
|
||||
#pragma warning disable 0618
|
||||
var optionsSetup = new RazorViewEngineOptionsSetup(Mock.Of<IHostingEnvironment>());
|
||||
#pragma warning restore 0618
|
||||
|
||||
var options = new RazorViewEngineOptions();
|
||||
optionsSetup.Configure(options);
|
||||
options.PageViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
|
||||
|
||||
if (expanders != null)
|
||||
{
|
||||
|
|
@ -1672,6 +1816,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
|||
}
|
||||
}
|
||||
|
||||
if (pageViewLocationFormats != null)
|
||||
{
|
||||
options.PageViewLocationFormats.Clear();
|
||||
|
||||
foreach (var location in pageViewLocationFormats)
|
||||
{
|
||||
options.PageViewLocationFormats.Add(location);
|
||||
}
|
||||
}
|
||||
|
||||
var optionsAccessor = new Mock<IOptions<RazorViewEngineOptions>>();
|
||||
optionsAccessor
|
||||
.SetupGet(v => v.Value)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||
{
|
||||
public class PageViewLocationExpanderTest
|
||||
{
|
||||
[Fact]
|
||||
public void PopulateValues_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext();
|
||||
|
||||
var expander = new PageViewLocationExpander();
|
||||
|
||||
// Act
|
||||
expander.PopulateValues(context);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(context.Values);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpandLocations_NoOp_ForNonPage()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(pageName: null);
|
||||
var locations = new string[]
|
||||
{
|
||||
"/ignore-me",
|
||||
};
|
||||
|
||||
var expander = new PageViewLocationExpander();
|
||||
|
||||
// Act
|
||||
var actual = expander.ExpandViewLocations(context, locations);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(locations, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpandLocations_NoOp_WhenLocationDoesNotContainPageToken()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(pageName: null);
|
||||
var locations = new string[]
|
||||
{
|
||||
"/ignore-me",
|
||||
};
|
||||
|
||||
var expander = new PageViewLocationExpander();
|
||||
|
||||
// Act
|
||||
var actual = expander.ExpandViewLocations(context, locations);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(locations, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/Index", new string[] { "/{0}.cshtml" })]
|
||||
[InlineData("/Edit", new string[] { "/{0}.cshtml" })]
|
||||
[InlineData("/Customers/Add", new string[] { "/Customers/{0}.cshtml", "/{0}.cshtml" })]
|
||||
public void ExpandLocations_ExpandsDirectories_WhenLocationContainsPage(
|
||||
string pageName,
|
||||
string[] expected)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(pageName: pageName);
|
||||
|
||||
var locations = new string[]
|
||||
{
|
||||
"/{1}/{0}.cshtml",
|
||||
};
|
||||
|
||||
var expander = new PageViewLocationExpander();
|
||||
|
||||
// Act
|
||||
var actual = expander.ExpandViewLocations(context, locations);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpandLocations_ExpandsDirectories_MultipleLocations()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(pageName: "/Customers/Edit");
|
||||
|
||||
var locations = new string[]
|
||||
{
|
||||
"/Pages/{1}/{0}.cshtml",
|
||||
"/More/Paths/{1}/{0}.cshtml",
|
||||
"/Views/Shared/{0}.cshtml",
|
||||
};
|
||||
|
||||
var expected = new string[]
|
||||
{
|
||||
"/Pages/Customers/{0}.cshtml",
|
||||
"/Pages/{0}.cshtml",
|
||||
"/More/Paths/Customers/{0}.cshtml",
|
||||
"/More/Paths/{0}.cshtml",
|
||||
"/Views/Shared/{0}.cshtml",
|
||||
};
|
||||
|
||||
var expander = new PageViewLocationExpander();
|
||||
|
||||
// Act
|
||||
var actual = expander.ExpandViewLocations(context, locations);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual.ToArray());
|
||||
}
|
||||
|
||||
private ViewLocationExpanderContext CreateContext(string viewName = "_LoginPartial.cshtml", string pageName = null)
|
||||
{
|
||||
return new ViewLocationExpanderContext(
|
||||
new ActionContext(),
|
||||
viewName,
|
||||
controllerName: null,
|
||||
areaName: null,
|
||||
pageName: pageName,
|
||||
isMainPage: true)
|
||||
{
|
||||
Values = new Dictionary<string, string>(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ using Microsoft.AspNetCore.Mvc.Razor;
|
|||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.TagHelpers;
|
||||
|
|
@ -358,10 +359,16 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
typeof(IConfigureOptions<RazorViewEngineOptions>),
|
||||
new[]
|
||||
{
|
||||
#pragma warning disable 0618
|
||||
typeof(RazorViewEngineOptionsSetup),
|
||||
#pragma warning restore 0618
|
||||
typeof(DependencyContextRazorViewEngineOptionsSetup)
|
||||
typeof(DependencyContextRazorViewEngineOptionsSetup),
|
||||
typeof(RazorPagesRazorViewEngineOptionsSetup),
|
||||
}
|
||||
},
|
||||
{
|
||||
typeof(IConfigureOptions<RazorPagesOptions>),
|
||||
new[]
|
||||
{
|
||||
typeof(RazorPagesOptionsSetup),
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
@page
|
||||
|
||||
@{
|
||||
await Html.RenderPartialAsync(Request.Query["partial"]);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
Hello from sibling
|
||||
|
|
@ -0,0 +1 @@
|
|||
Hello from parent
|
||||
|
|
@ -0,0 +1 @@
|
|||
Hello from shared
|
||||
|
|
@ -0,0 +1 @@
|
|||
Hello from root
|
||||
Loading…
Reference in New Issue