Add fallback attribute to partial tag helper.

Addresses #7515
This commit is contained in:
Alexej Timonin 2018-07-01 16:29:54 -07:00 committed by Pranav K
parent dee479fda7
commit dc2ae93c3f
No known key found for this signature in database
GPG Key ID: 1963DA6D96C3057A
9 changed files with 295 additions and 25 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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>

View File

@ -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);

View File

@ -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(

View File

@ -0,0 +1,3 @@
@page
<partial name="_DoesNotExist" fallback-name="./_Fallback" />

View File

@ -0,0 +1,3 @@
@page
<div id="content"><partial name="_DoesNotExist" optional="true" /></div>

View File

@ -0,0 +1 @@
<span id="content">Hello from fallback</span>

View File

@ -0,0 +1 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers