Blazor组件引用:第一次呈现时为空。

21

我有一个自定义组件,其中有一个名为TabChanged的事件Action。在我的Razor页面中,我设置了对它的引用,如下所示:

<TabSet @ref="tabSet">
 ...
</TabSet>

@code {
    private TabSet tabSet;   
    ...
}
OnAfterRenderAsync 方法中,我将一个处理程序分配给该事件:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if(firstRender)
    {
        tabSet.TabChanged += TabChanged;
    }       
}

第一次页面渲染时,我遇到了一个System.NullReferenceException: Object reference not set to an instance of an object 错误。

如果我切换到使用后续的渲染,它就可以正常工作:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if(!firstRender)
    {
        tabSet.TabChanged += TabChanged;
    }       
}

当然,这种方法很草率,每次渲染时会堆叠多个事件处理程序,我将对其进行解雇。

我如何在第一次渲染时仅分配一次引用?我正在按照此处所述的文档进行操作。

编辑

这是TabSet.razor文件:

@using Components.Tabs

<!-- Display the tab headers -->
<CascadingValue Value="this">
    <ul class="nav nav-tabs">
        @ChildContent
    </ul>
</CascadingValue>

<!-- Display body for only the active tab -->
<div class="nav-tabs-body" style="padding:15px; padding-top:30px;">
    @ActiveTab?.ChildContent
</div>

@code {

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

    public ITab ActiveTab { get; private set; }

    public event Action TabChanged;


    public void AddTab(ITab tab)
    {
        if (ActiveTab == null)
        {
            SetActiveTab(tab);
        }
    }

    public void RemoveTab(ITab tab)
    {
        if (ActiveTab == tab)
        {
            SetActiveTab(null);
        }
    }

    public void SetActiveTab(ITab tab)
    {
        if (ActiveTab != tab)
        {
            ActiveTab = tab;
            NotifyStateChanged();
            StateHasChanged();
        }
    }

    private void NotifyStateChanged() => TabChanged?.Invoke();

}

TabSet 同样使用 Tab.razor:

@using Components.Tabs
@implements ITab

<li>
    <a @onclick="Activate" class="nav-link @TitleCssClass" role="button">
        @Title
    </a>
</li>

@code {
    [CascadingParameter]
    public TabSet ContainerTabSet { get; set; }

    [Parameter]
    public string Title { get; set; }

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


    private string TitleCssClass => ContainerTabSet.ActiveTab == this ? "active" : null;

    protected override void OnInitialized()
    {
        ContainerTabSet.AddTab(this);
    }

    private void Activate()
    {
        ContainerTabSet.SetActiveTab(this);
    }
}

ITab.cs 接口

using Microsoft.AspNetCore.Components;

namespace PlatformAdmin.Components.Tabs
{
    public interface ITab
    {
        RenderFragment ChildContent { get;  }

        public string Title { get; }
    }
}

这是从Steve Sanderson的一个示例中获取的,可以在此处找到。

编辑2

这里是调试器显示tabSet在第一次渲染时为null:

进入图像描述

在后续渲染中不为null:

进入图像描述


1
我尝试了你的代码,但是在使用 if(firstRender) { tabSet.TabChanged += TabChanged; } 时没有得到空异常。 TabChanged 是一个委托吗?你能否展示一下如何重现这个问题? - itminus
1
感谢测试itminus。我更新了我的问题以包括TabSet组件文件。它源自Steve Sanderson的一个示例,链接在这里:https://gist.github.com/SteveSandersonMS/f10a552e1761ff759b1631d81a4428c3 但我添加了事件。 - INNVTV
1
我使用您的代码创建了一个Blazor客户端项目和一个Blazor服务器端项目。然而,对我来说它运行得非常好。请参见截图。我不知道缺少什么? - itminus
1
不确定。我再次运行它,并使用屏幕截图更新了我的问题,显示了第一次和额外渲染期间的调试器。在第一次渲染后,tabSet为空,但在下一次渲染时不为空! - INNVTV
2
你的 <TabSet @ref="tabSet"> 是否在 if ( 里面? - dani herrera
显示剩余2条评论
3个回答

24

正如Dani Herrera在评论中指出的那样,这可能是因为该组件位于if/else语句中,实际上就是这种情况。 以前,如果对象为空,则隐藏该组件:

@if(Account != null)
{
    <TabSet @ref="tabSet">
     ...
    </TabSet>
}

为了简洁起见,我省略了这部分内容,并错误地假设问题不是条件语句。我非常错误,因为在首次呈现时,该对象为null,因此组件不存在!所以一定要小心。我通过将条件转移到组件内的各个部分来解决了这个问题:

<TabSet @ref="tabSet">
    @if(Account != null)
    {
        <Tab>
         ...
        </Tab>
        <Tab>
         ...
        </Tab>
    }
</TabSet>

这非常有用!我遇到了一个类似的问题,就是在 OnAfterRenderAsync 中调用 JS 时,我无法弄清楚为什么 JS 的 getElementById 能够工作,但传递 Blazor 的 @ref 却不能工作。原来带有 @ref 的那个是在 if 语句中,因为它从 Web API 接收数据而被延迟加载,而另一个则是在第一次渲染时创建的。 - somedotnetguy

4

在首次渲染时,组件会被渲染。在第一次渲染后,引用对象将指向该组件。因此,在第一次时,组件的引用(ref)为 null(不是 ref)。

更多详细信息,请参见 这里 的 Blazor 文档。

tabSet 变量只有在组件被渲染并包含 TabSet 元素的输出后才会被填充。在此之前,没有任何引用可供使用。要在组件完成渲染后操作组件引用,请使用 OnAfterRenderAsync 或 OnAfterRender 方法。


2
也许你是对的,但是文档似乎表明引用应该在OnAfterRender或OnAfterRenderAsync中可用,就像我现在这样,并没有说明如果FirstRender为true它们将不可用。我会再仔细查一下。 - INNVTV
2
OnAfterRender 会被调用两次。 - Nguyễn Văn Biên

-4
在我的情况下,这个问题可以通过初始化对象来解决。
例如:private TabSet tabSet = new TabSet();

9
这对我没有用,我想不出任何情况下它会正常工作。这只是避免了异常,但创建了一个组件实例,该实例不一定(我假设从未)引用在初始化前包含 ref 的组件。在此 ref 上执行的方法可能实际上并未命中渲染的组件实例。 - Beltway

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