| File: CascadingValueSource.cs | Web Access |
| Project: src\aspnetcore\src\Components\Components\src\Microsoft.AspNetCore.Components.csproj (Microsoft.AspNetCore.Components) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components; /// <summary> /// Supplies a cascading value that can be received by components using /// <see cref="CascadingParameterAttribute"/>. /// </summary> public class CascadingValueSource<TValue> : ICascadingValueSupplier { // By *not* making this sealed, people who want to deal with value disposal can subclass this, // add IDisposable, and then do what they want during shutdown private readonly ConcurrentDictionary<Dispatcher, List<ComponentState>>? _subscribers; private readonly bool _isFixed; private readonly string? _name; // You can either provide an initial value to the constructor, or a func to provide one lazily private TValue? _currentValue; private Func<TValue>? _initialValueFactory; /// <summary> /// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>. /// </summary> /// <param name="value">The initial value.</param> /// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all recipients will subscribe for update notifications, which you can issue by calling <see cref="NotifyChangedAsync()"/>. These subscriptions come at a performance cost, so if the value will not change, set <paramref name="isFixed"/> to true.</param> public CascadingValueSource(TValue value, bool isFixed) : this(isFixed) { _currentValue = value; } /// <summary> /// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>. /// </summary> /// <param name="name">A name for the cascading value. If set, <see cref="CascadingParameterAttribute"/> can be configured to match based on this name.</param> /// <param name="value">The initial value.</param> /// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all recipients will subscribe for update notifications, which you can issue by calling <see cref="NotifyChangedAsync()"/>. These subscriptions come at a performance cost, so if the value will not change, set <paramref name="isFixed"/> to true.</param> public CascadingValueSource(string name, TValue value, bool isFixed) : this(value, isFixed) { ArgumentNullException.ThrowIfNull(name); _name = name; } /// <summary> /// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>. /// </summary> /// <param name="initialValueFactory">A callback that produces the initial value when first required.</param> /// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all recipients will subscribe for update notifications, which you can issue by calling <see cref="NotifyChangedAsync()"/>. These subscriptions come at a performance cost, so if the value will not change, set <paramref name="isFixed"/> to true.</param> public CascadingValueSource(Func<TValue> initialValueFactory, bool isFixed) : this(isFixed) { _initialValueFactory = initialValueFactory; } /// <summary> /// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>. /// </summary> /// <param name="name">A name for the cascading value. If set, <see cref="CascadingParameterAttribute"/> can be configured to match based on this name.</param> /// <param name="initialValueFactory">A callback that produces the initial value when first required.</param> /// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all recipients will subscribe for update notifications, which you can issue by calling <see cref="NotifyChangedAsync()"/>. These subscriptions come at a performance cost, so if the value will not change, set <paramref name="isFixed"/> to true.</param> public CascadingValueSource(string name, Func<TValue> initialValueFactory, bool isFixed) : this(initialValueFactory, isFixed) { ArgumentNullException.ThrowIfNull(name); _name = name; } private CascadingValueSource(bool isFixed) { _isFixed = isFixed; if (!_isFixed) { _subscribers = new(); } } /// <summary> /// Notifies subscribers that the value has changed (for example, if it has been mutated). /// </summary> /// <returns>A <see cref="Task"/> that completes when the notifications have been issued.</returns> public Task NotifyChangedAsync() { if (_isFixed) { throw new InvalidOperationException($"Cannot notify about changes because the {GetType()} is configured as fixed."); } if (_subscribers?.Count > 0) { var tasks = new List<Task>(); foreach (var (dispatcher, subscribers) in _subscribers) { tasks.Add(dispatcher.InvokeAsync(() => { var subscribersBuffer = new ComponentStateBuffer(); var subscribersCount = subscribers.Count; var subscribersCopy = subscribersCount <= ComponentStateBuffer.Capacity ? subscribersBuffer[..subscribersCount] : new ComponentState[subscribersCount]; subscribers.CopyTo(subscribersCopy); // We iterate over a copy of the list because new subscribers might get // added or removed during change notification foreach (var subscriber in subscribersCopy) { subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); } })); } return Task.WhenAll(tasks); } else { return Task.CompletedTask; } } /// <summary> /// Notifies subscribers that the value has changed, supplying a new value. /// </summary> /// <param name="newValue"></param> /// <returns>A <see cref="Task"/> that completes when the notifications have been issued.</returns> public Task NotifyChangedAsync(TValue newValue) { _currentValue = newValue; _initialValueFactory = null; // This definitely won't be used now return NotifyChangedAsync(); } bool ICascadingValueSupplier.IsFixed => _isFixed; bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo) { if (parameterInfo.Attribute is not CascadingParameterAttribute cascadingParameterAttribute || !parameterInfo.PropertyType.IsAssignableFrom(typeof(TValue))) { return false; } // We only consider explicitly requested names, not the property name. var requestedName = cascadingParameterAttribute.Name; return (requestedName == null && _name == null) // Match on type alone || string.Equals(requestedName, _name, StringComparison.OrdinalIgnoreCase); // Also match on name } object? ICascadingValueSupplier.GetCurrentValue(object? key, in CascadingParameterInfo parameterInfo) { if (_initialValueFactory is not null) { _currentValue = _initialValueFactory(); _initialValueFactory = null; } return _currentValue; } void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) { Dispatcher dispatcher = subscriber.Renderer.Dispatcher; dispatcher.AssertAccess(); // The .Add is threadsafe because we are in the sync context for this dispatcher _subscribers?.GetOrAdd(dispatcher, _ => new()).Add(subscriber); } void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) { Dispatcher dispatcher = subscriber.Renderer.Dispatcher; dispatcher.AssertAccess(); if (_subscribers?.TryGetValue(dispatcher, out var subscribersForDispatcher) == true) { // Threadsafe because we're in the sync context for this dispatcher subscribersForDispatcher.Remove(subscriber); if (subscribersForDispatcher.Count == 0) { _subscribers.Remove(dispatcher, out _); } } } [InlineArray(Capacity)] internal struct ComponentStateBuffer { public const int Capacity = 64; #pragma warning disable IDE0051 // Remove unused private members #pragma warning disable IDE0044 // Add readonly modifier private ComponentState _values; #pragma warning restore IDE0044 // Add readonly modifier #pragma warning restore IDE0051 // Remove unused private members } }