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
``` c#
public static class Helpers
{
public static IHtmlContent ForEach
{
var builder = new HtmlBuilder();
foreach (var item in data)
{
builder.AppendHtml(template(item));
}
return builder;
}
}
**View.cshtml**
``` html
<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.
@template DirectiveRather 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):
``` c#
public IHtmlContent HelloWorld(string name)
{
var builder = new HtmlBuilder();
builder.AppendHtml("
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:
``` html
@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)
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):
``` c#
public async Task
{
await Task.Delay(100);
var builder = new HtmlBuilder();
builder.AppendHtml("
builder.Append(name);
builder.AppendHtml("!
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.:
``` html
@template HelloWorld(string firstName, string lastName) {
<div>Hello @firstName @lastName!</div>
}
This would be roughly equivalent to (and thus generate):
``` c#
public IHtmlContent HelloWorld(string firstName, string lastName)
{
var builder = new HtmlBuilder();
builder.AppendHtml("
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:
``` html
@HelloWorld("Mary", "Lou")
They can also be passed in to other methods that accept the template delegate signature:
``` c#
public static class Helpers
{
public static IHtmlContent MyHelper
{
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**
``` c#
[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
``` c#
public class ListViewTagHelper
{
public IEnumerable
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));
}
}
}
``` html
@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_
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.
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)
:+1: :tada: :100:
Sounds very nice! No more painting over the telephone!
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
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.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.
@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
@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?
@grahamehorner In this example from the original post:
html @{ 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.
@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 :smile:
<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.
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.
As always when foreach tags start being proposed, I feel compelled to mention Charles Petzold's CSAML april fools joke... :wink:
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.
@JesperTreetop yeah lolz; loads of options anything is possible ;)
We should take the improvement from here: https://github.com/aspnet/Razor/issues/132 as part of this work
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();
});
}
}
Closing as this issue is outdated. Things which matter will come back up again.
Most helpful comment
Sounds very nice! No more painting over the telephone!