I am developing a Blazor server app. One of the classes is Employee.cs
which is injected at Startup.cs
Employee.cs
public class Employee
{
public string Name { get; set; } = "James";
}
Startup.cs
...
...
services.AddSingleton<Employee>();
To use this injected instance, I wrote a simple component Welcome.razor
Welcome.razor
@inject Employee Emp
<h1>Welcome [email protected]</h1>
The Welcome
component is a permanent part of the UI, and is displayed at the top of the page.
I created a razor page ChangeName.razor
that also displays the Name
from the injected instance, and a button to alter the value. When the button is clicked, the name is changed within the page containing the button, but has no effect on the Welcome.razor
even after calling StateHasChanged()
ChangeName.razor
@page "/"
@inject Employee Emp
<button @onclick="@ChangeName">Change Name</button>
<div>Employee name in page is: @Emp.Name</div>
@code {
void ChangeName() {
Emp.Name = "Mike";
StateHasChanged(); // has no effect on the Welcome.razor content
}
}
Once the button is clicked, the name in Welcome.razor
stays as James
but the name in ChangeName.razor
page is changed to Mike
. Why aren’t they synced?
Here is what I did as a work-around:
Welcome.razor
@code {
static Welcome _welcome;
protected override void OnInitialized()
{
base.OnInitialized();
_welcome = this; // grab a reference to the current instance
// and assign it to the static instance
}
public Refresh() {
_welcome.StateHasChanged(); // this can be called from any other component now
}
}
Now I can just call Welcome.Refresh()
from anywhere and the name will change. But this solution is ugly and there must be something wrong that I am doing..
What can I do to automatically trigger StateHasChanged()
for any component that is displaying a shared object?
[Polite] You are making several fundamental mistakes.
-
Read the Blazor Service Scope documentation –
Transient
is one instance per DI service provider request. The service needs to beScoped
to be a shared instance across the SPA session. -
Don’t tightly couple components by passing around references. You didn’t create the instances and aren’t in control of their lifecycles. You can end up with a reference a stale component that’s no longer part of the RenderTree and has been disposed.
-
Use the Notification pattern – a service or cascaded object with setters and one or more change events – to communicate between distant components i.e. where you can’t use parameters and Event Callbacks.
-
There are very few use cases where you need to call
StateHasChanged
. The primary one is in an Event Handler for a notification as outlined in 3. Outside that, if you’re resorting toStateHasChanged
to try and update the UI, you’re in trouble. Treat the cause not the symptom. Fix the logic.
Links to other questions and answers on the Notification Pattern – Search SO for “Blazor Notification Pattern” for more.
How can I trigger/refresh my main .RAZOR page from all of its sub-components within that main .RAZOR page when an API call is complete?
Passing shared parameter to blazorcomponents
Can a Blazor element access data in a peer element?
I need to update state of blazor component when other component changed
Using the Notification Pattern
You need to implement the Notification pattern.
Yes Refresh()
is not just ugly, it’s horrible. It just makes a protected
method public
. It’s protected
for some very good reasons. Respect the design decision.
The project template for this code is Net8.0 “Blazor Web App” with either InteractiveServer or InteractiveWebAssembly and Global interactivity options selected.
public record Employee(string Name);
public class EmployeeService
{
public Employee Employee { get; private set; } = new("Fred");
public event EventHandler<Employee>? EmployeeChanged;
public void UpdateEmployeee(Employee employee)
{
this.Employee = employee;
this.EmployeeChanged?.Invoke(this, employee);
}
}
Registered:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddScoped<EmployeeService>();
var app = builder.Build();
Welcome:
@inject EmployeeService EmployeeService
<h1>Welcome @EmployeeService.Employee.Name</h1>
@code {
protected override void OnInitialized()
=> this.EmployeeService.EmployeeChanged += this.OnEmployeeChanged;
private void OnEmployeeChanged(object? sender, Employee employee)
=> this.InvokeAsync(StateHasChanged);
public void Dispose()
=> this.EmployeeService.EmployeeChanged -= this.OnEmployeeChanged;
}
Demo Page:
@page "/"
@inject EmployeeService EmployeeService
<PageTitle>Home</PageTitle>
<Welcome />
<div>
<button class="btn btn-primary" @onclick="this.ChangeEmployee">Change Employee</button>
</div>
@code {
private Employee _fred = new("Fred");
private Employee _shaun = new("Shaun");
private void ChangeEmployee()
{
if (this.EmployeeService.Employee == _fred)
this.EmployeeService.UpdateEmployeee(_shaun);
else
this.EmployeeService.UpdateEmployeee(_fred);
}
}
I’ve used records [I believe in mutation control], but you can switch to classes if you prefer.
Your work-around shouldn’t work (because of AddTransient).
The
Employee
is transient, so for every@inject Employee Emp
you have a new instance… btw you might appreciate ` <SectionOutlet` that was added in .NET8.@ℍℍ I changed
Transient
toScoped
andSingleton
but got the same result@Alamakanambra I updated my question, I used
Singleton
and yet the same issue still happensMy other guess is that you crossing Interactivity boundaries. Blazor initialize its own set of services for SSR and for Blazor Server. When the Change name and Welcome are in different interactivity modes, they won’t have the same instance even if it’s a singleton. Check if the Employee is the same instance (assign random Id to it in default ctor and display this id)..