I am working on Blazor project (.NET 5). I got a problem with components rendering.
I have parent component with ChildContent
as RenderFragment
inside. And I use it like this:
<ParentComponent>
<ChildComponent1 Title="Component1"></ChildComponent1>
<ChildComponent2 Title="Component2" SampleEnum="SampleEnum.Bar"></ChildComponent2>
</ParentComponent>
Each ChildComponent
inherits ChildComponentBase
:
public class ChildComponent1 : ChildComponentBase
{
// some code
}
ChildComponentBase
contains ParentComponent
as cascading parameter and 2 parameters: one of them is string
(Immutable for Blazor Change Detection API) and another one is enum
(which is not Immutable) just for sake of example. And here we also
public partial class ChildComponentBase
{
[CascadingParameter]
public ParentComponent Parent { get; set; } = default !;
[Parameter]
public string? Title { get; set; } // Immutable
[Parameter]
public SampleEnum SampleEnum { get; set; } // not Immutable
}
In ParentComponent
I use a strategy of deferred rendering. Defer
component looks like this and being used in ParentComponent
:
// This is used to move its body rendering to the end of the render queue so we can collect
// the list of child components first.
public class Defer : ComponentBase
{
[Parameter]
public RenderFragment ChildContent { get; set; }
protected override void BuildRenderTree( RenderTreeBuilder builder )
{
builder.AddContent( 0, ChildContent );
}
}
In my project on first render I collect all ChildComponent
from ChildContent
like this:
ChildComponentBase.razor
@{
Parent.AddChild(this); // Parent is cascading parameter
}
And then I invoke a callback to process data. ParentComponent
looks like this:
ParentComponent.razor
<CascadingValue Value="this" IsFixed>
@{
StartCollectingChildren();
}
@ChildContent
<Defer>
@{
FinishCollectingChildren();
ProcessDataAsync();
}
@foreach (var o in _childComponents)
{
<p>@o.Title</p>
}
</Defer>
</CascadingValue>
ParentComponent.razor.cs
public partial class ParentComponent
{
[Parameter]
public RenderFragment ChildContent { get; set; }
private List<ChildComponentBase> _childComponents = new();
private bool _firstRender = true;
private bool _collectingChildren; // Children might re-render themselves arbitrarily. We only want to capture them at a defined time.
protected async Task ProcessDataAsync()
{
if (_firstRender)
{
//imitating re-render just like it would be an async call
await InvokeAsync(StateHasChanged);
_firstRender = false;
}
}
public void AddChild(ChildComponentBase child)
{
_childComponents.Add(child);
}
private void StartCollectingChildren()
{
_childComponents.Clear();
_collectingChildren = true;
}
private void FinishCollectingChildren()
{
_collectingChildren = false;
}
}
Due to invoke of callback - re-rendering happens. And due to re-rendering StartCollectingChildren()
is getting called again. This time on second render of ParentComponent
the ChildComponent1
doesn't re-render, because Blazor Change Detection API
skips it (because it contains only an Immutable parameter Title
while ChildComponent2
in addition contains enum
parameter).
Question: how to make this ChildComponent1
get re-rendered anyway?
I also added a Sample Project with code described above for you to try it out yourself.
I tried everything I could find in the google. The best workaround I found is to cache children collection on first render, but it looks dirty and could cause issues in a future.
CodePudding user response:
The quick fix to your problem is to modify the cascade and remove IsFixed
.
Once you do that any component that captures the cascade will always be rendered because this
is an object and therefore fails the equality check.
You can also drive render events on sub components that don't have object parameters using a Guid Cascade. Assign a new Guid to the mapped parameter whenever you want to force a render on any sub component the captures the cascade.