diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs
index fef6b7c0e8..617fa5d3a2 100644
--- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs
+++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs
@@ -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; }
+ ///
+ /// View to lookup if the view specified by cannot be located.
+ ///
+ [HtmlAttributeName(FallbackAttributeName)]
+ public string FallbackName { get; set; }
+
///
/// A to pass into the partial view.
///
@@ -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();
+
+ 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;
diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs
index 93ac7fa60e..f5b5d8c690 100644
--- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs
@@ -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);
+ ///
+ /// The fallback partial view '{0}' was not found. The following locations were searched:{1}
+ ///
+ internal static string ViewEngine_FallbackViewNotFound
+ {
+ get => GetString("ViewEngine_FallbackViewNotFound");
+ }
+
+ ///
+ /// The fallback partial view '{0}' was not found. The following locations were searched:{1}
+ ///
+ 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);
diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx
index 1d6166e955..96c56dff7f 100644
--- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx
+++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx
@@ -159,4 +159,7 @@
Cannot use '{0}' with both '{1}' and '{2}' attributes.
+
+ The fallback partial view '{0}' was not found. The following locations were searched:{1}
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs
index e0ce243842..32cb029485 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs
@@ -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);
diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs
index 21edd86f7d..489c78bd05 100644
--- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs
@@ -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();
+ view.Setup(v => v.RenderAsync(It.IsAny()))
+ .Callback((ViewContext v) =>
+ {
+ v.Writer.Write(expected);
+ })
+ .Returns(Task.CompletedTask);
+
+ var fallbackView = new Mock();
+ fallbackView.Setup(v => v.RenderAsync(It.IsAny()))
+ .Callback((ViewContext v) =>
+ {
+ v.Writer.Write("Hello from fallback partial!");
+ })
+ .Returns(Task.CompletedTask);
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false))
+ .Returns(ViewEngineResult.Found(partialName, view.Object));
+ viewEngine.Setup(v => v.GetView(It.IsAny(), 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();
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), 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(), 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(
+ () => 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();
+ view.Setup(v => v.RenderAsync(It.IsAny()))
+ .Callback((ViewContext v) =>
+ {
+ v.Writer.Write(expected);
+ })
+ .Returns(Task.CompletedTask);
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false))
+ .Returns(ViewEngineResult.NotFound(partialName, Array.Empty()));
+ viewEngine.Setup(v => v.FindView(viewContext, partialName, false))
+ .Returns(ViewEngineResult.NotFound(partialName, Array.Empty()));
+ viewEngine.Setup(v => v.GetView(It.IsAny(), 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();
+ view.Setup(v => v.RenderAsync(It.IsAny()))
+ .Callback((ViewContext v) =>
+ {
+ v.Writer.Write(expected);
+ })
+ .Returns(Task.CompletedTask);
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false))
+ .Returns(ViewEngineResult.NotFound(partialName, Array.Empty()));
+ viewEngine.Setup(v => v.FindView(viewContext, partialName, false))
+ .Returns(ViewEngineResult.NotFound(partialName, Array.Empty()));
+ viewEngine.Setup(v => v.GetView(It.IsAny(), fallbackName, false))
+ .Returns(ViewEngineResult.NotFound(fallbackName, Array.Empty()));
+ 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(
diff --git a/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Pages/PartialWithFallback.cshtml b/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Pages/PartialWithFallback.cshtml
new file mode 100644
index 0000000000..fc6b995ab1
--- /dev/null
+++ b/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Pages/PartialWithFallback.cshtml
@@ -0,0 +1,3 @@
+@page
+
+
\ No newline at end of file
diff --git a/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Pages/PartialWithOptional.cshtml b/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Pages/PartialWithOptional.cshtml
new file mode 100644
index 0000000000..c3c8ecd2cb
--- /dev/null
+++ b/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Pages/PartialWithOptional.cshtml
@@ -0,0 +1,3 @@
+@page
+
+
\ No newline at end of file
diff --git a/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Pages/_Fallback.cshtml b/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Pages/_Fallback.cshtml
new file mode 100644
index 0000000000..dee9334704
--- /dev/null
+++ b/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Pages/_Fallback.cshtml
@@ -0,0 +1 @@
+Hello from fallback
\ No newline at end of file
diff --git a/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Pages/_ViewImports.cshtml b/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Pages/_ViewImports.cshtml
new file mode 100644
index 0000000000..a757b413b9
--- /dev/null
+++ b/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Pages/_ViewImports.cshtml
@@ -0,0 +1 @@
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers