Aspnetcore: 基于模板的组件

创建于 2018-03-27  ·  60评论  ·  资料来源: dotnet/aspnetcore

例如,您应该能够创建一个 Grid 或 ListView 组件,该组件使用定义模板绑定到集合中的每个项目。

area-blazor

最有用的评论

我个人的观点是,Razor 应该尽可能接近 HTML 习语,而不是朝着我们在 XAML 中看到的习语漂移。 这意味着看起来更像您在<table>看到的元素层次结构,而不是典型的 XAML 内容。 我们可以足够通用以支持任何一个想法,但我现在仍然非常坚定地站在“Razor 是 HTML”的阵营中。

所有60条评论

具体来说,这意味着我们需要某种方式来表示组件中子内容的给定部分应该编译为Action<RenderTreeBuilder, T> ,对于某些类型 T,我们需要能够从设计中的使用动态解析时间。 然后,在该子元素中编写 Razor 表达式时,我们的设计时代码生成器将能够生成正确的智能感知上下文。

例子:

<MyListWithHeader RowItems=@People>
    <Title>
        Here I'm still in the outer context so can reference properties on the component itself 
        such as <strong i="8">@TitleText</strong>
    </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>

<strong i="9">@functions</strong>
{
    string TitleText { get; set; }
    IEnumerable<Person> People { get; set; }

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

我不确定我们想让这个功能有多普遍。 可能不如上面草图所示一般。 @rynowak将是在这里实施合理的专家。 所有的复杂性都在设计/编译时。 它不需要我能想到的任何运行时支持。

嗨,我试图通过我自己传递一个 IEnumerable 来实现这一点作为参数,我能够生成一个网格,但是当参数更改时我无法更新它。 这是代码:

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

  <strong i="8">@foreach</strong> (System.Data.DataRow row in table.Rows)
  {
    <tr>
      <strong i="9">@foreach</strong> (var col in Columns)
      {
        <td>@row[col]</td>
      }
      <strong i="10">@if</strong> (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 ,您的示例中的TitleRowTemplateMyListWithHeader或特殊组件的属性吗? 如果它们是属性,则可能值得考虑使用其他方法来表达这一点......

我经常使用 XAML,当我开始使用 Blazor 进行原型设计时,我很快就觉得需要这个功能。 您认为 XAML 的属性元素语法在这种情况下是否合理? 虽然相当冗长,但它清楚地表达了我们在这里分配属性而不是向MyListWithHeader添加子组件。 例子:

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

我个人的观点是,Razor 应该尽可能接近 HTML 习语,而不是朝着我们在 XAML 中看到的习语漂移。 这意味着看起来更像您在<table>看到的元素层次结构,而不是典型的 XAML 内容。 我们可以足够通用以支持任何一个想法,但我现在仍然非常坚定地站在“Razor 是 HTML”的阵营中。

完全支持这一点。 如果我们可以制作项目“ObservableCollection”而不是“IEnumerable”的列表,那将是一个奖励。 像pattrens一样向xaml发展为我们提供了一个合适的模板系统,这是html所缺乏的,web ui中所有流行的框架都有一个模板系统,这使得它们非常可扩展,显然blazor也应该有一个,而不是制作一个新的拥有一个已经被 MS 和许多 .Net 开发人员喜爱的软件会很棒,从技术上讲,它仍然是基于 html/css 的,所以所有 html 开发人员仍然会被这项技术所吸引,因为它不会是 xaml(不知道为什么是 html虽然 Guyx 讨厌 xaml )但它只会遵循 xaml 模式。

@DamianEdwards我完全同意你关于“Razor 是 HTML”的看法。 然而,HTML 中的属性值总是被指定为开始标记中的属性。 没有办法(也没有必要)将复杂的属性值(如模板)表达为子元素。 例如你不能写

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

所以我认为我们在技术上的目标不能用纯 HTML 来表达。
以下是我目前看到的选项的摘要:

  1. 坚持使用 HTML 语法:属性值只能在组件的开始标记中指定。

    • 认为这就是 React 选择的,例如:
      <MyListWithHeader RowTemplate={(data) => <p>...</p>} />
    • 优点:非常接近正常的 HTML 语法
    • Con :多行值变得混乱,即更复杂的模板
  2. 坚持使用 HTML 语法:根据上下文,组件的子节点可能表示嵌套元素或属性分配。

    • 优点:非常接近正常的 HTML 语法
    • Con :可能会令人困惑,因为属性分配和子元素在语法上无法区分。 在示例中, <Title>...</Title>可能是一个独立的组件,而<RowTemplate>...</RowTemplate>可能是周围<MyListWithHeader>一个属性(一个属性)——如果没有智能感知,你真的不知道
    • 缺点:如果存在同名的组件和属性,可能会导致命名冲突
  3. 发明新语法:XAML 的属性元素语法只是一种选择,它也可以是一些类似于<strong i="39">@functions</strong> { }或完全不同的 Razor 构造。

    • 优点:允许任意复杂的模板,同时仍然可读
    • 缺点:可能会疏远网络开发人员

我完全有可能在这里误解了一些东西。 如果是这样,请告诉我。

@SvenEV我不认为 xaml 语法会引起任何人的共鸣,如果你注意到,angular、vue、react 和数百万其他框架的模板语法都有不同的方式和不同的语法来做事,但仍然所有人都喜欢,不是吗? 同样,blazor 可以有自己的语法,在这种情况下,唯一的优点是我们不会发明一种新的语法,只是将已经流行的语法引入这个框架(因此 xaml 开发人员的学习曲线更短),但就 Web 开发人员而言,他们无论如何,无论 blazor 是否遵循 xaml,都将学习 blazor 模板,Web 开发人员的学习曲线保持不变,Web 开发人员远离此缺点的唯一方法是 blazor 是否直接使用 html,我认为这不会像模板模型。 :)

因此,全新的模板语法为每个开发人员提供了学习曲线。 但是类似 xaml 的语法至少解决了一半开发人员的问题。 但是 xaml 也很像 html,毕竟两者都是标记。 一旦你学会了它,这将是小菜一碟,好东西 abt xaml 方式是,你根本不必担心 abt css,所有样式都是通过控件上的直接属性完成的。 甚至完成样式也使用相同的语法,但另一方面 html 和 css 是完全不同的语言,具有不同的语法和服务结构。

但是为什么我们甚至需要模板? 我的意思是当你可以在 Razor 中执行 foreach 时。 对我来说似乎比模板容易得多。
还是因为复用? 我可以想象一个单独的组件在这种情况下执行 foreach。

@MihaMarkic模板标准化了一切,它增加了重用,最重要的是它隐藏了为一个控件编写的所有复杂代码,您只需使用 2 行代码即可使用该包装控件,并更多地关注您的业务逻辑而不是重新发明轮。

嗯,我不知道这个。 IMO foreach 是一个模板,只是一个更强大的模板,您可以将“foreach”模板放在不同的组件中,并使用这两行代码从任何地方调用它。

foreach 只是一个循环,是的,对于更简单的控件,我们可以这样做,就像对于基本的 ListView ( table ),我们可以简单地制作一个控件,它接受一些输入,执行 foreachon 并吐出一个表,然后我们只需要贴一个标签像这样的东西,然后我们将能够生成一个表格,如果我们想要一个定制的列表视图,具有非常现代的设计,高度交互等,那么我们可以使用 foreach 和很多 cutom css 制作另一个组件控件,等等blazor 代码在一个文件中,也许是多个文件,并将所有这些都包装在 1 个组件中,然后再次仅使用 1 个标签使用所有这些。 你明白我的意思了吗? 不管一个控件有多少代码,所有这些都可以被包装到 1 个标签中 :) @MihaMarkic

这不仅仅是使用相同的模板渲染多个项目。 还需要使组件的多个部分可定制。 目前,一个组件在其布局中只能有一个这样的“洞”,可以用ChildContent填充,这是一个预定义的属性,如下所示:

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

现在想象一下你想写一个有多个这样的“洞”的组件,比如一个对话框,让你自定义内容和标题,或者一个按钮,你可以自定义内部内容,但也可以为上下文菜单和工具提示指定模板也许。 因此,如果您可以编写如下内容(无论当时的语法如何),那就太好了:

<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是不是只有多个孔而不严格要求模板?

是的,这是另一个用例的好例子,每个部分都应该是可定制的。

@MihaMarkic这是真的,我们在这里谈论两个不同的事情:

  1. 支持多个孔( RenderFragment s / Action<RenderTreeBuilder> ),如我的示例
  2. 支持具有数据输入 ( Action<RenderTreeBuilder, T> ) 的模板,如 ListView 等。

我认为第一个是第二个的要求,我希望这两个功能都能在某个时候实现。 这将使我们能够构建一些强大的组件库。

这是要解决哪个问题? 我只是想知道,因为我还没有在其他 SPA 框架中看到这种事情。

它是否与基于组件的结构冲突? 此外,它是否违反了正常的 Web 开发实践,只是为了强制采用更“类似应用程序”的结构?

@AWulkan它不违背正常的组件结构,但是它确实略微倾向于类似 app-dev 的结构,但这并不意味着它脱离了 Web 开发,bcz 所有流行的框架也都在做同样的事情,整个目的SPA 概念是这样的。 SPA 的创建是为了使 Web 开发更像应用程序开发。 现在每个框架都在遵循 spa。 spa的下一步现在是pwa。 你知道 pwa 是对的吗? 它从原生应用程序中引入了一些主要功能,例如通知和离线同步,因此最终整个 Web 开发人员都在尝试遵循原生应用程序开发。

@AWulkan由于您似乎对 Vue 很熟悉,所以我只是查看了 Vue 文档以查看是否有类似的内容。 有,它被称为“插槽”,特别是“命名插槽”和“作用域插槽”——这正是这个问题的意义所在。 这是相同的概念。 也许这有助于您理解问题?

@SvenEV Okey 我有点明白你的意思了。 就我而言,这是一个非常小众的功能。 谢谢澄清。

它会使用这个草案作为模型吗?
https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Slots-Proposal.md

似乎 Vue.js 插槽类似于 XAML 数据模板。
模板提供了一种自定义现有组件的强大方法,我可以看到的一个例子是 ListView,默认情况下没有模板是它只是遍历字符串列表并显示它。

<ListView list=<strong i="7">@ListOfUrlsAsStrings</strong> />

使用模板,您可以覆盖它说,而是使用传递的Type T显示图像标签列表。

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

这将大大提高组件的可重用性,因为它们可以是动态的。 3rd 方组件供应商在以这种方式创作组件时可以更有创意。

是的,数据模板绑定变得更简单和可观察

我们非常希望 Blazor 组件能够很好地支持嵌套和模板。
之前,我们与@Mehul@NTaylorMullen@DamianEdwards就 Razor Tag Helpers 的同一主题进行了多次讨论,我们试图解释为什么我们需要更多类似 XAML 的方法。 相关 GitHub 问题:aspnet/Razor#474、aspnet/Razor#576、aspnet/Razor#570。

我们开发诸如数据网格之类的 UI 组件,并且配置小部件的典型标记可以像这样细枝末节:

<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>

对于这样的层次结构,开发人员希望拥有准确的上下文感知和智能感知。

@AlekseyMartynov从长远来看,我们也可以拥有像样式 + 动画这样的 xaml 吗?

我很高兴这个想法越来越受欢迎。 由于 MS 在 Silverlight、WPF 和 UWP 中率先提出了可模板化控件的想法,因此将其作为 Blazor 的一部分是有意义的。

一个问题是数据/代码上下文。 当 cshtml 模板引用绑定 C# 对象中的代码时,该代码可能与较大的组件不在同一上下文中。 那么不需要某种动态数据绑定吗? 或者模板是否有严格的 TargetType 要求,以便解析器知道如何解析对数据绑定对象成员的引用?

关于这一点,动态数据绑定和依赖属性呢? 据我所知,现在没有什么可以阻止这一点。 或者,如果 C# 或 IL 最终被编译为 wasm 而不是由单声道解释,反射会出现问题吗?

在我看来,实现@AlekseyMartynov所建议的(或某种变体)对于 Blazor 的成功至关重要。 在实践中,许多编写企业应用程序的开发人员依赖于一组丰富的易于使用的 3rd 方组件,我希望这个项目很快就会在不同供应商可以开始开发 Blazor 组件时出现。

在我的项目Xamzor 中,我实现了一些有限的模板支持,如下所示:

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

其中MyCustomTemplate是另一个 Blazor 组件,它具有由 ListView 分配的属性“DataContext”。 虽然这很有效,但仍然需要内联而不是在单独的组件中定义模板,尤其是在模板小而简单的情况下。

也许 ember.js 上下文组件可以提供一些启发: https ://www.mutuallyhuman.com/blog/2016/09/23/creating-an-accordion-with-contextual-components-in-ember

@SvenEV我同意在简单模板的情况下内联模板会有所帮助,但我们仍然应该像您在您的项目中实现的那样选择单独的组件模板,因为这有助于我们分离关注点和模板的可重用性:)

我脑子里的三件事:

  1. 使组件具有原生层次结构(例如 ParentComponentType 属性;以便您可以强制实施 MyTableComponent -> MyRowComponent -> MyColumnComponent)。 如果 ParentComponentType 为 null,则该组件可以是任何组件类型的子项(例如 MyTableComponent.ParentComponentType 可以为 null)。

层次结构是通过组件定义中的声明来强制执行的,例如:

```C#
// 在 MyRowComponent 中
类型 ParentComponentType { 获取; } = 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. 创建可作为可选基类 BlazorTemplate 实现的接口 ITemplate,可用于使用模板对象化编程。

MyColumnComponent 是 ITemplateable 的示例用法:

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

将 MyColumnComponent 定义为 ITemplateable,它支持包含<Template> … </Template>标记。 此标记中的条目会覆盖 MyColumnComponent 的默认呈现。

在内部,MyColumnComponent 可能有/使用 BlazorTemplate 实现,让它以编程方式处理特殊的模板行为、事件和功能。

我投票支持属性元素语法

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

  Right-click me!
</Button>

作为替代

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

我喜欢它,但网络纯粹主义者不会因为太像 XAML 而嗤之以鼻吗?

不,他们将被罚款, property element syntax替代html attribute syntax (不更换)。 但我正在退出它为组件开发人员(UI 控件库开发人员)提供的多少可能性

我也喜欢它,因为它更清楚地传达了子级与父级的关系,这在模板上特别好,因为您可以区分它是父级的数据模板,而不是与父级无关的自定义组件父组件。

像这样:

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

这让您知道 DataTemplate 与 List 相关,而不是整体上的自定义模板。 尽管我同意 XAML,但它应该只是一种替代方案。

如果“模板”是任何可模板化的 Blazor 组件的保留/特殊关键字怎么办? 当用作标签时,它可以包含模板代码。 当用作属性时,它可以指定一个命名模板,其中可以包含模板代码。

所以,让我们想象一个像这样的组件:

ComponentTemplateTest.cshtml

<strong i="7">@functions</strong>
{
  [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>

我们可以用它作为

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

    <TemplateWithInt params="param1">
        <strong i="11">@param1</strong>
    </TemplateWithInt>

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

我们需要像params这样的属性,因为 Blazor 将代码编写为

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

就够了吗? 缺了点什么?

@uazo请注意,如果没有属性元素语法,则无法区分propertycomponent

@danieldegtyarev我认为我们在这里谈论的是两件不同的事情......我的目标现在只是组件的模板......

看看https://github.com/uazo/Blazor/tree/dev-experimental-templated-component
您需要构建并安装该扩展...

用作标签的“模板”是声明性的并且自然内联:

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

“模板”用作标记以分配“命名模板”。 在内部,命名模板将可以访问其父组件,因此也可以使用MyCom.MyComProp内容:

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

“模板”用作分配“命名模板”的属性。 在内部,命名模板将可以访问其父组件,因此也可以使用MyCom.MyComProp内容:

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

用作标记或属性的“模板”应导致相同的编译,以便组件将呈现提供的模板信息,这只是覆盖组件的默认呈现。

“命名模板”是可重复使用的模板。 创建一个“命名模板”几乎就像创建一个组件。 但是,最大的区别在于命名模板只能通过组件对“模板”的支持来使用。

模板可以包含组件,但需要进行检查以防止潜在的无限循环,例如,当使用相同组件的模板的组件被编码时......也许是设计/编译时错误?

问题:命名模板可以从另一个模板继承吗? 是否可以将命名模板限制为仅与特定组件类型一起使用?

我认为你们应该尽可能地使用 HTML 语法,不要引入新的东西。 没有 Xamarin xaml 语法或丑陋的 asp.net webforms 组件(asp 中继器等)。
我来自asp.net mvc世界,我记得当我从“asp.net webforms”切换到“asp.net mvc”时的快乐和自由,不再有像asp:repeater之类的奇怪组件。
请不要让 BLAZOR 成为一个新的“asp.net webforms”。

我同意@GoranHalvarsson 。 Blazor 中的模板应该只是通用 RenderFragment 的实现,具有相同的用例( @etmendz帖子中的第一个示例)。

为此,我更新了我的分支,现在完全支持 Visual Studio 中的智能感知(参数和允许的孩子)。

有人可以给我一些反馈吗?

@GoranHalvarsson我同意。 MVP 可以让 Blazor 组件具有基本的模板支持。 从这个意义上说,“模板”可以包含 HTML、组件、内容和 Razor 代码。 在最基本的情况下,Blazor 组件+模板仍然应该是可识别的 HTML+Razor 代码。 声明式语法符合这个要求,例如可以是<Template>@template关键字。

下一阶段的项目可以是 Blazor 的 API/可扩展性库,使组件创建者能够推广他们自己的品牌——例如:HTML helper-like 语法(Progress Telerik/Kendo UI)或 Xamarin/XAML-like 语法(ooui.wasm、Platform .Uno) 可以改为第 3 方/供应商级别的交付。 在 Blazor 中,模板可编程性和编译灵活性可以与组件创建者合作伙伴密切合作,以解决特定问题/需求/要求。

这个线程中有一个关于可重用模板的建议。 可重用模板或“命名模板”可以促进代码重用。 虽然,老实说,我认为它没有多大价值,并且可能是低优先级的。 更有可能的是,如果它是为了重复使用,那么也许它应该是一个组件? ;-)

@uazo恕我直言,根据您的示例,将参数传递给<Template>似乎破坏了直接在<Template>标签内使用参数/值的声明性性质。

我也怀疑让组件支持多个模板属性。 从这个意义上说,让它太自由了。 我认为 Blazor 应该有一种更简单、更标准的方式来支持组件模板,也许是通过 ITemplate 接口和一些编译器调整。

@SteveSandersonMShttps://github.com/aspnet/Blazor/issues/404#issuecomment -377229910
使用 aspnet/AspNetCore#15829 代码将是

MyListWithHeader.cshtml

<strong i="10">@functions</strong>
{
    [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>
    <strong i="11">@Title</strong>
</div>

<strong i="12">@foreach</strong> (var item in RowItems)
{
    <div>
    @RowTemplate.Render(item)
    </div>
}

<strong i="13">@ChildContent</strong>

例子.cshtml

<strong i="17">@page</strong> "/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 <strong i="18">@TitleText</strong>
    </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>

<strong i="19">@functions</strong>
{
  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; }
  }
}

全力支持智能感知!

@uazo非常非常好! 只有一件事:让我们删除WithParams="item"并使用@item来访问模板的“上下文”

嗯 .. 但是如果item已经存在于组件example
由于用于访问模板的“上下文”的变量名称不能被发现,因此使用WithParams ,开发人员可以决定变量的名称。

您是否在考虑带有或不带有可选WithParams的有线默认名称(如ChildContent )? 或者像<MyListWithHeader.RowTemplate WithParams=@item>这样的语法?

回顾过去,默认名称不适用于嵌套模板......

让我们想想。 模板是带有一个参数的代码生成。 模板只能访问此项目。 因此,如果我们“保留”一个变量item - 它是安全的并且不会发生冲突。 嵌套模板有自己的作用域,使用@item仍然是安全的。

顺便说一句,看起来像这个模板 - scope也必须有他们的@functions

让我们

<MyListWithHeader RowItems=@People>
    <MyListWithHeader.Title>
        ...<strong i="6">@TitleText</strong>
    </MyListWithHeader.Title>

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

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

    </MyListWithHeader.RowTemplate>
</MyListWithHeader>

将被渲染为

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

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

是的,当然,您必须在代码生成期间解决它。
但是...我认为模板必须编译为component (可能是一个特殊组件)而不是RenderFragment因为我们需要管理它的状态,例如在调用 StateHasChanged() 时@item.FirstName变异。

是的,当然,您必须在代码生成期间解决它。

我不认为这是可能的。
在代码生成期间(这是剃刀修改的)所有节点都被交叉了,有些是 html,有些是 c# 节点:那些使用 c# 的节点是错误的,因为变量不会被定义,因此变量类型不会被知道(我们需要恢复RenderFragment<T> T类型的变量,因为我们知道T ) ...所以我不知道要使用的变量的名称是什么。
你可能会寻找那个未定义的变量,但它似乎一团糟

但是...我认为模板必须被编译为一个组件(可能是一个特殊的组件)而不是 RenderFragment 因为我们需要管理它的状态,例如在@ item.FirstName发生变化时调用 StateHasChanged()。

我认为没有必要介绍任何其他内容。
如果你需要一个组件使用BlazorComponent ,如果你需要一个“模板属性”使用RenderFragment<T>
毕竟,RenderFragment 的StateHasChanged()是由底层组件的StateHasChanged()调用的,我不需要以任何其他方式强制调用更改

很抱歉在这里插嘴,因为这听起来像是一场很棒的谈话——但是——

这不是用 WPF/UWP 之类的数据上下文概念来解决的吗? 然后在您的模板中,您将始终引用this.DataContext.[Member]

@peter0302我个人喜欢这个想法......

挑战仍然是强类型。 当然,这在 XAML 中是通过在运行时完成的绑定来解决的。 此处的数据模板可能必须支持通过泛型键入。 但绝对可行。

然后我也将支持完全动态的运行时绑定机制。

您可以通过声明以某种方式声明数据类型。 有点类似于 XAML 上的x:DataType ,但推断它应该更理想且不易出错。

@uazo我的反馈是它看起来非常好! 以下是我的想法...

1) 为RenderFragment类型的属性分配了像<MyListWithHeader.Title />这样的标签中属性的限定名称对我来说感觉没问题,但这种处理打破了我以前的直觉,即设置了带有 ParameterAttribute 的属性通过 html 属性。 如何为这些属性设置另一个 c# 属性来区分该行为? 喜欢[ContentParameter]吗?

2) RenderFragment<Person>类型的属性被分配了一个带有属性的属性标签
<MyListWithHeader.RowTemplate WithParams="item" />起初感觉有点奇怪。 它不是设置参数属性的属性,而是在该模板的本地代码生成期间在 lambda 表达式中使用的变量名称。 在这里再次使用另一种语法来区分差异会很好。 但我不确定一个人为了兼容工具和编译器可以有多大的创造力。 <MyListWithHeader.RowTemplate params(item) />

3) 如果我想显示<MyListWithHeader RowItems=<strong i="16">@PeopleOnline</strong> /><MyListWithHeader RowItems=<strong i="18">@PeopleOffline</strong> /> ,我是否必须再次指定模板? 有没有办法重用它们? 我在想也许通过这种机制进行一些模型驱动的初始化,类似于 EditorFor 使用相同模板的不同属性选择器。

当我刚接触框架时讨论实现可能有点冒险,但是......

如果您使用单独的参数属性来分配模板,如上面第 1 点所述,那么您可以向其添加一个布尔标志(默认为 true)以说明是否应应用默认值(如果未明确给出)。 然后你可以通过引入一个像 IDefaultTemplateProvider 这样的接口来解决上面的第 3 点,你可以用它来分别为模板属性标签提供定义。 然后可以通过反射以与发现 IComponent(s) 相同的方式发现它们,并在设置了 use-default 标志并且用户不提供标志的情况下使用它们。 您可以允许多个模板组件实现该接口,但如果多次找到属性标记,则会抛出明确的异常。 通过这种方式,您可以为特定于应用程序的通用组件提供标准化模板,这些组件被多次使用并在设计时获得解决方案,也许?

我认为围绕这​​个描述我的学习经历可以为您提供一个很好的背景……并且我有一些想法要讨论……

我想通过元数据和资源文件对组件进行模型驱动的配置,以检索静态翻译文本、验证等。 我在想这种模板机制可能是让它发生的事情。 为了评估它,我制作了一个接口public interface IRenderFragment<T> { RenderFragment UsingDataItem(T item); }并制作了一个普通组件,该组件也实现了该接口以用作模板。 然后像这样使用它制作一个通用组件

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

因此,通过这种方式, InputField 组件具有有关如何检索元数据等的信息,并且可以将其输入到在您提供的模板中呈现的模型中。 它运作良好。

在 XAML 中,当组件中实现了周围行为时,它是呈现事物的唯一机制,例如在列表视图中,然后您需要提供项目模板。 在 Blazor 中,它不是必需的,因为您可以使用带有循环的代码来代替周围的行为。 然后我意识到我可以在这里做同样的事情。 我本可以从代码中的元数据中检索模型,然后将其提供给普通组件。 @{ var inputModel = InputModelHelper.GetFor(ViewModel, x => x.NewTodo); } <InputBasic DataItem="@inputModel"/>

也许对某些人来说很明显……所以模板对于我的用例实际上并不是绝对必要的。 但它让我怀疑我真正想要的是什么。 也许它更像是将属性和内容注入现有元素? 也许可以通过定义自定义属性来实现,例如:

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

为了测试是否可以将属性节点添加到现有元素中,我在打开标签<label>@For(x => x.NewTodo)</label>之后创建了一个函数(为该内容节点返回空字符串),它通过添加属性节点(和一个内容节点(在标签元素的情况下)到构建器。 (构建器是在方法 BuildRenderTree 中捕获的。)构建器具有标签节点的位置,因此如果已设置属性,则可以在其缓冲区中轻松查看,因此不应被此机制覆盖。 它确实有效。 对于 input 元素来说有点困难,因为它是一个空元素(它需要重新打开并且函数不能放在属性之间),但作为一个概念证明它似乎有效。 如何使这一点得到支持,它是否有意义?

我想这个渲染延续/扩展功能可以挂在几个地方。 为了让你思考......也许有能力注入“RenderElementExtensionHandler”,如果一个组件有这样的属性,那么在所有属性都被添加后和在那些具有自定义元素的生成代码中关闭之后,对它的调用将被编译到位置属性,如本例中的 For。
这是可以用于其他东西的东西还是边缘情况? 是否有将行为扩展到现有元素的其他想法?

对不起,如果它有点冗长......

为什么不渲染道具?

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

完毕:
b2c8b1aec98e65054c590d356fdf15e6a5e6a822
409026c2f41926d85d18ea2abf345453fa083e34
ce11f517d370b501433a62a56fbd4be1704000ad

此页面是否有帮助?
0 / 5 - 0 等级