Skip to content
This repository was archived by the owner on Feb 25, 2021. It is now read-only.

Commit a0fc692

Browse files
Cascading parameters (#1545)
* Add Provider component * Implement discovery and matching rules for tree parameters * Remove artificial component hierarchy unit tests now they are redundant * Refactor: Have RenderTreeFrame point to the ComponentState instead of IComponent ... so we can more quickly find associated tree param state without having to do lookups based on the componentId. Also rename AssignComponentId to AttachAndInitComponent to be more descriptive. * Refactor: Add shared code path for updating parameters so there's only one place to attach tree parameters Now framework code should no longer call IComponent.SetParameters directly, except if it knows it's definitely dealing with a root component. * Refactor: Simplify Parameter by making it hold the name/value directly This will be necessary for tree parameters, which don't correspond to any RenderTreeFrame * Refactor: Wrap ParameterEnumerator logic in extra level of iterator so we can also add one for iterating tree params * Extend ParameterEnumerator to list tree parameters too * Include tree parameters in SetParameters calls * Refactor: Move parameter change detection logic into separate utility class ... so we include dotnet/jsinterop#3 * Refactor: Move tree parameter tests from RendererTest.cs their own file * Have Provider re-render consumers when value changes. Unit tests in next commit. * Components that accept tree parameters need to snapshot their direct params for later replay * Empty commit to reawaken CI * CR: Make name matching case-insensitive * Refactor: Rename Provider/TreeParameter to CascadingValue/CascadingParameter * Add dedicated [CascadingParameter] attribute. Remove FromTree flag. * CR: CascadingParameterState cleanups * CR: Extra unit test * CR: arguments/parameters * CR: Enumerator improvements * Fix test
1 parent 15da07f commit a0fc692

24 files changed

+1487
-190
lines changed

src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public void AddComponent(Type componentType, string domElementSelector)
7474
domElementSelector,
7575
componentId);
7676

77-
component.SetParameters(ParameterCollection.Empty);
77+
RenderRootComponent(componentId);
7878
}
7979

8080
/// <summary>

src/Microsoft.AspNetCore.Blazor.Server/Circuits/RemoteRenderer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public void AddComponent(Type componentType, string domElementSelector)
7373
componentId);
7474
CaptureAsyncExceptions(attachComponentTask);
7575

76-
component.SetParameters(ParameterCollection.Empty);
76+
RenderRootComponent(componentId);
7777
}
7878

7979
/// <summary>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.AspNetCore.Blazor.Components
7+
{
8+
/// <summary>
9+
/// Denotes the target member as a cascading component parameter. Its value will be
10+
/// supplied by the closest ancestor <see cref="CascadingValue{T}"/> component that
11+
/// supplies values with a compatible type and name.
12+
/// </summary>
13+
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
14+
public sealed class CascadingParameterAttribute : Attribute
15+
{
16+
/// <summary>
17+
/// If specified, the parameter value will be supplied by the closest
18+
/// ancestor <see cref="CascadingValue{T}"/> that supplies a value with
19+
/// this name.
20+
///
21+
/// If null, the parameter value will be supplied by the closest ancestor
22+
/// <see cref="CascadingValue{T}"/> that supplies a value with a compatible
23+
/// type.
24+
/// </summary>
25+
public string Name { get; set; }
26+
}
27+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Blazor.Rendering;
5+
using System;
6+
using System.Collections.Concurrent;
7+
using System.Collections.Generic;
8+
using System.Reflection;
9+
10+
namespace Microsoft.AspNetCore.Blazor.Components
11+
{
12+
internal readonly struct CascadingParameterState
13+
{
14+
private readonly static ConcurrentDictionary<Type, ReflectedCascadingParameterInfo[]> _cachedInfos
15+
= new ConcurrentDictionary<Type, ReflectedCascadingParameterInfo[]>();
16+
17+
public string LocalValueName { get; }
18+
public ICascadingValueComponent ValueSupplier { get; }
19+
20+
public CascadingParameterState(string localValueName, ICascadingValueComponent valueSupplier)
21+
{
22+
LocalValueName = localValueName;
23+
ValueSupplier = valueSupplier;
24+
}
25+
26+
public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(ComponentState componentState)
27+
{
28+
var componentType = componentState.Component.GetType();
29+
var infos = GetReflectedCascadingParameterInfos(componentType);
30+
31+
// For components known not to have any cascading parameters, bail out early
32+
if (infos == null)
33+
{
34+
return null;
35+
}
36+
37+
// Now try to find matches for each of the cascading parameters
38+
// Defer instantiation of the result list until we know there's at least one
39+
List<CascadingParameterState> resultStates = null;
40+
41+
var numInfos = infos.Length;
42+
for (var infoIndex = 0; infoIndex < numInfos; infoIndex++)
43+
{
44+
ref var info = ref infos[infoIndex];
45+
var supplier = GetMatchingCascadingValueSupplier(info, componentState);
46+
if (supplier != null)
47+
{
48+
if (resultStates == null)
49+
{
50+
// Although not all parameters might be matched, we know the maximum number
51+
resultStates = new List<CascadingParameterState>(infos.Length - infoIndex);
52+
}
53+
54+
resultStates.Add(new CascadingParameterState(info.ConsumerValueName, supplier));
55+
}
56+
}
57+
58+
return resultStates;
59+
}
60+
61+
private static ICascadingValueComponent GetMatchingCascadingValueSupplier(in ReflectedCascadingParameterInfo info, ComponentState componentState)
62+
{
63+
do
64+
{
65+
if (componentState.Component is ICascadingValueComponent candidateSupplier
66+
&& candidateSupplier.CanSupplyValue(info.ValueType, info.SupplierValueName))
67+
{
68+
return candidateSupplier;
69+
}
70+
71+
componentState = componentState.ParentComponentState;
72+
} while (componentState != null);
73+
74+
// No match
75+
return null;
76+
}
77+
78+
private static ReflectedCascadingParameterInfo[] GetReflectedCascadingParameterInfos(Type componentType)
79+
{
80+
if (!_cachedInfos.TryGetValue(componentType, out var infos))
81+
{
82+
infos = CreateReflectedCascadingParameterInfos(componentType);
83+
_cachedInfos[componentType] = infos;
84+
}
85+
86+
return infos;
87+
}
88+
89+
private static ReflectedCascadingParameterInfo[] CreateReflectedCascadingParameterInfos(Type componentType)
90+
{
91+
List<ReflectedCascadingParameterInfo> result = null;
92+
var candidateProps = ParameterCollectionExtensions.GetCandidateBindableProperties(componentType);
93+
foreach (var prop in candidateProps)
94+
{
95+
var attribute = prop.GetCustomAttribute<CascadingParameterAttribute>();
96+
if (attribute != null)
97+
{
98+
if (result == null)
99+
{
100+
result = new List<ReflectedCascadingParameterInfo>();
101+
}
102+
103+
result.Add(new ReflectedCascadingParameterInfo(
104+
prop.Name,
105+
prop.PropertyType,
106+
attribute.Name));
107+
}
108+
}
109+
110+
return result?.ToArray();
111+
}
112+
113+
readonly struct ReflectedCascadingParameterInfo
114+
{
115+
public string ConsumerValueName { get; }
116+
public string SupplierValueName { get; }
117+
public Type ValueType { get; }
118+
119+
public ReflectedCascadingParameterInfo(
120+
string consumerValueName, Type valueType, string supplierValueName)
121+
{
122+
ConsumerValueName = consumerValueName;
123+
SupplierValueName = supplierValueName;
124+
ValueType = valueType;
125+
}
126+
}
127+
}
128+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Blazor.Rendering;
5+
using Microsoft.AspNetCore.Blazor.RenderTree;
6+
using System;
7+
using System.Collections.Generic;
8+
9+
namespace Microsoft.AspNetCore.Blazor.Components
10+
{
11+
/// <summary>
12+
/// A component that provides a cascading value to all descendant components.
13+
/// </summary>
14+
public class CascadingValue<T> : ICascadingValueComponent, IComponent
15+
{
16+
private RenderHandle _renderHandle;
17+
private HashSet<ComponentState> _subscribers; // Lazily instantiated
18+
19+
/// <summary>
20+
/// The content to which the value should be provided.
21+
/// </summary>
22+
[Parameter] private RenderFragment ChildContent { get; set; }
23+
24+
/// <summary>
25+
/// The value to be provided.
26+
/// </summary>
27+
[Parameter] private T Value { get; set; }
28+
29+
/// <summary>
30+
/// Optionally gives a name to the provided value. Descendant components
31+
/// will be able to receive the value by specifying this name.
32+
///
33+
/// If no name is specified, then descendant components will receive the
34+
/// value based the type of value they are requesting.
35+
/// </summary>
36+
[Parameter] private string Name { get; set; }
37+
38+
object ICascadingValueComponent.CurrentValue => Value;
39+
40+
/// <inheritdoc />
41+
public void Init(RenderHandle renderHandle)
42+
{
43+
_renderHandle = renderHandle;
44+
}
45+
46+
/// <inheritdoc />
47+
public void SetParameters(ParameterCollection parameters)
48+
{
49+
// Implementing the parameter binding manually, instead of just calling
50+
// parameters.AssignToProperties(this), is just a very slight perf optimization
51+
// and makes it simpler impose rules about the params being required or not.
52+
53+
var hasSuppliedValue = false;
54+
var previousValue = Value;
55+
Value = default;
56+
ChildContent = null;
57+
Name = null;
58+
59+
foreach (var parameter in parameters)
60+
{
61+
if (parameter.Name.Equals(nameof(Value), StringComparison.OrdinalIgnoreCase))
62+
{
63+
Value = (T)parameter.Value;
64+
hasSuppliedValue = true;
65+
}
66+
else if (parameter.Name.Equals(nameof(ChildContent), StringComparison.OrdinalIgnoreCase))
67+
{
68+
ChildContent = (RenderFragment)parameter.Value;
69+
}
70+
else if (parameter.Name.Equals(nameof(Name), StringComparison.OrdinalIgnoreCase))
71+
{
72+
Name = (string)parameter.Value;
73+
if (string.IsNullOrEmpty(Name))
74+
{
75+
throw new ArgumentException($"The parameter '{nameof(Name)}' for component '{nameof(CascadingValue<T>)}' does not allow null or empty values.");
76+
}
77+
}
78+
else
79+
{
80+
throw new ArgumentException($"The component '{nameof(CascadingValue<T>)}' does not accept a parameter with the name '{parameter.Name}'.");
81+
}
82+
}
83+
84+
// It's OK for the value to be null, but some "Value" param must be suppled
85+
// because it serves no useful purpose to have a <CascadingValue> otherwise.
86+
if (!hasSuppliedValue)
87+
{
88+
throw new ArgumentException($"Missing required parameter '{nameof(Value)}' for component '{nameof(Parameter)}'.");
89+
}
90+
91+
// Rendering is most efficient when things are queued from rootmost to leafmost.
92+
// Given a components A (parent) -> B (child), you want them to be queued in order
93+
// [A, B] because if you had [B, A], then the render for A might change B's params
94+
// making it render again, so you'd render [B, A, B], which is wasteful.
95+
// At some point we might consider making the render queue actually enforce this
96+
// ordering during insertion.
97+
//
98+
// For the CascadingValue component, this observation is why it's important to render
99+
// ourself before notifying subscribers (which can be grandchildren or deeper).
100+
// If we rerendered subscribers first, then our own subsequent render might cause an
101+
// further update that makes those nested subscribers get rendered twice.
102+
_renderHandle.Render(Render);
103+
104+
if (_subscribers != null && ChangeDetection.MayHaveChanged(previousValue, Value))
105+
{
106+
NotifySubscribers();
107+
}
108+
}
109+
110+
bool ICascadingValueComponent.CanSupplyValue(Type requestedType, string requestedName)
111+
{
112+
if (!requestedType.IsAssignableFrom(typeof(T)))
113+
{
114+
return false;
115+
}
116+
117+
return (requestedName == null && Name == null) // Match on type alone
118+
|| string.Equals(requestedName, Name, StringComparison.OrdinalIgnoreCase); // Also match on name
119+
}
120+
121+
void ICascadingValueComponent.Subscribe(ComponentState subscriber)
122+
{
123+
if (_subscribers == null)
124+
{
125+
_subscribers = new HashSet<ComponentState>();
126+
}
127+
128+
_subscribers.Add(subscriber);
129+
}
130+
131+
void ICascadingValueComponent.Unsubscribe(ComponentState subscriber)
132+
{
133+
_subscribers.Remove(subscriber);
134+
}
135+
136+
private void NotifySubscribers()
137+
{
138+
foreach (var subscriber in _subscribers)
139+
{
140+
subscriber.NotifyCascadingValueChanged();
141+
}
142+
}
143+
144+
private void Render(RenderTreeBuilder builder)
145+
{
146+
builder.AddContent(0, ChildContent);
147+
}
148+
}
149+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.AspNetCore.Blazor.Components
7+
{
8+
internal class ChangeDetection
9+
{
10+
public static bool MayHaveChanged<T1, T2>(T1 oldValue, T2 newValue)
11+
{
12+
var oldIsNotNull = oldValue != null;
13+
var newIsNotNull = newValue != null;
14+
if (oldIsNotNull != newIsNotNull)
15+
{
16+
return true; // One's null and the other isn't, so different
17+
}
18+
else if (oldIsNotNull) // i.e., both are not null (considering previous check)
19+
{
20+
var oldValueType = oldValue.GetType();
21+
var newValueType = newValue.GetType();
22+
if (oldValueType != newValueType // Definitely different
23+
|| !IsKnownImmutableType(oldValueType) // Maybe different
24+
|| !oldValue.Equals(newValue)) // Somebody says they are different
25+
{
26+
return true;
27+
}
28+
}
29+
30+
// By now we know either both are null, or they are the same immutable type
31+
// and ThatType::Equals says the two values are equal.
32+
return false;
33+
}
34+
35+
// The contents of this list need to trade off false negatives against computation
36+
// time. So we don't want a huge list of types to check (or would have to move to
37+
// a hashtable lookup, which is differently expensive). It's better not to include
38+
// uncommon types here even if they are known to be immutable.
39+
private static bool IsKnownImmutableType(Type type)
40+
=> type.IsPrimitive
41+
|| type == typeof(string)
42+
|| type == typeof(DateTime)
43+
|| type == typeof(decimal);
44+
}
45+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Blazor.Rendering;
5+
using System;
6+
7+
namespace Microsoft.AspNetCore.Blazor.Components
8+
{
9+
internal interface ICascadingValueComponent
10+
{
11+
// This interface exists only so that CascadingParameterState has a way
12+
// to work with all CascadingValue<T> types regardless of T.
13+
14+
bool CanSupplyValue(Type valueType, string valueName);
15+
16+
object CurrentValue { get; }
17+
18+
void Subscribe(ComponentState subscriber);
19+
20+
void Unsubscribe(ComponentState subscriber);
21+
}
22+
}

0 commit comments

Comments
 (0)