I have a project in which I frequently use OnAfterRender() to call methods on child components. As a simple example, suppose I have two Blazor components, Counter.razor and CounterParent.razor:
Counter:
#page "/counter"
<p role="status">Current count: #currentCount</p>
<button class="btn btn-primary" #onclick="IncrementCount">Click me</button>
#code {
private int currentCount = 0;
// This method is private out of the box,
// but let's change it to public for this example
public void IncrementCount()
{
currentCount++;
StateHasChanged();
}
}
CounterParent:
#page "/counterParent"
<h3>Counter Parent</h3>
<Counter #ref="counterChild"></Counter>
#code {
private Counter counterChild;
private bool loaded;
protected override void OnAfterRender(bool firstRender)
{
if (!loaded && counterChild != null)
{
counterChild.IncrementCount(); // Do some operation on the child component
loaded = true;
}
}
}
The parent component in this example causes the child component (the counter) to be incremented by 1.
However, I have recently been advised that this is not a good practice, and that
On OnAfterRender{async} usage, it should (in general) only be used to do JS interop stuff.
So if this is poor practice what is the best practice for calling methods on child components when a page loads? Why might someone choose to
actually either disable it on ComponentBase based components, , or don't implement it on [their] own components?
What I tried:
Code like the above works. It is definitely slow and the user often sees an ugly mess as things load. I probably won't fix this current project (works well enough for what I need it to), but if there's a better way I'd like to be better informed.
The core problem here is #ref="counterChild" , that reference will not yet be set in OnInitialized or (the first) OnParametersSet of the parent.
"it should only be used to do JS interop stuff" is too strict, it should be used for logic that needs the render to be completed. Making use of a component reference qualifies too.
I would use one of:
Put this action in OnInitialized() of the child component. (Assumes you always run the same action on a new child).
trigger it by a parameter on the Child component. That could a simple IsLoaded boolean. Take action in OnParametersSet() of the child.
Keep it in OnAfterRender() but use firstRender instead of loading.
That last one is exactly what you already have and it's not totally wrong. But it automatically means you get 2 renders, not the best U/X.
It's better to use OnParametersSet instead of OnAfterRender.
OnAfterRender is called after each rendering of the component. At this stage, the task of loading the component, receiving information and displaying them is finished. One of its uses is the initialization of JavaScript components that require the DOM to work; Like displaying a Bootstrap modal.
Note: any changes made to the field values in these events are not applied to the UI; Because they are in the final stage of UI rendering.
OnParameterSet called once when the component is initially loaded and again whenever the child component receives a new parameter from the parent component.
Separate out the data from the UI code and you get a state object and a component.
public class CounterState
{
public event EventHandler? CounterUpdated;
public int Counter { get; private set; }
// Contrived as Async as in real coding there may well be async calls to DbContexts or HttpClient
public async ValueTask IncrementCounterAsync(object sender)
{
await Task.Delay(150);
Counter = Counter + this._appConfiguration.IncrementRate;
CounterUpdated?.Invoke(sender, EventArgs.Empty);
}
}
#page "/countercomponent"
#implements IDisposable
<p role="status">Current count: #this.State.Counter</p>
<button class="btn btn-primary" #onclick="IncrementCount">Click me</button>
#code {
// Need to define how we get this and it's nullability
private CounterState State { get; set; }
protected override void OnInitialized()
=> this.State.CounterUpdated += this.OnCounterUpdated;
private void OnCounterUpdated(object? sender, EventArgs e)
{
if (sender != this)
StateHasChanged();
}
public async Task IncrementCount(object sender)
=> await this.State.IncrementCounterAsync(sender);
public void Dispose()
=> this.State.CounterUpdated -= this.OnCounterUpdated;
}
The parent:
#page "/counter"
<h3>Counter Parent</h3>
<SimpleCounter />
#code {
// Need to define how we get this and it's nullability
private CounterState State { get; set; }
protected async override Task OnInitializedAsync()
=> await this.State.IncrementCounterAsync(this);
}
The Scoped Service
The simplest implementation is to register CounterState as a scoped Service:
builder.Services.AddScoped<CounterState>();
And then inject it into our two components:
[Inject] private CounterState State { get; set; } = default!;
This maintains state within the SPA session. Navigate away and return and the old count is retained. However, this is often too wide a scope.
The Cascade
Cascading restricts the scope of the state object to component and sub-component scope.
Capture the cascaded value in the children:
[CascadingParameter] private CounterState State { get; set; } = default!;
How and what you cascade depends on the dependancies and disposal requirements of the state object.
With no commitments, simply create an instance and cascade it.
<CascadingValue Value="State" IsFixed>
//...
<SimpleCounter />
</CascadingValue>
//....
private CounterState State { get; set; } = new();
If you have DI dependancies then you need to create the state instance in the context of the DI container. With no disposal requirements you can set the scope to Transient and get a new instance by injection.
However, if the object requires disposal, you need to use ActivatorUtilities to create an instance outside the Service Container, but in the context of the container to populate dependancies.
#page "/counter"
#inject IServiceProvider ServiceProvider
#implements IDisposable
#implements IAsyncDisposable
<h3>Counter Parent</h3>
<CascadingValue Value="State" IsFixed>
<CounterComponent />
</CascadingValue>
#code {
private CounterState State { get; set; } = default!;
// Run before any render takes place, so the cascaded value is the correct instance
protected override void OnInitialized()
=> this.State = ActivatorUtilities.CreateInstance<CounterState>(ServiceProvider);
protected async override Task OnInitializedAsync()
=> await this.State.IncrementCounterAsync(this);
// Demonstrates how to implement Dispose when you don't know if your object implements IDisposable
public void Dispose()
{
if (this.State is IDisposable disposable)
disposable.Dispose();
}
// Demonstrates how to implement Dispose when you don't know if your object implements IAsyncDisposable
public async ValueTask DisposeAsync()
{
if (this.State is IAsyncDisposable disposable)
await disposable.DisposeAsync();
}
}
Note the cascade is set as IsFixed to ensure it doesn't cause RenderTree Cascades.
Saving a few CPU Cycles
If you don't use OnAfterRender you can short circuit it (and save running a few lines of code) like this.
#implements IHandleAfterRender
//...
Task IHandleAfterRender.OnAfterRenderAsync()
=> Task.CompletedTask;
Related
I have a page component and within it a breadcrumbs component and a business data component that displays all of the businesses for that page based on a category alias it is passed through the route.
Some of the breadcrumb links direct the user to another routable/page component and these work fine. However, some of them require a refresh of the business data component and it's these I'm having an issue with.
I've tried creating a service with a event that the breadcrumbs component can invoke when a link is clicked and the data component can subscribe to (following this post), but I can't get that to work as the return type is wrong when I try and call GetBusinesses as a result.
Here is the front end of my page component:
#if (breadcrumbs != null)
{
<Breadcrumbs Items="breadcrumbs"></Breadcrumbs>
}
<BrowseByBusiness Alias="#Alias"></BrowseByBusiness>
I set up my breadcrumbs like so (the Alias on the API call is a route parameter):
protected async override Task OnInitializedAsync()
{
categoryDto = await _publicClient.Client.GetFromJsonAsync<CategoryDto>($"api/Categories/GetCategoryByAlias/{Alias}");
breadcrumbs = new List<BreadcrumbItem>
{
new BreadcrumbItem("Browse By Category", "/browse-by-category"),
new BreadcrumbItem(categoryDto.Name, $"/browse-by-category/{categoryDto.Alias}"),
};
if (categoryDto.ParentCategory.Name != null)
{
breadcrumbs.Insert(1, new BreadcrumbItem(categoryDto.ParentCategory.Name, $"/browse-by-category/{categoryDto.ParentCategory.Alias}"));
}
}
My BrowseByBusiness component:
#foreach (var businessDto in businessesDto)
{
<Animate Animation="Animations.Fade" Easing="Easings.Ease" DurationMilliseconds="2000">
<BusinessCard businessDto="businessDto"></BusinessCard>
</Animate>
}
#code {
[Inject] IBusinessHttpRepository _businessRepo { get; set; }
public MetaData metaData = new MetaData();
IEnumerable<BusinessDto> businessesDto;
protected async override Task OnInitializedAsync()
{
await GetBusinesses();
}
private async Task GetBusinesses()
{
var pagingResponse = await _businessRepo.GetBusinessesByCategory(Alias, businessParameters);
businessesDto = pagingResponse.Items;
metaData = pagingResponse.MetaData;
}
}
My Breadcrumbs component:
<div class="flex-row border-bottom pb-2">
#foreach (var item in Items)
{
if (item.Equals(Last))
{
<span class="me-1">#item.Name</span>
}
else
{
<a class="me-1" href="#item.Url">#item.Name </a>
<span class="me-1">/</span>
}
}
</div>
#code {
[Parameter] public List<BreadcrumbItem> Items { get; set; }
private BreadcrumbItem Last;
protected async override Task OnInitializedAsync()
{
Last = Items.Last();
}
}
I'm using Blazor WASM and .net 6.
I recently had a similar need and ended up rolling my own (extremely simple) message broker. You can see full details in this blog post, but the short story is that you add a message broker class (which in my blog post has a silly name, but should really be called something like MessageBroker)...
public class SoupDragon {
public Action<Widget> NewWidget;
public void RaiseNewWidget(Widget w) =>
NewWidget?.Invoke(w);
}
You then register this as a service, inject it into the component that is to send the message, and use it follows...
_soupDragon.RaiseNewWidget(newWidget);
The component that is to pick up the message would inject a soup dragon, and hook up a method to catch the message. Whilst you can do this with a lambda, for reasons explained in more detail over there, you are best off hooking up to a method...
_soupDragon.NewWidget += HandleNewWidget;
One nice thing about this approach is that if you're doing server-side Blazor, you get communication between different users for free by making the message broker static.
I'm a bit stumped here because I do have the parameter attribute applied. I seem to be simply following the documentation in a one to one fashion.
Error message
Unhandled exception rendering component: Object of type 'Onero.Client.Features.CourseManager.CourseModelRegister' has a property matching the name 'HighSchoolRegistrationModelId', but it does not have [ParameterAttribute] applied.
Component hierarchy
//parent
<CascadingValue Value="HighSchoolRegistrationModelId">
<CourseModelAddForm></CourseModelAddForm>
</CascadingValue>
#code {
public long HighSchoolRegistrationModelId;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
HighSchoolRegistrationModelId = await CourseService.GetHighSchool();
}
}
//middle component
#if (add)
{
<CourseModelRegister HighSchoolRegistrationModelId="#HighSchoolRegistrationModelId">
</CourseModelRegister>
}
#code {
[CascadingParameter]
protected long HighSchoolRegistrationModelId { get; set; }
private bool add = false;
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
}
private void ShowAddForm()
{
add = true;
}
}
//Grandchild where error occurs
#code {
[CascadingParameter]
protected long HighSchoolRegistrationModelId { get; set; }
// rest of code omitted
}
Am I missing something? Perhaps this has to do with lifecycle management? I tried using OnInitialized as well as OnParametersSet.
I am pretty I have found the issue, I had the exact same situation, and its because I was passing value to the component's parameter and it was define as a [CascadingParameter], so it created a conflict.
So on this line:
<CourseModelRegister HighSchoolRegistrationModelId="#HighSchoolRegistrationModelId">
</CourseModelRegister>
You don't need to pass HighSchoolRegistrationModelId a value, because it will be filled by the [CascadingParameter]. In your case, you might need to make sure your middle component also has a [CascadingParameter] for the value to be passed to its children.
I have these two components in my Blazor app:
Component1.razor:
<CascadingValue Value=this>
<Component2/>
</CascadingValue>
<p>#DisplayedText</p>
#code {
public string DisplayedText = string.Empty;
}
Component2.razor:
<button #onclick=#(e => { C1.DisplayedText = "testing"; })>Set DisplayedText</button>
#code {
[CascadingParameter]
public Component1 C1 { get; set; }
}
When I click the "Set DisplayedText" button, the text in the p element in Component1 should change to testing, but it does not. How can I fix this?
Merlin04, the following code snippet demonstrate how you can do it. Note that this is really a very simple sample, but it shows how you should code when communication between distant components is required.
Here's the code, copy and run it, and if you have more questions don't hesitate to ask.
MessageService.cs
public class MessageService
{
private string message;
public string Message
{
get => message;
set
{
if (message != value)
{
message = value;
if (Notify != null)
{
Notify?.Invoke();
}
}
}
}
public event Action Notify;
}
Note: The service is a normal class... It provides services to other objects, and it should be added to the DI container in Startup.ConfigureServices method to make it available to requesting clients. Add this: to the ConfigureServices method:
services.AddScoped<MessageService>();
Note: As you can see I define an event delegate of the Action type, which is invoked from the property's set accessor, when the user type text into a text box in Component3. Triggering this delegate causes the text entered by Components3 to be displayed in the Index component which is the parent of Component2 (see code below).
Index.razor
#page "/"
#inject MessageService MessageService
#implements IDisposable
<p>I'm the parent of Component2. I've got a message from my grand child:
#MessageService.Message</p>
<Component2 />
#code {
protected override void OnInitialized()
{
MessageService.Notify += OnNotify;
}
public void OnNotify()
{
InvokeAsync(() =>
{
StateHasChanged();
});
}
public void Dispose()
{
MessageService.Notify -= OnNotify;
}
}
Note that we directly bind to the MessageService.Message property, but the StateHasChanged method must be called to refresh the display of the text.
Component2.razor
<h3>Component2: I'm the parent of component three</h3>
<Component3/>
#code {
}
Component3.razor
#inject MessageService MessageService
<p>This is component3. Please type a message to my grand parent</p>
<input placeholder="Type a message to grandpa..." type="text"
#bind="#MessageService.Message" #bind:event="oninput" />
Note that in Component3 we bind the MessageService.Message to a text box, and the binding occurs each time you press a key board( input event versus change
event).
That is all, hope this helps, and don't hesitate to ask any question.
#Merlin04, this is an abuse and misuse of the cascading value feature. Ordinarily, a parent component communicates with its child via component parameters.
You can't update a cascaded value from a descendant.
Wrong...
The following code snippet demonstrate a better solution, based on what you do, though it is not optimal because your code and mine as well update a property from a method, when in matter of fact, we should changed the property's value directly, and not through a mediator code (that is a method)
Component2.razor
<button #onclick="#(() => SetDisplayedText.InvokeAsync("testing"))">Set
DisplayedText</button>
#code {
[Parameter]
public EventCallback<string> SetDisplayedText { get; set; }
}
Component1.razor
<Component2 SetDisplayedText="#SetDisplayedText"/>
<p>#DisplayedText</p>
#code {
private string DisplayedText = string.Empty;
public void SetDisplayedText(string newText)
{
DisplayedText = newText;
}
}
Note that calling the StateHasChanged method is not necessary, as this is the bonus you get when using the EventCallback 'delegate'
Hope this helps...
See enet's answer instead of this
You can't update a cascaded value from a descendant.
Instead, make a method in the parent (Component1) to set the value:
public void SetDisplayedText(string newText) {
DisplayedText = newText;
StateHasChanged();
}
Then, you can call that in the descendant:
<button #onclick=#(e => { C1.SetDisplayedText("testing"); })>Set DisplayedText</button>
Let's say I want to know when all my component are loaded so that I can do X.
In my MainLayout.razor I'have for example this
#inject MyService;
#Body
bool AllComponentsAreLoaded { get; set; }
protected override async Task OnInitializedAsync()
{
AllComponentsAreLoaded = false;
}
protected override async Task OnAfterRenderAsync(bool firstrender)
{
AllComponentsAreLoaded = true;
if (AllComponentsAreLoaded)
{
// Nice I can start X
}
}
This code will work but my problem is that there are components in #Body that aren't loaded.
The OnAfterRenderAsync will fire but it will do so before all components are done rendering.
How can I know that all components are done rendering?
To do this, you can use a 'NotifierService' that you inject into the startup that each component can call (example in the below link).
https://learn.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.1
For example, each component would send a notification to this service, that can let it know if it was loaded or not. The main 'Body' would then be notified when it was completed (you'd have to implement your own logic here).
For demonstration purposes let's say I have a class called StateManager:
public class StateManager
{
public StateManager()
{
IsRunning = false;
}
public void Initialize()
{
Id = Guid.NewGuid().ToString();
IsRunning = true;
KeepSession();
}
public void Dispose()
{
Id = null;
IsRunning = false;
}
public string Id { get; private set; }
public bool IsRunning { get; private set; }
private async void KeepSession()
{
while(IsRunning)
{
Console.WriteLine($"{Id} checking in...");
await Task.Delay(5000);
}
}
}
It has a method that runs after it is initiated that writes it's Id to the console every 5 seconds.
In my Startup class I add it as a Scoped service:
services.AddScoped<StateManager>();
Maybe I am using the wrong location but in my MainLayout.razor file I am initializing it on OnInitializedAsync()
#inject Models.StateManager StateManager
...
#code{
protected override async Task OnInitializedAsync()
{
StateManager.Initialize();
}
}
When running the application after it renders the first page the console output is showing that there are 2 instances running:
bcf76a96-e343-4186-bda8-f7622f18fb27 checking in...
e5c9824b-8c93-45e7-a5c3-6498b19ed647 checking in...
If I run Dispose() on the object it ends the KeepSession() while loop on one of the instances but the other keeps running. If I run Initialize() a new instance appears and every time I run Initialize() new instances are generated and they are all writing to the console with their unique id's. I am able to create as many as I want without limit.
I thought injecting a Scoped<> service into the DI guaranteed a single instance of that object per circuit? I also tried initializing within the OnAfterRender() override in case the pre-rendering process was creating dual instances (although this does not explain why I can create so many within a page that has the service injected).
Is there something I am not handling properly? Is there a better location to initialize the StateManager aside from MainLayout?
I also tried initializing within the OnAfterRender() override in case the pre-rendering process was creating dual instances
It is caused by pre-rendering & the StateManager is not disposed.
But you cannot avoid it by putting the initialization within OnAfterRender(). An easy way is to use the RenderMode.Server instead.
<app>
#(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
#(await Html.RenderComponentAsync<App>(RenderMode.Server))
</app>
Since your StateManager requires a knowledge on StateManagerEx, let's firstly take a dummy StateManagerEx as an example, which is easier than your scenario:
public class StateManagerEx
{
public StateManagerEx()
{
this.Id = Guid.NewGuid().ToString();
}
public string Id { get; private set; }
}
When you render it in Layout in RenderMode.Server Mode:
<p> #StateManagerEx.Id </p>
You'll get the Id only once. However, if you render it in RenderMode.ServerPrerendered mode, you'll find that:
When browser sends a request to server ( but before Blazor connection has been established), the server pre-renders the App and returns a HTTP response. This is the first time the StateManagerEx is created.
And then after the Blazor connection is established, another StateManagerEx is created.
I create a screen recording and increase the duration of each frame by +100ms, you can see that its behavior is exactly the same as what we describe above (The Id gets changed):
The same goes for the StateManager. When you render in ServerPrerendered mode, there will be two StateManager, one is created before the Blazor connection has been established, and the other one resides in the circuit. So you'll see two instances running.
If I run Initialize() a new instance appears and every time I run Initialize() new instances are generated and they are all writing to the console with their unique id's.
Whenever you run Initialize(), a new Guid is created. However, the StateManager instance keeps the same ( while StateManager.Id is changed by Initialize()).
Is there something I am not handling properly?
Your StateManager did not implements the IDisposable. If I change the class as below:
public class StateManager : IDisposable
{
...
}
even if I render the App in ServerPrerendered mode, there's only one 91238a28-9332-4860-b466-a30f8afa5173 checking in... per connection at the same time: