Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Razor Templating Improvements #4988

Closed
DamianEdwards opened this issue Jun 14, 2016 · 14 comments
Closed

Razor Templating Improvements #4988

DamianEdwards opened this issue Jun 14, 2016 · 14 comments
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-razor-pages
Milestone

Comments

@DamianEdwards
Copy link
Member

Introduction

Today, Razor doesn't expose much functionality to enable app developers to leverage Razor templates at runtime. That is, Razor is a templating language, but it doesn't have many features that allow the developer to use templates written in Razor as a first-class primitive in view/page composition.

The one feature that does exist is that of "Templated Razor Delegates" (or "Template Expressions"), which allow for fragments of Razor to be captured as delegates and thus passed into functions, including HTML Helpers, and then executed to produce HTML output. E.g. in the following code, the block @<li>@item</li> is the "template expression".

Helpers.cs

public static class Helpers
{
    public static IHtmlContent ForEach<T>(IEnumerable<T> data, Func<T, IHtmlContent> template)
    {
        var builder = new HtmlBuilder();
        foreach (var item in data)
        {
            builder.AppendHtml(template(item));
        }
        return builder;
    }
}

View.cshtml

<ul>
@ForEach(new [] { "one", "two", "three" }, @<li>@item</li>)
</ul>

This feature has a number of limitations that if addressed, and expanded upon, can provide a more powerful and expressive set of first-class templating features in Razor.

Support Declarative Template Authoring & Re-use via an @template Directive

Rather than trying to add new features to the existing template expressions feature, which could be limited by issues of backwards compatibility, we'll add a new directive to support declaratively creating Razor templates in CSHTML files:

@template HelloWorld(string name) {
    <div>Hello @name!</div>
}

This would be roughly equivalent to (and thus generate):

public IHtmlContent HelloWorld(string name)
{
    var builder = new HtmlBuilder();
    builder.AppendHtml("<div>Hello ");
    builder.Append(name);
    builder.AppendHtml("!</div>")
    return builder;
}

This could be declared in any Razor file, including in _ViewImports.cshtml in an MVC application, to allow for easy re-use across the application.

The templates could then be executed directly in Razor files. In this way they're very similar to the @helper directive from ASP.NET Web Pages:

@HelloWorld("Frank")

They can also be passed in to other methods that accept the template delegate signature:

@{ var names = new [] { "Frank", "Mary", "Jane" }; }
@ForEach(names, HelloWorld)

Support Truly Async Template Expressions

Currently, while the delegates generated by template expressions support async statements, they are always evaluated synchronously (i.e. the calling thread is blocked while the delegate is executed). This logic is actually in MVC, so we'd need to make some changes to the contract between Razor and a Razor host, or limit this functionality to the new @template directive (see above).

@template async HelloWorldAsync(string name) {
    await Task.Delay(100);
    <div>Hello @name!</div>
}

This would be roughly equivalent to (and thus generate):

public async Task<IHtmlContent> HelloWorld(string name)
{
    await Task.Delay(100);
    var builder = new HtmlBuilder();
    builder.AppendHtml("<div>Hello ");
    builder.Append(name);
    builder.AppendHtml("!</div>")
    return builder;
}

Support Multiple Arguments

Template expressions are currently limited to a single argument. We should extend this to support multiple arguments when using the @template directive, e.g.:

@template HelloWorld(string firstName, string lastName) {
    <div>Hello @firstName @lastName!</div>
}

This would be roughly equivalent to (and thus generate):

public IHtmlContent HelloWorld(string firstName, string lastName)
{
    var builder = new HtmlBuilder();
    builder.AppendHtml("<div>Hello ");
    builder.Append(firstName);
    builder.AppendHtml(" ");
    builder.Append(lastName);
    builder.AppendHtml("!</div>")
    return builder;
}

The templates could then be executed directly in Razor files. In this way they're very similar to the @helper directive from ASP.NET Web Pages:

@HelloWorld("Mary", "Lou")

They can also be passed in to other methods that accept the template delegate signature:

public static class Helpers
{
    public static IHtmlContent MyHelper<T1, T2>(T1 data, T2 state, Func<T1, T2, IHtmlContent> template)
    {
        var builder = new HtmlBuilder();
        builder.AppendHtml(template(data, state));
        return builder;
    }
}

Support Templated Tag Helpers

We should extend Tag Helpers to make working with templates a first-class experience. Binding templates to properties as well as treating the content or even the entire element of a Tag Helper as a template should be possible.

Tag Helper with content as a template

[HtmlTargetElement("*", Attributes = "asp-repeat")]
public class RepeatTagHelper<TItem> : TemplatedTagHelper<TItem>
{
    [HtmlTargetAttribute("asp-repeat")]
    public IEnumerable<TItem> Items { get; set; }

    public override async Task ProcessAsync(TagHelperContext<TItem> context, TagHelperOutput output)
    {
        foreach (var item in Items)
        {
            output.AppendHtml(await context.GetChildContentAsync(item));
        }
    }
}
@{
    var customers = DB.GetCustomers().ToList();
}
<table>
    <tbody asp-repeat="customers">
        <tr>
            <td>@item.FirstName</td>
            <td>@item.LastName</td>
        </tr>
    </tbody>
</table>

Tag Helper with template properties

public class ListViewTagHelper<TItem> : TagHelper
{
    public IEnumerable<TItem> Data { get; set; }

    public Action<IHtmlContent> HeaderTemplate { get; set; }

    public Func<TItem, IHtmlContent> ItemTemplate { get; set; }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        foreach (var item in Items)
        {
            output.AppendHtml(await context.GetChildContentAsync(item));
        }
    }
}
@model IEnumerable<Customer>
@template ContentTemplate(IEnumerable<T> items, Func<T,IHtmlContent> rowTemplate) {
    <tbody>
        @foreach (var item in items)
        {
            rowTemplate(item);
        }
    </tbody>
}
@template HeaderTemplate() {
    <thead>
        <tr>
            <th>@Html.DisplayNameFor(m => m.FirstName)</th>
            <th>@Html.DisplayNameFor(m => m.LastName)</th>
        </tr>
    </thead>
}
@template CustomerRowTemplate(Customer customer) {
    <tr>
            <td>@item.FirstName</td>
            <td>@item.LastName</td>
        </tr>
}
<table>
<list-view data="customers" header-template="HeaderTemplate" content-template="ContentTemplate" item-template="CustomerRowTemplate" />
</table>

TODO: Update with some details about integration with #791

Support Template Compilation & Packaging

To enable sharing of declared templates across projects without having to restore to writing them manually in C#, we should build a tool that allows compiling templates from CSHTML files directly to assemblies/NuGet packages. This could be a project tool that works with the .NET Core CLI (e.g. dotnet razor compile-templates) or a Roslyn compilation extension/module that allows for templates (and potentially other Razor primitives) to be compiled as part of project compilation.

@analogrelay
Copy link
Contributor

I'm still not 100% clear what the difference is between this and the @helper syntax that existed in Razor before (which I now notice is not present in the current version). I'm in favor of the rename though, template sounds better :).

I love templated tag helpers as a way to have cleaner inline templates. That original syntax was awful (I say having participated in designing it ;P)

👍 🎉 💯

@seriouz
Copy link

seriouz commented Jun 14, 2016

Sounds very nice! No more painting over the telephone!

@grahamehorner
Copy link
Contributor

Im loving the sound of this feature, this would be very useful for creating html report solutions using templates; it would be great if the feature could also accept IQueryable outputting to a Stream asynchronously in cases where large datasets and outputs are needed to be persisted or downloaded to a client as a file.

@JesperTreetop
Copy link

JesperTreetop commented Jun 15, 2016

First: Thank god the previous feature was actually mentioned. I thought I'd dreamed it all up. I've almost never seen it used or mentioned aside from Phil Haack's post and it is the worst possible syntax to Google, maybe aside from the <%: nuggets...

This sounds good even though I agree with @anurse wondering what the difference between this and helpers are. Since helpers were supposed to go in App_Code, I understand the desire to make a distinction though.

I think the magical uses of item need to disappear. For at least two reasons:

  • item just appears out of nowhere. In C#, inside a setter, for example, you have some sort of syntactical cue that value is going to be a special keyword. Here, it is a variable that's never being declared. You have no opportunity to get a sense of the scope in which it is valid.
  • I might want to nest them. It is possible that this way lies spaghetti code, but deciding on item makes it impossible to even try.

That said, I understand that the syntax for declaring a variable name in an HTML attribute is awkward and that there's no really Razor-y way to do it - but you could argue that using the names of the templates are too.

@grahamehorner
Copy link
Contributor

grahamehorner commented Jun 15, 2016

@JesperTreetop not sure I agree with the

magical uses of item

the item is obtained from the foreach of the items; which as the example shows is an IEnumerable and after all it's up to you as the template developer to name the parameter/variables; however that said I do think there is a error where

@template CustomerRowTemplate(Customer customer) {
    <tr>
            <td>@item.FirstName</td>
            <td>@item.LastName</td>
        </tr>
}

should be

@template CustomerRowTemplate(Customer customer) {
    <tr>
            <td>@customer.FirstName</td>
            <td>@customer.LastName</td>
        </tr>
}

having said that I'm thinking why doesn't the razor template use the keyword of model the same as a razor view?

@JesperTreetop
Copy link

@grahamehorner In this example from the original post:

@{
    var customers = DB.GetCustomers().ToList();
}
<table>
    <tbody asp-repeat="customers">
        <tr>
            <td>@item.FirstName</td>
            <td>@item.LastName</td>
        </tr>
    </tbody>
</table>```

...that's where item is suddenly used. customers is clearly the collection to iterate, so the lack of something to define which variable to use instead, and the decision to (always?) use item, is what I'm referring to. This is also the same behavior as the "templated razor delegates" which is referred to, the @<li>@item.FirstName @item.LastName</li> thing. There's a case to be made for consistency, but it's consistency with a feature almost no one knows exists, and it shouldn't constrain a new feature.

@DamianEdwards
Copy link
Member Author

@JesperTreetop you're right regarding the generated item variable, and indeed the issue is find an elegant way to allow defining the generated parameter names when you don't have an explicit function declaration (like you do when using @template). There are certainly ways to do it for the Tag Helper support, but I don't particularly like any of them yet, but I haven't give up 😄

@jarrettv
Copy link

jarrettv commented Jun 16, 2016

<table>
   <tbody asp-foreach="cust in Model.Customers">
     <tr>
       <td>@cust.FirstName</td>
       <td>@cust.LastName</td>
     </tr>
   </tbody>
</table>

Inspired by the lovely spark view engine.

@grahamehorner
Copy link
Contributor

or even something like

@{
    var customers = DB.GetCustomers().ToList();
}
<table>
    <tbody asp-template-model="customers" asp-template-model-type="ISomeInterface">
<foreach item-type="" item-name="item" in="model">
        <tr>
            <td>@item.FirstName</td>
            <td>@item.LastName</td>
        </tr>
</foreach>
    </tbody>
</table>

where by you tell the template engine the model variable name and the type or interface which it implements; then inside the template the tag of foreach with the item-type, item-name and in allows the developer to inform the template engine of what/how to consume/process the model instance its was supplied.

@JesperTreetop
Copy link

JesperTreetop commented Jun 16, 2016

As always when foreach tags start being proposed, I feel compelled to mention Charles Petzold's CSAML april fools joke... 😉

Razor offers easy access to C# and Tag Helpers are a valuable addition because they're a way of making the declarative, domain specific parts and the conceptual templates look more like markup. Having tags that just enable C# code to be written as HTML would defeat the purpose, in my opinion. If you want to write a foreach, just write @foreach (...) { ... }. For all its problems, even Web Forms mostly got this right.

The most Razor-like thing that I can imagine is to just say:

@foreach (var item in items) {
    <whatever-template model="item" />
}

...like in the <list-view /> example @DamianEdwards mentioned. This way, Razor is still Razor.

@grahamehorner
Copy link
Contributor

@JesperTreetop yeah lolz; loads of options anything is possible ;)

@rynowak rynowak changed the title [WIP] Razor Templating Improvements Razor Templating Improvements May 4, 2017
@rynowak
Copy link
Member

rynowak commented May 4, 2017

We should take the improvement from here: aspnet/Razor#132 as part of this work

@aspnet-hello aspnet-hello transferred this issue from aspnet/Razor Dec 14, 2018
@aspnet-hello aspnet-hello added this to the Backlog milestone Dec 14, 2018
@aspnet-hello aspnet-hello added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature feature-razor-pages labels Dec 14, 2018
@pranavkm pranavkm added the c label Aug 22, 2019
@Danielku15
Copy link

Danielku15 commented Oct 2, 2019

It is quite unfortunate that this topic is not followed up as with the removal of @helper syntax it is a bit hard to have some page-local piece of code for templates. The templated razor delegates are not really a proper replacement for real templates. I attempted to use a simple local function that returns a IHtmlContent but this also does not work as the @<text>SOME_RAZOR_SNIPPET</text> translates to a lambda.

The following snippet is the closest what I could write to "well-formed" syntax in Razor:

@{
    IHtmlContent MyTemplate(string param1, double param2)
    {
        if (param2 < 0.5)
        {
            return @<text>@param1</text>;
        }
        else
        {
            return @<text>@param1.ToUpper()</text>;
        }
    }
}
@MyTemplate("a", 1)
@MyTemplate("a", 0)

But the output expression translates to a lambda instead of a simple HelperResult:

IHtmlContent MyTemplate(string param1, double param2)
{
    if (param2 < 0.5)
    {
        return item => new global::Microsoft.AspNetCore.Mvc.Razor.HelperResult(async(__razor_template_writer) => {
            PushWriter(__razor_template_writer);
            BeginContext(6257, 6, false);
                 Write(param1);
            EndContext();
            PopWriter();
        });
    }
    else
    {
        return item => new global::Microsoft.AspNetCore.Mvc.Razor.HelperResult(async(__razor_template_writer) => {
            PushWriter(__razor_template_writer);
            BeginContext(6336, 16, false);
                 Write(param1.ToUpper());
            EndContext();
            PopWriter();
        });
    }
}

@mkArtakMSFT
Copy link
Member

Closing as this issue is outdated. Things which matter will come back up again.

@ghost ghost locked as resolved and limited conversation to collaborators Dec 4, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-razor-pages
Projects
None yet
Development

No branches or pull requests