Carlos solution of using TagHelper is awesome. I simplified his code down to a single class RazorTagHelper, (as I think it is a bit more aligned with the aspnetcore nomenclature.)
Here's the class
/// <summary>
/// Implements a tag helper as a Razor view as the template
/// </summary>
/// <remarks>
/// uses convention that /TagHelpers/ has razor template based views for tags
/// For a folder /TagHelpers/Foo
/// * FooTagHelper.cs -> Defines the properties with HtmlAttribute on it (derived from ViewTagHelper)
/// * default.cshtml -> Defines the template with Model=>FooTagHelper
/// </remarks>
public class RazorTagHelper : TagHelper
{
private string _viewPath;
public RazorTagHelper()
{
_viewPath = $"~/TagHelpers/{GetType().Namespace.Split('.').Last()}/Default.cshtml";
}
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext? ViewContext { get; set; }
public TagHelperContent? ChildContent { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
throw new Exception("Use ProcessAsync()");
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (ViewContext is null)
{
throw new ArgumentNullException(nameof(ViewContext));
}
// get child content and capture it in our model so we can insert it in our output
ChildContent = await output.GetChildContentAsync();
IHtmlHelper? htmlHelper = ViewContext.HttpContext.RequestServices.GetService<IHtmlHelper>();
ArgumentNullException.ThrowIfNull(htmlHelper);
(htmlHelper as IViewContextAware)!.Contextualize(ViewContext);
var content = await htmlHelper.PartialAsync(_viewPath, this);
output.TagName = null;
output.Content.SetHtmlContent(content);
}
}
NOTE: I default the view path to a convention which is ~/TagHelpers/TagNameFolder/default.cshtml
A sample TagHelper then looks like this:
~/TagHelpers/Person/PersonTagHelper.cs
namespace Project1.TagHelpers.Person
{
[HtmlTargetElement("Person")]
public class PersonTagHelper : ViewTagHelper
{
[HtmlAttributeName]
public string? Name { get; set; } = string.Empty;
[HtmlAttributeName]
public string? ImageUrl { get; set; } = string.Empty;
}
}
and template
~/TagHelpers/Person/default.cshtml
@using Project1.TagHelpers.Person
@model PersonTagHelper
<h1>@Model.Name</h1>
<table>
<tr>
<th colspan=2>@Model.Name</th>
</tr>
<tr>
<td><img src="@Model.ImageUrl"/></td>
<td>@Model.ChildContent</td>
</tr>
</table>
And of course this creates the Person tag which can be used in templates:
<Person Name="Bob">
<b>The thing about bob ...</b>
</Person>
Don't forget to include a @addTagHelper
directive pointing to the assembly containing your taghelpers in your _ViewImports.cshtml
file.
Again, thanks to Carlos and his ServerComponent library, it was very informative.