aspnetcore/src/Microsoft.AspNetCore.Blazor/Components/CascadingValue.cs

183 lines
7.2 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.AspNetCore.Blazor.RenderTree;
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Blazor.Components
{
/// <summary>
/// A component that provides a cascading value to all descendant components.
/// </summary>
public class CascadingValue<T> : ICascadingValueComponent, IComponent
{
private RenderHandle _renderHandle;
private HashSet<ComponentState> _subscribers; // Lazily instantiated
private bool _hasSetParametersPreviously;
/// <summary>
/// The content to which the value should be provided.
/// </summary>
[Parameter] private RenderFragment ChildContent { get; set; }
/// <summary>
/// The value to be provided.
/// </summary>
[Parameter] private T Value { get; set; }
/// <summary>
/// Optionally gives a name to the provided value. Descendant components
/// will be able to receive the value by specifying this name.
///
/// If no name is specified, then descendant components will receive the
/// value based the type of value they are requesting.
/// </summary>
[Parameter] private string Name { get; set; }
/// <summary>
/// If true, indicates that <see cref="Value"/> will not change. This is a
/// performance optimization that allows the framework to skip setting up
/// change notifications. Set this flag only if you will not change
/// <see cref="Value"/> during the component's lifetime.
/// </summary>
[Parameter] private bool IsFixed { get; set; }
object ICascadingValueComponent.CurrentValue => Value;
bool ICascadingValueComponent.CurrentValueIsFixed => IsFixed;
/// <inheritdoc />
public void Init(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
/// <inheritdoc />
public void SetParameters(ParameterCollection parameters)
{
// Implementing the parameter binding manually, instead of just calling
// parameters.AssignToProperties(this), is just a very slight perf optimization
// and makes it simpler impose rules about the params being required or not.
var hasSuppliedValue = false;
var previousValue = Value;
var previousFixed = IsFixed;
Value = default;
ChildContent = null;
Name = null;
IsFixed = false;
foreach (var parameter in parameters)
{
if (parameter.Name.Equals(nameof(Value), StringComparison.OrdinalIgnoreCase))
{
Value = (T)parameter.Value;
hasSuppliedValue = true;
}
else if (parameter.Name.Equals(nameof(ChildContent), StringComparison.OrdinalIgnoreCase))
{
ChildContent = (RenderFragment)parameter.Value;
}
else if (parameter.Name.Equals(nameof(Name), StringComparison.OrdinalIgnoreCase))
{
Name = (string)parameter.Value;
if (string.IsNullOrEmpty(Name))
{
throw new ArgumentException($"The parameter '{nameof(Name)}' for component '{nameof(CascadingValue<T>)}' does not allow null or empty values.");
}
}
else if (parameter.Name.Equals(nameof(IsFixed), StringComparison.OrdinalIgnoreCase))
{
IsFixed = (bool)parameter.Value;
}
else
{
throw new ArgumentException($"The component '{nameof(CascadingValue<T>)}' does not accept a parameter with the name '{parameter.Name}'.");
}
}
if (_hasSetParametersPreviously && IsFixed != previousFixed)
{
throw new InvalidOperationException($"The value of {nameof(IsFixed)} cannot be changed dynamically.");
}
_hasSetParametersPreviously = true;
// It's OK for the value to be null, but some "Value" param must be suppled
// because it serves no useful purpose to have a <CascadingValue> otherwise.
if (!hasSuppliedValue)
{
throw new ArgumentException($"Missing required parameter '{nameof(Value)}' for component '{nameof(Parameter)}'.");
}
// Rendering is most efficient when things are queued from rootmost to leafmost.
// Given a components A (parent) -> B (child), you want them to be queued in order
// [A, B] because if you had [B, A], then the render for A might change B's params
// making it render again, so you'd render [B, A, B], which is wasteful.
// At some point we might consider making the render queue actually enforce this
// ordering during insertion.
//
// For the CascadingValue component, this observation is why it's important to render
// ourself before notifying subscribers (which can be grandchildren or deeper).
// If we rerendered subscribers first, then our own subsequent render might cause an
// further update that makes those nested subscribers get rendered twice.
_renderHandle.Render(Render);
if (_subscribers != null && ChangeDetection.MayHaveChanged(previousValue, Value))
{
NotifySubscribers();
}
}
bool ICascadingValueComponent.CanSupplyValue(Type requestedType, string requestedName)
{
if (!requestedType.IsAssignableFrom(typeof(T)))
{
return false;
}
return (requestedName == null && Name == null) // Match on type alone
|| string.Equals(requestedName, Name, StringComparison.OrdinalIgnoreCase); // Also match on name
}
void ICascadingValueComponent.Subscribe(ComponentState subscriber)
{
#if DEBUG
if (IsFixed)
{
// Should not be possible. User code cannot trigger this.
// Checking only to catch possible future framework bugs.
throw new InvalidOperationException($"Cannot subscribe to a {typeof(CascadingValue<>).Name} when {nameof(IsFixed)} is true.");
}
#endif
if (_subscribers == null)
{
_subscribers = new HashSet<ComponentState>();
}
_subscribers.Add(subscriber);
}
void ICascadingValueComponent.Unsubscribe(ComponentState subscriber)
{
_subscribers.Remove(subscriber);
}
private void NotifySubscribers()
{
foreach (var subscriber in _subscribers)
{
subscriber.NotifyCascadingValueChanged();
}
}
private void Render(RenderTreeBuilder builder)
{
builder.AddContent(0, ChildContent);
}
}
}