From c2fcfabdf3f7bbcaaecbc77ecbfe358b3b7e0092 Mon Sep 17 00:00:00 2001 From: Alexej Timonin Date: Sat, 30 Jun 2018 01:02:38 +0200 Subject: [PATCH] Add optional property to PartialTagHelper (#7991) * Add optional to PartialTagHelper Addresses #7268 --- .../PartialTagHelper.cs | 39 +++++++++++----- .../PartialTagHelperTest.cs | 44 ++++++++++++++++++- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs index 1a463966cf..fef6b7c0e8 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 OptionalAttributeName = "optional"; private object _model; private bool _hasModel; private bool _hasFor; @@ -74,6 +75,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } } + /// + /// When optional, executing the tag helper will no-op if the view cannot be located. + /// Otherwise will throw stating the view could not be found. + /// + [HtmlAttributeName(OptionalAttributeName)] + public bool Optional { get; set; } + /// /// A to pass into the partial view. /// @@ -96,16 +104,21 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers throw new ArgumentNullException(nameof(context)); } - var model = ResolveModel(); - var viewBuffer = new ViewBuffer(_viewBufferScope, Name, ViewBuffer.PartialViewPageSize); - using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8)) - { - await RenderPartialViewAsync(writer, model); + var viewEngineResult = FindView(); - // Reset the TagName. We don't want `partial` to render. - output.TagName = null; - output.Content.SetHtmlContent(viewBuffer); + 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; } // Internal for testing @@ -139,7 +152,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers return ViewContext.ViewData.Model; } - private async Task RenderPartialViewAsync(TextWriter writer, object model) + private ViewEngineResult FindView() { var viewEngineResult = _viewEngine.GetView(ViewContext.ExecutingFilePath, Name, isMainPage: false); var getViewLocations = viewEngineResult.SearchedLocations; @@ -148,7 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers viewEngineResult = _viewEngine.FindView(ViewContext, Name, isMainPage: false); } - if (!viewEngineResult.Success) + if (!viewEngineResult.Success && !Optional) { var searchedLocations = Enumerable.Concat(getViewLocations, viewEngineResult.SearchedLocations); var locations = string.Empty; @@ -161,7 +174,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers Resources.FormatViewEngine_PartialViewNotFound(Name, locations)); } - var view = viewEngineResult.View; + return viewEngineResult; + } + + private async Task RenderPartialViewAsync(TextWriter writer, object model, IView view) + { // Determine which ViewData we should use to construct a new ViewData var baseViewData = ViewData ?? ViewContext.ViewData; var newViewData = new ViewDataDictionary(baseViewData, model); diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs index 21ac445239..21edd86f7d 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs @@ -569,7 +569,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } [Fact] - public async Task ProcessAsync_Throws_IfGetViewAndFindReturnNotFoundResults() + public async Task ProcessAsync_Throws_If_NotOptional_And_GetViewAndFindReturnNotFoundResults() { // Arrange var bufferScope = new TestViewBufferScope(); @@ -596,6 +596,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers Name = partialName, ViewContext = viewContext, ViewData = viewData, + Optional = false }; var tagHelperContext = GetTagHelperContext(); var output = GetTagHelperOutput(); @@ -605,6 +606,47 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers () => tagHelper.ProcessAsync(tagHelperContext, output)); Assert.Equal(expected, exception.Message); } + + [Fact] + public async Task ProcessAsync_IfOptional_And_ViewIsNotFound_WillNotRenderAnything() + { + // Arrange + var expected = string.Empty; + var bufferScope = new TestViewBufferScope(); + var partialName = "_ThisViewDoesNotExists"; + 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, searchedLocations: Array.Empty())); + viewEngine.Setup(v => v.FindView(viewContext, partialName, false)) + .Returns(ViewEngineResult.NotFound(partialName, searchedLocations: Array.Empty())); + + var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope) + { + Name = partialName, + ViewContext = viewContext, + Optional = true + }; + var tagHelperContext = GetTagHelperContext(); + var output = GetTagHelperOutput(); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder()); + Assert.Empty(content); + } private static ViewContext GetViewContext() {