Reputation: 385
After much effort I finally tracked down the root cause of what I was experiencing. Whenever the RouteData is injected as a CascadingParameter into a Layout and then you navigate to a different Layout, Blazor will construct two new components and initialize them both before disposing of one of them. So any data fetched during OnInitializedAsync will be performed twice without any way of detecting it.
Does anyone have a workaround for this? I found this old ticket on their repo but looks like there isn't much activity. https://github.com/dotnet/aspnetcore/issues/20637
App.razor:
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<CascadingValue Value="@routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</CascadingValue>
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
MyLayout.razor:
@inherits LayoutComponentBase
@layout MainLayout
<a href="/someOtherPageWithoutThisLayout">Go Back</a>
@Body
@code {
[CascadingParameter] RouteData RouteData { get; set; }
}
The lifecycle of the destination component will look something like the following:
MyComponent constructed: HashCode=216708300
MyComponent OnInitializedAsync: HashCode=216708300
MyComponent constructed: HashCode=785069820
MyComponent OnInitializedAsync: HashCode=785069820
MyComponent disposing: HashCode=216708300
Upvotes: 5
Views: 1381
Reputation: 385
The following approach appears to make this issue go away. It relies on accessing the RouteData via the Body rather than injecting it.
protected override void OnInitialized()
{
if (Body?.Target is RouteView routeView)
{
var value = routeView.RouteData.RouteValues["routeParameterName"];
}
}
Edit 1: The above approach didn't work for nested layouts. The following sample handled those situations.
public static bool TryGetRouteData(this Delegate renderFragment, out RouteData routeData)
{
routeData = null;
if (renderFragment?.Target is RouteView routeView)
{
routeData = routeView.RouteData;
}
else if (renderFragment?.Target is object target)
{
// the target may be a CompilerGenerated class
var bodyField = target.GetType().GetField("bodyParam");
if (bodyField is not null)
{
var value = bodyField.GetValue(target);
if (value is Delegate childDel)
{
TryGetRouteData(childDel, out routeData);
}
}
}
return routeData is not null;
}
Then in the layout/component class:
protected override void OnInitialized()
{
if (Body?.TryGetRouteData(out var routeData) == true)
{
// stuff here
}
}
Upvotes: 3
Reputation: 30167
Answer changed as original contained the wrong information.
I think your problem lies here - I missed the added cascade when I first went thro' the code!
<CascadingValue Value="@routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</CascadingValue>
To cut a long story short, we aren't in control of Render process, the Renderer is. What I think is happening is the added cascade causes the Renderer to update the RenderTree (and the components in it) in the wrong order. You've created a bit of a short circuit. The Renderer adds ThingList
to ThingLayout
. It then replaces ThingLayout
and disposes it along with the initial instance of ThingList
.
If you need RouteData
in your components use a DI scoped service to pass it around.
There's no fix because it's not really a bug. It's edge condition behaviour: You're doing something unexpected.
Upvotes: 0