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>
/// RenderBody can only be called from a layout page.
/// {0} can only be called from a layout page.
/// </summary>
internal static string RenderBodyCannotBeCalled
{
@ -99,11 +99,27 @@ namespace Microsoft.AspNet.Mvc.Razor
}
/// <summary>
/// RenderBody can only be called from a layout page.
/// {0} can only be called from a layout page.
/// </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>
@ -122,6 +138,22 @@ namespace Microsoft.AspNet.Mvc.Razor
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>
/// Section '{0}' is not defined.
/// </summary>
@ -138,6 +170,22 @@ namespace Microsoft.AspNet.Mvc.Razor
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>
/// The partial view '{0}' was not found. The following locations were searched:{1}
/// </summary>

View File

@ -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<string> _renderedSections = new HashSet<string>(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"));
}
}
}
}
}

View File

@ -133,14 +133,23 @@
<value>Only one '{0}' statement is allowed in a file.</value>
</data>
<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 name="SectionAlreadyDefined" xml:space="preserve">
<value>Section '{0}' is already defined.</value>
</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">
<value>Section '{0}' is not defined.</value>
</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">
<value>The partial view '{0}' was not found. The following locations were searched:{1}</value>
</data>

View File

@ -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<InvalidOperationException>(
() => 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<InvalidOperationException>(
() => 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<InvalidOperationException>(
() => v.RenderSection("bar"));
v.RenderSection("bar");
});
var viewContext = CreateViewContext(layoutView);
// Act
await view.RenderAsync(viewContext);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => 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<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)
{
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();
}
}
}
}