# CascadingValueProvider
## Description
Replaces a stack of nested CascadingValue components with one flat list. Each value is cascaded by its type or by a name, a later value shadows an earlier one of the same type and name, and changing a value refreshes only the components that consume it.
## Notes
Every value renders a real CascadingValue, so consumers match by type and name exactly as they do with a hand-written one, and the provider adds no markup of its own. For a value the whole app needs, prefer a root-level cascading value (AddCascadingValue on the service collection).
## Parameters
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| ChildContent | `RenderFragment?` | null | The content to which the values should be provided. |
| Values | `IEnumerable<BitCascadingValue>?` | null | The cascading values to be provided for the children. These values are provided after (so they take precedence over) the ones of the ValueList parameter. |
| ValueList | `BitCascadingValueList?` | null | The cascading value list to be provided for the children. These values are provided before (so they can be overridden by) the ones of the Values parameter. |
## Sub Classes
### BitCascadingValue Properties
One value to cascade: what is cascaded, as which type, under which name, and whether it is fixed or provided at all. Bare values and (value, name) tuples of the common primitive, date, string and BitDir types convert to it implicitly.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| BitCascadingValue(object? value, string? name, bool isFixed, Type? valueType = null, bool enabled = true) | `constructor` | | Creates a cascading value. Shorter overloads take (value, name), (value, isFixed), (value, valueType) and (value, name, valueType). The valueType is required when the value is null. |
| Value | `object?` | null | The value to be provided. Assigning a value not assignable to ValueType throws an ArgumentException; assigning a different value raises Changed. A lazy factory runs on the first read, a computed one on every read; assigning replaces either factory for good, so a computed value stops tracking its source. |
| Name | `string?` | null | The optional name of the cascading value, matched case-insensitively; an empty or white-space name means no name. Renaming a live value re-creates its CascadingValue so the consumers are matched again under the new name. |
| IsFixed | `bool` | false | Marks a value that never changes, so its consumers are not subscribed for change notifications. Toggling it re-creates the underlying CascadingValue. |
| Enabled | `bool` | true | Whether the value is provided at all. A disabled value is skipped as if it had never been added, so an outer or root-level value of the same type and name shows through. |
| AutoNotify | `bool` | false | Watches the cascaded object itself, so an INotifyPropertyChanged or INotifyCollectionChanged value raises Changed on its own. The watch is only held while a provider is listening. |
| ValueType | `Type` | Value?.GetType() | The TValue of the underlying CascadingValue, which decides the cascading parameters the value reaches. Read-only; defaults to the runtime type of the value, so pass it explicitly for null values, nullable value types, base types and interfaces. |
| IsValueCreated | `bool` | true | Whether the value is already available. It is only false for a lazily created value whose factory has not run yet. |
| IsComputed | `bool` | false | Whether the value is re-read from a factory on every read, as the Computed factories create it. |
| Changed | `event Action<BitCascadingValue>?` | | Raised whenever the value changes; the hosting provider listens to it and re-renders on its own. |
| ChangedAsync | `event Func<BitCascadingValue, Task>?` | | The awaitable counterpart of Changed, which the provider subscribes to so NotifyChangedAsync completes only once it has re-rendered. |
| NotifyChanged() | `void` | | Raises Changed and ChangedAsync on demand, which pushes an object that was mutated in place down to the consumers. |
| NotifyChangedAsync() | `Task` | | The awaitable form of NotifyChanged; completes once every listening provider has re-rendered, like CascadingValueSource.NotifyChangedAsync. |
| From<T>(T value, string? name = null, bool isFixed = false, bool enabled = true) | `BitCascadingValue` | | Creates a cascading value whose ValueType is the static type of T. |
| Fixed<T>(T value, string? name = null, bool enabled = true) | `BitCascadingValue` | | Creates a fixed (IsFixed) cascading value whose ValueType is the static type of T. |
| Lazy<T>(Func<T> valueFactory, string? name = null, bool isFixed = false, bool enabled = true) | `BitCascadingValue` | | Creates a value whose factory runs once, the first time it is provided, so a disabled or shadowed value is never built. A failing factory is retried on the next read. An overload taking an explicit ValueType is available as well. |
| Computed<T>(Func<T> valueFactory, string? name = null, bool isFixed = false, bool enabled = true) | `BitCascadingValue` | | Creates a value whose factory runs every time it is provided, so one long-lived value keeps tracking the state it is derived from. An overload taking an explicit ValueType is available as well. |
| Observed<T>(T value, string? name = null, bool enabled = true) | `BitCascadingValue` | | Creates a value with AutoNotify turned on, so an object reporting its own mutations refreshes the consumers without any call to NotifyChanged. |
### BitCascadingValue<T> Properties
A BitCascadingValue whose ValueType is T, so its value is read and assigned as a T, like the framework's CascadingValueSource<T>. It goes anywhere a BitCascadingValue does. Every value created from a static type is one - the factories, the typed BitCascadingValueList helpers and the implicit conversions - so a type pattern reaches its typed members; one created with an explicit Type or an untyped constructor is not.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| BitCascadingValue<T>(T value, string? name = null, bool isFixed = false, bool enabled = true) | `constructor` | | Creates a cascading value that is cascaded as T. |
| Value | `T` | | The value to be provided, typed as T. Assigning a different value refreshes the consumers. It may be null even for a non-nullable reference T, since the untyped Value accepts null. |
| NotifyChangedAsync(T newValue) | `Task` | | Assigns Value and completes once every listening provider has re-rendered with it, like CascadingValueSource.NotifyChangedAsync(newValue). Notifies even when the value is unchanged, and like assigning Value, replaces a lazy or computed factory for good. |
### BitCascadingValueList Properties
A List<BitCascadingValue> with typed helpers for building and revising a set of cascading values; its collection initializer takes { value, name } pairs.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Add<T>(T value, string? name = null, bool isFixed = false, bool enabled = true) | `void` | | Adds a typed BitCascadingValue to the list, cascading the value as the static type of T. |
| Add(BitCascadingValue? value) | `void` | | Adds an already created BitCascadingValue to the list. A null item is ignored. An overload taking a BitCascadingValue<T> adds it as it is too, rather than cascading the value object itself. |
| Add(object? value, Type valueType, string? name = null, bool isFixed = false, bool enabled = true) | `void` | | Adds a BitCascadingValue with an explicit ValueType to the list, for when the cascaded type is only known at runtime. |
| AddIf<T>(bool condition, T value, string? name = null, bool isFixed = false, bool enabled = true) | `void` | | Adds a typed BitCascadingValue to the list only when the given condition is true. |
| AddIf(bool condition, BitCascadingValue? value) | `void` | | Adds an already created BitCascadingValue only when the condition is true; paired with a lazy value, the value of a skipped entry is never built. An overload taking a BitCascadingValue<T> is available as well. |
| AddFixed<T>(T value, string? name = null, bool enabled = true) | `void` | | Adds a fixed (IsFixed) typed BitCascadingValue to the list. |
| AddFixed(object? value, Type valueType, string? name = null, bool enabled = true) | `void` | | Adds a fixed (IsFixed) BitCascadingValue with an explicit ValueType to the list. |
| AddLazy<T>(Func<T> valueFactory, string? name = null, bool isFixed = false, bool enabled = true) | `void` | | Adds a lazy value, whose factory runs once, the first time it is provided. An overload taking an explicit ValueType is available as well. |
| AddComputed<T>(Func<T> valueFactory, string? name = null, bool isFixed = false, bool enabled = true) | `void` | | Adds a computed value, whose factory runs every time it is provided. An overload taking an explicit ValueType is available as well. |
| AddObserved<T>(T value, string? name = null, bool enabled = true) | `void` | | Adds a typed BitCascadingValue that watches the value itself, so an INotifyPropertyChanged or INotifyCollectionChanged object refreshes the consumers on its own. |
| Find<T>(string? name = null) | `BitCascadingValue?` | | Finds the entry the type and name resolve to: the last one matching both, since it shadows the others. An entry created from a static type is a BitCascadingValue<T>, so match it with a type pattern rather than a cast. An overload taking an explicit ValueType is available as well. |
| Contains<T>(string? name = null) | `bool` | | Whether the list holds an entry of the static type of T carrying the given name, regardless of whether it is enabled. |
| Remove<T>(string? name = null) | `bool` | | Removes every entry of the static type of T carrying the given name, and reports whether anything was removed. An overload taking an explicit ValueType is available as well. |
| Set<T>(T value, string? name = null, bool isFixed = false, bool enabled = true) | `void` | | Replaces every entry of the static type of T and the given name with one new entry, in the place of the first, or appends it when there is none. |
## Examples
\n**Basic**:
```razor
```
`CascadingValueDemoConsumer.razor`:
```razor
@code {
[Parameter] public string? Title { get; set; }
[CascadingParameter(Name = "Theme")]
public string? Theme { get; set; }
[CascadingParameter(Name = "NotificationCount")]
public int? NotificationCount { get; set; }
[CascadingParameter(Name = "IsAuthenticated")]
public bool? IsAuthenticated { get; set; }
[CascadingParameter(Name = "NamedUser")]
public CascadingDemoUser? NamedUser { get; set; }
[CascadingParameter]
public CascadingDemoUser? TypedUser { get; set; }
private static string? FormatUser(CascadingDemoUser? user) => user is null ? null : $"{user.Name} [{user.Role}]";
}
```
`CascadingDemoUser.cs`:
```csharp
public sealed record CascadingDemoUser(string Name, string Role);
```
\n**Live values**:
```razor
Switch to @nextTheme theme
Add notification (@notificationCount)
```
```csharp
private bool isAuthenticated = true;
private string currentTheme = "Light";
private int notificationCount = 2;
private string userName = "Ava Smith";
private string userRole = "Product manager";
private string nextTheme => currentTheme == "Light" ? "Dark" : "Light";
private IEnumerable values =>
[
(currentTheme, "Theme"),
(isAuthenticated, "IsAuthenticated"),
(notificationCount, "NotificationCount"),
new (new CascadingDemoUser("Saleh Xafan", "CTO"), "NamedUser"),
new (new CascadingDemoUser(userName, userRole))
];
```
`CascadingValueDemoConsumer.razor`:
```razor
@code {
[Parameter] public string? Title { get; set; }
[CascadingParameter(Name = "Theme")]
public string? Theme { get; set; }
[CascadingParameter(Name = "NotificationCount")]
public int? NotificationCount { get; set; }
[CascadingParameter(Name = "IsAuthenticated")]
public bool? IsAuthenticated { get; set; }
[CascadingParameter(Name = "NamedUser")]
public CascadingDemoUser? NamedUser { get; set; }
[CascadingParameter]
public CascadingDemoUser? TypedUser { get; set; }
private static string? FormatUser(CascadingDemoUser? user) => user is null ? null : $"{user.Name} [{user.Role}]";
}
```
`CascadingDemoUser.cs`:
```csharp
public sealed record CascadingDemoUser(string Name, string Role);
```
\n**ValueList**:
```razor
Set NotificationCount (@listNotificationCount)
@(listValues.Contains() ? "Remove" : "Add") the typed user
```
```csharp
private int listNotificationCount;
private readonly BitCascadingValueList listValues = new()
{
{ "Light", "Theme" },
{ (int?)null, "NotificationCount" },
{ (bool?)null, "IsAuthenticated" },
{ new CascadingDemoUser("Ava Smith", "Product manager") }
};
private void SetListNotificationCount() => listValues.Set(++listNotificationCount, "NotificationCount");
private void ToggleListUser()
{
if (listValues.Contains())
{
listValues.Remove();
}
else
{
listValues.Add(new CascadingDemoUser("Ava Smith", "Product manager"));
}
}
```
`CascadingValueDemoConsumer.razor`:
```razor
@code {
[Parameter] public string? Title { get; set; }
[CascadingParameter(Name = "Theme")]
public string? Theme { get; set; }
[CascadingParameter(Name = "NotificationCount")]
public int? NotificationCount { get; set; }
[CascadingParameter(Name = "IsAuthenticated")]
public bool? IsAuthenticated { get; set; }
[CascadingParameter(Name = "NamedUser")]
public CascadingDemoUser? NamedUser { get; set; }
[CascadingParameter]
public CascadingDemoUser? TypedUser { get; set; }
private static string? FormatUser(CascadingDemoUser? user) => user is null ? null : $"{user.Name} [{user.Role}]";
}
```
`CascadingDemoUser.cs`:
```csharp
public sealed record CascadingDemoUser(string Name, string Role);
```
\n**Nesting**:
```razor
```
`CascadingValueDemoConsumer.razor`:
```razor
@code {
[Parameter] public string? Title { get; set; }
[CascadingParameter(Name = "Theme")]
public string? Theme { get; set; }
[CascadingParameter(Name = "NotificationCount")]
public int? NotificationCount { get; set; }
[CascadingParameter(Name = "IsAuthenticated")]
public bool? IsAuthenticated { get; set; }
[CascadingParameter(Name = "NamedUser")]
public CascadingDemoUser? NamedUser { get; set; }
[CascadingParameter]
public CascadingDemoUser? TypedUser { get; set; }
private static string? FormatUser(CascadingDemoUser? user) => user is null ? null : $"{user.Name} [{user.Role}]";
}
```
`CascadingDemoUser.cs`:
```csharp
public sealed record CascadingDemoUser(string Name, string Role);
```
\n**ValueType**:
```razor
```
```csharp
private readonly IEnumerable nullCountValues = [BitCascadingValue.From(null)];
```
`CascadingValueDemoTypeConsumer.razor`:
```razor
```
```csharp
private readonly BitCascadingValue flagTheme;
private readonly BitCascadingValue flagUser;
private readonly IEnumerable flagValues;
public MyPage()
{
// Created once: a fixed value is the cheapest thing to cascade only while it is the same instance.
flagTheme = BitCascadingValue.Fixed("Dark", "Theme");
flagUser = new(new CascadingDemoUser("Ava Smith", "Product manager"));
flagValues = [flagTheme, BitCascadingValue.Fixed((3) as int?, "NotificationCount"), flagUser];
}
private bool provideTheme
{
get => flagTheme.Enabled;
set => flagTheme.Enabled = value;
}
private bool provideUser
{
get => flagUser.Enabled;
set => flagUser.Enabled = value;
}
```
`CascadingValueDemoConsumer.razor`:
```razor
@code {
[Parameter] public string? Title { get; set; }
[CascadingParameter(Name = "Theme")]
public string? Theme { get; set; }
[CascadingParameter(Name = "NotificationCount")]
public int? NotificationCount { get; set; }
[CascadingParameter(Name = "IsAuthenticated")]
public bool? IsAuthenticated { get; set; }
[CascadingParameter(Name = "NamedUser")]
public CascadingDemoUser? NamedUser { get; set; }
[CascadingParameter]
public CascadingDemoUser? TypedUser { get; set; }
private static string? FormatUser(CascadingDemoUser? user) => user is null ? null : $"{user.Name} [{user.Role}]";
}
```
`CascadingDemoUser.cs`:
```csharp
public sealed record CascadingDemoUser(string Name, string Role);
```
\n**Change notifications**:
```razor
Run a background job
```
```csharp
private bool jobIsRunning;
private readonly CascadingDemoStatus jobStatus = new();
private readonly BitCascadingValue jobStatusValue;
private readonly BitCascadingValue jobProgressValue;
private readonly IEnumerable notifyingValues;
public MyPage()
{
jobStatusValue = new(jobStatus);
jobProgressValue = new(null, "Progress");
notifyingValues = [jobStatusValue, jobProgressValue];
}
private void RunBackgroundJob()
{
if (jobIsRunning) return;
jobIsRunning = true;
_ = Task.Run(async () =>
{
try
{
// The cascaded status object is mutated in place, so there is no assignment to notice.
jobStatus.Text = "Running";
await jobStatusValue.NotifyChangedAsync();
for (var i = 1; i <= 5; i++)
{
await Task.Delay(500);
// Assigns the typed Value and waits until the consumers have rendered it.
await jobProgressValue.NotifyChangedAsync(i);
}
jobStatus.Text = "Done";
await jobStatusValue.NotifyChangedAsync();
}
finally
{
jobIsRunning = false;
}
});
}
```
`CascadingValueDemoStatusConsumer.razor`:
```razor
@code {
[Parameter] public string? Title { get; set; }
[CascadingParameter]
public CascadingDemoStatus? Status { get; set; }
[CascadingParameter(Name = "Progress")]
public int? Progress { get; set; }
}
```
`CascadingDemoStatus.cs`:
```csharp
///
/// A mutable state holder, cascaded as a single instance that is updated in place, which is the case
/// that BitCascadingValue.NotifyChanged exists for.
///
public sealed class CascadingDemoStatus
{
public string Text { get; set; } = "Idle";
}
```
\n**AutoNotify**:
```razor
Run a background job
```
```csharp
private bool observableJobIsRunning;
private readonly CascadingDemoObservableStatus observableStatus = new();
private readonly IEnumerable observableValues;
public MyPage()
{
observableValues = [BitCascadingValue.Observed(observableStatus)];
}
private void RunObservableJob()
{
if (observableJobIsRunning) return;
observableJobIsRunning = true;
_ = Task.Run(async () =>
{
try
{
// Nothing here notifies anything: the status object reports its own changes.
observableStatus.Text = "Running";
for (var i = 1; i <= 5; i++)
{
await Task.Delay(500);
observableStatus.Count = i;
}
observableStatus.Text = "Done";
}
finally
{
observableJobIsRunning = false;
}
});
}
```
`CascadingValueDemoObservableConsumer.razor`:
```razor
@code {
[Parameter] public string? Title { get; set; }
[CascadingParameter]
public CascadingDemoObservableStatus? Status { get; set; }
}
```
`CascadingDemoObservableStatus.cs`:
```csharp
using System.ComponentModel;
using System.Runtime.CompilerServices;
///
/// A state holder that reports its own mutations, which is what BitCascadingValue.Observed watches so that
/// the consumers refresh without a single call to NotifyChanged.
///
public sealed class CascadingDemoObservableStatus : INotifyPropertyChanged
{
private int _count;
private string _text = "Idle";
public event PropertyChangedEventHandler? PropertyChanged;
public string Text
{
get => _text;
set
{
if (_text == value) return;
_text = value;
OnPropertyChanged();
}
}
public int Count
{
get => _count;
set
{
if (_count == value) return;
_count = value;
OnPropertyChanged();
}
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
```
\n**Lazy & computed values**:
```razor
Click me (@computedClicks)
```
```csharp
private int computedClicks;
private int lazyUserFactoryCalls;
private readonly BitCascadingValue lazyTypedUser;
private readonly BitCascadingValue lazyNamedUser;
private readonly IEnumerable factoryValues;
public MyPage()
{
lazyTypedUser = BitCascadingValue.Lazy(() => CreateLazyUser("Ava Smith", "Product manager"));
lazyNamedUser = BitCascadingValue.Lazy(() => CreateLazyUser("Saleh Xafan", "CTO"), "NamedUser", enabled: false);
factoryValues =
[
lazyTypedUser,
lazyNamedUser,
BitCascadingValue.Computed(() => computedClicks % 2 == 0 ? "Light" : "Dark", "Theme"),
BitCascadingValue.Computed(() => computedClicks, "NotificationCount")
];
}
private bool provideLazyNamedUser
{
get => lazyNamedUser.Enabled;
set => lazyNamedUser.Enabled = value;
}
private CascadingDemoUser CreateLazyUser(string name, string role)
{
lazyUserFactoryCalls++;
return new CascadingDemoUser(name, role);
}
```
`CascadingValueDemoConsumer.razor`:
```razor
@code {
[Parameter] public string? Title { get; set; }
[CascadingParameter(Name = "Theme")]
public string? Theme { get; set; }
[CascadingParameter(Name = "NotificationCount")]
public int? NotificationCount { get; set; }
[CascadingParameter(Name = "IsAuthenticated")]
public bool? IsAuthenticated { get; set; }
[CascadingParameter(Name = "NamedUser")]
public CascadingDemoUser? NamedUser { get; set; }
[CascadingParameter]
public CascadingDemoUser? TypedUser { get; set; }
private static string? FormatUser(CascadingDemoUser? user) => user is null ? null : $"{user.Name} [{user.Role}]";
}
```
`CascadingDemoUser.cs`:
```csharp
public sealed record CascadingDemoUser(string Name, string Role);
```
@(Title ?? "Child component with cascading parameters:")
Theme:
@(Theme ?? "null")
Notifications:
@(NotificationCount?.ToString() ?? "null")
Authenticated:
@(IsAuthenticated?.ToString() ?? "null")
User (named parameter):
@(FormatUser(NamedUser) ?? "null")
User (typed parameter):
@(FormatUser(TypedUser) ?? "null")
@(Title ?? "Child component with cascading parameters:")
Theme:
@(Theme ?? "null")
Notifications:
@(NotificationCount?.ToString() ?? "null")
Authenticated:
@(IsAuthenticated?.ToString() ?? "null")
User (named parameter):
@(FormatUser(NamedUser) ?? "null")
User (typed parameter):
@(FormatUser(TypedUser) ?? "null")
@(Title ?? "Child component with cascading parameters:")
Theme:
@(Theme ?? "null")
Notifications:
@(NotificationCount?.ToString() ?? "null")
Authenticated:
@(IsAuthenticated?.ToString() ?? "null")
User (named parameter):
@(FormatUser(NamedUser) ?? "null")
User (typed parameter):
@(FormatUser(TypedUser) ?? "null")
@(Title ?? "Child component with cascading parameters:")
Theme:
@(Theme ?? "null")
Notifications:
@(NotificationCount?.ToString() ?? "null")
Authenticated:
@(IsAuthenticated?.ToString() ?? "null")
User (named parameter):
@(FormatUser(NamedUser) ?? "null")
User (typed parameter):
@(FormatUser(TypedUser) ?? "null")
@Title
@(Count?.ToString() ?? "null")
@code {
[Parameter] public string? Title { get; set; }
[CascadingParameter]
public int? Count { get; set; }
}
```
\n**IsFixed & Enabled**:
```razor
@(Title ?? "Child component with cascading parameters:")
Theme:
@(Theme ?? "null")
Notifications:
@(NotificationCount?.ToString() ?? "null")
Authenticated:
@(IsAuthenticated?.ToString() ?? "null")
User (named parameter):
@(FormatUser(NamedUser) ?? "null")
User (typed parameter):
@(FormatUser(TypedUser) ?? "null")
@(Title ?? "Child component with cascading parameters:")
Status:
@(Status?.Text ?? "null")
Progress:
@(Progress?.ToString() ?? "null")
@(Title ?? "Child component with cascading parameters:")
Status:
@(Status?.Text ?? "null")
Count:
@(Status?.Count.ToString() ?? "null")
Lazy factory runs so far: @lazyUserFactoryCalls
@(Title ?? "Child component with cascading parameters:")
Theme:
@(Theme ?? "null")
Notifications:
@(NotificationCount?.ToString() ?? "null")
Authenticated:
@(IsAuthenticated?.ToString() ?? "null")
User (named parameter):
@(FormatUser(NamedUser) ?? "null")
User (typed parameter):
@(FormatUser(TypedUser) ?? "null")