Skip to content

Commit aa51973

Browse files
authored
Cascading values+params for state management (#34779)
1 parent 003dc1f commit aa51973

File tree

2 files changed

+199
-2
lines changed

2 files changed

+199
-2
lines changed

aspnetcore/blazor/components/cascading-values-and-parameters.md

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ The following `Daleks` component displays the cascaded values.
7575

7676
:::moniker range=">= aspnetcore-8.0"
7777

78-
In the following example, `Dalek` is registered as a cascading value using [`CascadingValueSource<T>`](xref:Microsoft.AspNetCore.Components.CascadingValueSource%601), where `<T>` is the type. The `isFixed` flag indicates whether the value is fixed. If false, all recipients are subscribed for update notifications, which are issued by calling <xref:Microsoft.AspNetCore.Components.CascadingValueSource%601.NotifyChangedAsync%2A>. Subscriptions create overhead and reduce performance, so set `isFixed` to `true` if the value doesn't change.
78+
In the following example, `Dalek` is registered as a cascading value using [`CascadingValueSource<T>`](xref:Microsoft.AspNetCore.Components.CascadingValueSource%601), where `<T>` is the type. The `isFixed` flag indicates whether the value is fixed. If `false`, all recipients are subscribed for update notifications. Subscriptions create overhead and reduce performance, so set `isFixed` to `true` if the value doesn't change.
7979

8080
```csharp
8181
builder.Services.AddCascadingValue(sp =>
@@ -94,6 +94,191 @@ builder.Services.AddCascadingValue(sp =>
9494
>
9595
> Avoid using <xref:Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue%2A> to register a component type as a cascading value. Instead, wrap the `<Router>...</Router>` in the `Routes` component (`Components/Routes.razor`) with the component and adopt global interactive server-side rendering (interactive SSR). For an example, see the [`CascadingValue` component](#cascadingvalue-component) section.
9696
97+
Calling <xref:Microsoft.AspNetCore.Components.CascadingValueSource%601.NotifyChangedAsync%2A> to issue update notifications can be used to signal multiple Razor component subscribers that a cascading value has changed. Notifications aren't possible for subscribers that adopt static server-side rendering (static SSR), so subscribers must adopt an interactive render mode.
98+
99+
In the following example:
100+
101+
* `NotifyingDalek` implements <xref:System.ComponentModel.INotifyPropertyChanged> to notify clients that a property value has changed. When the `Units` property is set, the <xref:System.ComponentModel.PropertyChangedEventHandler> (`PropertyChanged`) is invoked.
102+
* The `SetUnitsToOneThousandAsync` method can be triggered by subscribers to set `Units` to 1,000 with a simulated processing delay.
103+
104+
Keep in mind for production code that any change in state (any property value change of the class) causes all subscribed components to rerender, regardless of which part of the state they use. We recommend creating granular classes, cascading them separately with specific subscriptions to ensure that only components subscribed to a specific portion of the application state are affected by changes.
105+
106+
`NotifyingDalek.cs`:
107+
108+
```csharp
109+
using System.ComponentModel;
110+
using System.Runtime.CompilerServices;
111+
112+
public class NotifyingDalek : INotifyPropertyChanged
113+
{
114+
public event PropertyChangedEventHandler? PropertyChanged;
115+
private int units;
116+
117+
public int Units
118+
{
119+
get => units;
120+
set
121+
{
122+
if (units != value)
123+
{
124+
units = value;
125+
OnPropertyChanged();
126+
}
127+
}
128+
}
129+
130+
protected virtual void OnPropertyChanged(
131+
[CallerMemberName] string? propertyName = default)
132+
=> PropertyChanged?.Invoke(this, new(propertyName));
133+
134+
public async Task SetUnitsToOneThousandAsync()
135+
{
136+
// Simulate a three second delay in processing
137+
await Task.Delay(3000);
138+
139+
Units = 1000;
140+
}
141+
}
142+
```
143+
144+
The following `CascadingStateServiceCollectionExtensions` creates a <xref:Microsoft.AspNetCore.Components.CascadingValueSource%601> from a type that implements <xref:System.ComponentModel.INotifyPropertyChanged>.
145+
146+
`CascadingStateServiceCollectionExtensions.cs`:
147+
148+
```csharp
149+
using System.ComponentModel;
150+
using Microsoft.AspNetCore.Components;
151+
152+
namespace Microsoft.Extensions.DependencyInjection;
153+
154+
public static class CascadingStateServiceCollectionExtensions
155+
{
156+
public static IServiceCollection AddNotifyingCascadingValue<T>(
157+
this IServiceCollection services, T state, bool isFixed = false)
158+
where T : INotifyPropertyChanged
159+
{
160+
return services.AddCascadingValue<T>(sp =>
161+
{
162+
return new CascadingStateValueSource<T>(state, isFixed);
163+
});
164+
}
165+
166+
private sealed class CascadingStateValueSource<T>
167+
: CascadingValueSource<T>, IDisposable where T : INotifyPropertyChanged
168+
{
169+
private readonly T state;
170+
private readonly CascadingValueSource<T> source;
171+
172+
public CascadingStateValueSource(T state, bool isFixed = false)
173+
: base(state, isFixed = false)
174+
{
175+
this.state = state;
176+
source = new CascadingValueSource<T>(state, isFixed);
177+
this.state.PropertyChanged += HandlePropertyChanged;
178+
}
179+
180+
private void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e)
181+
{
182+
_ = NotifyChangedAsync();
183+
}
184+
185+
public void Dispose()
186+
{
187+
state.PropertyChanged -= HandlePropertyChanged;
188+
}
189+
}
190+
}
191+
```
192+
193+
The type's <xref:System.ComponentModel.PropertyChangedEventHandler> (`HandlePropertyChanged`) calls the <xref:Microsoft.AspNetCore.Components.CascadingValueSource%601>'s <xref:Microsoft.AspNetCore.Components.CascadingValueSource%601.NotifyChangedAsync%2A> method to notify subscribers that the cascading value has changed. The <xref:System.Threading.Tasks.Task> is discarded when calling <xref:Microsoft.AspNetCore.Components.CascadingValueSource%601.NotifyChangedAsync%2A> because the call only represents the duration of the dispatch to the synchronous context. Exceptions are handled internally by dispatching them to the renderer within the context of whichever component threw when receiving the update. This is the same way that exceptions are processed with a <xref:Microsoft.AspNetCore.Components.CascadingValue%601>, which isn't notified about exceptions that happen inside notification recipients. The event handler is disconnected in the `Dispose` method to prevent a memory leak.
194+
195+
In the `Program` file&dagger;, `NotifyingDalek` is passed to create a <xref:Microsoft.AspNetCore.Components.CascadingValueSource%601> with an initial `Unit` value of 888 units:
196+
197+
```csharp
198+
builder.Services.AddNotifyingCascadingValue(new NotifyingDalek() { Units = 888 });
199+
```
200+
201+
> [!NOTE]
202+
> &dagger;For Blazor Web App solutions consisting of server and client (`.Client`) projects:
203+
>
204+
> * The preceding `NotifyingDalek.cs` and `CascadingStateServiceCollectionExtensions.cs` files are placed in the `.Client` project.
205+
> * The preceding code is placed into each project's `Program` file.
206+
207+
The following component is used to demonstrate how changing the value of `NotifyingDalek.Units` notifies subscribers.
208+
209+
`Daleks.razor`:
210+
211+
```razor
212+
<h2>Daleks component</h2>
213+
214+
<div>
215+
<b>Dalek Units:</b> @Dalek?.Units
216+
</div>
217+
218+
<div>
219+
<label>
220+
<span style="font-weight:bold">New Unit Count:</span>
221+
<input @bind="dalekCount" />
222+
</label>
223+
<button @onclick="Update">Update</button>
224+
</div>
225+
226+
<div>
227+
<button @onclick="SetOneThousandUnits">Set Units to 1,000</button>
228+
</div>
229+
230+
<p>
231+
Dalek© <a href="https://www.imdb.com/name/nm0622334/">Terry Nation</a><br>
232+
Doctor Who© <a href="https://www.bbc.co.uk/programmes/b006q2x0">BBC</a>
233+
</p>
234+
235+
@code {
236+
private int dalekCount;
237+
238+
[CascadingParameter]
239+
public NotifyingDalek? Dalek { get; set; }
240+
241+
private void Update()
242+
{
243+
if (Dalek is not null)
244+
{
245+
Dalek.Units = dalekCount;
246+
dalekCount = 0;
247+
}
248+
}
249+
250+
private async Task SetOneThousandUnits()
251+
{
252+
if (Dalek is not null)
253+
{
254+
await Dalek.SetUnitsToOneThousandAsync();
255+
}
256+
}
257+
}
258+
```
259+
260+
To demonstrate multiple subscriber notifications, the following `DaleksMain` component renders three `Daleks` components. When the unit count (`Units`) of one `Dalek` component is updated, the other two `Dalek` component subscribers are updated.
261+
262+
`DaleksMain.razor`:
263+
264+
```razor
265+
@page "/daleks-main"
266+
267+
<PageTitle>Daleks Main</PageTitle>
268+
269+
<h1>Daleks Main</h1>
270+
271+
<Daleks />
272+
273+
<Daleks />
274+
275+
<Daleks />
276+
```
277+
278+
Because the <xref:Microsoft.AspNetCore.Components.CascadingValueSource%601>'s type in this example (`NotifyingDalek`) is a class type, you can meet virtually any state management feature specification requirement. However, subscriptions create overhead and reduce performance, so benchmark the performance of this approach in your app and compare it to other [state management approaches](xref:blazor/state-management) before adopting it in a production app with constrained processing and memory resources.
279+
280+
Any change in state (any property value change of the class) causes all subscribed components to rerender, regardless of which part of the state they use. **Avoid creating a single large class representing the entire global application state.** Instead, create granular classes and cascade them separately with specific subscriptions to cascading parameters, ensuring that only components subscribed to a specific portion of the application state are affected by changes.
281+
97282
:::moniker-end
98283

99284
## `CascadingValue` component

aspnetcore/blazor/state-management.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Common locations exist for persisting state:
6161
* [URL](#url-server)
6262
* [Browser storage](#browser-storage-server)
6363
* [In-memory state container service](#in-memory-state-container-service)
64+
* [Cascading values and parameters](#cascading-values-and-parameters)
6465

6566
<h2 id="server-side-storage-server">Server-side storage</h2>
6667

@@ -620,7 +621,8 @@ Common locations exist for persisting state:
620621
* [Server-side storage](#server-side-storage-wasm)
621622
* [URL](#url-wasm)
622623
* [Browser storage](#browser-storage-wasm)
623-
* [In-memory state container service](#in-memory-state-container-service)
624+
* [In-memory state container service](#in-memory-state-container-service)
625+
* [Cascading values and parameters](#cascading-values-and-parameters)
624626

625627
<h2 id="server-side-storage-wasm">Server-side storage</h2>
626628

@@ -805,6 +807,16 @@ services.AddScoped<StateContainer>();
805807

806808
The preceding components implement <xref:System.IDisposable>, and the `OnChange` delegates are unsubscribed in the `Dispose` methods, which are called by the framework when the components are disposed. For more information, see <xref:blazor/components/lifecycle#component-disposal-with-idisposable-and-iasyncdisposable>.
807809

810+
## Cascading values and parameters
811+
812+
Use [cascading values and parameters](xref:blazor/components/cascading-values-and-parameters) to manage state by flowing data from an ancestor Razor component to descendent components.
813+
814+
:::moniker range=">= aspnetcore-8.0"
815+
816+
Root-level cascading values with a <xref:Microsoft.AspNetCore.Components.CascadingValueSource%601> permit Razor component subscriber notifications of changed cascading values. For more information and a working example, see the `NotifyingDalek` example in <xref:blazor/components/cascading-values-and-parameters#root-level-cascading-values>.
817+
818+
:::moniker-end
819+
808820
## Additional approaches
809821

810822
When implementing custom state storage, a useful approach is to adopt [cascading values and parameters](xref:blazor/components/cascading-values-and-parameters):

0 commit comments

Comments
 (0)