Search code examples
blazorblazor-webassembly

Blazor 8 InvokeAsync(StateHasChanged) doesn't work in widget


I have upgraded to a Blazor 8 WebAssembly project from a Blazor 7. In my Blazor 7 project I used a component to update the title and SEO information dynamically. The header component code looks like this:

@if (headerState is not null && headerState.header is not null)
{
    <PageTitle>@headerState.header.Title</PageTitle>
    <HeadContent>
        <meta name="description" content="@headerState.header.Description">
        <meta name="keywords" content="@headerState.header.Keywords">
        <meta name="og:url" content="@headerState.header.Url">
        <meta name="og:title" content="@headerState.header.Title">
        <meta name="og:description" content="@headerState.header.Description">
        <meta name="og:image" content="@headerState.header.Image">
        <meta name="twitter:card" content="´summary">
        <meta name="twitter:site" content="@headerState.header.TwitterSite">
        <meta name="twitter:text:description" content="@headerState.header.Description">

        <link rel="canonical" href="@headerState.header.Url">
    </HeadContent>
}

public partial class HeaderComponent : IDisposable
{
    [Inject] 
    public PageHeaderState headerState { get; set; }

    protected override void OnInitialized()
    {
        headerState.StateChanged += async (Source, Property) => await HeaderState_StateChanged(Source, Property);
    }

    private async Task HeaderState_StateChanged(ComponentBase Source, string Property)
    {
        if (Source != this)
            await InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        headerState.StateChanged -= async (Source, Property) => await HeaderState_StateChanged(Source, Property);
    }
}

I invoke it from then page like this:

 protected void LoadHeader(string title, string descr)
 {
     header.Title = $"{title}";
     header.Description = descr;
     header.Keywords = "programs, video, undervisning, dokumentärer, intervjuer, reportage, koncerter, barnprogram";
     header.TwitterSite = "@Sverige";
     header.Url = $"{_navmanager.Uri}";
     header.Image = $"{Config.WbImageUrl}/tv/d4893099-6694-47ff-838e-8fcf6ee72a93.jpg";

     headerState.SetHeader(this, header);
 }

I all works fine as long as I place the code on the a razor component that has a @page directive but when I try to invoke it inside a widget nested in the page, I can see the state is changed but it is not reflected in the HeadOutlet and rendered. For example this is a blogpost component where I try to invoke the new state but nothing happens.

protected override void OnInitialized()
{
    headerState.StateChanged += async (Source, Property) => await HeaderState_StateChanged(Source, Property);
}

protected override async Task OnParametersSetAsync()
{
    await GetItems();
}

private async Task HeaderState_StateChanged(ComponentBase Source, string Property)
{
    if (Source != this)
        await InvokeAsync(StateHasChanged);
}

protected async Task Navigate(int type)
{
    string url = "";

    if (type == 1)
    {
        url = itemForward.ItemUrl;
    }
    else if (type == 2)
    {
        url = itemBack.ItemUrl;
    }

    _navmanager.NavigateTo($"/bloggar/blogpost/{url}");
}

protected async Task GetItems()
{
    listItem = await _repoListItem.GetSingle(EndPoints.ListItemEndpoint + $"GetByUrl?siteId=5&url={url}");

    if (listItem is not null)
    {
        listItems = await _repoListItem.Get(EndPoints.ListItemEndpoint + $"GetItemsByListPublished?Id={listItem.ListId}&q=5&rowno=1");
        allListItems = await _repoListItem.Get(EndPoints.ListItemEndpoint + $"GetItemsByListPublished?Id={listItem.ListId}&q=0&rowno=1");

        GetNextAndPrevious(allListItems);
        LoadHeader();

        // StateHasChanged();
    }
}

protected void LoadHeader()
{
    header.Title = $"{listItem.Title} | Kanal10.se";
    header.Description = listItem.Descr;
    header.Keywords = "kanal10, kanal 10, kristen, tv, live, gudstjänster, undervisning, dokumentärer, intervjuer, reportage, koncerter, barnprogram";
    header.TwitterSite = "@kanal10Sverige";
    header.Url = $"{_navmanager.Uri}";
    header.Image = Config.ImageUrl + "/" + listItem.ItemImage;

    headerState.SetHeader(this, header);
}

public void Dispose()
{
    headerState.StateChanged -= async (Source, Property) => await HeaderState_StateChanged(Source, Property);
}

protected void GetNextAndPrevious(List<ListItemModel> allListItems)
{
    int index = allListItems.FindIndex(x => x.ListItemId == listItem.ListItemId);
    int fI = 0;

    if (index >= 0)
    {
        fI = index + 1;
    }

    int fB = index - 1;

    if (index >= 0)
    {
        fB = index - 1;
    }
    else 
    { 
        fB = -1; 
    }

    if (fB == -1)
    {
        itemForward = null;
    }
    else
    {
        itemForward = allListItems[fB];
    }

    itemBack = allListItems[fI];
}

If anyone has any idea for what has changed in the two versions I'd be grateful.

This is how the App.razor looks like and this is where I've put the Headercomponent:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />

    <!-- Syncfusion -->
    <link href="_content/Syncfusion.Blazor.Themes/Bootstrap5.css" rel="stylesheet" />

    <!-- Owl Carousel -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.carousel.min.css">

    <!-- Telerik -->
    <script src="_content/Telerik.UI.for.Blazor/js/telerik-blazor.js" defer></script>
    <link rel="stylesheet" href="_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" />

    <!-- Allmänna -->
@*     <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
 *@
    <script src="https://kit.fontawesome.com/9995bafc85.js" crossorigin="anonymous"></script>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.carousel.min.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/[email protected]/css/flag-icons.min.css" />

    <!-- Egna -->
    @* <link rel="stylesheet" href="app.css" /> *@
    @* <link href="css/Carousel.css" rel="stylesheet" /> *@
    <link href="css/Custom.css" rel="stylesheet" />
    <link href="css/style.css" rel="stylesheet" />

    <!-- Inbyggda -->
    <link rel="stylesheet" href="Kanal10.Web.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />

    <HeaderComponent />
    <HeadOutlet />
</head>

<body>
    <Routes />
    <script>
        window.getDimensions = function () {
            return {
                width: window.innerWidth,
                height: window.innerHeight
            };
        };
    </script>
 

    <script src="_framework/blazor.web.js"></script>
    <script src="_content/Syncfusion.Blazor/scripts/syncfusion-blazor.min.js" type="text/javascript"></script>
    <script src="https://code.jquery.com/jquery-3.6.4.min.js" integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/owl.carousel.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
    <script src="js/main.js"></script>
    <script src="/JS/carousel.js"></script>

</body>

</html>


Solution

  • You haven't strictly done an upgrade. The closest Blazor Web App configuration to the Net7 Hosted WASM template has these settings:

    enter image description here

    You chose Per page/component which is the default until you've changed it to something else. You can verify this by checking the setting on the Route component in App.razor.

    Yours looks like this:

        <Routes />
    

    Which means the router is being rendered in Static SSR mode. Only individual pages within the MainLayout component that specify their render mode as InteractiveWebAssembly are being rendered in CSR mode.

    If you set App.razor like this then it should render correctly:

        <HeaderComponent  @rendermode="@InteractiveWebAssembly" />
        <HeadOutlet  @rendermode="@InteractiveWebAssembly" />
    
        <Routes @rendermode="@InteractiveWebAssembly" />
    

    You may also be able to set the Header info directly in HeadOutlet using <HeadContent> like this:

    <PageTitle>@title</PageTitle>
    
    <HeadContent>
        <meta name="description" content="@description">
    </HeadContent>
    

    See this similar question:

    Blazor Server / .NET 8 / Calling async code on initial load is freezing the screen until first async call is completed?

    And this answer and Github site that demonstrates how to get the render mode of a component:

    Blazor .Net 8 splash screen

    https://github.com/ShaunCurtis/Blazr.RenderState