10

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

INNVTV
  • 2,623
  • 3
  • 30
  • 54
  • 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 ? – itminus Nov 13 '19 at 04:45
  • Thanks for testing itminus. I updated my question to include the TabSet component files. It's taken from a Steve Sanderson example here: https://gist.github.com/SteveSandersonMS/f10a552e1761ff759b1631d81a4428c3 But I added the event. – INNVTV Nov 13 '19 at 05:22
  • 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](https://i.stack.imgur.com/P4PQR.gif) . I don't know what is missing? – itminus Nov 13 '19 at 07:35
  • 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! – INNVTV Nov 13 '19 at 14:42
  • Is your `` inside and `if (`? – dani herrera Nov 14 '19 at 07:29
  • Hey Dani, Yes it is actually. I'll do a test outside of that to see if it resolves the issue. – INNVTV Nov 14 '19 at 14:26
  • That was it Dani. I will add an answer and credit you. – INNVTV Nov 14 '19 at 15:11

3 Answers3

7

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>
ViRuSTriNiTy
  • 4,581
  • 1
  • 27
  • 52
INNVTV
  • 2,623
  • 3
  • 30
  • 54
1

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.

  • 1
    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. – INNVTV Nov 13 '19 at 19:59
  • OnAfterRender is called twice. – Nguyễn Văn Biên Nov 14 '19 at 13:33
0

in my case it's solved by initialize the object

EX: private TabSet tabSet = new TabSet();

Mohamed Omera
  • 91
  • 1
  • 3