如何在 Blazor 的父组件中渲染多个子组件而不调用 @ChildContent?

8
在我们的Blazor WebAssembly-App中,我们有一个ParentComponent,它嵌套了多个ChildComponents。共有3个文件:
  1. Test.razor是客户端,使用了ParentComponent和ClientComponent。
  2. ParentComponent.razor嵌套多个ChildComponent。
  3. ChildComponent.razor是ParentComponent的子组件。

enter image description here

文件:Test.razor

<ParentComponent DataSource="@AccountList">
    <ChildComponent TData="Account"><span>@context.FirstName</span></ChildComponent>
    <ChildComponent TData="Account"><i>@context.LastName</i></ChildComponent>
    <ChildComponent TData="Account"><b>@context.Age</b></ChildComponent>
</ParentComponent>

@code {

    // The datasource:
    private List<Account> AccountList = new List<Account>() {
        new Account() { FirstName = "Sam", LastName = "Soldier", Age = 33},
        new Account() { FirstName = "Lisa", LastName = "Johnson", Age = 25},
        new Account() { FirstName = "Jonas", LastName = "Peer", Age = 50 }
    };

    // A Simple Account-Class for the datasource
    class Account
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}

上面的文件按预期显示以下数据:

result

文件:ParentComponent.razor

@typeparam TData

<CascadingValue Value="this" Name="ParentComponent">
    @ChildContent
    <div class="parent-component">
        @foreach (var lData in DataSource)
        {
            @foreach (var lChild in m_Children)
            {
                <div class="child-component">
                    @lChild.ChildContent(lData);
                </div>
            }
        }

    </div>
</CascadingValue>

@code {

    [Parameter]
    public List<TData> DataSource { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    /// Collection of all added child components
    private List<ChildComponent<TData>> m_Children = new List<ChildComponent<TData>>();

    /// Add a child component (will be done by the child itself)
    public void AddChildComponent(ChildComponent<TData> pChildComponent)
    {
        m_Children.Add(pChildComponent);
        StateHasChanged();
    }
}

文件名:ChildComponent.razor

@typeparam TData

@code {

    /// Reference to the parent component
    [CascadingParameter(Name = "ParentComponent")]
    ParentComponent<TData> ParentComponent { get; set; }

    /// Child content of the component.
    [Parameter]
    public RenderFragment<TData> ChildContent { get; set; }

    // add ChildComponent to ParentComponent
    protected override void OnInitialized()
    {
        Console.WriteLine("ChildComponent.OnInitialized");
        ParentComponent.AddChildComponent(this);
    }
}
问题:文件 ParentComponent.razor 包含语句 @ChildContent,在这里没有意义,因为我们想通过 @lChild.ChildContent(lData) 渲染子元素的数据。
// [...]
<CascadingValue Value="this" Name="ParentComponent">
@ChildContent // <- required, but want to remove, because here is othing to show
<div class="parent-component">
    @foreach (var lData in DataSource)
    {
        @foreach (var lChild in m_Children)
        {
            <div class="child-component">
                @lChild.ChildContent(lData); // We wender the child here
            </div>
        }
    }
// [...]

当我们移除@ChildContent时,将不会呈现任何内容。因此,似乎需要调用@ChildContent来呈现子组件,但似乎是不必要的,因为我们调用了@lChild.ChildContent(lData);。如何在没有@ChildContent语句的情况下呈现数据?
也许在@code块中有可能进行与@ChildContent相同的调用吗?例如:

ParentComponent.razor:

@code {

    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);

        if (firstRender)
        {
            ChildContent.Invoke(...); // ???
        }
    }
}

你使用RenderTreeBuilder吗? - JoeGER94
@JoeGER94:不,我没有使用RenderTreeBuilder。 - Simon
这对我来说似乎是过于复杂的方法。这种方法会给你带来什么好处? - Bahtiyar Özdere
2个回答

9

组件实例

组件是类,你需要创建一个组件类的实例才能访问它的成员 - 例如它的 ChildContent。

当Blazor首次渲染时,会创建组件实例。

为了呈现子组件,它们需要作为 RenderTree 批处理的一部分。

如果"拥有"包含子组件的 RenderFragment(Parent.ChildContent)的组件(父级)本身没有呈现该 ChildContent,则子组件实例将不会被创建。

像这样的一行代码正在访问子组件的实例属性,这要求在调用之前必须存在子组件实例。

@lChild.ChildContent(lData); // We render the child here

简要来说,您需要在父组件中呈现ChildContent以创建Child组件的实例并将其注册到RenderTree中。
稍有不同的是,如果您的Child Component确实没有内部逻辑(就像您问题中的示例一样),那么它根本不需要成为一个组件 - 它可以是一个RenderFragment,这样您需要在每个子项中包装额外标记的要求会变得更加容易,因为RenderFragment不像组件那样被实例化。
如果您编写父级接受多个子级RenderFragments,可以像这样:
@typeparam TData

<div class="parent-component">
  @foreach (var lData in DataSource)
    {
        @foreach (var lChild in m_Children)
        {
            <div class="child-component">
                @lChild(lData);
            </div>
        }
    }

</div>

@code {
    [Parameter]
    public List<TData> DataSource { get; set; }

    [Parameter]
    public RenderFragment<TData> ChildFragment { set => m_Children.Add(value); }

    /// Collection of all added child fragments
    private List<RenderFragment<TData>> m_Children = new List<RenderFragment<TData>>();
}

你可以像这样使用它:
<ParentComponent DataSource="@AccountList">
    <ChildFragment><span>@context.FirstName</span></ChildFragment>
    <ChildFragment><i>@context.LastName</i></ChildFragment>
    <ChildFragment><b>@context.Age</b></ChildFragment>
</ParentComponent>

作为额外的福利,您不再需要指定 TData 的类型,因为它可以被推断出来。
**注意:此技术仅适用于 RenderFragments,但您当然可以将任何内容放入 ChildFragment(s) 中,包括子组件,并仍然可以控制输出渲染。

我喜欢你的回答。 - Simon
我相信这是正确的方法。 另一种方法是在子元素中呈现内容,而不是将其用作数据容器。 - Bahtiyar Özdere

0

我自己找到了一个解决方案,通过将多个ChildComponent包装在自己的RenderFragment中,命名为<MultiComponents>

Test.razor:

<ParentComponent DataSource="@AccountList">
    <MultiComponents> // <-- Custom RenderFragment
        <ChildComponent TData="Account"><span>@context.FirstName</span></ChildComponent>
        <ChildComponent TData="Account"><i>@context.LastName</i></ChildComponent>
        <ChildComponent TData="Account"><b>@context.Age</b></ChildComponent>
    <MultiComponents> 
</ParentComponent>

@code {

    // The datasource:
    private List<Account> AccountList = new List<Account>() {
        new Account() { FirstName = "Sam", LastName = "Soldier", Age = 33},
        new Account() { FirstName = "Lisa", LastName = "Johnson", Age = 25},
        new Account() { FirstName = "Jonas", LastName = "Peer", Age = 50 }
    };

    // A Simple Account-Class for the datasource
    class Account
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}

ParentComponent.razor:

在我的父组件中,我现在可以用@MultiComponents替换调用@ChildComponent
<CascadingValue Value="this" Name="ParentComponent">
    @MultiComponents // <-- Call MultiComponents instead @ChildComponents
    <div class="parent-component">
        @foreach (var lData in DataSource)
        {
            @foreach (var lChild in m_Children)
            {
                <div class="child-component">
                    @lChild.ChildContent(lData);
                </div>
            }
        }

    </div>
</CascadingValue>

@code {

    [Parameter]
    public List<TData> DataSource { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }
    
    [Parameter] 
    public RenderFragment MultiComponents { get; set; } 

    /// Collection of all added child components
    private List<ChildComponent<TData>> m_Children = new List<ChildComponent<TData>>();

    /// Add a child component (will be done by the child itself)
    public void AddChildComponent(ChildComponent<TData> pChildComponent)
    {
        m_Children.Add(pChildComponent);
        StateHasChanged();
    }
}

这样做的好处是,我可以选择性地在<ParentComponent>中使用@ChildComponent和其他自定义的RenderFragment

[...]
<CascadingValue Value="this" Name="ParentComponent">
    @ChildComponent
    @MultiComponents
    // Further Render Fragments

    <div class="parent-component">
    [...]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接