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