diff --git a/src/RazorBlade.Library/HtmlLayout.cs b/src/RazorBlade.Library/HtmlLayout.cs
index bf20c03..f3104fd 100644
--- a/src/RazorBlade.Library/HtmlLayout.cs
+++ b/src/RazorBlade.Library/HtmlLayout.cs
@@ -9,11 +9,9 @@ namespace RazorBlade;
///
public abstract class HtmlLayout : HtmlTemplate, IRazorLayout
{
- private IRazorLayout.IExecutionResult? _layoutInput;
+ private IRazorExecutionResult? _layoutInput;
- private IRazorLayout.IExecutionResult LayoutInput => _layoutInput ?? throw new InvalidOperationException("No layout is being rendered.");
-
- async Task IRazorLayout.ExecuteLayoutAsync(IRazorLayout.IExecutionResult input)
+ async Task IRazorLayout.ExecuteLayoutAsync(IRazorExecutionResult input)
{
try
{
@@ -30,7 +28,7 @@ public abstract class HtmlLayout : HtmlTemplate, IRazorLayout
/// Returns the inner page body.
///
protected internal IEncodedContent RenderBody()
- => LayoutInput.Body;
+ => GetLayoutInput().Body;
///
/// Renders a required section and returns the result as encoded content.
@@ -71,7 +69,7 @@ protected internal Task RenderSectionAsync(string name)
/// The content to write to the output.
protected internal async Task RenderSectionAsync(string name, bool required)
{
- var result = await LayoutInput.RenderSectionAsync(name).ConfigureAwait(false);
+ var result = await GetLayoutInput().RenderSectionAsync(name).ConfigureAwait(false);
if (result is not null)
return result;
@@ -79,6 +77,19 @@ protected internal async Task RenderSectionAsync(string name, b
if (required)
throw new InvalidOperationException($"Section '{name}' is not defined.");
- return StringBuilderEncodedContent.Empty;
+ return HtmlString.Empty;
}
+
+ ///
+ /// Indicates if a given section is defined.
+ ///
+ /// The section name.
+ protected internal bool IsSectionDefined(string name)
+ => GetLayoutInput().IsSectionDefined(name);
+
+ ///
+ /// Ensures the template is being executed as a layout and returns the input data.
+ ///
+ private IRazorExecutionResult GetLayoutInput()
+ => _layoutInput ?? throw new InvalidOperationException("The template is not being executed as a layout.");
}
diff --git a/src/RazorBlade.Library/HtmlString.cs b/src/RazorBlade.Library/HtmlString.cs
index 594d061..6d4939c 100644
--- a/src/RazorBlade.Library/HtmlString.cs
+++ b/src/RazorBlade.Library/HtmlString.cs
@@ -9,6 +9,11 @@ public sealed class HtmlString : IEncodedContent
{
private readonly string _value;
+ ///
+ /// Represents an empty HTML-encoded string.
+ ///
+ public static HtmlString Empty { get; } = new(string.Empty);
+
///
/// Creates a HTML-encoded string.
///
diff --git a/src/RazorBlade.Library/HtmlTemplate.cs b/src/RazorBlade.Library/HtmlTemplate.cs
index 1a421fe..aaef0a3 100644
--- a/src/RazorBlade.Library/HtmlTemplate.cs
+++ b/src/RazorBlade.Library/HtmlTemplate.cs
@@ -155,7 +155,5 @@ protected HtmlTemplate(TModel model)
/// This constructor is provided for the designer only. Do not use.
///
protected HtmlTemplate()
- {
- throw new NotSupportedException("Use the constructor overload that takes a model.");
- }
+ => throw new NotSupportedException("Use the constructor overload that takes a model.");
}
diff --git a/src/RazorBlade.Library/IRazorExecutionResult.cs b/src/RazorBlade.Library/IRazorExecutionResult.cs
new file mode 100644
index 0000000..af6fd4d
--- /dev/null
+++ b/src/RazorBlade.Library/IRazorExecutionResult.cs
@@ -0,0 +1,38 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace RazorBlade;
+
+///
+/// The execution result of a Razor template.
+///
+internal interface IRazorExecutionResult
+{
+ ///
+ /// The rendered body contents.
+ ///
+ IEncodedContent Body { get; }
+
+ ///
+ /// The layout this execution result should be wrapped in.
+ ///
+ IRazorLayout? Layout { get; }
+
+ ///
+ /// The cancellation token.
+ ///
+ CancellationToken CancellationToken { get; }
+
+ ///
+ /// Renders a section.
+ ///
+ /// The section name.
+ /// The rendered output, or null if the section is not defined.
+ Task RenderSectionAsync(string name);
+
+ ///
+ /// Indicates if a given section is defined.
+ ///
+ /// The section name.
+ bool IsSectionDefined(string name);
+}
diff --git a/src/RazorBlade.Library/IRazorLayout.cs b/src/RazorBlade.Library/IRazorLayout.cs
index 8b38879..d1ce858 100644
--- a/src/RazorBlade.Library/IRazorLayout.cs
+++ b/src/RazorBlade.Library/IRazorLayout.cs
@@ -1,45 +1,16 @@
-using System.Threading;
-using System.Threading.Tasks;
+using System.Threading.Tasks;
namespace RazorBlade;
///
/// Represents a Razor layout page.
///
-public interface IRazorLayout
+internal interface IRazorLayout
{
///
/// Renders the layout for a given page.
///
/// The input data.
/// The output data after rendering the layout, which can be used for the next layout.
- Task ExecuteLayoutAsync(IExecutionResult input);
-
- ///
- /// The execution result of a page.
- ///
- public interface IExecutionResult
- {
- ///
- /// The rendered body contents.
- ///
- IEncodedContent Body { get; }
-
- ///
- /// The layout this execution result needs to be wrapped in.
- ///
- IRazorLayout? Layout { get; }
-
- ///
- /// The cancellation token.
- ///
- CancellationToken CancellationToken { get; }
-
- ///
- /// Renders a section.
- ///
- /// The section name.
- /// The rendered output, or null if the section is not defined.
- Task RenderSectionAsync(string name);
- }
+ Task ExecuteLayoutAsync(IRazorExecutionResult input);
}
diff --git a/src/RazorBlade.Library/RazorTemplate.cs b/src/RazorBlade.Library/RazorTemplate.cs
index d0acd2f..5f6f894 100644
--- a/src/RazorBlade.Library/RazorTemplate.cs
+++ b/src/RazorBlade.Library/RazorTemplate.cs
@@ -32,7 +32,7 @@ public abstract class RazorTemplate : IEncodedContent
///
/// The layout to use.
///
- protected internal IRazorLayout? Layout { get; set; }
+ private protected IRazorLayout? Layout { get; set; }
///
/// Renders the template synchronously and returns the result as a string.
@@ -112,6 +112,9 @@ public async Task RenderAsync(TextWriter textWriter, CancellationToken cancellat
#endif
}
+ ///
+ /// Renders the template asynchronously including its layout and returns the result as a .
+ ///
private async Task RenderAsyncCore(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -124,7 +127,7 @@ private async Task RenderAsyncCore(CancellationToken cancellation
executionResult = await layout.ExecuteLayoutAsync(executionResult).ConfigureAwait(false);
}
- if (executionResult.Body is StringBuilderEncodedContent { StringBuilder: var outputStringBuilder })
+ if (executionResult.Body is EncodedContent { Output: var outputStringBuilder })
return outputStringBuilder;
var outputStringWriter = new StringWriter();
@@ -132,13 +135,16 @@ private async Task RenderAsyncCore(CancellationToken cancellation
return outputStringWriter.GetStringBuilder();
}
- private protected async Task ExecuteAsyncCore(CancellationToken cancellationToken)
+ ///
+ /// Calls the method in a new .
+ ///
+ private protected async Task ExecuteAsyncCore(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using var executionScope = new ExecutionScope(this, cancellationToken);
await ExecuteAsync().ConfigureAwait(false);
- return new ExecutionResult(this, executionScope.Output);
+ return new ExecutionResult(executionScope);
}
///
@@ -226,7 +232,47 @@ protected internal void DefineSection(string name, Func action)
void IEncodedContent.WriteTo(TextWriter textWriter)
=> Render(textWriter, CancellationToken.None);
- private class ExecutionResult : IRazorLayout.IExecutionResult
+ ///
+ /// Saves the current state, resets it, and restores it when disposed.
+ ///
+ private class ExecutionScope : IDisposable
+ {
+ private readonly Dictionary>? _previousSections;
+ private readonly TextWriter _previousOutput;
+ private readonly CancellationToken _previousCancellationToken;
+ private readonly IRazorLayout? _previousLayout;
+
+ public RazorTemplate Page { get; }
+ public StringBuilder Output { get; } = new();
+
+ public ExecutionScope(RazorTemplate page, CancellationToken cancellationToken)
+ {
+ Page = page;
+
+ _previousSections = page._sections;
+ _previousOutput = page.Output;
+ _previousCancellationToken = page.CancellationToken;
+ _previousLayout = page.Layout;
+
+ page._sections = null;
+ page.Output = new StringWriter(Output);
+ page.CancellationToken = cancellationToken;
+ page.Layout = null;
+ }
+
+ public void Dispose()
+ {
+ Page._sections = _previousSections;
+ Page.Output = _previousOutput;
+ Page.CancellationToken = _previousCancellationToken;
+ Page.Layout = _previousLayout;
+ }
+ }
+
+ ///
+ /// Stores the result of a template execution.
+ ///
+ private class ExecutionResult : IRazorExecutionResult
{
private readonly RazorTemplate _page;
private readonly IReadOnlyDictionary>? _sections;
@@ -235,73 +281,48 @@ private class ExecutionResult : IRazorLayout.IExecutionResult
public IRazorLayout? Layout { get; }
public CancellationToken CancellationToken { get; }
- public ExecutionResult(RazorTemplate page, StringBuilder body)
+ public ExecutionResult(ExecutionScope executionScope)
{
- _page = page;
- _sections = page._sections;
- Body = new StringBuilderEncodedContent(body);
- Layout = page.Layout;
- CancellationToken = page.CancellationToken;
+ _page = executionScope.Page;
+ _sections = _page._sections;
+ Body = new EncodedContent(executionScope.Output);
+ Layout = _page.Layout;
+ CancellationToken = _page.CancellationToken;
}
+ public bool IsSectionDefined(string name)
+ => _sections?.ContainsKey(name) is true;
+
public async Task RenderSectionAsync(string name)
{
if (_sections is null || !_sections.TryGetValue(name, out var sectionAction))
return null;
using var executionScope = new ExecutionScope(_page, CancellationToken);
+ _page.Layout = Layout; // The section might reference this instance.
await sectionAction().ConfigureAwait(false);
- return new StringBuilderEncodedContent(executionScope.Output);
+ return new EncodedContent(executionScope.Output);
}
}
- private protected class StringBuilderEncodedContent : IEncodedContent
+ ///
+ /// Stores the output of a template execution as a .
+ ///
+ ///
+ /// StringBuilders can be combined more efficiently than strings, which is useful for layouts.
+ /// has a dedicated Write overload for .
+ ///
+ private class EncodedContent : IEncodedContent
{
- public static IEncodedContent Empty { get; } = new StringBuilderEncodedContent(new StringBuilder());
-
- public StringBuilder StringBuilder { get; }
+ public StringBuilder Output { get; }
- public StringBuilderEncodedContent(StringBuilder stringBuilder)
- => StringBuilder = stringBuilder;
+ public EncodedContent(StringBuilder value)
+ => Output = value;
public void WriteTo(TextWriter textWriter)
- => textWriter.Write(StringBuilder);
+ => textWriter.Write(Output);
public override string ToString()
- => StringBuilder.ToString();
- }
-
- private class ExecutionScope : IDisposable
- {
- private readonly RazorTemplate _page;
-
- private readonly Dictionary>? _sections;
- private readonly TextWriter _output;
- private readonly CancellationToken _cancellationToken;
- private readonly IRazorLayout? _layout;
-
- public StringBuilder Output { get; }
-
- public ExecutionScope(RazorTemplate page, CancellationToken cancellationToken)
- {
- _page = page;
-
- _sections = page._sections;
- _output = page.Output;
- _cancellationToken = page.CancellationToken;
- _layout = page.Layout;
-
- Output = new StringBuilder();
- page.Output = new StringWriter(Output);
- page.CancellationToken = cancellationToken;
- }
-
- public void Dispose()
- {
- _page._sections = _sections;
- _page.Output = _output;
- _page.CancellationToken = _cancellationToken;
- _page.Layout = _layout;
- }
+ => Output.ToString();
}
}
diff --git a/src/RazorBlade.Tests/HtmlLayoutTests.cs b/src/RazorBlade.Tests/HtmlLayoutTests.cs
index 99c4fb9..fce4422 100644
--- a/src/RazorBlade.Tests/HtmlLayoutTests.cs
+++ b/src/RazorBlade.Tests/HtmlLayoutTests.cs
@@ -104,6 +104,31 @@ public void should_render_nested_layout_sections()
page.Render().ShouldEqual("innerSection pageSection");
}
+ [Test]
+ public void should_keep_the_layout_in_sections()
+ {
+ var sectionRendered = false;
+
+ var layout = new Layout(t => t.Write(t.RenderSection("section")));
+
+ var page = new Template(t =>
+ {
+ t.Layout = layout;
+ t.DefineSection(
+ "section",
+ () =>
+ {
+ t.Layout.ShouldBeTheSameAs(layout);
+ sectionRendered = true;
+ return Task.CompletedTask;
+ }
+ );
+ });
+
+ page.Render();
+ sectionRendered.ShouldBeTrue();
+ }
+
private class Template : HtmlTemplate
{
private readonly Action _executeAction;