When using Blazor cascading values, especially root-level,
the build-in mechanism will not notify other components of
changes in the value properties. This post showcases an
improved mechanism that leverages CascadingValueSource
and INotifyPropertyChanged
to automatically notify all
components.
As reported to the team the
problem is that the CascadingValueSource
doesn’t itself
invoke its NotifyChangedAsync
method so that components consuming the cascading value can
refresh their rendering. This is a problem when you change
properties of the shared object across components that are
otherwise not in the same hierarchy (parent-child where
the cascading value is provided by the parent).
So far, the team responded that the provided mechanism is sufficient. From the documentation on CascadingValue component:
Blazor Web Apps provide alternative approaches for cascading values
- Wrap the markup of the Routes component in a CascadingValue
and then:
In the App component (Components/App.razor), adopt an interactive render mode for the entire app
Hm, seems like quite the limitation not being able to use
InteractiveAuto
render mode. Luckily there’s another alternative:
Specify a root-level cascading value as a service by calling the AddCascadingValue extension method on the service collection builder.
But as mentioned in the bug report, this doesn’t work either.
This can be seen in action in a repro project as follows:
-
You have a regular DTO that will be your cascading value, such as:
public class User { public int Clicks { get; set; } }
-
You add the value as a root-cascading value via the app builder (both Server and Client projects, for
InteractiveAuto
support:builder.Services.AddCascadingValue(s => new CascadingValueSource<User>(new User(), isFixed: false));
-
Two otherwise unrelated components consume the cascading value as usual, such as:
@rendermode InteractiveAuto <h3>UserInfo</h3> <p role="status">User clicks: @User.Clicks</p> @code { [CascadingParameter] public required User User { get; set; } }
Another component, for example, makes changes to the value, either programmatically (e.g. a button click) or via bi-directional binding:
@rendermode InteractiveAuto <h3>Counter</h3> <p role="status">Current count: @User.Clicks</p> <button @onclick="IncrementCount">Increment</button> <p> <!-- binding to value directly --> <input type="number" title="Clicks" @bind-value:event="oninput" @bind-value="User.Clicks"> </p> @code { [CascadingParameter] public required User User { get; set; } void IncrementCount() => User.Clicks++; }
-
When using these two components in a single page, you will notice that clicking (or setting the input value directly) doesn’t cause the other component to refresh.
FWIW, I couldn’t make the wrapper
ComponentValue
and app-wide interactive server-side rendering (SSR) work across sibling components either.
Good old INotifyPropertyChanged to the rescue
According to the API documentation,
we can invoke NotifyChangedAsync
to notify “subscribers that the value has changed (for example, if it has been mutated).”, which seems just like what we need.
We can create our own CascadingValueSource
factory class that supports
INotifyPropertyChanged
and automatically invokes NotifyChangedAsync
when the value changes:
public static class CascadingValueSource
{
public static CascadingValueSource<T> CreateNotifying<T>(T value, bool isFixed = false) where T : INotifyPropertyChanged
{
var source = new CascadingValueSource<T>(value, isFixed);
value.PropertyChanged += (sender, args) => source.NotifyChangedAsync();
return source;
}
}
We can use it now in our app builder:
builder.Services.AddCascadingValue(s => CascadingValueSource.CreateNotifying(new User()));
And of course, we need to implement INotifyPropertyChanged
in our DTO:
public class User : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
int clicks;
public int Clicks
{
get => clicks;
set
{
if (clicks != value)
{
clicks = value;
OnPropertyChanged();
}
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = default)
=> PropertyChanged?.Invoke(this, new(propertyName));
}
And that’s it. No changes needed anywhere in any components. All components consuming the cascading value will now automatically refresh when the value changes, even if the components are not in the same hierarchy. This makes for a very convenient way to share state across components!
Hopefully this is something that will become built-in in the future, but in the meantime, this is a very simple workaround that can be used in any Blazor app.
Now for the running demo of the above code: first part shows the out of the box problem, with the clicking and binding only updating the top component on the page, but not the one below the separator line, which is another entirely different component. The second part shows the same components working as expected, with the cascading value automatically notifying all components of changes in the value, even a component that lives in the navbar as part of the layout.
Happy coding!