parent
dee479fda7
commit
dc2ae93c3f
|
|
@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
{
|
||||
private const string ForAttributeName = "for";
|
||||
private const string ModelAttributeName = "model";
|
||||
private const string FallbackAttributeName = "fallback-name";
|
||||
private const string OptionalAttributeName = "optional";
|
||||
private object _model;
|
||||
private bool _hasModel;
|
||||
|
|
@ -82,6 +83,12 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
[HtmlAttributeName(OptionalAttributeName)]
|
||||
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>
|
||||
/// A <see cref="ViewDataDictionary"/> to pass into the partial view.
|
||||
/// </summary>
|
||||
|
|
@ -104,21 +111,46 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
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.
|
||||
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
|
||||
|
|
@ -152,26 +184,19 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
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;
|
||||
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 locations = string.Empty;
|
||||
if (searchedLocations.Any())
|
||||
{
|
||||
locations += Environment.NewLine + string.Join(Environment.NewLine, searchedLocations);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
Resources.FormatViewEngine_PartialViewNotFound(Name, locations));
|
||||
return ViewEngineResult.NotFound(partialName, searchedLocations);
|
||||
}
|
||||
|
||||
return viewEngineResult;
|
||||
|
|
|
|||
|
|
@ -206,6 +206,20 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
internal static string FormatPartialTagHelper_InvalidModelAttributes(object p0, object p1, object 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)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -159,4 +159,7 @@
|
|||
<data name="PartialTagHelper_InvalidModelAttributes" xml:space="preserve">
|
||||
<value>Cannot use '{0}' with both '{1}' and '{2}' attributes.</value>
|
||||
</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>
|
||||
|
|
@ -585,6 +585,34 @@ Products: Music Systems, Televisions (3)";
|
|||
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)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
|
|
|||
|
|
@ -648,6 +648,198 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
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()
|
||||
{
|
||||
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