From 931d18b85148e634e5caa1083805f5297cdfd027 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 4 Apr 2014 15:12:10 -0700 Subject: [PATCH] Adding exceptions for RenderSection, DefineSection and RenderBody --- .../Properties/Resources.Designer.cs | 56 ++++++- src/Microsoft.AspNet.Mvc.Razor/RazorView.cs | 33 +++- src/Microsoft.AspNet.Mvc.Razor/Resources.resx | 11 +- .../RazorViewTest.cs | 155 ++++++++++++++++-- 4 files changed, 234 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index 97c59ef724..a1b18d347c 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -91,7 +91,7 @@ namespace Microsoft.AspNet.Mvc.Razor } /// - /// RenderBody can only be called from a layout page. + /// {0} can only be called from a layout page. /// internal static string RenderBodyCannotBeCalled { @@ -99,11 +99,27 @@ namespace Microsoft.AspNet.Mvc.Razor } /// - /// RenderBody can only be called from a layout page. + /// {0} can only be called from a layout page. /// - internal static string FormatRenderBodyCannotBeCalled() + internal static string FormatRenderBodyCannotBeCalled(object p0) { - return GetString("RenderBodyCannotBeCalled"); + return string.Format(CultureInfo.CurrentCulture, GetString("RenderBodyCannotBeCalled"), p0); + } + + /// + /// {0} must be called from a layout page. + /// + internal static string RenderBodyNotCalled + { + get { return GetString("RenderBodyNotCalled"); } + } + + /// + /// {0} must be called from a layout page. + /// + internal static string FormatRenderBodyNotCalled(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("RenderBodyNotCalled"), p0); } /// @@ -122,6 +138,22 @@ namespace Microsoft.AspNet.Mvc.Razor return string.Format(CultureInfo.CurrentCulture, GetString("SectionAlreadyDefined"), p0); } + /// + /// {0} has already been called for the section named '{1}'. + /// + internal static string SectionAlreadyRendered + { + get { return GetString("SectionAlreadyRendered"); } + } + + /// + /// {0} has already been called for the section named '{1}'. + /// + internal static string FormatSectionAlreadyRendered(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("SectionAlreadyRendered"), p0, p1); + } + /// /// Section '{0}' is not defined. /// @@ -138,6 +170,22 @@ namespace Microsoft.AspNet.Mvc.Razor return string.Format(CultureInfo.CurrentCulture, GetString("SectionNotDefined"), p0); } + /// + /// The following sections have been defined but have not been rendered: '{0}'. + /// + internal static string SectionsNotRendered + { + get { return GetString("SectionsNotRendered"); } + } + + /// + /// The following sections have been defined but have not been rendered: '{0}'. + /// + internal static string FormatSectionsNotRendered(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("SectionsNotRendered"), p0); + } + /// /// The partial view '{0}' was not found. The following locations were searched:{1} /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 0b4a293e76..c66ec1752f 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; @@ -11,6 +12,9 @@ namespace Microsoft.AspNet.Mvc.Razor { public abstract class RazorView : IView { + private readonly HashSet _renderedSections = new HashSet(StringComparer.OrdinalIgnoreCase); + private bool _renderedBody; + public IViewComponentHelper Component { get { return Context == null ? null : Context.Component; } @@ -59,6 +63,9 @@ namespace Microsoft.AspNet.Mvc.Razor { context.Writer = bodyWriter; await ExecuteAsync(); + + // Verify that RenderBody is called, or that RenderSection is called for all sections + VerifyRenderedBodyOrSections(); } finally { @@ -245,8 +252,9 @@ namespace Microsoft.AspNet.Mvc.Razor { if (BodyContent == null) { - throw new InvalidOperationException(Resources.RenderBodyCannotBeCalled); + throw new InvalidOperationException(Resources.FormatRenderBodyCannotBeCalled("RenderBody")); } + _renderedBody = true; return new HtmlString(BodyContent); } @@ -273,10 +281,15 @@ namespace Microsoft.AspNet.Mvc.Razor public HelperResult RenderSection([NotNull] string name, bool required) { EnsureMethodCanBeInvoked("RenderSection"); + if (_renderedSections.Contains(name)) + { + throw new InvalidOperationException(Resources.FormatSectionAlreadyRendered("RenderSection", name)); + } HelperResult action; if (PreviousSectionWriters.TryGetValue(name, out action)) { + _renderedSections.Add(name); return action; } else if (required) @@ -298,5 +311,23 @@ namespace Microsoft.AspNet.Mvc.Razor throw new InvalidOperationException(Resources.FormatView_MethodCannotBeCalled(methodName)); } } + + private void VerifyRenderedBodyOrSections() + { + if (BodyContent != null) + { + var sectionsNotRendered = PreviousSectionWriters.Keys.Except(_renderedSections, StringComparer.OrdinalIgnoreCase); + if (sectionsNotRendered.Any()) + { + var sectionNames = String.Join(", ", sectionsNotRendered); + throw new InvalidOperationException(Resources.FormatSectionsNotRendered(sectionNames)); + } + else if (!_renderedBody) + { + // If a body was defined, then RenderBody should have been called. + throw new InvalidOperationException(Resources.FormatRenderBodyNotCalled("RenderBody")); + } + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index 995ee0fa76..333ff7cea7 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -133,14 +133,23 @@ Only one '{0}' statement is allowed in a file. - RenderBody can only be called from a layout page. + {0} can only be called from a layout page. + + + {0} must be called from a layout page. Section '{0}' is already defined. + + {0} has already been called for the section named '{1}'. + Section '{0}' is not defined. + + The following sections have been defined but have not been rendered: '{0}'. + The partial view '{0}' was not found. The following locations were searched:{1} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index ad69c60821..12952f174e 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -16,21 +16,19 @@ namespace Microsoft.AspNet.Mvc.Razor.Test public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined() { // Arrange - Exception ex = null; var view = CreateView(v => { - v.DefineSection("foo", new HelperResult(action: null)); - - ex = Assert.Throws( - () => v.DefineSection("foo", new HelperResult(action: null))); + v.DefineSection("qux", new HelperResult(action: null)); + v.DefineSection("qux", new HelperResult(action: null)); }); var viewContext = CreateViewContext(layoutView: null); // Act - await view.RenderAsync(viewContext); + var ex = await Assert.ThrowsAsync( + () => view.RenderAsync(viewContext)); // Assert - Assert.Equal("Section 'foo' is already defined.", ex.Message); + Assert.Equal("Section 'qux' is already defined.", ex.Message); } [Fact] @@ -47,6 +45,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test var layoutView = CreateView(v => { actual = v.RenderSection("bar"); + v.RenderBodyPublic(); }); var viewContext = CreateViewContext(layoutView); @@ -81,7 +80,6 @@ namespace Microsoft.AspNet.Mvc.Razor.Test { // Arrange var expected = new HelperResult(action: null); - Exception ex = null; var view = CreateView(v => { v.DefineSection("baz", expected); @@ -89,13 +87,12 @@ namespace Microsoft.AspNet.Mvc.Razor.Test }); var layoutView = CreateView(v => { - ex = Assert.Throws( - () => v.RenderSection("bar")); + v.RenderSection("bar"); }); var viewContext = CreateViewContext(layoutView); // Act - await view.RenderAsync(viewContext); + var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext)); // Assert Assert.Equal("Section 'bar' is not defined.", ex.Message); @@ -126,6 +123,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test var layoutView = CreateView(v => { actual = v.IsSectionDefined("foo"); + v.RenderSection("baz"); + v.RenderBodyPublic(); }); // Act @@ -142,12 +141,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Test bool? actual = null; var view = CreateView(v => { - v.DefineSection("foo", new HelperResult(writer => { })); + v.DefineSection("baz", new HelperResult(writer => { })); v.Layout = LayoutPath; }); var layoutView = CreateView(v => { - actual = v.IsSectionDefined("foo"); + actual = v.IsSectionDefined("baz"); + v.RenderSection("baz"); + v.RenderBodyPublic(); }); // Act @@ -157,9 +158,125 @@ namespace Microsoft.AspNet.Mvc.Razor.Test Assert.Equal(true, actual); } - public static RazorView CreateView(Action executeAction) + + [Fact] + public async Task RenderSection_ThrowsIfSectionIsRenderedMoreThanOnce() { - var view = new Mock { CallBase = true }; + // Arrange + var expected = new HelperResult(action: null); + var view = CreateView(v => + { + v.DefineSection("header", expected); + v.Layout = LayoutPath; + }); + var layoutView = CreateView(v => + { + v.RenderSection("header"); + v.RenderSection("header"); + }); + var viewContext = CreateViewContext(layoutView); + + // Act + var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext)); + + // Assert + Assert.Equal("RenderSection has already been called for the section named 'header'.", ex.Message); + } + + [Fact] + public async Task RenderAsync_ThrowsIfDefinedSectionIsNotRendered() + { + // Arrange + var expected = new HelperResult(action: null); + var view = CreateView(v => + { + v.DefineSection("header", expected); + v.DefineSection("footer", expected); + v.DefineSection("sectionA", expected); + v.Layout = LayoutPath; + }); + var layoutView = CreateView(v => + { + v.RenderSection("sectionA"); + v.RenderBodyPublic(); + }); + var viewContext = CreateViewContext(layoutView); + + // Act + var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext)); + + // Assert + Assert.Equal("The following sections have been defined but have not been rendered: 'header, footer'.", ex.Message); + } + + [Fact] + public async Task RenderAsync_ThrowsIfRenderBodyIsNotCalledFromPage() + { + // Arrange + var expected = new HelperResult(action: null); + var view = CreateView(v => + { + v.Layout = LayoutPath; + }); + var layoutView = CreateView(v => + { + }); + var viewContext = CreateViewContext(layoutView); + + // Act + var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext)); + + // Assert + Assert.Equal("RenderBody must be called from a layout page.", ex.Message); + } + + [Fact] + public async Task RenderAsync_RendersSectionsAndBody() + { + // Arrange + var expected = @"Layout start +Header section +body content +Footer section +Layout end +"; + var view = CreateView(v => + { + v.Layout = LayoutPath; + v.WriteLiteral("body content" + Environment.NewLine); + + v.DefineSection("footer", new HelperResult(writer => + { + writer.WriteLine("Footer section"); + })); + + v.DefineSection("header", new HelperResult(writer => + { + writer.WriteLine("Header section"); + })); + }); + var layoutView = CreateView(v => + { + v.WriteLiteral("Layout start" + Environment.NewLine); + v.Write(v.RenderSection("header")); + v.Write(v.RenderBodyPublic()); + v.Write(v.RenderSection("footer")); + v.WriteLiteral("Layout end" + Environment.NewLine); + + }); + var viewContext = CreateViewContext(layoutView); + + // Act + await view.RenderAsync(viewContext); + + // Assert + var actual = ((StringWriter)viewContext.Writer).ToString(); + Assert.Equal(expected, actual); + } + + private static TestableRazorView CreateView(Action executeAction) + { + var view = new Mock { CallBase = true }; if (executeAction != null) { view.Setup(v => v.ExecuteAsync()) @@ -183,5 +300,13 @@ namespace Microsoft.AspNet.Mvc.Razor.Test Writer = new StringWriter() }; } + + public abstract class TestableRazorView : RazorView + { + public HtmlString RenderBodyPublic() + { + return base.RenderBody(); + } + } } } \ No newline at end of file