CshtmlComponent - ASP.NET Core MVC and Razor Pages Component V1.0.0
Using components in ASP.NET Core MVC or Razor Pages, out of the box, is annoying to say the least (read: a real PITA). Tag Helpers do not support Razor syntax, View Components can not access nested child content. Razor Components do not support runtime compilation and do not work too well in standard MVC or Razor Page projects. CshtmlComponent, from the perspective of an MVC or Razor Pages app, combines the best features of these technologies.
Note: This document assumes that you have a good understanding of C#, Razor markup and ASP.NET Core.
Install the Nuget package.
CshtmlComponent
- Razor Syntax
- Nested Child Content
- Runtime Compilation
- MVC & Razor Pages
- Lenient File Structure
Tag Helper
- Razor Syntax
- Nested Child Content
- Runtime Compilation
- MVC & Razor Pages
- Lenient File Structure
View Component
- Razor Syntax
- Nested Child Content
- Runtime Compilation
- MVC & Razor Pages
- Lenient File Structure
Razor Component
- Razor Syntax
- Nested Child Content
- Runtime Compilation
- MVC & Razor Pages
- Lenient File Structure
Basic Example
This is the C# source code of a basic CshtmlComponent.
using Acmion.CshtmlComponent;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SampleRazorPagesApplication
{
// The associated tag of the component.
[HtmlTargetElement("ExampleComponent")]
public class ExampleComponent : CshtmlComponentBase
{
// These properties are explicitly named.
[HtmlAttributeName("ItemCount")]
public int ItemCount { get; set; } = 0;
[HtmlAttributeName("ItemsPerPage")]
public int ItemsPerPage { get; set; } = 0;
[HtmlAttributeName("PrefixString")]
public string PrefixString { get; set; } = "";
// These properties will default to their kebabcased variants.
public string FontSize { get; set; } = "1rem";
public string BackgroundColor { get; set; } = "rgba(255, 0, 0, 0.1)";
// A not HTML bound properties, that is can not be accessed as a attribute in the component tag.
[HtmlAttributeNotBound]
public int PageCount { get; set; } = 0;
public ExampleComponent(IHtmlHelper htmlHelper) : base(htmlHelper, "/Pages/Components/Example/ExampleComponent.cshtml", "div")
{
// The constructor.
// Note: Only dependency injected arguments.
// "/Pages/Components/Example/ExampleComponent.cshtml" is the path to the associated .cshtml file.
// "div" is the output tag name.
// Properties should not be accessed here, because they will not yet be set.
}
protected override Task ProcessComponent()
{
// This method is called just before the associated .cshtml file is execute.
// Properties have been initialized and can be accessed.
// The property ChildContent is a string that contains the child content.
// Use this method to edit some other properties or fields.
PageCount = (int)Math.Ceiling((ItemCount + 0.0) / ItemsPerPage);
return base.ProcessComponent();
}
}
}
This is the CSHTML source code of the corresponding basic CshtmlComponent.
@* Reference the associated component as model. **@
@using SampleRazorPagesApplication
@model ExampleComponent
@* The content of the component. **@
<div class="example-component" style="padding: 1rem; background-color: @Model.BackgroundColor; font-size: @Model.FontSize">
<div class="example-component-content">
@{
var totCount = 0;
}
@for (var i = 0; i < Model.PageCount; i++)
{
<div>
<p>
<strong>
Page @i
</strong>
</p>
@for (var j = 0; j < Model.ItemsPerPage && totCount < Model.ItemCount; j++)
{
<p>
@Model.PrefixString@j
</p>
totCount++;
}
</div>
}
</div>
<div class="example-component-child-content">
@* Render the child content. **@
@Html.Raw(Model.ChildContent)
</div>
</div>
This is how the component is instantiated from Razor.
<ExampleComponent font-size="1.2rem" background-color="rgba(0, 0, 255, 0.1)" ItemCount="99" ItemsPerPage="10" PrefixString="ItemPrefix">
<div>
<h2>
This is some custom HTML child content.
</h2>
<ExampleComponent font-size="1rem" background-color="rgba(0, 0, 255, 0.1)" ItemCount="9" ItemsPerPage="4" PrefixString="NestedItemPrefix">
Nested components are also supported.
</ExampleComponent>
</div>
</ExampleComponent>
The Code
This is the entire source code of CshtmlComponent.
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Acmion.CshtmlComponent
{
public abstract class CshtmlComponentBase : TagHelper
{
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext
{
get { return _viewContext; }
set { SetViewContext(value); }
}
[HtmlAttributeNotBound]
public string PartialViewName { get; set; }
[HtmlAttributeNotBound]
public string? OutputTagName { get; set; }
[HtmlAttributeNotBound]
public string ChildContent { get; set; }
private IHtmlHelper _htmlHelper;
private ViewContext _viewContext;
// IHtmlHelper htmlHelper is dependency injected by ASP.NET Core
// string partialViewName should be provided by the class that implements CshtmlComponentBase
// string outputTagName should be provided by the class that implements CshtmlComponentBase
public CshtmlComponentBase(IHtmlHelper htmlHelper, string partialViewName, string? outputTagName)
{
_htmlHelper = htmlHelper;
PartialViewName = partialViewName;
OutputTagName = outputTagName;
ChildContent = ""; // Will be replaced in ProcessAsync
_viewContext = null!; // Will be replaced in SetViewContext
}
private void SetViewContext(ViewContext vc)
{
_viewContext = vc;
((IViewContextAware)_htmlHelper).Contextualize(ViewContext);
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
ChildContent = (await output.GetChildContentAsync()).GetContent();
await ProcessComponent();
var content = await _htmlHelper.PartialAsync(PartialViewName, this);
output.TagName = OutputTagName;
output.Content.SetHtmlContent(content);
}
protected virtual Task ProcessComponent()
{
return Task.CompletedTask;
}
}
}
Usage
To create ExampleComponent
with CshtmlComponent, just do the following:
-
Install the Nuget package or copy the code from The Code and paste it into a
.cs
file in your project. -
Create the following files
ExampleComponent.cshtml.cs
andExampleComponent.cshtml
under thePages
orViews
directory, depending on your project type (and other configurations). -
In
ExampleComponent.cs
:-
Inherit
CshtmlComponentBase
. -
Implement the constructor so that all arguments are dependency injected arguments. In most cases,
it is enough that
IHtmlHelper htmlHelper
is the only argument. However, in thebase
call, you must give the path toExampleComponent.cshtml
and what output tag name the component uses. See more about output tag names in the TagHelper docs. -
Add
[HtmlTargetElement("ExampleComponentTag")]
to the component class, whereExampleComponentTag
is the tag that the component will be associated with. -
Optionally, specify how the tag should be closed in
HtmlTargetElement
, see more about how this is achieved in the TagHelper docs. -
List all component attributes as C# properties. ASP.NET Core will translate the properties to their kebabcased
variants, unless otherwise specified with
[HtmlAttributeName("AttributeName")]
, whereAttributeName
is the name of the attribute. -
Create fields or properties for all other values you wish to use in
ExampleComponent.cshtml
. -
Optionally, override
protected virtual Task ProcessComponent()
, which is called beforeExampleComponent.cshtml
is executed. Here you can extract information from properties and access other fields etc. This can not be done in the constructor, since any provided properties will not have been set yet. The nested child content can be accessed in this method by accessingChildContent
.
-
Inherit
-
In
ExampleComponent.cshtml
:-
Set the model to
ExampleComponent
with@model ExampleComponent
. You may need to add appropriate using statements. -
Component properties and field are accessible with
Model.PropertyName
. -
The nested child content can be accessed with
Model.ChildContent
. To render the child content, you can useHtml.Raw(Model.ChildContent)
, but note that this does not encode the HTML, which means that XSS attacks are possible if non-validated user input is used as child content. -
Render your markup and use
.cshtml
files as you wish.
-
Set the model to
-
Add a reference to the newly created component by adding
@addTagHelper *, NameOfTheProjectInWhichExampleComponentResides
, whereNameOfTheProjectInWhichExampleComponentResides
is the name of the project in which ExampleComponent can be found, to a_ViewImports.cshtml
file. -
Initialize
ExampleComponent
on a page and enjoy!
Note: See the sample project in the CshtmlComponent GitHub repository for concrete examples.
Notes
Some notes about CshtmlComponent.
- A CshtmlComponent is just a TagHelper with some "magic". In practice, everything that applies to ASP.NET Core TagHelpers apply to CshtmlComponents.
-
The entire CshtmlComponent is passed as Model to the
.cshtml
file. This includes properties inherited from TagHelper.
Credits
CshtmlComponent was developed by Acmion (GitHub).
Contribute to CshtmlComponent in it's GitHub repository.