|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | + |
| 4 | +using System.Collections.Concurrent; |
| 5 | +using Microsoft.AspNetCore.Components.Rendering; |
| 6 | + |
| 7 | +namespace Microsoft.AspNetCore.Components; |
| 8 | + |
| 9 | +/// <summary> |
| 10 | +/// Supplies a cascading value that can be received by components using |
| 11 | +/// <see cref="CascadingParameterAttribute"/>. |
| 12 | +/// </summary> |
| 13 | +public class CascadingValueSource<TValue> : ICascadingValueSupplier |
| 14 | +{ |
| 15 | + // By *not* making this sealed, people who want to deal with value disposal can subclass this, |
| 16 | + // add IDisposable, and then do what they want during shutdown |
| 17 | + |
| 18 | + private readonly ConcurrentDictionary<Dispatcher, List<ComponentState>>? _subscribers; |
| 19 | + private readonly bool _isFixed; |
| 20 | + private readonly string? _name; |
| 21 | + |
| 22 | + // You can either provide an initial value to the constructor, or a func to provide one lazily |
| 23 | + private TValue? _currentValue; |
| 24 | + private Func<TValue>? _initialValueFactory; |
| 25 | + |
| 26 | + /// <summary> |
| 27 | + /// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>. |
| 28 | + /// </summary> |
| 29 | + /// <param name="value">The initial value.</param> |
| 30 | + /// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all receipients 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> |
| 31 | + public CascadingValueSource(TValue value, bool isFixed) : this(isFixed) |
| 32 | + { |
| 33 | + _currentValue = value; |
| 34 | + } |
| 35 | + |
| 36 | + /// <summary> |
| 37 | + /// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>. |
| 38 | + /// </summary> |
| 39 | + /// <param name="name">A name for the cascading value. If set, <see cref="CascadingParameterAttribute"/> can be configured to match based on this name.</param> |
| 40 | + /// <param name="value">The initial value.</param> |
| 41 | + /// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all receipients 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> |
| 42 | + public CascadingValueSource(string name, TValue value, bool isFixed) : this(value, isFixed) |
| 43 | + { |
| 44 | + ArgumentNullException.ThrowIfNull(name); |
| 45 | + _name = name; |
| 46 | + } |
| 47 | + |
| 48 | + /// <summary> |
| 49 | + /// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>. |
| 50 | + /// </summary> |
| 51 | + /// <param name="valueFactory">A callback that produces the initial value when first required.</param> |
| 52 | + /// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all receipients 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> |
| 53 | + public CascadingValueSource(Func<TValue> valueFactory, bool isFixed) : this(isFixed) |
| 54 | + { |
| 55 | + _initialValueFactory = valueFactory; |
| 56 | + } |
| 57 | + |
| 58 | + /// <summary> |
| 59 | + /// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>. |
| 60 | + /// </summary> |
| 61 | + /// <param name="name">A name for the cascading value. If set, <see cref="CascadingParameterAttribute"/> can be configured to match based on this name.</param> |
| 62 | + /// <param name="valueFactory">A callback that produces the initial value when first required.</param> |
| 63 | + /// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all receipients 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> |
| 64 | + public CascadingValueSource(string name, Func<TValue> valueFactory, bool isFixed) : this(valueFactory, isFixed) |
| 65 | + { |
| 66 | + ArgumentNullException.ThrowIfNull(name); |
| 67 | + _name = name; |
| 68 | + } |
| 69 | + |
| 70 | + private CascadingValueSource(bool isFixed) |
| 71 | + { |
| 72 | + _isFixed = isFixed; |
| 73 | + |
| 74 | + if (!_isFixed) |
| 75 | + { |
| 76 | + _subscribers = new(); |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + /// <summary> |
| 81 | + /// Notifies subscribers that the value has changed (for example, if it has been mutated). |
| 82 | + /// </summary> |
| 83 | + /// <returns>A <see cref="Task"/> that completes when the notifications have been issued.</returns> |
| 84 | + public Task NotifyChangedAsync() |
| 85 | + { |
| 86 | + if (_isFixed) |
| 87 | + { |
| 88 | + throw new InvalidOperationException($"Cannot notify about changes because the {GetType()} is configured as fixed."); |
| 89 | + } |
| 90 | + |
| 91 | + if (_subscribers?.Count > 0) |
| 92 | + { |
| 93 | + var tasks = new List<Task>(); |
| 94 | + |
| 95 | + foreach (var (dispatcher, subscribers) in _subscribers) |
| 96 | + { |
| 97 | + tasks.Add(dispatcher.InvokeAsync(() => |
| 98 | + { |
| 99 | + foreach (var subscriber in subscribers) |
| 100 | + { |
| 101 | + subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); |
| 102 | + } |
| 103 | + })); |
| 104 | + } |
| 105 | + |
| 106 | + return Task.WhenAll(tasks); |
| 107 | + } |
| 108 | + else |
| 109 | + { |
| 110 | + return Task.CompletedTask; |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + /// <summary> |
| 115 | + /// Notifies subscribers that the value has changed, supplying a new value. |
| 116 | + /// </summary> |
| 117 | + /// <param name="newValue"></param> |
| 118 | + /// <returns>A <see cref="Task"/> that completes when the notifications have been issued.</returns> |
| 119 | + public Task NotifyChangedAsync(TValue newValue) |
| 120 | + { |
| 121 | + _currentValue = newValue; |
| 122 | + _initialValueFactory = null; // This definitely won't be used now |
| 123 | + |
| 124 | + return NotifyChangedAsync(); |
| 125 | + } |
| 126 | + |
| 127 | + bool ICascadingValueSupplier.IsFixed => _isFixed; |
| 128 | + |
| 129 | + bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo) |
| 130 | + { |
| 131 | + if (parameterInfo.Attribute is not CascadingParameterAttribute cascadingParameterAttribute || !parameterInfo.PropertyType.IsAssignableFrom(typeof(TValue))) |
| 132 | + { |
| 133 | + return false; |
| 134 | + } |
| 135 | + |
| 136 | + // We only consider explicitly requested names, not the property name. |
| 137 | + var requestedName = cascadingParameterAttribute.Name; |
| 138 | + return (requestedName == null && _name == null) // Match on type alone |
| 139 | + || string.Equals(requestedName, _name, StringComparison.OrdinalIgnoreCase); // Also match on name |
| 140 | + } |
| 141 | + |
| 142 | + object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo) |
| 143 | + { |
| 144 | + if (_initialValueFactory is not null) |
| 145 | + { |
| 146 | + _currentValue = _initialValueFactory(); |
| 147 | + _initialValueFactory = null; |
| 148 | + } |
| 149 | + |
| 150 | + return _currentValue; |
| 151 | + } |
| 152 | + |
| 153 | + void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) |
| 154 | + { |
| 155 | + Dispatcher dispatcher = subscriber.Renderer.Dispatcher; |
| 156 | + dispatcher.AssertAccess(); |
| 157 | + |
| 158 | + // The .Add is threadsafe because we are in the sync context for this dispatcher |
| 159 | + _subscribers?.GetOrAdd(dispatcher, _ => new()).Add(subscriber); |
| 160 | + } |
| 161 | + |
| 162 | + void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) |
| 163 | + { |
| 164 | + Dispatcher dispatcher = subscriber.Renderer.Dispatcher; |
| 165 | + dispatcher.AssertAccess(); |
| 166 | + |
| 167 | + if (_subscribers?.TryGetValue(dispatcher, out var subscribersForDispatcher) == true) |
| 168 | + { |
| 169 | + // Threadsafe because we're in the sync context for this dispatcher |
| 170 | + subscribersForDispatcher.Remove(subscriber); |
| 171 | + if (subscribersForDispatcher.Count == 0) |
| 172 | + { |
| 173 | + _subscribers.Remove(dispatcher, out _); |
| 174 | + } |
| 175 | + } |
| 176 | + } |
| 177 | +} |
0 commit comments