Aspnetcore: Template based components

Created on 27 Mar 2018  ·  60Comments  ·  Source: dotnet/aspnetcore

For example, you should be able to create a Grid or ListView component that binds to each item in a collection using a define template.

area-blazor

Most helpful comment

My personal view is that Razor should stick as close as possible to HTML idioms, rather than drifting towards the idioms we see in XAML. That would mean looking more like the hierarchy of elements you see in <table> rather than typical XAML things. We could be generic enough to support either mind you, but I'm still very firmly in the camp of "Razor is HTML" right now.

All 60 comments

Specifically, this means we need some way to signal that a given part of the child content within a component should get compiled as an Action<RenderTreeBuilder, T>, for some type T that we need to be able to resolve dynamically from usage at design time. Then our design-time codegen will be able to produce the correct intellisense context when writing Razor expressions inside that child element.

Example:

<MyListWithHeader RowItems=@People>
    <Title>
        Here I'm still in the outer context so can reference properties on the component itself 
        such as @TitleText
    </Title>
    <RowTemplate> <!-- Somehow need intellisense to know that RowTemplate is meaningful here, and that its "item" value is taken from the generic type arg on the RowItems property -->
        Here I'm in the context of an Action<RenderTreeBuilder, Person> because we supplied
        an IEnumerable<Person> as the value for RowItems, so I can write @item.FirstName
    </RowTemplate>
</MyListWithHeader>

@functions
{
    string TitleText { get; set; }
    IEnumerable<Person> People { get; set; }

    class Person
    {
        public string FirstName { get; set; }
    }
}

I'm not sure how general we want to make this feature. Possibly less general than indicated in the sketch above. @rynowak would be the expert on what's reasonable to implement here. All the complexity is at design/compile time. It doesn't require any runtime support that I can think of.

Hi tried to accomplish this by my own passing an IEnumerable as a parameter, I was able to produce a grid but i was not able to update it when the parameter change. This is the code:

<table class="@ClassName">
  <tr>
    @foreach (var col in Columns)
    {
      <th>@col</th>
    }
  </tr>

  @foreach (System.Data.DataRow row in table.Rows)
  {
    <tr>
      @foreach (var col in Columns)
      {
        <td>@row[col]</td>
      }
      @if (Select != null)
      {
        <td><a @onclick(() => Select(row))>Seleccionar</a></td>
      }
    </tr>
  }
</table>

@functions{
    public string ClassName { get; set; }
    public List<string> Columns { get; set; }
    public object DataSource { get; set; }
    private System.Data.DataTable table;


    private string Log;

    //call backs it seens I will have many of them

    public Action Change { get; set; }
    public Action<object> Select { get; set; }

    protected void UpdateState()
    {
      int count = ((IEnumerable<object>)DataSource).Count();
      Log = $"Table count rows:{table.Rows.Count} count object ${count}";
      StateHasChanged();
    }

    protected override void OnInit()
    {
      base.OnInit();

      Change += StateHasChanged;

      var enumerable = (IEnumerable<object>)DataSource;
      table = new System.Data.DataTable();

      if (Columns == null)
      {
        Columns = new List<string>();
        Columns.AddRange(enumerable.FirstOrDefault().GetType().GetProperties().Select(x => x.Name).ToList());
      }


      Columns.ForEach(x =>
      {
        table.Columns.Add(x);
      });

      enumerable.ToList()
                .ForEach(x =>
                {
                  var row = table.NewRow();

                  foreach (var property in x.GetType().GetProperties())
                  {
                    row[property.Name] = property.GetValue(x);
                  }

                  table.Rows.Add(row);
                });

      StateHasChanged();
    }
}

@SteveSandersonMS, are Title and RowTemplate in your example meant to be properties of MyListWithHeader or special components? If they are properties, it might be worth thinking about alternative ways to express this...

I am working with XAML a lot and when I started prototyping with Blazor I quickly felt the need for this feature. Do you think XAML's property element syntax could be reasonable in this scenario? While quite verbose, it expresses clearly that we are assigning properties here instead of adding child components to MyListWithHeader. Example:

<MyListWithHeader>
    <MyListWithHeader.Title>
       ...
    </MyListWithHeader.Title>
    <MyListWithHeader.RowTemplate>
       ...
    </MyListWithHeader.RowTemplate>
</MyListWithHeader>

My personal view is that Razor should stick as close as possible to HTML idioms, rather than drifting towards the idioms we see in XAML. That would mean looking more like the hierarchy of elements you see in <table> rather than typical XAML things. We could be generic enough to support either mind you, but I'm still very firmly in the camp of "Razor is HTML" right now.

totally in support of this. if we can make the list of item "ObservableCollection" rather than a "IEnumerable" that would be a bonus. moving towards xaml like pattrens gives us a proper template system, which html lacks, all of popular frameworks in web ui have a template system, which makes them very extensible, obviously blazor should have one as well, and instead of making a new one it would be great to have one which is already loved by MS and many .Net devs as well, technically it will still be html/css based so all html developers will still be attracted towards the technology because it wont be xaml ( dont knw why html guyx hate xaml though ) but it will only follow xaml pattrens.

@DamianEdwards I fully agree with you regarding "Razor is HTML". However, attribute values in HTML are always specified as, well, attributes in the opening tag. There is no way to (and no need to) express complex attribute values like templates as child elements. For example you can't write

<a>
    <href>http://dot.net</href>
</a>

So I think what we are aiming for technically just cannot be expressed in pure HTML.
Here's a summary of the options I currently see:

  1. Stick with HTML syntax: Property values can only be specified in the opening tag of a component.

    • I think that is what React opted for, example:
      <MyListWithHeader RowTemplate={(data) => <p>...</p>} />
    • Pro: Very close to normal HTML syntax
    • Con: Gets messy for multiline values, i.e. more complex templates
  2. Stick with HTML syntax: Depending on the context, child nodes of a component may either represent nested elements or property assignments.

    • Pro: Very close to normal HTML syntax
    • Con: Might be confusing since property assignments and child elements are syntactically indistinguishable. In the example, <Title>...</Title> might be a standalone component while <RowTemplate>...</RowTemplate> might be a property (an attribute) of the surrounding <MyListWithHeader> - without IntelliSense you don't really know
    • Con: Might cause naming conflicts if there's a component and a property with the same name
  3. Invent new syntax: XAML's property element syntax is just one option, it could just as well be some Razor construct similar to @functions { } or something totally different.

    • Pro: Allows arbitrarily complex templates while still being readable
    • Con: Might alienate web devs

It is totally possible that I am misunderstanding something here. If so, please let me know.

@SvenEV I dont think xaml syntax will alianate anyone, if u notice, the template syntax for angular, vue, react and million other frameworks, all have different way and different syntax to do stuff, still all of them are loved, NO? similarly blazor can have its own syntax, the only pro in its case will be we wont be inventing a new syntax just bringing over already popular syntax to this framework ( hence less learning curve for xaml devs ) but as far as web devs are concered they will be learning blazor templates anyway whether blazor follow xaml or not, learning curve for web devs remain the same, the only way web devs stay away from this drawback is if blazor uses straight up html, which I dont think will be as extensible as a template model. :)

so a totally new template syntax gives learning curve to every developer. but a xaml like syntax solves atleast problem of half developers. but xaml is also a lot like html, both are markup afterall. once u learn it, it will be piece of cake, good thing abt xaml way is, u dnt have to worry abt css at all, all styling is done with straight up properties on the controls. and even comples styling also done with same syntax, but on the other hand html and css are totally different languages with different syntax and structure of doing things.

But why do we even need templates? I mean when you can do foreach right there in Razor. Seems much easier to me than templating.
Or is this because of reuse? I can imagine a separate component that does foreach in that case.

@MihaMarkic templates standardize everything, it increases re-use, and most importantly it hides away all the complex code written for one control and u can just use that wrap control with 2 lines of code and focus more on ur business logic instead of re inventing the wheel.

Well, I don't know about this. IMO foreach is a template, just a more powerful one and you can place a "foreach" template in a different component and call it from anywhere with those two lines of code.

foreach is just a loop and yes for a simpler control we can do this, like for a basic ListView ( table ) we can simply make a control which takes some input, does the foreachon it and spits out a table, and then we just need to put one tag something like this and then we will be able to produce a table, also if we want a customized listview , with very modern design, highly interactive etc, then we can make another component control with foreach, and a lot of cutom css, and lot of blazor code in one file, maybe multiple files, and wrap all of that in 1 component, and then again use all of that just with 1 tag. you see where I am getting with this? no matter how much code a control has, all of that can be wrapped into 1 tag :) @MihaMarkic

It's not just about rendering multiple items with the same template. It's also needed to make multiple parts of a component customizable. Currently, a component can only have one such "hole" in its layout that can be filled with ChildContent, a predefined property, like this:

<div style="some fancy styling">
    @ChildContent
</div>

Now imagine you want to write a component with multiple such "holes" like a dialog that lets you customize the content as well as the header, or a button where you can customize the inner content but also specify templates for the context menu and the tooltip maybe. So it would be nice if you could write something like the following (whatever the syntax will be then):

<MyButton>
    <h2>I am the button caption (the usual ChildContent)</h2>

    <MyButton.ContextMenu>
        <button>Context menu button 1</button>
        <button>Context menu button 2</button>
    </MyButton.ContextMenu>

    <MyButton.ToolTip>
        This explains <strong>what the button does</strong>. It is displayed when hovering over the button.
    </MyButton.ToolTip>
</MyButton>

@SvenEV Isn't that just having multiple holes and not strictly requiring a template?

yes tht is good example of another usecase, every part should be customizeable.

@MihaMarkic It's true, we are talking about two different things here:

  1. Support for multiple holes (RenderFragments / Action<RenderTreeBuilder>) as in my example
  2. Support for templates that have data input (Action<RenderTreeBuilder, T>) as in ListView etc.

I think the first is a requirement for the second and I hope that both features will be implemented at some point. That would enable us to build some powerful component libraries.

Which problem is this trying to solve? I'm just wondering since I haven't seen this kind of thing in other SPA frameworks.

Does it conflict with a component based structure? Also, does it go against normal web development practices just to force a more "app-like" structure?

@AWulkan it doesnt go against normal component structure, but yeah it does go slightly towards app-dev like structure, but it doesn't mean it goes away from web development, bcz all popular frameworks are also doing the same thing, the whole purpose of SPA concept was this. SPA was created to make web development more like app development. and now every framework is following spa. the next step of spa is now pwa. u knw wht a pwa is right? it introduces some major features from a native apps like notifications and offline sync, so ultimately web dev as a whole is trying to follow native app development.

@AWulkan Since you seem to be familiar with Vue, I just looked into the Vue docs to see if there's something similar. There is and it's called "Slots", in particular "Named Slots" and "Scoped Slots" - that's exactly what this issue is about. It's the same concept. Maybe this helps you understand the issue?

@SvenEV Okey I kinda see what you mean then. It's a pretty niche feature as far as I'm concerned. Thanks for clarifying.

Will it be using this draft as a model?
https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Slots-Proposal.md

Seems Vue.js slots is similar to XAML data templating.
Templating offers a powerful way of customizing existing components, one case I could see is a ListView where by default without templating is that it just iterates over the a list of strings and displays it.

<ListView list=@ListOfUrlsAsStrings />

With templating you could override that to say, show instead a list of image tags using the passed Type T.

<ListView list=@ListOfUrlsAsStrings>
    <ListView.ListTemplate>
         <img src=@item alt="overriden by a template" />
        <CustomComponentHere />
    </ListView.ListTemplate>
</ListView>

This would greatly increase reusability of components as they can be dynamic. 3rd party component vendors can be more creative on authoring components that way.

yes exactly with data template binding gets simpler and observable

We very much hope that Blazor components will get a good support for nesting and templating.
Previously we had a number of discussions with @Mehul, @NTaylorMullen, @DamianEdwards on the same topic for Razor Tag Helpers in which we tried to explain why we need more XAML-like approach. Relevant GitHub issues: aspnet/Razor#474, aspnet/Razor#576, aspnet/Razor#570.

We develop UI components such as data grids and the typical markup to configure a widget can be as twiggy as this:

<TabPanel>
    <Grid>
        <DataSource>
            <More about data source />
        </DataSource>

        <Column />
        <Column>
            <Lookup>
                <DataSource>
            . . .
            <CellTemplate>
                arbitrary html
            </CellTemplate>
        </Column>

        <DetailTemplate>
            <div>arbitrary content</div>
            <ThirdPartyComponent />

            <Grid>
                detail grid configuration with own templates (and there may be more levels)
            </Grid>
        </DetailTemplate>
    </Grid>
</TabPanel>

For such a hierarchy, a developer would want to have accurate context awareness and intellisense.

@AlekseyMartynov can we have xaml like styling + animations as well in the long run ?

I'm so glad this idea is getting traction. Since MS pioneered the idea of templatable controls with Silverlight, WPF and UWP it makes sense to make this a part of Blazor.

One question would be data/code contexts. When the cshtml template references code in the bound C# object that is probably not going to be in the same context as the larger component. So would not some kind of dynamic data binding be required? Or would a template have a strict TargetType requirement so the parser knows how to resolve references to data bound object members?

On that note, what about dynamic data binding and dependency properties? As far as I can tell there would be nothing preventing this right now. Or is reflection going to be problematic if the C# or IL winds up getting compiled to wasm rather than interpreted by mono?

Implementing what @AlekseyMartynov is suggesting (or a variation of some sorts) is in my opinion critical to the success of Blazor. In practice, many developers writing enterprise applications depend on a rich set of easy-to-use 3rd party components and I'm hoping that this project will be very soon at the point when different vendors can start developing Blazor components.

In my project Xamzor I have implemented some limited templating support like this:

<ListView ItemTemplate="typeof(MyCustomTemplate)" ... />

where MyCustomTemplate is another Blazor component with a property "DataContext" that gets assigned by the ListView. While this works quite well, it is still desirable to define templates inline instead of in a separate component, especially if the templates are small and simple.

Maybe ember.js contextual components can provide some inspiration: https://www.mutuallyhuman.com/blog/2016/09/23/creating-an-accordion-with-contextual-components-in-ember

@SvenEV I agree that in case of simple templates inline templates would be helpful but we still should have option for separate component templates like you implemented in ur project, because that helps us in separation of concerns and re usability of the template :)

Three things in my head:

  1. Make components natively hierarchical (ex. ParentComponentType property; so that you can enforce MyTableComponent -> MyRowComponent -> MyColumnComponent). If ParentComponentType is null, the component can be a child of any component type (ex. MyTableComponent.ParentComponentType can be null).

The hierarchy is enforced via declarations in the components' definitions like:

```C#
// In MyRowComponent
Type ParentComponentType { get; } = typeof(MyTableComponent);
...

// In MyColumnComponent
Type ParentComponentType {get; } = typeof(MyRowComponent); 
...
Thus, MyColumnComponent can only be created within a MyRowComponent, and MyRowComponent can only be created within a MyTableComponent.

2. Allow components to be extended via ITemplateable interface. When the ITemplateable.Template [Tag] property is valued, it overrides the component's default rendering. Internally, it's something like:

```C#
    if (component is ITemplateable)
    {
        if (component.Template != null)
        {
            … return template rendering ...
        }
    }
    … return default rendering ...
  1. Create an interface ITemplate that can be implemented as an optional base class BlazorTemplate, which can be used to objectify programming with templates.

Sample usage where MyColumnComponent is ITemplateable:

    <MyTableComponent>  
        <MyRowComponent>  
            <MyColumnComponent>  
                <Template>  
                    …  template codes here; can contain a call to another component ...
                </Template>  
            </MyColumnComponent>  
        </MyRowComponent>  
    </MyTableComponent>  

With MyColumnComponent defined as ITemplateable, it supports containing a <Template> … </Template> tag. The entries in this tag overrides MyColumnComponent's default rendering.

Internally, MyColumnComponent may have/use a BlazorTemplate implementation to let it programmatically handle special template behaviors, events and features.

I am voting for Property Element Syntax

<Button>
  <Button.ContextMenu>
    <ContextMenu>
      <MenuItem Header="1">First item</MenuItem>
      <MenuItem Header="2">Second item</MenuItem>
    </ContextMenu>
  </Button.ContextMenu>

  Right-click me!
</Button>

As an alternative to

<Button ContextMenu=@GetContextMenuRenderFragment()>
  Right-click me!
</Button>

I like it but wouldn't the web purists turn their noses up at this as being too much like XAML?

No, they will be fine, property element syntax is an alternative to html attribute syntax (not replacement). But I am exiting how many posibilities it provides for component developers (UI controls library devs)

I like it as well since it conveys more clearly how the child relates to the parent, this is especially good on Templating since you'd be able to distinguish that it's a data template from a parent rather than a custom component that has no relation to the parent component.

Like this:

<List>
    <List.DataTemplate>
      <!-- Template Here -->
    </List.DataTemplate>
    <ComponentWithNoRelationToTheList />
</List>

This gives you an idea that DataTemplate is related to List than a custom template overall. Though I agree like XAML it should be just an alternative.

What if "Template" is a reserved/special keyword for any templatable Blazor component? When used as a tag, it can contain the template codes. When used as an attribute, it can specify a named template, which can contain the template codes.

so, let's imagine a component like:

ComponentTemplateTest.cshtml

@functions
{
  [Parameter]
  public RenderFragment Template { get; set; }

  [Parameter]
  public RenderFragment<int> TemplateWithInt { get; set; }

  [Parameter]
  public RenderFragment<TestObject> TemplateWithObject { get; set; }

  public class TestObject
  {
      public string PropString { get; set; }
      public int PropInt { get; set; }
  }

  private TestObject testobject = new TestObject();
}

<div>
    <div><button onclick="@((x) => this.testobject.PropInt++)">test</button></div>
    <div>@Template</div>
    <div>@((RenderFragment)(x => TemplateWithInt(x, testobject.PropInt)))</div>
    <div>@((RenderFragment)(x => TemplateWithObject(x, testobject)))</div>
</div>

we can use it as

<ComponentTemplateTest>
    <Template>
        test simple template
    </Template>

    <TemplateWithInt params="param1">
        @param1
    </TemplateWithInt>

    <TemplateWithObject params="paramObject">
        @paramObject.PropInt
    </TemplateWithObject>
</ComponentTemplateTest>

we need an attribute like params because Blazor write the code as

            builder.OpenComponent<StandaloneApp.ComponentTemplateTest>(16);
            builder.AddAttribute(17, "template", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
                builder2.AddContent(18, "\n        test simple template\n    ");
            }
            ));
            builder.AddAttribute(19, "templatewithint", (Microsoft.AspNetCore.Blazor.RenderFragment<System.Int32>)((builder2, param1) => {
                builder2.AddContent(20, param1);
            }
            ));
            builder.AddAttribute(21, "templatewithobject", (Microsoft.AspNetCore.Blazor.RenderFragment<StandaloneApp.ComponentTemplateTest.TestObject>)((builder2, paramObject) => {
                builder2.AddContent(22, "\n        ");
                builder2.AddContent(23, paramObject.PropInt);
                builder2.AddContent(24, "\n    ");
            }
            ));
            builder.CloseComponent();

it would be enough? something is missing?

@uazo Just note, that without Property Element Syntax it is imposible to distinguish between property and component.

@danieldegtyarev I think that we are talking here about two different things... my goal is only template for components for now...

take a look at https://github.com/uazo/Blazor/tree/dev-experimental-templated-component
you need to build and install that extension...

"Template" used as a tag is declarative and naturally inline:

<MyCom MyComProp="MyComPropValue">
    <Template>
        Components, @MyCom.MyComProp, HTML, content and @values
    </Template>
</MyCom>

"Template" used as a tag to assign a "named template". Internally, the named template will have access to its parent component, so that something like MyCom.MyComProp is also possible:

<MyCom MyComProp="MyComPropValue">
    <Template Name="MyTemplate {Content: @content; Values: @values}" />
</MyCom>

"Template" used as an attribute to assign a "named template". Internally, the named template will have access to its parent component, so that something like MyCom.MyComProp is also possible:

<MyCom 
    MyComProp="MyComPropValue"
    Template="MyTemplate {Content: @content; Values: @values}" />

The "Template" used either as tag or as attribute should result to the same compilation, such that the component would render the provided template info, which simply overrides the component's default render.

A "named template" is a re-usable template. Creating a "named template" is almost like creating a component. However, the big difference is that a named template can only be used through a component's support for "Template".

A template can contain components, but checks need to be put in to prevent potential infinite loops when, for example, a component that uses a template that uses the same component is coded... design/compile-time error perhaps?

Questions: Can a named template inherit from another template? Can a named template be restricted to use only with specific component type(s)?

I think you guys should go for the HTML syntax as much as possible, don't introduce new stuff. No Xamarin xaml syntax or ugly asp.net webforms components(asp repeater and so on).
I'm from the asp.net mvc world, I remember the joy and freedom when I switched from "asp.net webforms" to "asp.net mvc", no more strange components like asp:repeater and so on.
Please please, don't let BLAZOR become a new "asp.net webforms".

I agree with @GoranHalvarsson . Templating in Blazor should be only the implementation of generic RenderFragment, with same use case (first example in @etmendz post) .

For that, I've updated my branch, now with full support for intellisense in visual studio (params and allowed child).

can someone give me some feedback?

@GoranHalvarsson I agree. The MVP can be for Blazor components to have basic template support. In this sense, a "Template" can contain HTML, component, content and Razor codes. At its most basic, Blazor components+templates should still be recognizably HTML+Razor codes. Declarative syntax fits this bill, which can be <Template> or @template keyword, for example.

A next phase item can be Blazor's APIs/extensibility libraries that enable component creators to promote their own brand -- ex.: HTML helper-like syntax (Progress Telerik/Kendo UI) or Xamarin/XAML-like syntax (ooui.wasm, Platform.Uno) can instead be 3rd party/vendor-level deliveries. Within Blazor, template programmability and compilation flexibility can be worked on closely with component creator partners to address specific issues/needs/requirements.

There is a suggestion in this thread about re-useable templates. Re-useable templates or "named templates" can promote code re-use. Although, honestly, I don't see much value in it and can be low priority. More likely, if it's meant for re-use, then perhaps it should be a component? ;-)

@uazo IMHO, based on your examples, passing params to <Template> seems to defeat the declarative nature of just using the parameter/value directly inside the <Template> tag.

I also have doubts about letting components support multiple template properties. In this sense, making it too freestyle. I think Blazor should have a simpler and more standard way of supporting component templates, perhaps via ITemplate interface and some compiler tweaks here and there.

@SteveSandersonMS for https://github.com/aspnet/Blazor/issues/404#issuecomment-377229910
with aspnet/AspNetCore#15829 code will be

MyListWithHeader.cshtml

@functions
{
    [Parameter]
    public RenderFragment Title { get; set; }

// NOT MANDATORY
    [Parameter]
    public RenderFragment ChildContent { get; set; }
// *******************

    [Parameter]
    public IEnumerable<example.Person> RowItems { get; set; }

    [Parameter]
    public RenderFragment<example.Person> RowTemplate { get; set; }
}

<div>
    @Title
</div>

@foreach (var item in RowItems)
{
    <div>
    @RowTemplate.Render(item)
    </div>
}

@ChildContent

example.cshtml

@page "/component-test"

<MyListWithHeader RowItems=@People>
    <MyListWithHeader.Title>
        Here I'm still in the outer context so can reference properties on the component itself
        such as @TitleText
    </MyListWithHeader.Title>

    <MyListWithHeader.RowTemplate WithParams="item">
        <!-- Somehow need intellisense to know that RowTemplate is meaningful here, and that its "item" value is taken from the generic type arg on the RowItems property -->
        Here I'm in the context of an Action RenderTreeBuilder, Person
        because we supplied
        an IEnumerable Person as the value for RowItems, so I can write <b>@item.FirstName</b>
    </MyListWithHeader.RowTemplate>
</MyListWithHeader>

@functions
{
  string TitleText { get; set; }
  IEnumerable<Person> People { get; set; }

  protected override void OnInit()
  {
      var people = new List<Person>();
      people.Add(new Person { FirstName = "uno" });
      people.Add(new Person { FirstName = "due" });
      people.Add(new Person { FirstName = "tre" });

      this.People = people;
  }

  public class Person
  {
      public string FirstName { get; set; }
  }
}

with full support for intellisense!

@uazo Very very good! There is only one thing: lets remove WithParams="item" and just use@item to access the 'context' of template

uhm .. but if item already existed in the component example?
Since the variable name used to access the 'context' of template can't be discoreved, so with WithParams, developers can decide the name of the variable.

are you thinking of a wired default name (like ChildContent), with or without optional WithParams? Or a sintax like <MyListWithHeader.RowTemplate WithParams=@item>?

Looking back, a default name would not be fine for nested templates...

Lets think. Template is code generation with one parameter. Template has only access to this item. So if we 'reserve' a variable item - it is safe and can not conflict. Nested templates have their own scope and using @item is still safe.

BTW, looks like this template-scope have to have their @functions block as well

let's have

<MyListWithHeader RowItems=@People>
    <MyListWithHeader.Title>
        ...@TitleText
    </MyListWithHeader.Title>

    <MyListWithHeader.RowTemplate WithParams="item">
        ... <b>@item.FirstName</b>

        <MyListWithHeader>
            <MyListWithHeader.RowTemplate WithParams="item">
                @item.FirstName
            </MyListWithHeader.RowTemplate>
        </MyListWithHeader>

    </MyListWithHeader.RowTemplate>
</MyListWithHeader>

will be renderer as

protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
    base.BuildRenderTree(builder);
    builder.OpenComponent<StandaloneApp.Pages.testtemplated.MyListWithHeader>(0);
    builder.AddAttribute(1, "RowItems", Microsoft.AspNetCore.Blazor.Components.RuntimeHelpers.TypeCheck<System.Collections.Generic.IEnumerable<StandaloneApp.Pages.testtemplated.example.Person>>(People));
    builder.AddAttribute(2, "Title", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
        builder2.AddContent(3, "\n        Here I\'m still in the outer context so can reference properties on the component itself\n        such as ");
        builder2.AddContent(4, TitleText);
        builder2.AddContent(5, "\n    ");
    }
    ));
    builder.AddAttribute(6, "RowTemplate", (Microsoft.AspNetCore.Blazor.RenderFragment<StandaloneApp.Pages.testtemplated.example.Person>)((builder2, item) => {
        builder2.AddContent(7, "\n        ");
        builder2.AddContent(8, "\n        Here I\'m in the context of an Action RenderTreeBuilder, Person\n        because we supplied\n        an IEnumerable Person as the value for RowItems, so I can write ");
        builder2.OpenElement(9, "b");
        builder2.AddContent(10, item.FirstName);
        builder2.CloseElement();
        builder2.AddContent(11, "\n\n        ");
        builder2.OpenComponent<StandaloneApp.Pages.testtemplated.MyListWithHeader>(12);
// line 43
        builder2.AddAttribute(13, "RowTemplate", (Microsoft.AspNetCore.Blazor.RenderFragment<StandaloneApp.Pages.testtemplated.example.Person>)((builder3, item) => {
            builder3.AddContent(14, "\n                ");
            builder3.AddContent(15, item.FirstName);
            builder3.AddContent(16, "\n            ");
        }
        ));
        builder2.CloseComponent();
        builder2.AddContent(17, "\n\n    ");
    }
    ));
    builder.CloseComponent();
}

with

Error   CS0136  A local or parameter named 'item' cannot be declared in this scope because that name is used in an enclosing local scope to define a local or parameter

StandaloneApp\obj\Debug\netstandard2.0\Pages\testtemplated\example.g.cs line 43

Yes, sure, you have to resolve it during codegen.
But... I think template have to be compiled to a component (possibly a special component) rather than RenderFragment because we need to manage with its state, for instance to invoke StateHasChanged() when@item.FirstName mutates.

Yes, sure, you have to resolve it during codegen.

I do not think it is possible.
During the codegen (that's razor modified) all nodes are crossed, some are html, others are c# node: those with c# would be wrong because the variables would not be defined and therefore the variable type would not be known ( and we need to recover a variable of type T of RenderFragment<T>, because we know T) ... so I do not know what the name of the variable to use is.
You could probably look for that undefined variable, but it seems a mess

But... I think template have to be compiled to a component (possibly a special component) rather than RenderFragment because we need to manage with its state, for instance to invoke StateHasChanged() [email protected] mutates.

I do not think it is necessary to introduce anything else.
If you need a component use BlazorComponent, if you need a "template property " use RenderFragment<T>
After all, StateHasChanged() for a RenderFragment is invoked by the underlying component's StateHasChanged(), I do not need to force invoking changes in any other way

Sorry to butt in here because it sounds like a great conversation - but -

Wouldn't this be solved with a Data Context concept like WPF/UWP? Then in your template you'd always be referencing this.DataContext.[Member].

@peter0302 I personally like this idea...

The challenge does remain being strongly typed. Of course this is addressed in XAML by the binding being done at run time. The data template here would probably have to support typing through generics. But definitely doable.

Then again I would support a fully dynamic runtime binding mechanism also.

You could go for stating the data type somehow via declaration. Somewhat similar to x:DataType on XAML, though inferring it should be more ideal and less prone to errors.

@uazo My feedback is that it looks very good! Here are my thoughts...

1) That a property of type RenderFragmentis assigned with a qualified name of a property in a tag like <MyListWithHeader.Title /> feels ok to me, but the handling breaks my former intuition that properties with ParameterAttribute are set via html-attributes. How about having another c#-attribute for those properties to distinguish that behavior? Like [ContentParameter]?

2) That a property of type RenderFragment<Person> is assigned with a property tag with attributes
<MyListWithHeader.RowTemplate WithParams="item" /> felt little odd at first. It is not an attribute that is setting a parameter property, but it is what variable name to use in the lambda expression during code generation local to that template. Here again it would be good to have another syntax to tell the difference. But I am uncertain how creative one can be in order to be compliant with tooling and compiler. <MyListWithHeader.RowTemplate params(item) /> perhaps?

3) If I want to show <MyListWithHeader RowItems=@PeopleOnline /> and <MyListWithHeader RowItems=@PeopleOffline />, do I have to specify the templates again? Is there a way to reuse them? I was thinking perhaps making some model-driven initialization through this mechanism, similar to EditorFor with different property-selectors using the same templates.

It might be a bit risky to discuss implementation when I am new to the framework but…

if you go with having a separate parameter attribute for assigning templates as discussed under point 1 above, then you could add a Boolean flag (default true) to it to say if a default value (if not explicitly given) should be applied. Then you can solve point 3 above with introducing an interface like IDefaultTemplateProvider that one can use to provide definitions for the template-property-tags separately. Then those can be discovered via reflection in the same way you discover IComponent(s) and use them in cases where the use-default flag is set, and the user doesn’t provide one. You could allow several template components implementing the interface but throw an unambiguous exception if a property tag is found multiple times. In that way you can provide standardized templates to a generic component specific to an app that are used multiple times and get the resolution at design-time, perhaps?

I think describing my learning experience around this gives you a good background…, and I have some thoughts to discuss…

I want to make model-driven configuration of components through meta-data and resource files for retrieving static translated text, validation and the like. I was thinking this mechanism of templating could be the thing to make it happen. To evaluate it I made an interface public interface IRenderFragment<T> { RenderFragment UsingDataItem(T item); } and made an ordinary component that also implements that interface to be used as a template. Then made a generic component using it like this

  <InputField For=@DataSource(Model, x => x.NewTodo) Template="@InputBasic.Template" />

So, in this way the InputField component has information on how to retrieve meta-data, etc and can feed that into a model that is rendered in a template you provide. It works very well.

In XAML it is the only mechanism to render things when there is a surrounding behavior implemented in a component, like in a list-view then you need to provide an item-template. In Blazor it is not necessary since you may use code with a loop to have that surrounding behavior instead. I then realized that I could have done the same thing here. I could have retrieved a model from meta-data in code instead and feed that into a normal component. @{ var inputModel = InputModelHelper.GetFor(ViewModel, x => x.NewTodo); } <InputBasic DataItem="@inputModel"/>

Maybe obvious to some… so templates is actually not absolutely necessary for my use-case. But it got me question what it is I really want. Maybe it is more along the lines of injecting attributes and content into existing elements? And maybe doing so by being able to define a custom attribute like:

  <label For=”@DataSource(Model, x => x.NewTodo)”></label>
  <input For=”@DataSource(Model, x => x.NewTodo)” />

To test if it is even possible to add attribute nodes into an existing element I made a function (returns empty string for that content node) after the open tag <label>@For(x => x.NewTodo)</label> which extends rendering process by adding attribute nodes (and a content node in the case of the label element) to the builder. (The builder was captured in the method BuildRenderTree.) The builder has the position of the label node so one can easily see in its buffer if an attribute has been set and therefor should not be overridden by this mechanism. It actually works. It is a bit harder for the input element since it is a void element (it needs to be reopened and the function cannot be placed among the attributes), but as a proof-of-concept it seems to work. How can one make this supported and does it make sense?

I guess this render continuation/extension function can be hooked in on several places. To get you thinking… maybe having ability to inject a “RenderElementExtensionHandler” and if a component has such property then a call to it is compiled into the position when all attributes have been added and after close in the generated code on those elements that have custom attributes, like For in this case.
Is this something that can be used for other stuff or is this an edge case? Are there other ideas of extending behavior to existing elements?

Sorry if it got a bit lengthy…

Why not render props?

<List RenderHeader={(text) => (<h4>{text}</h4>)}>
    {({ item, index }) => (
        <li value={index} class={item.Type}>
            {item.Content}
        </li>)}
</List>

Done:
b2c8b1aec98e65054c590d356fdf15e6a5e6a822
409026c2f41926d85d18ea2abf345453fa083e34
ce11f517d370b501433a62a56fbd4be1704000ad

Was this page helpful?
0 / 5 - 0 ratings