parent
dee479fda7
commit
dc2ae93c3f
|
|
@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
{
|
{
|
||||||
private const string ForAttributeName = "for";
|
private const string ForAttributeName = "for";
|
||||||
private const string ModelAttributeName = "model";
|
private const string ModelAttributeName = "model";
|
||||||
|
private const string FallbackAttributeName = "fallback-name";
|
||||||
private const string OptionalAttributeName = "optional";
|
private const string OptionalAttributeName = "optional";
|
||||||
private object _model;
|
private object _model;
|
||||||
private bool _hasModel;
|
private bool _hasModel;
|
||||||
|
|
@ -82,6 +83,12 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
[HtmlAttributeName(OptionalAttributeName)]
|
[HtmlAttributeName(OptionalAttributeName)]
|
||||||
public bool Optional { get; set; }
|
public bool Optional { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// View to lookup if the view specified by <see cref="Name"/> cannot be located.
|
||||||
|
/// </summary>
|
||||||
|
[HtmlAttributeName(FallbackAttributeName)]
|
||||||
|
public string FallbackName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A <see cref="ViewDataDictionary"/> to pass into the partial view.
|
/// A <see cref="ViewDataDictionary"/> to pass into the partial view.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -104,21 +111,46 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
throw new ArgumentNullException(nameof(context));
|
throw new ArgumentNullException(nameof(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
var viewEngineResult = FindView();
|
|
||||||
|
|
||||||
if (viewEngineResult.Success)
|
|
||||||
{
|
|
||||||
var model = ResolveModel();
|
|
||||||
var viewBuffer = new ViewBuffer(_viewBufferScope, Name, ViewBuffer.PartialViewPageSize);
|
|
||||||
using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8))
|
|
||||||
{
|
|
||||||
await RenderPartialViewAsync(writer, model, viewEngineResult.View);
|
|
||||||
output.Content.SetHtmlContent(viewBuffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the TagName. We don't want `partial` to render.
|
// Reset the TagName. We don't want `partial` to render.
|
||||||
output.TagName = null;
|
output.TagName = null;
|
||||||
|
|
||||||
|
var result = FindView(Name);
|
||||||
|
var viewSearchedLocations = result.SearchedLocations;
|
||||||
|
var fallBackViewSearchedLocations = Enumerable.Empty<string>();
|
||||||
|
|
||||||
|
if (!result.Success && !string.IsNullOrEmpty(FallbackName))
|
||||||
|
{
|
||||||
|
result = FindView(FallbackName);
|
||||||
|
fallBackViewSearchedLocations = result.SearchedLocations;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
if (Optional)
|
||||||
|
{
|
||||||
|
// Could not find the view or fallback view, but the partial is marked as optional.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var locations = Environment.NewLine + string.Join(Environment.NewLine, viewSearchedLocations);
|
||||||
|
var errorMessage = Resources.FormatViewEngine_PartialViewNotFound(Name, locations);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(FallbackName))
|
||||||
|
{
|
||||||
|
locations = Environment.NewLine + string.Join(Environment.NewLine, result.SearchedLocations);
|
||||||
|
errorMessage += Environment.NewLine + Resources.FormatViewEngine_FallbackViewNotFound(FallbackName, locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = ResolveModel();
|
||||||
|
var viewBuffer = new ViewBuffer(_viewBufferScope, result.ViewName, ViewBuffer.PartialViewPageSize);
|
||||||
|
using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
await RenderPartialViewAsync(writer, model, result.View);
|
||||||
|
output.Content.SetHtmlContent(viewBuffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal for testing
|
// Internal for testing
|
||||||
|
|
@ -152,26 +184,19 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
return ViewContext.ViewData.Model;
|
return ViewContext.ViewData.Model;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ViewEngineResult FindView()
|
private ViewEngineResult FindView(string partialName)
|
||||||
{
|
{
|
||||||
var viewEngineResult = _viewEngine.GetView(ViewContext.ExecutingFilePath, Name, isMainPage: false);
|
var viewEngineResult = _viewEngine.GetView(ViewContext.ExecutingFilePath, partialName, isMainPage: false);
|
||||||
var getViewLocations = viewEngineResult.SearchedLocations;
|
var getViewLocations = viewEngineResult.SearchedLocations;
|
||||||
if (!viewEngineResult.Success)
|
if (!viewEngineResult.Success)
|
||||||
{
|
{
|
||||||
viewEngineResult = _viewEngine.FindView(ViewContext, Name, isMainPage: false);
|
viewEngineResult = _viewEngine.FindView(ViewContext, partialName, isMainPage: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!viewEngineResult.Success && !Optional)
|
if (!viewEngineResult.Success)
|
||||||
{
|
{
|
||||||
var searchedLocations = Enumerable.Concat(getViewLocations, viewEngineResult.SearchedLocations);
|
var searchedLocations = Enumerable.Concat(getViewLocations, viewEngineResult.SearchedLocations);
|
||||||
var locations = string.Empty;
|
return ViewEngineResult.NotFound(partialName, searchedLocations);
|
||||||
if (searchedLocations.Any())
|
|
||||||
{
|
|
||||||
locations += Environment.NewLine + string.Join(Environment.NewLine, searchedLocations);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
Resources.FormatViewEngine_PartialViewNotFound(Name, locations));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return viewEngineResult;
|
return viewEngineResult;
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,20 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
internal static string FormatPartialTagHelper_InvalidModelAttributes(object p0, object p1, object p2)
|
internal static string FormatPartialTagHelper_InvalidModelAttributes(object p0, object p1, object p2)
|
||||||
=> string.Format(CultureInfo.CurrentCulture, GetString("PartialTagHelper_InvalidModelAttributes"), p0, p1, p2);
|
=> string.Format(CultureInfo.CurrentCulture, GetString("PartialTagHelper_InvalidModelAttributes"), p0, p1, p2);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The fallback partial view '{0}' was not found. The following locations were searched:{1}
|
||||||
|
/// </summary>
|
||||||
|
internal static string ViewEngine_FallbackViewNotFound
|
||||||
|
{
|
||||||
|
get => GetString("ViewEngine_FallbackViewNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The fallback partial view '{0}' was not found. The following locations were searched:{1}
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatViewEngine_FallbackViewNotFound(object p0, object p1)
|
||||||
|
=> string.Format(CultureInfo.CurrentCulture, GetString("ViewEngine_FallbackViewNotFound"), p0, p1);
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -159,4 +159,7 @@
|
||||||
<data name="PartialTagHelper_InvalidModelAttributes" xml:space="preserve">
|
<data name="PartialTagHelper_InvalidModelAttributes" xml:space="preserve">
|
||||||
<value>Cannot use '{0}' with both '{1}' and '{2}' attributes.</value>
|
<value>Cannot use '{0}' with both '{1}' and '{2}' attributes.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewEngine_FallbackViewNotFound" xml:space="preserve">
|
||||||
|
<value>The fallback partial view '{0}' was not found. The following locations were searched:{1}</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -585,6 +585,34 @@ Products: Music Systems, Televisions (3)";
|
||||||
Assert.Empty(banner.TextContent);
|
Assert.Empty(banner.TextContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PartialTagHelper_AllowsUsingFallback()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var url = "/Customer/PartialWithFallback";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var document = await Client.GetHtmlDocumentAsync(url);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var content = document.RequiredQuerySelector("#content");
|
||||||
|
Assert.Equal("Hello from fallback", content.TextContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PartialTagHelper_AllowsUsingOptional()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var url = "/Customer/PartialWithOptional";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var document = await Client.GetHtmlDocumentAsync(url);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var content = document.RequiredQuerySelector("#content");
|
||||||
|
Assert.Empty(content.TextContent);
|
||||||
|
}
|
||||||
|
|
||||||
private static HttpRequestMessage RequestWithLocale(string url, string locale)
|
private static HttpRequestMessage RequestWithLocale(string url, string locale)
|
||||||
{
|
{
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
|
|
|
||||||
|
|
@ -648,6 +648,198 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
Assert.Empty(content);
|
Assert.Empty(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessAsync_RendersMainPartial_If_FallbackIsSet_AndMainPartialIsFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = "Hello from partial!";
|
||||||
|
var bufferScope = new TestViewBufferScope();
|
||||||
|
var partialName = "_Partial";
|
||||||
|
var fallbackName = "_Fallback";
|
||||||
|
var model = new object();
|
||||||
|
var viewContext = GetViewContext();
|
||||||
|
|
||||||
|
var view = new Mock<IView>();
|
||||||
|
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
|
||||||
|
.Callback((ViewContext v) =>
|
||||||
|
{
|
||||||
|
v.Writer.Write(expected);
|
||||||
|
})
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var fallbackView = new Mock<IView>();
|
||||||
|
fallbackView.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
|
||||||
|
.Callback((ViewContext v) =>
|
||||||
|
{
|
||||||
|
v.Writer.Write("Hello from fallback partial!");
|
||||||
|
})
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var viewEngine = new Mock<ICompositeViewEngine>();
|
||||||
|
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), partialName, false))
|
||||||
|
.Returns(ViewEngineResult.Found(partialName, view.Object));
|
||||||
|
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), fallbackName, false))
|
||||||
|
.Returns(ViewEngineResult.Found(fallbackName, fallbackView.Object));
|
||||||
|
|
||||||
|
var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
|
||||||
|
{
|
||||||
|
Name = partialName,
|
||||||
|
ViewContext = viewContext,
|
||||||
|
FallbackName = fallbackName
|
||||||
|
};
|
||||||
|
var tagHelperContext = GetTagHelperContext();
|
||||||
|
var output = GetTagHelperOutput();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tagHelper.ProcessAsync(tagHelperContext, output);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder());
|
||||||
|
Assert.Equal(expected, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessAsync_IfHasFallback_Throws_When_MainPartialAndFallback_AreNotFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bufferScope = new TestViewBufferScope();
|
||||||
|
var partialName = "_Partial";
|
||||||
|
var fallbackName = "_Fallback";
|
||||||
|
var expected = string.Join(
|
||||||
|
Environment.NewLine,
|
||||||
|
$"The partial view '{partialName}' was not found. The following locations were searched:",
|
||||||
|
"PartialNotFound1",
|
||||||
|
"PartialNotFound2",
|
||||||
|
"PartialNotFound3",
|
||||||
|
"PartialNotFound4",
|
||||||
|
$"The fallback partial view '{fallbackName}' was not found. The following locations were searched:",
|
||||||
|
"FallbackNotFound1",
|
||||||
|
"FallbackNotFound2",
|
||||||
|
"FallbackNotFound3",
|
||||||
|
"FallbackNotFound4");
|
||||||
|
var viewData = new ViewDataDictionary(new TestModelMetadataProvider(), new ModelStateDictionary());
|
||||||
|
var viewContext = GetViewContext();
|
||||||
|
|
||||||
|
var view = Mock.Of<IView>();
|
||||||
|
var viewEngine = new Mock<ICompositeViewEngine>();
|
||||||
|
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), partialName, false))
|
||||||
|
.Returns(ViewEngineResult.NotFound(partialName, new[] { "PartialNotFound1", "PartialNotFound2" }));
|
||||||
|
|
||||||
|
viewEngine.Setup(v => v.FindView(viewContext, partialName, false))
|
||||||
|
.Returns(ViewEngineResult.NotFound(partialName, new[] { $"PartialNotFound3", $"PartialNotFound4" }));
|
||||||
|
|
||||||
|
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), fallbackName, false))
|
||||||
|
.Returns(ViewEngineResult.NotFound(partialName, new[] { "FallbackNotFound1", "FallbackNotFound2" }));
|
||||||
|
|
||||||
|
viewEngine.Setup(v => v.FindView(viewContext, fallbackName, false))
|
||||||
|
.Returns(ViewEngineResult.NotFound(partialName, new[] { $"FallbackNotFound3", $"FallbackNotFound4" }));
|
||||||
|
|
||||||
|
var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
|
||||||
|
{
|
||||||
|
Name = partialName,
|
||||||
|
ViewContext = viewContext,
|
||||||
|
ViewData = viewData,
|
||||||
|
FallbackName = fallbackName
|
||||||
|
};
|
||||||
|
var tagHelperContext = GetTagHelperContext();
|
||||||
|
var output = GetTagHelperOutput();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => tagHelper.ProcessAsync(tagHelperContext, output));
|
||||||
|
Assert.Equal(expected, exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessAsync_RendersFallbackView_If_MainIsNotFound_AndGetViewReturnsView()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = "Hello from fallback!";
|
||||||
|
var bufferScope = new TestViewBufferScope();
|
||||||
|
var partialName = "_Partial";
|
||||||
|
var fallbackName = "_Fallback";
|
||||||
|
var model = new object();
|
||||||
|
var viewContext = GetViewContext();
|
||||||
|
|
||||||
|
var view = new Mock<IView>();
|
||||||
|
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
|
||||||
|
.Callback((ViewContext v) =>
|
||||||
|
{
|
||||||
|
v.Writer.Write(expected);
|
||||||
|
})
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var viewEngine = new Mock<ICompositeViewEngine>();
|
||||||
|
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), partialName, false))
|
||||||
|
.Returns(ViewEngineResult.NotFound(partialName, Array.Empty<string>()));
|
||||||
|
viewEngine.Setup(v => v.FindView(viewContext, partialName, false))
|
||||||
|
.Returns(ViewEngineResult.NotFound(partialName, Array.Empty<string>()));
|
||||||
|
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), fallbackName, false))
|
||||||
|
.Returns(ViewEngineResult.Found(fallbackName, view.Object));
|
||||||
|
|
||||||
|
var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
|
||||||
|
{
|
||||||
|
Name = partialName,
|
||||||
|
ViewContext = viewContext,
|
||||||
|
FallbackName = fallbackName
|
||||||
|
};
|
||||||
|
var tagHelperContext = GetTagHelperContext();
|
||||||
|
var output = GetTagHelperOutput();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tagHelper.ProcessAsync(tagHelperContext, output);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder());
|
||||||
|
Assert.Equal(expected, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessAsync_RendersFallbackView_If_MainIsNotFound_AndFindViewReturnsView()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = "Hello from fallback!";
|
||||||
|
var bufferScope = new TestViewBufferScope();
|
||||||
|
var partialName = "_Partial";
|
||||||
|
var fallbackName = "_Fallback";
|
||||||
|
var model = new object();
|
||||||
|
var viewContext = GetViewContext();
|
||||||
|
|
||||||
|
var view = new Mock<IView>();
|
||||||
|
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
|
||||||
|
.Callback((ViewContext v) =>
|
||||||
|
{
|
||||||
|
v.Writer.Write(expected);
|
||||||
|
})
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var viewEngine = new Mock<ICompositeViewEngine>();
|
||||||
|
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), partialName, false))
|
||||||
|
.Returns(ViewEngineResult.NotFound(partialName, Array.Empty<string>()));
|
||||||
|
viewEngine.Setup(v => v.FindView(viewContext, partialName, false))
|
||||||
|
.Returns(ViewEngineResult.NotFound(partialName, Array.Empty<string>()));
|
||||||
|
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), fallbackName, false))
|
||||||
|
.Returns(ViewEngineResult.NotFound(fallbackName, Array.Empty<string>()));
|
||||||
|
viewEngine.Setup(v => v.FindView(viewContext, fallbackName, false))
|
||||||
|
.Returns(ViewEngineResult.Found(fallbackName, view.Object));
|
||||||
|
|
||||||
|
var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
|
||||||
|
{
|
||||||
|
Name = partialName,
|
||||||
|
ViewContext = viewContext,
|
||||||
|
FallbackName = fallbackName
|
||||||
|
};
|
||||||
|
var tagHelperContext = GetTagHelperContext();
|
||||||
|
var output = GetTagHelperOutput();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tagHelper.ProcessAsync(tagHelperContext, output);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder());
|
||||||
|
Assert.Equal(expected, content);
|
||||||
|
}
|
||||||
|
|
||||||
private static ViewContext GetViewContext()
|
private static ViewContext GetViewContext()
|
||||||
{
|
{
|
||||||
return new ViewContext(
|
return new ViewContext(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@page
|
||||||
|
|
||||||
|
<partial name="_DoesNotExist" fallback-name="./_Fallback" />
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@page
|
||||||
|
|
||||||
|
<div id="content"><partial name="_DoesNotExist" optional="true" /></div>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<span id="content">Hello from fallback</span>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
Loading…
Reference in New Issue