-
Notifications
You must be signed in to change notification settings - Fork 648
Cascading parameters #1545
Cascading parameters #1545
Changes from all commits
1d5aee2
b57e897
0ab23ab
4bb3977
56e40d1
cc78a57
2af19a4
a3ede31
8cd0f9c
6ad2b65
332f9de
2dc609b
79d0906
f8c4d64
c57e96f
c76af8f
8948a8a
fded30f
7fb12a6
d5e4c6a
5756ebf
14d6522
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// 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 System; | ||
|
||
namespace Microsoft.AspNetCore.Blazor.Components | ||
{ | ||
/// <summary> | ||
/// Denotes the target member as a cascading component parameter. Its value will be | ||
/// supplied by the closest ancestor <see cref="CascadingValue{T}"/> component that | ||
/// supplies values with a compatible type and name. | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] | ||
public sealed class CascadingParameterAttribute : Attribute | ||
{ | ||
/// <summary> | ||
/// If specified, the parameter value will be supplied by the closest | ||
/// ancestor <see cref="CascadingValue{T}"/> that supplies a value with | ||
/// this name. | ||
/// | ||
/// If null, the parameter value will be supplied by the closest ancestor | ||
/// <see cref="CascadingValue{T}"/> that supplies a value with a compatible | ||
/// type. | ||
/// </summary> | ||
public string Name { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// 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 System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Reflection; | ||
|
||
namespace Microsoft.AspNetCore.Blazor.Components | ||
{ | ||
internal readonly struct CascadingParameterState | ||
{ | ||
private readonly static ConcurrentDictionary<Type, ReflectedCascadingParameterInfo[]> _cachedInfos | ||
= new ConcurrentDictionary<Type, ReflectedCascadingParameterInfo[]>(); | ||
|
||
public string LocalValueName { get; } | ||
public ICascadingValueComponent ValueSupplier { get; } | ||
|
||
public CascadingParameterState(string localValueName, ICascadingValueComponent valueSupplier) | ||
{ | ||
LocalValueName = localValueName; | ||
ValueSupplier = valueSupplier; | ||
} | ||
|
||
public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(ComponentState componentState) | ||
{ | ||
var componentType = componentState.Component.GetType(); | ||
var infos = GetReflectedCascadingParameterInfos(componentType); | ||
|
||
// For components known not to have any cascading parameters, bail out early | ||
if (infos == null) | ||
{ | ||
return null; | ||
} | ||
|
||
// Now try to find matches for each of the cascading parameters | ||
// Defer instantiation of the result list until we know there's at least one | ||
List<CascadingParameterState> resultStates = null; | ||
|
||
var numInfos = infos.Length; | ||
for (var infoIndex = 0; infoIndex < numInfos; infoIndex++) | ||
{ | ||
ref var info = ref infos[infoIndex]; | ||
var supplier = GetMatchingCascadingValueSupplier(info, componentState); | ||
if (supplier != null) | ||
{ | ||
if (resultStates == null) | ||
{ | ||
// Although not all parameters might be matched, we know the maximum number | ||
resultStates = new List<CascadingParameterState>(infos.Length - infoIndex); | ||
} | ||
|
||
resultStates.Add(new CascadingParameterState(info.ConsumerValueName, supplier)); | ||
} | ||
} | ||
|
||
return resultStates; | ||
} | ||
|
||
private static ICascadingValueComponent GetMatchingCascadingValueSupplier(in ReflectedCascadingParameterInfo info, ComponentState componentState) | ||
{ | ||
do | ||
{ | ||
if (componentState.Component is ICascadingValueComponent candidateSupplier | ||
&& candidateSupplier.CanSupplyValue(info.ValueType, info.SupplierValueName)) | ||
{ | ||
return candidateSupplier; | ||
} | ||
|
||
componentState = componentState.ParentComponentState; | ||
} while (componentState != null); | ||
|
||
// No match | ||
return null; | ||
} | ||
|
||
private static ReflectedCascadingParameterInfo[] GetReflectedCascadingParameterInfos(Type componentType) | ||
{ | ||
if (!_cachedInfos.TryGetValue(componentType, out var infos)) | ||
{ | ||
infos = CreateReflectedCascadingParameterInfos(componentType); | ||
_cachedInfos[componentType] = infos; | ||
} | ||
|
||
return infos; | ||
} | ||
|
||
private static ReflectedCascadingParameterInfo[] CreateReflectedCascadingParameterInfos(Type componentType) | ||
{ | ||
List<ReflectedCascadingParameterInfo> result = null; | ||
var candidateProps = ParameterCollectionExtensions.GetCandidateBindableProperties(componentType); | ||
foreach (var prop in candidateProps) | ||
{ | ||
var attribute = prop.GetCustomAttribute<CascadingParameterAttribute>(); | ||
if (attribute != null) | ||
{ | ||
if (result == null) | ||
{ | ||
result = new List<ReflectedCascadingParameterInfo>(); | ||
} | ||
|
||
result.Add(new ReflectedCascadingParameterInfo( | ||
prop.Name, | ||
prop.PropertyType, | ||
attribute.Name)); | ||
} | ||
} | ||
|
||
return result?.ToArray(); | ||
} | ||
|
||
readonly struct ReflectedCascadingParameterInfo | ||
{ | ||
public string ConsumerValueName { get; } | ||
public string SupplierValueName { get; } | ||
public Type ValueType { get; } | ||
|
||
public ReflectedCascadingParameterInfo( | ||
string consumerValueName, Type valueType, string supplierValueName) | ||
{ | ||
ConsumerValueName = consumerValueName; | ||
SupplierValueName = supplierValueName; | ||
ValueType = valueType; | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
// 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 | ||
|
||
/// <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; } | ||
|
||
object ICascadingValueComponent.CurrentValue => Value; | ||
|
||
/// <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; | ||
Value = default; | ||
ChildContent = null; | ||
Name = null; | ||
|
||
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 | ||
{ | ||
throw new ArgumentException($"The component '{nameof(CascadingValue<T>)}' does not accept a parameter with the name '{parameter.Name}'."); | ||
} | ||
} | ||
|
||
// 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 (_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); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// 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 System; | ||
|
||
namespace Microsoft.AspNetCore.Blazor.Components | ||
{ | ||
internal class ChangeDetection | ||
{ | ||
public static bool MayHaveChanged<T1, T2>(T1 oldValue, T2 newValue) | ||
{ | ||
var oldIsNotNull = oldValue != null; | ||
var newIsNotNull = newValue != null; | ||
if (oldIsNotNull != newIsNotNull) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seeing code like this really makes me wish we had constexpr utilities like |
||
{ | ||
return true; // One's null and the other isn't, so different | ||
} | ||
else if (oldIsNotNull) // i.e., both are not null (considering previous check) | ||
{ | ||
var oldValueType = oldValue.GetType(); | ||
var newValueType = newValue.GetType(); | ||
rynowak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (oldValueType != newValueType // Definitely different | ||
|| !IsKnownImmutableType(oldValueType) // Maybe different | ||
|| !oldValue.Equals(newValue)) // Somebody says they are different | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there really any value in this check about known immutable types? Is this here to avoid boxing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's because conceptually, the values we're comparing aren't really If this was public API, we'd have to be much clearer about the semantics of this check and do some naming review to try to communicate this better. We could still change the name, but since it's With these semantics in mind, the only case where we can return It's unfortunate that .NET doesn't have any native concept of (im)mutability. That limits us to hard-coding a set of commonly-used known-immutable types. |
||
{ | ||
return true; | ||
} | ||
} | ||
|
||
// By now we know either both are null, or they are the same immutable type | ||
// and ThatType::Equals says the two values are equal. | ||
return false; | ||
} | ||
|
||
// The contents of this list need to trade off false negatives against computation | ||
// time. So we don't want a huge list of types to check (or would have to move to | ||
// a hashtable lookup, which is differently expensive). It's better not to include | ||
// uncommon types here even if they are known to be immutable. | ||
private static bool IsKnownImmutableType(Type type) | ||
=> type.IsPrimitive | ||
|| type == typeof(string) | ||
|| type == typeof(DateTime) | ||
|| type == typeof(decimal); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// 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 System; | ||
|
||
namespace Microsoft.AspNetCore.Blazor.Components | ||
{ | ||
internal interface ICascadingValueComponent | ||
{ | ||
// This interface exists only so that CascadingParameterState has a way | ||
// to work with all CascadingValue<T> types regardless of T. | ||
|
||
bool CanSupplyValue(Type valueType, string valueName); | ||
|
||
object CurrentValue { get; } | ||
|
||
void Subscribe(ComponentState subscriber); | ||
|
||
void Unsubscribe(ComponentState subscriber); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seeing that the only usage of this in this commit is for boxed values, I'm curious to see how this will get used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I considered doing this without the generics, but TBH I didn't have any reason not to use them. Even if we can't benefit from having the types statically right now, maybe we will later. AFAICT it's not going to make any difference either way right now.
I know it means JITting different versions of the method for each combination of value types, but that's a limited fixed cost overall for any given app so I didn't think it was a drawback to worry about.
Let me know if you have concerns as I'm happy to change this. For now will proceed as-is.