// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Razor
{
///
/// Represents properties and methods that are needed in order to render a view that uses Razor syntax.
///
public abstract class RazorPage : RazorPageBase
{
private readonly HashSet _renderedSections = new HashSet(StringComparer.OrdinalIgnoreCase);
private bool _renderedBody;
private bool _ignoreBody;
private HashSet _ignoredSections;
///
/// An representing the current request execution.
///
public HttpContext Context => ViewContext?.HttpContext;
///
/// In a Razor layout page, renders the portion of a content page that is not within a named section.
///
/// The HTML content to render.
protected virtual IHtmlContent RenderBody()
{
if (BodyContent == null)
{
var message = Resources.FormatRazorPage_MethodCannotBeCalled(nameof(RenderBody), Path);
throw new InvalidOperationException(message);
}
_renderedBody = true;
return BodyContent;
}
///
/// In a Razor layout page, ignores rendering the portion of a content page that is not within a named section.
///
public void IgnoreBody()
{
_ignoreBody = true;
}
///
/// Creates a named content section in the page that can be invoked in a Layout page using
/// or .
///
/// The name of the section to create.
/// The to execute when rendering the section.
public override void DefineSection(string name, RenderAsyncDelegate section)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (section == null)
{
throw new ArgumentNullException(nameof(section));
}
if (SectionWriters.ContainsKey(name))
{
throw new InvalidOperationException(Resources.FormatSectionAlreadyDefined(name));
}
SectionWriters[name] = section;
}
///
/// Returns a value that indicates whether the specified section is defined in the content page.
///
/// The section name to search for.
/// true if the specified section is defined in the content page; otherwise, false.
public bool IsSectionDefined(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
EnsureMethodCanBeInvoked(nameof(IsSectionDefined));
return PreviousSectionWriters.ContainsKey(name);
}
///
/// In layout pages, renders the content of the section named .
///
/// The name of the section to render.
/// An empty .
/// The method writes to the and the value returned is a token
/// value that allows the Write (produced due to @RenderSection(..)) to succeed. However the
/// value does not represent the rendered content.
public HtmlString RenderSection(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
return RenderSection(name, required: true);
}
///
/// In layout pages, renders the content of the section named .
///
/// The section to render.
/// Indicates if this section must be rendered.
/// An empty .
/// The method writes to the and the value returned is a token
/// value that allows the Write (produced due to @RenderSection(..)) to succeed. However the
/// value does not represent the rendered content.
public HtmlString RenderSection(string name, bool required)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
EnsureMethodCanBeInvoked(nameof(RenderSection));
var task = RenderSectionAsyncCore(name, required);
return task.GetAwaiter().GetResult();
}
///
/// In layout pages, asynchronously renders the content of the section named .
///
/// The section to render.
///
/// A that on completion returns an empty .
///
/// The method writes to the and the value returned is a token
/// value that allows the Write (produced due to @RenderSection(..)) to succeed. However the
/// value does not represent the rendered content.
public Task RenderSectionAsync(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
return RenderSectionAsync(name, required: true);
}
///
/// In layout pages, asynchronously renders the content of the section named .
///
/// The section to render.
/// Indicates the section must be registered
/// (using @section) in the page.
///
/// A that on completion returns an empty .
///
/// The method writes to the and the value returned is a token
/// value that allows the Write (produced due to @RenderSection(..)) to succeed. However the
/// value does not represent the rendered content.
/// if is true and the section
/// was not registered using the @section in the Razor page.
public Task RenderSectionAsync(string name, bool required)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
EnsureMethodCanBeInvoked(nameof(RenderSectionAsync));
return RenderSectionAsyncCore(name, required);
}
private async Task RenderSectionAsyncCore(string sectionName, bool required)
{
if (_renderedSections.Contains(sectionName))
{
var message = Resources.FormatSectionAlreadyRendered(nameof(RenderSectionAsync), Path, sectionName);
throw new InvalidOperationException(message);
}
if (PreviousSectionWriters.TryGetValue(sectionName, out var renderDelegate))
{
_renderedSections.Add(sectionName);
await renderDelegate();
// Return a token value that allows the Write call that wraps the RenderSection \ RenderSectionAsync
// to succeed.
return HtmlString.Empty;
}
else if (required)
{
// If the section is not found, and it is not optional, throw an error.
var message = Resources.FormatSectionNotDefined(
ViewContext.ExecutingFilePath,
sectionName,
ViewContext.View.Path);
throw new InvalidOperationException(message);
}
else
{
// If the section is optional and not found, then don't do anything.
return null;
}
}
///
/// In layout pages, ignores rendering the content of the section named .
///
/// The section to ignore.
public void IgnoreSection(string sectionName)
{
if (sectionName == null)
{
throw new ArgumentNullException(nameof(sectionName));
}
if (!PreviousSectionWriters.ContainsKey(sectionName))
{
// If the section is not defined, throw an error.
throw new InvalidOperationException(Resources.FormatSectionNotDefined(
ViewContext.ExecutingFilePath,
sectionName,
ViewContext.View.Path));
}
if (_ignoredSections == null)
{
_ignoredSections = new HashSet(StringComparer.OrdinalIgnoreCase);
}
_ignoredSections.Add(sectionName);
}
///
public override void EnsureRenderedBodyOrSections()
{
// a) all sections defined for this page are rendered.
// b) if no sections are defined, then the body is rendered if it's available.
if (PreviousSectionWriters != null && PreviousSectionWriters.Count > 0)
{
var sectionsNotRendered = PreviousSectionWriters.Keys.Except(
_renderedSections,
StringComparer.OrdinalIgnoreCase);
string[] sectionsNotIgnored;
if (_ignoredSections != null)
{
sectionsNotIgnored = sectionsNotRendered.Except(_ignoredSections, StringComparer.OrdinalIgnoreCase).ToArray();
}
else
{
sectionsNotIgnored = sectionsNotRendered.ToArray();
}
if (sectionsNotIgnored.Length > 0)
{
var sectionNames = string.Join(", ", sectionsNotIgnored);
throw new InvalidOperationException(Resources.FormatSectionsNotRendered(Path, sectionNames, nameof(IgnoreSection)));
}
}
else if (BodyContent != null && !_renderedBody && !_ignoreBody)
{
// There are no sections defined, but RenderBody was NOT called.
// If a body was defined and the body not ignored, then RenderBody should have been called.
var message = Resources.FormatRenderBodyNotCalled(nameof(RenderBody), Path, nameof(IgnoreBody));
throw new InvalidOperationException(message);
}
}
public override void BeginContext(int position, int length, bool isLiteral)
{
const string BeginContextEvent = "Microsoft.AspNetCore.Mvc.Razor.BeginInstrumentationContext";
if (DiagnosticSource?.IsEnabled(BeginContextEvent) == true)
{
DiagnosticSource.Write(
BeginContextEvent,
new
{
httpContext = Context,
path = Path,
position = position,
length = length,
isLiteral = isLiteral,
});
}
}
public override void EndContext()
{
const string EndContextEvent = "Microsoft.AspNetCore.Mvc.Razor.EndInstrumentationContext";
if (DiagnosticSource?.IsEnabled(EndContextEvent) == true)
{
DiagnosticSource.Write(
EndContextEvent,
new
{
httpContext = Context,
path = Path,
});
}
}
private void EnsureMethodCanBeInvoked(string methodName)
{
if (PreviousSectionWriters == null)
{
throw new InvalidOperationException(Resources.FormatRazorPage_MethodCannotBeCalled(methodName, Path));
}
}
}
}