Search code examples
c#local-storageblazorblazor-webassembly

Null reference when trying to use localstorage service in mainlayout


I am trying to impliment a darkmode toggle in a blazor webassembly app. I inject the localstorage and it works on other pages that use local storage but when trying to impliment it on the main layout oninitalized method I get a null reference error on load.

public partial class MainLayout
{
[Inject]
    protected ILocalStorageService localStorage { get; set; }
protected override async Task OnInitializedAsync()
    {
        _darkmode = false;
        try
        {

            if (await localStorage.GetItemAsStringAsync("theme") == null) return;
            var mode = await localStorage.GetItemAsync<string>("theme");
            if (!string.IsNullOrEmpty(mode))
                _darkmode = mode == "darkmode" ? true : false;

            Themer.SetTheme(_darkmode);
            StateHasChanged();
        }
        catch (Exception ex)
        {
            _darkmode = false;

        }         

    }
}

This is the error I get as soon as I have anything to do with the local storage in the main layout.

crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: Object reference not set to an instance of an object. System.NullReferenceException: Object reference not set to an instance of an object. at SeekaPortal.Client.Shared.MainLayout.BuildRenderTree(RenderTreeBuilder __builder) at Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0(RenderTreeBuilder builder) at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment) at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry) at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()


Solution

  • Blazor has some odd startup sequenceing behavior due to it rendering while pages are still running their startup logic (good old async, await...). Not sure if this is your issue or not. This is what I had to do

    @if (BootComplete) {
        <Router AppAssembly="@typeof(App).Assembly">
            <Found Context="routeData">
                <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
                <FocusOnNavigate RouteData="@routeData" Selector="h1" />
            </Found>
            <NotFound>
                <PageTitle>Not found</PageTitle>
                <LayoutView Layout="@typeof(MainLayout)">
                    <p role="alert">Sorry, there's nothing at this address.</p>
                </LayoutView>
            </NotFound>
        </Router>
    }
    @inject IJSRuntime JS
    @inject SavedData Saver
    
    
    @code 
    {
    
        // boot code. By hiding everything nothing starts until bootcomplete is set to true
        // that is done after the successful exit from OnInitializedAsync
        // App.Boot has to be async becuase it does 'IO'
    
        bool BootComplete = false;
    
        protected override async Task OnInitializedAsync() {
            EarWorm.Code.Util.Init(JS);
            await Saver.Boot();
            BootComplete = true;
        }
     }
    

    ie hide the UI until all injects, inits, reading of state etc had completed. I did it for the entire app (as you can see this is App.razor) maybe you need it for a few pages.