From 1e5b0b9becab1e6ba2a4978df880d74fba234439 Mon Sep 17 00:00:00 2001 From: Shahriar Gholami Date: Mon, 1 Feb 2016 14:08:35 +0330 Subject: [PATCH] Allow to define section or/and body without the need to render it --- .../Properties/Resources.Designer.cs | 16 ++-- .../RazorPage.cs | 61 +++++++++++-- .../Resources.resx | 4 +- .../RazorPageTest.cs | 91 ++++++++++++++++++- .../RazorViewTest.cs | 12 ++- 5 files changed, 161 insertions(+), 23 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs index 2d17448aec..5343888a29 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs @@ -219,7 +219,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor } /// - /// {0} has not been called for the page at '{1}'. + /// {0} has not been called for the page at '{1}'. To ignore call {2}(). /// internal static string RenderBodyNotCalled { @@ -227,11 +227,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor } /// - /// {0} has not been called for the page at '{1}'. + /// {0} has not been called for the page at '{1}'. To ignore call {2}(). /// - internal static string FormatRenderBodyNotCalled(object p0, object p1) + internal static string FormatRenderBodyNotCalled(object p0, object p1, object p2) { - return string.Format(CultureInfo.CurrentCulture, GetString("RenderBodyNotCalled"), p0, p1); + return string.Format(CultureInfo.CurrentCulture, GetString("RenderBodyNotCalled"), p0, p1, p2); } /// @@ -283,7 +283,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor } /// - /// The following sections have been defined but have not been rendered by the page at '{0}': '{1}'. + /// The following sections have been defined but have not been rendered by the page at '{0}': '{1}'. To ignore an unrendered section call {2}("sectionName"). /// internal static string SectionsNotRendered { @@ -291,11 +291,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor } /// - /// The following sections have been defined but have not been rendered by the page at '{0}': '{1}'. + /// The following sections have been defined but have not been rendered by the page at '{0}': '{1}'. To ignore an unrendered section call {2}("sectionName"). /// - internal static string FormatSectionsNotRendered(object p0, object p1) + internal static string FormatSectionsNotRendered(object p0, object p1, object p2) { - return string.Format(CultureInfo.CurrentCulture, GetString("SectionsNotRendered"), p0, p1); + return string.Format(CultureInfo.CurrentCulture, GetString("SectionsNotRendered"), p0, p1, p2); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs index c6ca34b420..2002ca9718 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs @@ -12,7 +12,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.Rendering; @@ -40,6 +39,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor private TagHelperAttributeInfo _tagHelperAttributeInfo; private HtmlContentWrapperTextWriter _valueBuffer; private IViewBufferScope _bufferScope; + private bool _ignoreBody; + private HashSet _ignoredSections; public RazorPage() { @@ -655,6 +656,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor return BodyContent; } + /// + /// In a Razor layout page, ignores rendering the portion of a content page that is not within a named section. + /// + public void IgnoreBody() + { + _ignoreBody = true; + } + /// /// Creates a named content section in the page that can be invoked in a Layout page using /// or . @@ -815,6 +824,34 @@ namespace Microsoft.AspNetCore.Mvc.Razor } } + /// + /// In layout pages, ignores rendering the content of the section named . + /// + /// The section to ignore. + public void IgnoreSection(string sectionName) + { + if (sectionName == null) + { + throw new ArgumentNullException(nameof(sectionName)); + } + + if (!PreviousSectionWriters.ContainsKey(sectionName)) + { + // If the section is not defined, throw an error. + throw new InvalidOperationException(Resources.FormatSectionNotDefined( + ViewContext.ExecutingFilePath, + sectionName, + ViewContext.View.Path)); + } + + if (_ignoredSections == null) + { + _ignoredSections = new HashSet(StringComparer.OrdinalIgnoreCase); + } + + _ignoredSections.Add(sectionName); + } + /// /// Invokes on and /// on the response stream, writing out any buffered content to the . @@ -860,17 +897,27 @@ namespace Microsoft.AspNetCore.Mvc.Razor _renderedSections, StringComparer.OrdinalIgnoreCase); - if (sectionsNotRendered.Any()) + string[] sectionsNotIgnored; + if (_ignoredSections != null) { - var sectionNames = string.Join(", ", sectionsNotRendered); - throw new InvalidOperationException(Resources.FormatSectionsNotRendered(Path, sectionNames)); + sectionsNotIgnored = sectionsNotRendered.Except(_ignoredSections, StringComparer.OrdinalIgnoreCase).ToArray(); + } + else + { + sectionsNotIgnored = sectionsNotRendered.ToArray(); + } + + if (sectionsNotIgnored.Length > 0) + { + var sectionNames = string.Join(", ", sectionsNotIgnored); + throw new InvalidOperationException(Resources.FormatSectionsNotRendered(Path, sectionNames, nameof(IgnoreSection))); } } - else if (BodyContent != null && !_renderedBody) + else if (BodyContent != null && !_renderedBody && !_ignoreBody) { // There are no sections defined, but RenderBody was NOT called. - // If a body was defined, then RenderBody should have been called. - var message = Resources.FormatRenderBodyNotCalled(nameof(RenderBody), Path); + // If a body was defined and the body not ignored, then RenderBody should have been called. + var message = Resources.FormatRenderBodyNotCalled(nameof(RenderBody), Path, nameof(IgnoreBody)); throw new InvalidOperationException(message); } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx index 6c36f0d352..f94c7316aa 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx @@ -157,7 +157,7 @@ {0} invocation in '{1}' is invalid. {0} can only be called from a layout page. - {0} has not been called for the page at '{1}'. + {0} has not been called for the page at '{1}'. To ignore call {2}(). Section '{0}' is already defined. @@ -169,7 +169,7 @@ The layout page '{0}' cannot find the section '{1}' in the content page '{2}'. - The following sections have been defined but have not been rendered by the page at '{0}': '{1}'. + The following sections have been defined but have not been rendered by the page at '{0}': '{1}'. To ignore an unrendered section call {2}("sectionName"). View of type '{0}' cannot be activated by '{1}'. diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs index 5e4f3983dd..3779180b14 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs @@ -302,6 +302,29 @@ namespace Microsoft.AspNetCore.Mvc.Razor Assert.Equal(message, ex.Message); } + [Fact] + public async Task IgnoreSection_ThrowsIfSectionIsNotFound() + { + // Arrange + var context = CreateViewContext(viewPath: "/Views/TestPath/Test.cshtml"); + context.ExecutingFilePath = "/Views/Shared/_Layout.cshtml"; + var page = CreatePage(v => + { + v.Path = "/Views/TestPath/Test.cshtml"; + v.IgnoreSection("bar"); + }, context); + page.PreviousSectionWriters = new Dictionary + { + { "baz", _nullRenderAsyncDelegate } + }; + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => page.ExecuteAsync()); + var message = $"The layout page '/Views/Shared/_Layout.cshtml' cannot find the section 'bar'" + + " in the content page '/Views/TestPath/Test.cshtml'."; + Assert.Equal(message, ex.Message); + } + [Fact] public void IsSectionDefined_ThrowsIfPreviousSectionWritersIsNotRegistered() { @@ -471,7 +494,24 @@ namespace Microsoft.AspNetCore.Mvc.Razor // Act & Assert var ex = Assert.Throws(() => page.EnsureRenderedBodyOrSections()); - Assert.Equal($"RenderBody has not been called for the page at '{path}'.", ex.Message); + Assert.Equal($"RenderBody has not been called for the page at '{path}'. To ignore call IgnoreBody().", ex.Message); + } + + [Fact] + public async Task EnsureRenderedBodyOrSections_SucceedsIfRenderBodyIsNotCalledFromPage_AndNoSectionsAreDefined_AndBodyIgnored() + { + // Arrange + var path = "page-path"; + var page = CreatePage(v => + { + }); + page.Path = path; + page.BodyContent = new HtmlString("some content"); + page.IgnoreBody(); + + // Act & Assert (does not throw) + await page.ExecuteAsync(); + page.EnsureRenderedBodyOrSections(); } [Fact] @@ -495,10 +535,57 @@ namespace Microsoft.AspNetCore.Mvc.Razor var ex = Assert.Throws(() => page.EnsureRenderedBodyOrSections()); Assert.Equal( "The following sections have been defined but have not been rendered by the page at " + - $"'{path}': '{sectionName}'.", + $"'{path}': '{sectionName}'. To ignore an unrendered section call IgnoreSection(\"sectionName\").", ex.Message); } + [Fact] + public async Task EnsureRenderedBodyOrSections_SucceedsIfDefinedSectionsAreNotRendered_AndIgnored() + { + // Arrange + var path = "page-path"; + var sectionName = "sectionA"; + var page = CreatePage(v => + { + }); + page.Path = path; + page.BodyContent = new HtmlString("some content"); + page.PreviousSectionWriters = new Dictionary + { + { sectionName, _nullRenderAsyncDelegate } + }; + page.IgnoreSection(sectionName); + + // Act & Assert (does not throw) + await page.ExecuteAsync(); + page.EnsureRenderedBodyOrSections(); + } + + [Fact] + public async Task ExecuteAsync_RendersSectionsThatAreNotIgnored() + { + // Arrange + var path = "page-path"; + var page = CreatePage(async p => + { + p.IgnoreSection("ignored"); + p.Write(await p.RenderSectionAsync("not-ignored-section")); + }); + page.Path = path; + page.BodyContent = new HtmlString("some content"); + page.PreviousSectionWriters = new Dictionary + { + { "ignored", _nullRenderAsyncDelegate }, + { "not-ignored-section", writer => writer.WriteAsync("not-ignored-section-content") } + }; + + // Act + await page.ExecuteAsync(); + + // Assert + Assert.Equal("not-ignored-section-content", page.RenderedContent); + } + [Fact] public async Task EnsureRenderedBodyOrSections_SucceedsIfRenderBodyIsNotCalled_ButAllDefinedSectionsAreRendered() { diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs index d0ba5c0fd6..08308b505d 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs @@ -684,7 +684,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor // Act and Assert var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext)); Assert.Equal("The following sections have been defined but have not been rendered by the page " - + $"at '{LayoutPath}': 'head, foot'.", ex.Message); + + $"at '{LayoutPath}': 'head, foot'. To ignore an unrendered section call IgnoreSection(\"sectionName\").", + ex.Message); } [Fact] @@ -879,7 +880,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor // Act and Assert var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext)); Assert.Equal("The following sections have been defined but have not been rendered by the page at " - + "'/Shared/Layout1.cshtml': 'foo'.", ex.Message); + + "'/Shared/Layout1.cshtml': 'foo'. To ignore an unrendered section call IgnoreSection(\"sectionName\").", + ex.Message); } [Fact] @@ -946,7 +948,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor // Act and Assert var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext)); Assert.Equal("The following sections have been defined but have not been rendered by the page at " + - "'/Shared/Layout1.cshtml': 'foo'.", ex.Message); + "'/Shared/Layout1.cshtml': 'foo'. To ignore an unrendered section call IgnoreSection(\"sectionName\").", + ex.Message); } [Fact] @@ -978,7 +981,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor // Act and Assert var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext)); - Assert.Equal($"RenderBody has not been called for the page at '{LayoutPath}'.", ex.Message); + Assert.Equal($"RenderBody has not been called for the page at '{LayoutPath}'. To ignore call IgnoreBody().", + ex.Message); } [Fact]