1
votes

I have a couple components that I want to share throughout my Blazor app. These happen to be SyncFusion components - one is an SfToast and one is an SfDialog. I thought an easy way to do this would be to put the components on MainLayout.razor and then use a <CascadingValue> for each to pass the reference along to all child pages and components.

This works fine as long as navigation to a page occurs via a <NavLink> element or using NavigationManager.NavigateTo(). However, if a page is deep linked or refreshed, the outermost <CascadingValue> becomes null. To work around this I have created an additional dummy <CascadingValue> as the outermost which ensures that the values I truly care about are populated on a refresh or direct link, but this feels like a hack. I'd like to know if there is something inherently wrong with the way I am doing this that is causing the outermost <CascadingValue> to become null on a refresh.

Below is some sample code to illustrate the problem. This is very contrived, but it's the only way I could figure out to create a minimal reproducible example that shows the issue.

If you run the project and click on the "Go to Sample Page" button, you will see that both CompOne and CompTwo component references are set to values, as expected. However, if you then refresh the page you will see that the CompOne reference (the outermost <CascadingValue>) is now null.

The layout of the project is as follows (created from the default Blazor Server template so I've only shown the areas where I made modifications):

+ BlazorSample
| + Components (I added this folder)
  | - ComponentOne.razor
  | - ComponentTwo.razor
| + Pages
  | - Index.razor
  | - SamplePage.razor
| + Shared
  | - MainLayout.razor

MainLayout.razor

@inherits LayoutComponentBase

<div class="sidebar">
    <NavMenu />
</div>

<div class="main">
    <div class="top-row px-4">
        <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
    </div>

    <div class="content px-4">
        <CascadingValue Name="CompOne" Value="ComponentOne">
            <CascadingValue Name="CompTwo" Value="ComponentTwo">
                @Body
            </CascadingValue>
        </CascadingValue>
    </div>

    <BlazorSample.Components.ComponentOne @ref="ComponentOne"></BlazorSample.Components.ComponentOne>
    <BlazorSample.Components.ComponentTwo @ref="ComponentTwo"></BlazorSample.Components.ComponentTwo>
</div>

@code{ 
    public BlazorSample.Components.ComponentOne ComponentOne;
    public BlazorSample.Components.ComponentTwo ComponentTwo;
}

ComponentOne.razor

@code {
    // this would normally contain something useful
    public string ThisIsNotUseful = "This is just a sample";
}

ComponentTwo.razor

@code {
    // this would normally contain something useful
    public string ThisIsNotUsefulEither = "This is just a sample";
}

Index.razor

@page "/"
@inject NavigationManager NavigationManager

<button class="btn btn-primary"
        @onclick="@(() => NavigationManager.NavigateTo("/samplepage"))">
    Go to Sample Page
</button>

SamplePage.razor

@page "/samplepage"

<div class="row">
    <div class="col-12">
        CompOne is null: @(CompOne == null)
    </div>
</div>
<div class="row">
    <div class="col-12">
        CompTwo is null: @(CompTwo == null)
    </div>
</div>

@code{
    [CascadingParameter(Name = "CompOne")]
    public BlazorSample.Components.ComponentOne CompOne { get; set; }

    [CascadingParameter(Name = "CompTwo")]
    public BlazorSample.Components.ComponentTwo CompTwo { get; set; }
}
2
Refs are fixed up after the first render, so they'll be null to start with. - Peter Morris
@PeterMorris Not sure I'm following. They aren't null if the route is accessed via NavigationManager.NavigateTo("/samplepage") - only on a refresh or a direct link. Even then it's only the outermost <CascadingValue> that is null. I guess my solution of wrapping another <CascadingValue> around the outside is the only fix then? - Lex
Both this question and the original appear to be linked to syncfusion. I'll add that as a tag. - Henk Holterman
I expect you interact with the page to execute NavigateTo. That interaction will cause a rerender. Have a bool hasRendered. In OnAfterRender, if it's false then set it to true and call StateHasChanged. Only render what is inside the cascaded values if hasRendered is true - Peter Morris
@PeterMorris Brilliant! Add this as an answer and I will gladly accept it. - Lex

2 Answers

5
votes

Cause

Just as referenced HTML elements are not linked until after a render, referenced components aren't either. This is because the component is created as part of BuildRenderTree, at which point the reference is linked up, like so.

<SurveyPrompt @ref=MySurveyPrompt Title="How is Blazor working for you?" />

@code

{
    SurveyPrompt MySurveyPrompt;
}

Will transpile to the followint C# (look in obj\Debug\net5.0\Razor\Pages)

__builder.OpenComponent<BlazorApp58.Shared.SurveyPrompt>(1);
__builder.AddAttribute(2, "Title", "How is Blazor working for you?");

// This is the line that captures the reference
__builder.AddComponentReferenceCapture(3, (__value) => {
      MySurveyPrompt = (BlazorApp58.Shared.SurveyPrompt)__value;
  });
  __builder.CloseComponent();

So, the first time you render (i.e. direct navigation) you do not have the reference during the render (which is when CascadingValue is rendered) - but when you click a button or something to cause NavigationManager.NavigateTo Blazor will automatically re-render to check for changes. At this point, you now have references to be rendered.

Solution

  • First, in your layout add a field or property bool HasRendered.
  • Then in your UI, make sure the first render (before you have references) doesn't render any child content other than the components you are referencing.
@if (HasRendered)
{
  <CascadingValue Value=@ComponentOne>
    <CascadingValue Value=@ComponentTwo>
      ... your markup here ...
    </CascadingValue>
  </CascadingValue>
}
<ComponentOne ...../>
<ComponentTwo ...../>
  • Then, after the first render has fixed up your component references, allow full rendering of the consumer markup and tell Blazor to re-render.

In OnAfterPaint do the following

if (firstRender)
{
  HasRendered = true;
  StateHasChanged();
}

Additional

Because the values of the CascadingValue components won't change whilst any consumers are using them you can also add IsFixed=True, which will improve render performance.

0
votes

The cascading parameters execution is from downwards, with the page refresh the parent component instance is not considered hence it is null. Check the below link for reference.

Parent instance: Blazor component : refresh parent when model is updated from child component