Adding exceptions for RenderSection, DefineSection and RenderBody

This commit is contained in:
Pranav K 2014-04-04 15:12:10 -07:00
parent 1cd15fbf60
commit 931d18b851
4 changed files with 234 additions and 21 deletions

View File

@ -91,7 +91,7 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
/// <summary> /// <summary>
/// RenderBody can only be called from a layout page. /// {0} can only be called from a layout page.
/// </summary> /// </summary>
internal static string RenderBodyCannotBeCalled internal static string RenderBodyCannotBeCalled
{ {
@ -99,11 +99,27 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
/// <summary> /// <summary>
/// RenderBody can only be called from a layout page. /// {0} can only be called from a layout page.
/// </summary> /// </summary>
internal static string FormatRenderBodyCannotBeCalled() internal static string FormatRenderBodyCannotBeCalled(object p0)
{ {
return GetString("RenderBodyCannotBeCalled"); return string.Format(CultureInfo.CurrentCulture, GetString("RenderBodyCannotBeCalled"), p0);
}
/// <summary>
/// {0} must be called from a layout page.
/// </summary>
internal static string RenderBodyNotCalled
{
get { return GetString("RenderBodyNotCalled"); }
}
/// <summary>
/// {0} must be called from a layout page.
/// </summary>
internal static string FormatRenderBodyNotCalled(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("RenderBodyNotCalled"), p0);
} }
/// <summary> /// <summary>
@ -122,6 +138,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("SectionAlreadyDefined"), p0); return string.Format(CultureInfo.CurrentCulture, GetString("SectionAlreadyDefined"), p0);
} }
/// <summary>
/// {0} has already been called for the section named '{1}'.
/// </summary>
internal static string SectionAlreadyRendered
{
get { return GetString("SectionAlreadyRendered"); }
}
/// <summary>
/// {0} has already been called for the section named '{1}'.
/// </summary>
internal static string FormatSectionAlreadyRendered(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("SectionAlreadyRendered"), p0, p1);
}
/// <summary> /// <summary>
/// Section '{0}' is not defined. /// Section '{0}' is not defined.
/// </summary> /// </summary>
@ -138,6 +170,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("SectionNotDefined"), p0); return string.Format(CultureInfo.CurrentCulture, GetString("SectionNotDefined"), p0);
} }
/// <summary>
/// The following sections have been defined but have not been rendered: '{0}'.
/// </summary>
internal static string SectionsNotRendered
{
get { return GetString("SectionsNotRendered"); }
}
/// <summary>
/// The following sections have been defined but have not been rendered: '{0}'.
/// </summary>
internal static string FormatSectionsNotRendered(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("SectionsNotRendered"), p0);
}
/// <summary> /// <summary>
/// The partial view '{0}' was not found. The following locations were searched:{1} /// The partial view '{0}' was not found. The following locations were searched:{1}
/// </summary> /// </summary>

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Net; using System.Net;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -11,6 +12,9 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
public abstract class RazorView : IView public abstract class RazorView : IView
{ {
private readonly HashSet<string> _renderedSections = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private bool _renderedBody;
public IViewComponentHelper Component public IViewComponentHelper Component
{ {
get { return Context == null ? null : Context.Component; } get { return Context == null ? null : Context.Component; }
@ -59,6 +63,9 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
context.Writer = bodyWriter; context.Writer = bodyWriter;
await ExecuteAsync(); await ExecuteAsync();
// Verify that RenderBody is called, or that RenderSection is called for all sections
VerifyRenderedBodyOrSections();
} }
finally finally
{ {
@ -245,8 +252,9 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
if (BodyContent == null) if (BodyContent == null)
{ {
throw new InvalidOperationException(Resources.RenderBodyCannotBeCalled); throw new InvalidOperationException(Resources.FormatRenderBodyCannotBeCalled("RenderBody"));
} }
_renderedBody = true;
return new HtmlString(BodyContent); return new HtmlString(BodyContent);
} }
@ -273,10 +281,15 @@ namespace Microsoft.AspNet.Mvc.Razor
public HelperResult RenderSection([NotNull] string name, bool required) public HelperResult RenderSection([NotNull] string name, bool required)
{ {
EnsureMethodCanBeInvoked("RenderSection"); EnsureMethodCanBeInvoked("RenderSection");
if (_renderedSections.Contains(name))
{
throw new InvalidOperationException(Resources.FormatSectionAlreadyRendered("RenderSection", name));
}
HelperResult action; HelperResult action;
if (PreviousSectionWriters.TryGetValue(name, out action)) if (PreviousSectionWriters.TryGetValue(name, out action))
{ {
_renderedSections.Add(name);
return action; return action;
} }
else if (required) else if (required)
@ -298,5 +311,23 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new InvalidOperationException(Resources.FormatView_MethodCannotBeCalled(methodName)); 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"));
}
}
}
} }
} }

View File

@ -133,14 +133,23 @@
<value>Only one '{0}' statement is allowed in a file.</value> <value>Only one '{0}' statement is allowed in a file.</value>
</data> </data>
<data name="RenderBodyCannotBeCalled" xml:space="preserve"> <data name="RenderBodyCannotBeCalled" xml:space="preserve">
<value>RenderBody can only be called from a layout page.</value> <value>{0} can only be called from a layout page.</value>
</data>
<data name="RenderBodyNotCalled" xml:space="preserve">
<value>{0} must be called from a layout page.</value>
</data> </data>
<data name="SectionAlreadyDefined" xml:space="preserve"> <data name="SectionAlreadyDefined" xml:space="preserve">
<value>Section '{0}' is already defined.</value> <value>Section '{0}' is already defined.</value>
</data> </data>
<data name="SectionAlreadyRendered" xml:space="preserve">
<value>{0} has already been called for the section named '{1}'.</value>
</data>
<data name="SectionNotDefined" xml:space="preserve"> <data name="SectionNotDefined" xml:space="preserve">
<value>Section '{0}' is not defined.</value> <value>Section '{0}' is not defined.</value>
</data> </data>
<data name="SectionsNotRendered" xml:space="preserve">
<value>The following sections have been defined but have not been rendered: '{0}'.</value>
</data>
<data name="ViewEngine_PartialViewNotFound" xml:space="preserve"> <data name="ViewEngine_PartialViewNotFound" xml:space="preserve">
<value>The partial view '{0}' was not found. The following locations were searched:{1}</value> <value>The partial view '{0}' was not found. The following locations were searched:{1}</value>
</data> </data>

View File

@ -16,21 +16,19 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined() public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined()
{ {
// Arrange // Arrange
Exception ex = null;
var view = CreateView(v => var view = CreateView(v =>
{ {
v.DefineSection("foo", new HelperResult(action: null)); v.DefineSection("qux", new HelperResult(action: null));
v.DefineSection("qux", new HelperResult(action: null));
ex = Assert.Throws<InvalidOperationException>(
() => v.DefineSection("foo", new HelperResult(action: null)));
}); });
var viewContext = CreateViewContext(layoutView: null); var viewContext = CreateViewContext(layoutView: null);
// Act // Act
await view.RenderAsync(viewContext); var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => view.RenderAsync(viewContext));
// Assert // Assert
Assert.Equal("Section 'foo' is already defined.", ex.Message); Assert.Equal("Section 'qux' is already defined.", ex.Message);
} }
[Fact] [Fact]
@ -47,6 +45,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
var layoutView = CreateView(v => var layoutView = CreateView(v =>
{ {
actual = v.RenderSection("bar"); actual = v.RenderSection("bar");
v.RenderBodyPublic();
}); });
var viewContext = CreateViewContext(layoutView); var viewContext = CreateViewContext(layoutView);
@ -81,7 +80,6 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{ {
// Arrange // Arrange
var expected = new HelperResult(action: null); var expected = new HelperResult(action: null);
Exception ex = null;
var view = CreateView(v => var view = CreateView(v =>
{ {
v.DefineSection("baz", expected); v.DefineSection("baz", expected);
@ -89,13 +87,12 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
}); });
var layoutView = CreateView(v => var layoutView = CreateView(v =>
{ {
ex = Assert.Throws<InvalidOperationException>( v.RenderSection("bar");
() => v.RenderSection("bar"));
}); });
var viewContext = CreateViewContext(layoutView); var viewContext = CreateViewContext(layoutView);
// Act // Act
await view.RenderAsync(viewContext); var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => view.RenderAsync(viewContext));
// Assert // Assert
Assert.Equal("Section 'bar' is not defined.", ex.Message); Assert.Equal("Section 'bar' is not defined.", ex.Message);
@ -126,6 +123,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
var layoutView = CreateView(v => var layoutView = CreateView(v =>
{ {
actual = v.IsSectionDefined("foo"); actual = v.IsSectionDefined("foo");
v.RenderSection("baz");
v.RenderBodyPublic();
}); });
// Act // Act
@ -142,12 +141,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
bool? actual = null; bool? actual = null;
var view = CreateView(v => var view = CreateView(v =>
{ {
v.DefineSection("foo", new HelperResult(writer => { })); v.DefineSection("baz", new HelperResult(writer => { }));
v.Layout = LayoutPath; v.Layout = LayoutPath;
}); });
var layoutView = CreateView(v => var layoutView = CreateView(v =>
{ {
actual = v.IsSectionDefined("foo"); actual = v.IsSectionDefined("baz");
v.RenderSection("baz");
v.RenderBodyPublic();
}); });
// Act // Act
@ -157,9 +158,125 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
Assert.Equal(true, actual); Assert.Equal(true, actual);
} }
public static RazorView CreateView(Action<RazorView> executeAction)
[Fact]
public async Task RenderSection_ThrowsIfSectionIsRenderedMoreThanOnce()
{ {
var view = new Mock<RazorView> { 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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<TestableRazorView> executeAction)
{
var view = new Mock<TestableRazorView> { CallBase = true };
if (executeAction != null) if (executeAction != null)
{ {
view.Setup(v => v.ExecuteAsync()) view.Setup(v => v.ExecuteAsync())
@ -183,5 +300,13 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
Writer = new StringWriter() Writer = new StringWriter()
}; };
} }
public abstract class TestableRazorView : RazorView
{
public HtmlString RenderBodyPublic()
{
return base.RenderBody();
}
}
} }
} }