25

I have a custom component with an event Action called TabChanged. In my Razor page I set the reference to it up like so:

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

@code {
    private TabSet tabSet;   
    ...
}

In the OnAfterRenderAsync method I assign a handler to the event:

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

The first time the page renders I get a System.NullReferenceException: Object reference not set to an instance of an object error.

If I switch to use subsequent renders it works fine:

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

But of course this is sloppy and I will be firing multiple event handlers as they stack up during renders.

How can I assign the reference one time and on first render? I am following the docs as outlined here

EDIT

Here is the TabSet.razor file:

@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 also uses 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);
    }
}

And ITab.cs Interface

using Microsoft.AspNetCore.Components;

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

        public string Title { get; }
    }
}

It's taken from a Steve Sanderson example found here

EDIT 2

Here is the debugger showing tabSet is null on first render:

enter image description here

And not null on additional renders:

enter image description here

7
  • 1
    I tried you code, but I didn't get a null exception when using if(firstRender) { tabSet.TabChanged += TabChanged; }. Is TabChanged a delegate ? Could you please show us a way to reproduce ? Commented Nov 13, 2019 at 4:45
  • 1
    Thanks for testing itminus. I updated my question to include the TabSet component files. It's taken from a Steve Sanderson example here: gist.github.com/SteveSandersonMS/… But I added the event. Commented Nov 13, 2019 at 5:22
  • 1
    I create both a blazor client-side proj & a blazor server-side proj using your code. However, it works pretty fine for me. See screenshot . I don't know what is missing? Commented Nov 13, 2019 at 7:35
  • 1
    Not sure either. I ran it again and updated my question with screenshots showing the debugger during both first and additional renders. tabSet is null after first render but not null on next one! Commented Nov 13, 2019 at 14:42
  • 3
    Is your <TabSet @ref="tabSet"> inside and if (? Commented Nov 14, 2019 at 7:29

2 Answers 2

33

As Dani Herrera pointed out in the comments this may be due to the component being withing an if/else statement and indeed it was. Previously I had the component hidden if an object was null:

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

I left this out for brevity and made the incorrect assumption that the issue was not the conditional. I was very wrong as on first render the object is null and therefore the component does not exist! So be careful out there. I resolved it by moving my conditionals to the sections within the component:

<TabSet @ref="tabSet">
    @if(Account != null)
    {
        <Tab>
         ...
        </Tab>
        <Tab>
         ...
        </Tab>
    }
</TabSet>
Sign up to request clarification or add additional context in comments.

1 Comment

This was most useful! I had a similar problem with invoking JS in OnAfterRenderAsync and I could not figure out, why JS getElementById was working but passing a Blazor @ref was not working. Turns out the one with the @ref was inside an if which was loaded delayed because it received data from an web API, while the other one was created on first render.
5

In first render, component is rendered. After first render, referent object is refer to component. So, at first time, ref of component is null(not ref). For more detail, document of blazor is detail issue in here

The tabSet variable is only populated after the component is rendered and its output includes the TabSet element. Until that point, there's nothing to reference. To manipulate components references after the component has finished rendering, use the OnAfterRenderAsync or OnAfterRender methods.

2 Comments

Perhaps you are correct but the documents seem to state that the reference should be available OnAfterRender or OnAfterRenderAsync as I have it and do not state that they would be unavailable ifFirstRender is true. I'll dig around a bit more to see.
OnAfterRender is called twice.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.