MiniRazor
MiniRazor copied to clipboard
Add support for templated razor delegates
I have recently had a desire to use Templated Razor Delegates in some of my razor templates recently, and found that the body ends up erased in the generated code. After looking into the diagnostics returned by the razor engine and further research, I believe this feature can be easily enabled. The feature requires adding an ITemplateTargetExtension
as a target extension, and razor provides an implementation in TemplateTargetExtension
. This requires that the template has access to a PushWriter(TextWriter)
method and a PopWriter()
method to temporarily swap the current TextWriter
being used by the template, and access to a type which will wrap the TextWriter
, has a constructor which will accept a delegate compatible with Func<TextWriter, Task>
, and can be passed to the template's Write()
method in order to render the contents of the wrapped TextWriter
.
From what I can tell, the implementation of the wrapper type can be fairly simple and still work.
public class TemplateResult {
private readonly Func<TextWriter, Task> _TemplateDelegate;
public TemplateResult(Func<TextWriter, Task> templateDelegate) => _TemplateDelegate = templateDelegate;
public override ToString() {
using var output = new StringWriter();
// Proper async code could probably be generated by implementing a GetAwaiter() method and requiring the result of
// the invocation of the razor delegate be awaited. However, all implementations I've seen, including the implementation
// built in to MVC use this method to convert the async delegate into a sync call.
_TemplateDelegate(output).GetAwaiter().GetResult();
return output.ToString();
}
}
I have never used templated razor delegates, so I'm probably missing something, but how are they different from just functions returning HTML? Can you show a use case that can't be achieved with inline functions?
The main difference I see between the two is that, with a function, html code put inside is translated directly as a call to WriteLiteral()
, thus the html is written directly to the output buffer as a side effect of calling the function. Razor delegates, on the other hand, return a buffer holder that contains the output generated by the template, without relying on side effects to render the output. They are most useful when calling a method which expects a lambda, and the lambda should return html output, and more control is needed for when the output is rendered rather than just when the lambda is invoked.
I see, that makes sense.
Adding push/pop for output also sounds reasonable.
Can you clarify what do we need TemplateResult
for?
The TemplateResult
type is necessary because that is how the built in TemplateTargetExtension
works (by default it wants a type named Template
, but has a property to change the name to something else, which can help with name clashing). When a template block is detected in the razor code, TemplateTargetExtension
renders it in C# as creating a lambda taking a parameter named item
and returning a new TemplateResult
, passing into the constructor an async lambda with a parameter __razor_template_writer
for obtaining the TextWriter
managed by the TemplateResult
, where the lambda first calls PushWriter(__razor_template_writer)
, then renders the razor delegate as C# code, then calls PopWriter()
. I'm not at my work PC right now, so I can't copy over the exact generated code, so the code below is based on my memory of what was output when I was testing the process.
Original razor code:
@{
System.Func<Person, TemplateResult> template = @<div><span>@item.Name</span><span>@item.Age</span></div>;
@template(new Person { Name = "George Washington", Age = 290 })
}
Simplified generated C# code:
System.Func<Person, TemplateResult> template = item => new TemplateResult(async(__razor_template_writer) => {
PushWriter(__razor_template_writer);
WriteLiteral("<div><span>");
Write(item.Name);
WriteLiteral("</span><span>");
Write(item.Age);
WriteLiteral("</span></div>");
PopWriter();
})
Write(template(new Person { Name = "George Washington", Age = 290 }));
The purpose of PushWriter(__razor_template_writer)
and PopWriter()
is to make sure the calls to Write
, WriteLiteral
, and others write to the TextWriter
owned by the TemplateResult
and do not write to the TextWriter
representing the output of the razor page itself.
The feature would be enabled by adding options.AddTargetExtension(new TemplateTargetExtension { TemplateTypeName = "TemplateResult" })
. If a different type name is desired, it can be changed as needed. It can also be made to be more specific if necessary, such as TemplateTypeName = "global::MiniRazor.TemplateResult"
.
The implementation used by MVC, which makes use of IHtmlContent
and how the provided Write
methods interact with IHtmlContent
can be found at https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor/src/HelperResult.cs.
Closing due to inactivity and lack of interest