MiniRazor icon indicating copy to clipboard operation
MiniRazor copied to clipboard

Add support for templated razor delegates

Open TheJayMann opened this issue 3 years ago • 4 comments

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();
    }
}

TheJayMann avatar Mar 12 '21 05:03 TheJayMann

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?

Tyrrrz avatar Mar 12 '21 15:03 Tyrrrz

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.

TheJayMann avatar Mar 12 '21 16:03 TheJayMann

I see, that makes sense.

Adding push/pop for output also sounds reasonable.

Can you clarify what do we need TemplateResult for?

Tyrrrz avatar Mar 12 '21 18:03 Tyrrrz

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.

TheJayMann avatar Mar 12 '21 18:03 TheJayMann

Closing due to inactivity and lack of interest

Tyrrrz avatar Aug 25 '22 19:08 Tyrrrz