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 https://github.com/dotnet/jsinterop/pull/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
This commit is contained in:
Steve Sanderson 2018-10-15 11:11:46 +01:00 committed by GitHub
parent fa2b61773a
commit 211ad636fd
24 changed files with 1487 additions and 190 deletions

View File

@ -74,7 +74,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
domElementSelector, domElementSelector,
componentId); componentId);
component.SetParameters(ParameterCollection.Empty); RenderRootComponent(componentId);
} }
/// <summary> /// <summary>

View File

@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
componentId); componentId);
CaptureAsyncExceptions(attachComponentTask); CaptureAsyncExceptions(attachComponentTask);
component.SetParameters(ParameterCollection.Empty); RenderRootComponent(componentId);
} }
/// <summary> /// <summary>

View File

@ -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; }
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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)
{
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();
if (oldValueType != newValueType // Definitely different
|| !IsKnownImmutableType(oldValueType) // Maybe different
|| !oldValue.Equals(newValue)) // Somebody says they are different
{
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);
}
}

View File

@ -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);
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Blazor.RenderTree;
namespace Microsoft.AspNetCore.Blazor.Components namespace Microsoft.AspNetCore.Blazor.Components
{ {
/// <summary> /// <summary>
@ -11,31 +9,20 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// </summary> /// </summary>
public readonly struct Parameter public readonly struct Parameter
{ {
private readonly RenderTreeFrame[] _frames;
private readonly int _frameIndex;
internal Parameter(RenderTreeFrame[] frames, int currentIndex)
{
_frames = frames;
_frameIndex = currentIndex;
}
/// <summary> /// <summary>
/// Gets the name of the parameter. /// Gets the name of the parameter.
/// </summary> /// </summary>
public string Name public string Name { get; }
=> _frames[_frameIndex].AttributeName;
/// <summary> /// <summary>
/// Gets the value of the parameter. /// Gets the value being supplied for the parameter.
/// </summary> /// </summary>
public object Value public object Value { get; }
=> _frames[_frameIndex].AttributeValue;
/// <summary> internal Parameter(string name, object value)
/// Gets the <see cref="RenderTreeFrame" /> that holds the parameter name and value. {
/// </summary> Name = name;
internal ref RenderTreeFrame Frame Value = value;
=> ref _frames[_frameIndex]; }
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -19,15 +19,22 @@ namespace Microsoft.AspNetCore.Blazor.Components
}; };
private static readonly ParameterCollection _emptyCollection private static readonly ParameterCollection _emptyCollection
= new ParameterCollection(_emptyCollectionFrames, 0); = new ParameterCollection(_emptyCollectionFrames, 0, null);
private readonly RenderTreeFrame[] _frames; private readonly RenderTreeFrame[] _frames;
private readonly int _ownerIndex; private readonly int _ownerIndex;
private readonly IReadOnlyList<CascadingParameterState> _cascadingParametersOrNull;
internal ParameterCollection(RenderTreeFrame[] frames, int ownerIndex) internal ParameterCollection(RenderTreeFrame[] frames, int ownerIndex)
: this(frames, ownerIndex, null)
{
}
private ParameterCollection(RenderTreeFrame[] frames, int ownerIndex, IReadOnlyList<CascadingParameterState> cascadingParametersOrNull)
{ {
_frames = frames; _frames = frames;
_ownerIndex = ownerIndex; _ownerIndex = ownerIndex;
_cascadingParametersOrNull = cascadingParametersOrNull;
} }
/// <summary> /// <summary>
@ -40,7 +47,7 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// </summary> /// </summary>
/// <returns>The enumerator.</returns> /// <returns>The enumerator.</returns>
public ParameterEnumerator GetEnumerator() public ParameterEnumerator GetEnumerator()
=> new ParameterEnumerator(_frames, _ownerIndex); => new ParameterEnumerator(_frames, _ownerIndex, _cascadingParametersOrNull);
/// <summary> /// <summary>
/// Gets the value of the parameter with the specified name. /// Gets the value of the parameter with the specified name.
@ -99,6 +106,9 @@ namespace Microsoft.AspNetCore.Blazor.Components
return result; return result;
} }
internal ParameterCollection WithCascadingParameters(IReadOnlyList<CascadingParameterState> cascadingParameters)
=> new ParameterCollection(_frames, _ownerIndex, cascadingParameters);
// It's internal because there isn't a known use case for user code comparing // It's internal because there isn't a known use case for user code comparing
// ParameterCollection instances, and even if there was, it's unlikely it should // ParameterCollection instances, and even if there was, it's unlikely it should
// use these equality rules which are designed for their effect on rendering. // use these equality rules which are designed for their effect on rendering.
@ -156,40 +166,35 @@ namespace Microsoft.AspNetCore.Blazor.Components
var oldValue = oldFrame.AttributeValue; var oldValue = oldFrame.AttributeValue;
var newValue = newFrame.AttributeValue; var newValue = newFrame.AttributeValue;
var oldIsNotNull = oldValue != null; if (ChangeDetection.MayHaveChanged(oldValue, newValue))
var newIsNotNull = newValue != null;
if (oldIsNotNull != newIsNotNull)
{ {
return false; // One's null and the other isn't, so different return false;
}
else if (oldIsNotNull) // i.e., both are not null (considering previous check)
{
var oldValueType = oldValue.GetType();
var newValueType = newValue.GetType();
if (oldValueType != newValueType // Definitely different
|| !IsKnownImmutableType(oldValueType) // Maybe different
|| !oldValue.Equals(newValue)) // Somebody says they are different
{
return false;
}
}
else
{
// Both null, hence equal, so continue
} }
} }
} }
} }
} }
// The contents of this list need to trade off false negatives against computation internal void CaptureSnapshot(ArrayBuilder<RenderTreeFrame> builder)
// 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 builder.Clear();
// uncommon types here even if they are known to be immutable.
private bool IsKnownImmutableType(Type type) var numEntries = 0;
=> type.IsPrimitive foreach (var entry in this)
|| type == typeof(string) {
|| type == typeof(DateTime) numEntries++;
|| type == typeof(decimal); }
// We need to prefix the captured frames with an "owner" frame that
// describes the length of the buffer so that ParameterCollection
// knows how far to iterate through it.
var owner = RenderTreeFrame.PlaceholderChildComponentWithSubtreeLength(1 + numEntries);
builder.Append(owner);
if (numEntries > 0)
{
builder.Append(_frames, _ownerIndex + 1, numEntries);
}
}
} }
} }

View File

@ -1,12 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Blazor.Reflection; using Microsoft.AspNetCore.Blazor.Reflection;
using Microsoft.AspNetCore.Blazor.RenderTree;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Reflection; using System.Reflection;
namespace Microsoft.AspNetCore.Blazor.Components namespace Microsoft.AspNetCore.Blazor.Components
@ -18,7 +16,7 @@ namespace Microsoft.AspNetCore.Blazor.Components
{ {
private const BindingFlags _bindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase; private const BindingFlags _bindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase;
private delegate void WriteParameterAction(ref RenderTreeFrame frame, object target); private delegate void WriteParameterAction(object target, object parameterValue);
private readonly static IDictionary<Type, IDictionary<string, WriteParameterAction>> _cachedParameterWriters private readonly static IDictionary<Type, IDictionary<string, WriteParameterAction>> _cachedParameterWriters
= new ConcurrentDictionary<Type, IDictionary<string, WriteParameterAction>>(); = new ConcurrentDictionary<Type, IDictionary<string, WriteParameterAction>>();
@ -55,7 +53,7 @@ namespace Microsoft.AspNetCore.Blazor.Components
try try
{ {
parameterWriter(ref parameter.Frame, target); parameterWriter(target, parameter.Value);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -66,12 +64,22 @@ namespace Microsoft.AspNetCore.Blazor.Components
} }
} }
internal static IEnumerable<PropertyInfo> GetCandidateBindableProperties(Type targetType)
=> MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags);
private static IDictionary<string, WriteParameterAction> CreateParameterWriters(Type targetType) private static IDictionary<string, WriteParameterAction> CreateParameterWriters(Type targetType)
{ {
var result = new Dictionary<string, WriteParameterAction>(StringComparer.OrdinalIgnoreCase); var result = new Dictionary<string, WriteParameterAction>(StringComparer.OrdinalIgnoreCase);
foreach (var propertyInfo in GetBindableProperties(targetType)) foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
{ {
var shouldCreateWriter = propertyInfo.IsDefined(typeof(ParameterAttribute))
|| propertyInfo.IsDefined(typeof(CascadingParameterAttribute));
if (!shouldCreateWriter)
{
continue;
}
var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo); var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo);
var propertyName = propertyInfo.Name; var propertyName = propertyInfo.Name;
@ -82,19 +90,15 @@ namespace Microsoft.AspNetCore.Blazor.Components
$"name '{propertyName.ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique."); $"name '{propertyName.ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique.");
} }
result.Add(propertyName, (ref RenderTreeFrame frame, object target) => result.Add(propertyName, (object target, object parameterValue) =>
{ {
propertySetter.SetValue(target, frame.AttributeValue); propertySetter.SetValue(target, parameterValue);
}); });
} }
return result; return result;
} }
private static IEnumerable<PropertyInfo> GetBindableProperties(Type targetType)
=> MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags)
.Where(property => property.IsDefined(typeof(ParameterAttribute)));
private static void ThrowForUnknownIncomingParameterName(Type targetType, string parameterName) private static void ThrowForUnknownIncomingParameterName(Type targetType, string parameterName)
{ {
// We know we're going to throw by this stage, so it doesn't matter that the following // We know we're going to throw by this stage, so it doesn't matter that the following
@ -102,11 +106,11 @@ namespace Microsoft.AspNetCore.Blazor.Components
var propertyInfo = targetType.GetProperty(parameterName, _bindablePropertyFlags); var propertyInfo = targetType.GetProperty(parameterName, _bindablePropertyFlags);
if (propertyInfo != null) if (propertyInfo != null)
{ {
if (!propertyInfo.IsDefined(typeof(ParameterAttribute))) if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) && !propertyInfo.IsDefined(typeof(CascadingParameterAttribute)))
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " + $"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " +
$"but it does not have [{nameof(ParameterAttribute)}] applied."); $"but it does not have [{nameof(ParameterAttribute)}] or [{nameof(CascadingParameterAttribute)}] applied.");
} }
else else
{ {

View File

@ -1,8 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Blazor.RenderTree; using Microsoft.AspNetCore.Blazor.RenderTree;
using System; using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Blazor.Components namespace Microsoft.AspNetCore.Blazor.Components
{ {
@ -11,49 +12,126 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// </summary> /// </summary>
public struct ParameterEnumerator public struct ParameterEnumerator
{ {
private readonly RenderTreeFrame[] _frames; private RenderTreeFrameParameterEnumerator _directParamsEnumerator;
private readonly int _ownerIndex; private CascadingParameterEnumerator _cascadingParameterEnumerator;
private readonly int _ownerDescendantsEndIndexExcl; private bool _isEnumeratingDirectParams;
private int _currentIndex;
internal ParameterEnumerator(RenderTreeFrame[] frames, int ownerIndex) internal ParameterEnumerator(RenderTreeFrame[] frames, int ownerIndex, IReadOnlyList<CascadingParameterState> cascadingParameters)
{ {
_frames = frames; _directParamsEnumerator = new RenderTreeFrameParameterEnumerator(frames, ownerIndex);
_ownerIndex = ownerIndex; _cascadingParameterEnumerator = new CascadingParameterEnumerator(cascadingParameters);
_ownerDescendantsEndIndexExcl = ownerIndex + _frames[ownerIndex].ElementSubtreeLength; _isEnumeratingDirectParams = true;
_currentIndex = ownerIndex;
} }
/// <summary> /// <summary>
/// Gets the current value of the enumerator. /// Gets the current value of the enumerator.
/// </summary> /// </summary>
public Parameter Current public Parameter Current => _isEnumeratingDirectParams
=> _currentIndex > _ownerIndex ? _directParamsEnumerator.Current
? new Parameter(_frames, _currentIndex) : _cascadingParameterEnumerator.Current;
: throw new InvalidOperationException("Iteration has not yet started.");
/// <summary> /// <summary>
/// Instructs the enumerator to move to the next value in the sequence. /// Instructs the enumerator to move to the next value in the sequence.
/// </summary> /// </summary>
/// <returns></returns> /// <returns>A flag to indicate whether or not there is a next value.</returns>
public bool MoveNext() public bool MoveNext()
{ {
// Stop iteration if you get to the end of the owner's descendants... if (_isEnumeratingDirectParams)
var nextIndex = _currentIndex + 1;
if (nextIndex == _ownerDescendantsEndIndexExcl)
{ {
return false; if (_directParamsEnumerator.MoveNext())
{
return true;
}
else
{
_isEnumeratingDirectParams = false;
}
} }
// ... or if you get to its first non-attribute descendant (because attributes return _cascadingParameterEnumerator.MoveNext();
// are always before any other type of descendant) }
if (_frames[nextIndex].FrameType != RenderTreeFrameType.Attribute)
struct RenderTreeFrameParameterEnumerator
{
private readonly RenderTreeFrame[] _frames;
private readonly int _ownerIndex;
private readonly int _ownerDescendantsEndIndexExcl;
private int _currentIndex;
private Parameter _current;
internal RenderTreeFrameParameterEnumerator(RenderTreeFrame[] frames, int ownerIndex)
{ {
return false; _frames = frames;
_ownerIndex = ownerIndex;
_ownerDescendantsEndIndexExcl = ownerIndex + _frames[ownerIndex].ElementSubtreeLength;
_currentIndex = ownerIndex;
_current = default;
} }
_currentIndex = nextIndex; public Parameter Current => _current;
return true;
public bool MoveNext()
{
// Stop iteration if you get to the end of the owner's descendants...
var nextIndex = _currentIndex + 1;
if (nextIndex == _ownerDescendantsEndIndexExcl)
{
return false;
}
// ... or if you get to its first non-attribute descendant (because attributes
// are always before any other type of descendant)
if (_frames[nextIndex].FrameType != RenderTreeFrameType.Attribute)
{
return false;
}
_currentIndex = nextIndex;
ref var frame = ref _frames[_currentIndex];
_current = new Parameter(frame.AttributeName, frame.AttributeValue);
return true;
}
}
struct CascadingParameterEnumerator
{
private readonly IReadOnlyList<CascadingParameterState> _cascadingParameters;
private int _currentIndex;
private Parameter _current;
public CascadingParameterEnumerator(IReadOnlyList<CascadingParameterState> cascadingParameters)
{
_cascadingParameters = cascadingParameters;
_currentIndex = -1;
_current = default;
}
public Parameter Current => _current;
public bool MoveNext()
{
// Bail out early if there are no cascading parameters
if (_cascadingParameters == null)
{
return false;
}
var nextIndex = _currentIndex + 1;
if (nextIndex < _cascadingParameters.Count)
{
_currentIndex = nextIndex;
var state = _cascadingParameters[_currentIndex];
_current = new Parameter(state.LocalValueName, state.ValueSupplier.CurrentValue);
return true;
}
else
{
return false;
}
}
} }
} }
} }

View File

@ -267,11 +267,10 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
var newTree = diffContext.NewTree; var newTree = diffContext.NewTree;
ref var oldComponentFrame = ref oldTree[oldComponentIndex]; ref var oldComponentFrame = ref oldTree[oldComponentIndex];
ref var newComponentFrame = ref newTree[newComponentIndex]; ref var newComponentFrame = ref newTree[newComponentIndex];
var componentId = oldComponentFrame.ComponentId; var componentState = oldComponentFrame.ComponentState;
var componentInstance = oldComponentFrame.Component;
// Preserve the actual componentInstance // Preserve the actual componentInstance
newComponentFrame = newComponentFrame.WithComponentInstance(componentId, componentInstance); newComponentFrame = newComponentFrame.WithComponent(componentState);
// As an important rendering optimization, we want to skip parameter update // As an important rendering optimization, we want to skip parameter update
// notifications if we know for sure they haven't changed/mutated. The // notifications if we know for sure they haven't changed/mutated. The
@ -287,7 +286,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
var newParameters = new ParameterCollection(newTree, newComponentIndex); var newParameters = new ParameterCollection(newTree, newComponentIndex);
if (!newParameters.DefinitelyEquals(oldParameters)) if (!newParameters.DefinitelyEquals(oldParameters))
{ {
componentInstance.SetParameters(newParameters); componentState.SetDirectParameters(newParameters);
} }
} }
@ -633,18 +632,18 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
var frames = diffContext.NewTree; var frames = diffContext.NewTree;
ref var frame = ref frames[frameIndex]; ref var frame = ref frames[frameIndex];
if (frame.Component != null) if (frame.ComponentState != null)
{ {
throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}"); throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}");
} }
var parentComponentId = diffContext.ComponentId; var parentComponentId = diffContext.ComponentId;
diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId); diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId);
var childComponentInstance = frame.Component; var childComponentState = frame.ComponentState;
// Set initial parameters // Set initial parameters
var initialParameters = new ParameterCollection(frames, frameIndex); var initialParameters = new ParameterCollection(frames, frameIndex);
childComponentInstance.SetParameters(initialParameters); childComponentState.SetDirectParameters(initialParameters);
} }
private static void InitializeNewAttributeFrame(ref DiffContext diffContext, ref RenderTreeFrame newFrame) private static void InitializeNewAttributeFrame(ref DiffContext diffContext, ref RenderTreeFrame newFrame)
@ -692,7 +691,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
for (var i = startIndex; i < endIndexExcl; i++) for (var i = startIndex; i < endIndexExcl; i++)
{ {
ref var frame = ref frames[i]; ref var frame = ref frames[i];
if (frame.FrameType == RenderTreeFrameType.Component && frame.Component != null) if (frame.FrameType == RenderTreeFrameType.Component && frame.ComponentState != null)
{ {
batchBuilder.ComponentDisposalQueue.Enqueue(frame.ComponentId); batchBuilder.ComponentDisposalQueue.Enqueue(frame.ComponentId);
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Rendering;
namespace Microsoft.AspNetCore.Blazor.RenderTree namespace Microsoft.AspNetCore.Blazor.RenderTree
{ {
@ -112,11 +113,17 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// </summary> /// </summary>
[FieldOffset(16)] public readonly Type ComponentType; [FieldOffset(16)] public readonly Type ComponentType;
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Component"/>,
/// gets the child component state object. Otherwise, the value is undefined.
/// </summary>
[FieldOffset(24)] internal readonly ComponentState ComponentState;
/// <summary> /// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Component"/>, /// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Component"/>,
/// gets the child component instance. Otherwise, the value is undefined. /// gets the child component instance. Otherwise, the value is undefined.
/// </summary> /// </summary>
[FieldOffset(24)] public readonly IComponent Component; public IComponent Component => ComponentState?.Component;
// -------------------------------------------------------------------------------- // --------------------------------------------------------------------------------
// RenderTreeFrameType.Region // RenderTreeFrameType.Region
@ -195,11 +202,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
ComponentSubtreeLength = componentSubtreeLength; ComponentSubtreeLength = componentSubtreeLength;
} }
private RenderTreeFrame(int sequence, Type componentType, int subtreeLength, int componentId, IComponent component) private RenderTreeFrame(int sequence, Type componentType, int subtreeLength, ComponentState componentState)
: this(sequence, componentType, subtreeLength) : this(sequence, componentType, subtreeLength)
{ {
ComponentId = componentId; ComponentId = componentState.ComponentId;
Component = component; ComponentState = componentState;
} }
private RenderTreeFrame(int sequence, string textContent) private RenderTreeFrame(int sequence, string textContent)
@ -283,6 +290,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
internal static RenderTreeFrame ChildComponent(int sequence, Type componentType) internal static RenderTreeFrame ChildComponent(int sequence, Type componentType)
=> new RenderTreeFrame(sequence, componentType, 0); => new RenderTreeFrame(sequence, componentType, 0);
internal static RenderTreeFrame PlaceholderChildComponentWithSubtreeLength(int subtreeLength)
=> new RenderTreeFrame(0, typeof(IComponent), subtreeLength);
internal static RenderTreeFrame Region(int sequence) internal static RenderTreeFrame Region(int sequence)
=> new RenderTreeFrame(sequence, regionSubtreeLength: 0); => new RenderTreeFrame(sequence, regionSubtreeLength: 0);
@ -301,8 +311,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
internal RenderTreeFrame WithAttributeSequence(int sequence) internal RenderTreeFrame WithAttributeSequence(int sequence)
=> new RenderTreeFrame(sequence, attributeName: AttributeName, attributeValue: AttributeValue); => new RenderTreeFrame(sequence, attributeName: AttributeName, attributeValue: AttributeValue);
internal RenderTreeFrame WithComponentInstance(int componentId, IComponent component) internal RenderTreeFrame WithComponent(ComponentState componentState)
=> new RenderTreeFrame(Sequence, ComponentType, ComponentSubtreeLength, componentId, component); => new RenderTreeFrame(Sequence, ComponentType, ComponentSubtreeLength, componentState);
internal RenderTreeFrame WithAttributeEventHandlerId(int eventHandlerId) internal RenderTreeFrame WithAttributeEventHandlerId(int eventHandlerId)
=> new RenderTreeFrame(Sequence, AttributeName, AttributeValue, eventHandlerId); => new RenderTreeFrame(Sequence, AttributeName, AttributeValue, eventHandlerId);

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.RenderTree; using Microsoft.AspNetCore.Blazor.RenderTree;
@ -18,10 +19,16 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
private readonly ComponentState _parentComponentState; private readonly ComponentState _parentComponentState;
private readonly IComponent _component; private readonly IComponent _component;
private readonly Renderer _renderer; private readonly Renderer _renderer;
private readonly IReadOnlyList<CascadingParameterState> _cascadingParameters;
private RenderTreeBuilder _renderTreeBuilderCurrent; private RenderTreeBuilder _renderTreeBuilderCurrent;
private RenderTreeBuilder _renderTreeBuilderPrevious; private RenderTreeBuilder _renderTreeBuilderPrevious;
private ArrayBuilder<RenderTreeFrame> _latestDirectParametersSnapshot; // Lazily instantiated
private bool _componentWasDisposed; private bool _componentWasDisposed;
public int ComponentId => _componentId;
public IComponent Component => _component;
public ComponentState ParentComponentState => _parentComponentState;
/// <summary> /// <summary>
/// Constructs an instance of <see cref="ComponentState"/>. /// Constructs an instance of <see cref="ComponentState"/>.
/// </summary> /// </summary>
@ -35,8 +42,14 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
_parentComponentState = parentComponentState; _parentComponentState = parentComponentState;
_component = component ?? throw new ArgumentNullException(nameof(component)); _component = component ?? throw new ArgumentNullException(nameof(component));
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
_cascadingParameters = CascadingParameterState.FindCascadingParameters(this);
_renderTreeBuilderCurrent = new RenderTreeBuilder(renderer); _renderTreeBuilderCurrent = new RenderTreeBuilder(renderer);
_renderTreeBuilderPrevious = new RenderTreeBuilder(renderer); _renderTreeBuilderPrevious = new RenderTreeBuilder(renderer);
if (_cascadingParameters != null)
{
AddCascadingParameterSubscriptions();
}
} }
public void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment) public void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
@ -74,6 +87,11 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
} }
RenderTreeDiffBuilder.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames()); RenderTreeDiffBuilder.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames());
if (_cascadingParameters != null)
{
RemoveCascadingParameterSubscriptions();
}
} }
public void DispatchEvent(EventHandlerInvoker binding, UIEventArgs eventArgs) public void DispatchEvent(EventHandlerInvoker binding, UIEventArgs eventArgs)
@ -93,9 +111,61 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
public void NotifyRenderCompleted() public void NotifyRenderCompleted()
=> (_component as IHandleAfterRender)?.OnAfterRender(); => (_component as IHandleAfterRender)?.OnAfterRender();
// TODO: Remove this once we can remove TemporaryGetParentComponentIdForTest public void SetDirectParameters(ParameterCollection parameters)
// from Renderer.cs and corresponding unit test. {
public int? TemporaryParentComponentIdForTests // Note: We should be careful to ensure that the framework never calls
=> _parentComponentState?._componentId; // IComponent.SetParameters directly elsewhere. We should only call it
// via ComponentState.SetParameters (or NotifyCascadingValueChanged below).
// If we bypass this, the component won't receive the cascading parameters nor
// will it update its snapshot of direct parameters.
// TODO: Consider adding a "static" mode for tree params in which we don't
// subscribe for updates, and hence don't have to do any of the parameter
// snapshotting. This would be useful for things like FormContext that aren't
// going to change.
if (_cascadingParameters != null)
{
// We may need to replay these direct parameters later (in NotifyCascadingValueChanged),
// but we can't guarantee that the original underlying data won't have mutated in the
// meantime, since it's just an index into the parent's RenderTreeFrames buffer.
if (_latestDirectParametersSnapshot == null)
{
_latestDirectParametersSnapshot = new ArrayBuilder<RenderTreeFrame>();
}
parameters.CaptureSnapshot(_latestDirectParametersSnapshot);
parameters = parameters.WithCascadingParameters(_cascadingParameters);
}
Component.SetParameters(parameters);
}
public void NotifyCascadingValueChanged()
{
var directParams = _latestDirectParametersSnapshot != null
? new ParameterCollection(_latestDirectParametersSnapshot.Buffer, 0)
: ParameterCollection.Empty;
var allParams = directParams.WithCascadingParameters(_cascadingParameters);
Component.SetParameters(allParams);
}
private void AddCascadingParameterSubscriptions()
{
var numCascadingParameters = _cascadingParameters.Count;
for (var i = 0; i < numCascadingParameters; i++)
{
_cascadingParameters[i].ValueSupplier.Subscribe(this);
}
}
private void RemoveCascadingParameterSubscriptions()
{
var numCascadingParameters = _cascadingParameters.Count;
for (var i = 0; i < numCascadingParameters; i++)
{
_cascadingParameters[i].ValueSupplier.Unsubscribe(this);
}
}
} }
} }

View File

@ -49,16 +49,28 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
/// <param name="component">The component.</param> /// <param name="component">The component.</param>
/// <returns>The component's assigned identifier.</returns> /// <returns>The component's assigned identifier.</returns>
protected int AssignRootComponentId(IComponent component) protected int AssignRootComponentId(IComponent component)
=> AssignComponentId(component, -1); => AttachAndInitComponent(component, -1).ComponentId;
private int AssignComponentId(IComponent component, int parentComponentId) /// <summary>
/// Performs the first render for a root component. After this, the root component
/// makes its own decisions about when to re-render, so there is no need to call
/// this more than once.
/// </summary>
/// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
protected void RenderRootComponent(int componentId)
{
GetRequiredComponentState(componentId)
.SetDirectParameters(ParameterCollection.Empty);
}
private ComponentState AttachAndInitComponent(IComponent component, int parentComponentId)
{ {
var componentId = _nextComponentId++; var componentId = _nextComponentId++;
var parentComponentState = GetOptionalComponentState(parentComponentId); var parentComponentState = GetOptionalComponentState(parentComponentId);
var componentState = new ComponentState(this, componentId, component, parentComponentState); var componentState = new ComponentState(this, componentId, component, parentComponentState);
_componentStateById.Add(componentId, componentState); _componentStateById.Add(componentId, componentState);
component.Init(new RenderHandle(this, componentId)); component.Init(new RenderHandle(this, componentId));
return componentId; return componentState;
} }
/// <summary> /// <summary>
@ -103,14 +115,14 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
throw new ArgumentException($"The frame's {nameof(RenderTreeFrame.FrameType)} property must equal {RenderTreeFrameType.Component}", nameof(frame)); throw new ArgumentException($"The frame's {nameof(RenderTreeFrame.FrameType)} property must equal {RenderTreeFrameType.Component}", nameof(frame));
} }
if (frame.Component != null) if (frame.ComponentState != null)
{ {
throw new ArgumentException($"The frame already has a non-null component instance", nameof(frame)); throw new ArgumentException($"The frame already has a non-null component instance", nameof(frame));
} }
var newComponent = InstantiateComponent(frame.ComponentType); var newComponent = InstantiateComponent(frame.ComponentType);
var newComponentId = AssignComponentId(newComponent, parentComponentId); var newComponentState = AttachAndInitComponent(newComponent, parentComponentId);
frame = frame.WithComponentInstance(newComponentId, newComponent); frame = frame.WithComponent(newComponentState);
} }
internal void AssignEventHandlerId(ref RenderTreeFrame frame) internal void AssignEventHandlerId(ref RenderTreeFrame frame)
@ -144,15 +156,6 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
} }
} }
/// <summary>
/// This only needs to exist until there's some other unit-testable functionality
/// that makes use of walking the ancestor hierarchy.
/// </summary>
/// <param name="componentId">The component ID.</param>
/// <returns>The parent component's ID, or null if the component was at the root.</returns>
internal int? TemporaryGetParentComponentIdForTest(int componentId)
=> GetRequiredComponentState(componentId).TemporaryParentComponentIdForTests;
private ComponentState GetRequiredComponentState(int componentId) private ComponentState GetRequiredComponentState(int componentId)
=> _componentStateById.TryGetValue(componentId, out var componentState) => _componentStateById.TryGetValue(componentId, out var componentState)
? componentState ? componentState

View File

@ -116,7 +116,7 @@ namespace Test
// Assert // Assert
Assert.Equal( Assert.Equal(
"Object of type 'Test.MyComponent' has a property matching the name 'IntProperty', " + "Object of type 'Test.MyComponent' has a property matching the name 'IntProperty', " +
"but it does not have [ParameterAttribute] applied.", "but it does not have [ParameterAttribute] or [CascadingParameterAttribute] applied.",
ex.Message); ex.Message);
} }

View File

@ -0,0 +1,440 @@
// 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.Components;
using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Test
{
public class CascadingParameterStateTest
{
[Fact]
public void FindCascadingParameters_IfHasNoParameters_ReturnsNull()
{
// Arrange
var componentState = CreateComponentState(new ComponentWithNoParams());
// Act
var result = CascadingParameterState.FindCascadingParameters(componentState);
// Assert
Assert.Null(result);
}
[Fact]
public void FindCascadingParameters_IfHasNoCascadingParameters_ReturnsNull()
{
// Arrange
var componentState = CreateComponentState(new ComponentWithNoCascadingParams());
// Act
var result = CascadingParameterState.FindCascadingParameters(componentState);
// Assert
Assert.Null(result);
}
[Fact]
public void FindCascadingParameters_IfHasNoAncestors_ReturnsNull()
{
// Arrange
var componentState = CreateComponentState(new ComponentWithCascadingParams());
// Act
var result = CascadingParameterState.FindCascadingParameters(componentState);
// Assert
Assert.Null(result);
}
[Fact]
public void FindCascadingParameters_IfHasNoMatchesInAncestors_ReturnsNull()
{
// Arrange: Build the ancestry list
var states = CreateAncestry(
new ComponentWithNoParams(),
CreateCascadingValueComponent("Hello"),
new ComponentWithNoParams(),
new ComponentWithCascadingParams());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Null(result);
}
[Fact]
public void FindCascadingParameters_IfHasPartialMatchesInAncestors_ReturnsMatches()
{
// Arrange
var states = CreateAncestry(
new ComponentWithNoParams(),
CreateCascadingValueComponent(new ValueType2()),
new ComponentWithNoParams(),
new ComponentWithCascadingParams());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Collection(result, match =>
{
Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam2), match.LocalValueName);
Assert.Same(states[1].Component, match.ValueSupplier);
});
}
[Fact]
public void FindCascadingParameters_IfHasMultipleMatchesInAncestors_ReturnsMatches()
{
// Arrange
var states = CreateAncestry(
new ComponentWithNoParams(),
CreateCascadingValueComponent(new ValueType2()),
new ComponentWithNoParams(),
CreateCascadingValueComponent(new ValueType1()),
new ComponentWithCascadingParams());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Collection(result.OrderBy(x => x.LocalValueName),
match => {
Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam1), match.LocalValueName);
Assert.Same(states[3].Component, match.ValueSupplier);
},
match => {
Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam2), match.LocalValueName);
Assert.Same(states[1].Component, match.ValueSupplier);
});
}
[Fact]
public void FindCascadingParameters_InheritedParameters_ReturnsMatches()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent(new ValueType1()),
CreateCascadingValueComponent(new ValueType3()),
new ComponentWithInheritedCascadingParams());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Collection(result.OrderBy(x => x.LocalValueName),
match => {
Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam1), match.LocalValueName);
Assert.Same(states[0].Component, match.ValueSupplier);
},
match => {
Assert.Equal(nameof(ComponentWithInheritedCascadingParams.CascadingParam3), match.LocalValueName);
Assert.Same(states[1].Component, match.ValueSupplier);
});
}
[Fact]
public void FindCascadingParameters_ComponentRequestsBaseType_ReturnsMatches()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent(new CascadingValueTypeDerivedClass()),
new ComponentWithGenericCascadingParam<CascadingValueTypeBaseClass>());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Collection(result, match => {
Assert.Equal(nameof(ComponentWithGenericCascadingParam<object>.LocalName), match.LocalValueName);
Assert.Same(states[0].Component, match.ValueSupplier);
});
}
[Fact]
public void FindCascadingParameters_ComponentRequestsImplementedInterface_ReturnsMatches()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent(new CascadingValueTypeDerivedClass()),
new ComponentWithGenericCascadingParam<ICascadingValueTypeDerivedClassInterface>());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Collection(result, match => {
Assert.Equal(nameof(ComponentWithGenericCascadingParam<object>.LocalName), match.LocalValueName);
Assert.Same(states[0].Component, match.ValueSupplier);
});
}
[Fact]
public void FindCascadingParameters_ComponentRequestsDerivedType_ReturnsNull()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent(new CascadingValueTypeBaseClass()),
new ComponentWithGenericCascadingParam<CascadingValueTypeDerivedClass>());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Null(result);
}
[Fact]
public void FindCascadingParameters_TypeAssignmentIsValidForNullValue_ReturnsMatches()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent((CascadingValueTypeDerivedClass)null),
new ComponentWithGenericCascadingParam<CascadingValueTypeBaseClass>());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Collection(result, match => {
Assert.Equal(nameof(ComponentWithGenericCascadingParam<object>.LocalName), match.LocalValueName);
Assert.Same(states[0].Component, match.ValueSupplier);
});
}
[Fact]
public void FindCascadingParameters_TypeAssignmentIsInvalidForNullValue_ReturnsNull()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent((object)null),
new ComponentWithGenericCascadingParam<ValueType1>());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Null(result);
}
[Fact]
public void FindCascadingParameters_SupplierSpecifiesNameButConsumerDoesNot_ReturnsNull()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent(new ValueType1(), "MatchOnName"),
new ComponentWithCascadingParams());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Null(result);
}
[Fact]
public void FindCascadingParameters_ConsumerSpecifiesNameButSupplierDoesNot_ReturnsNull()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent(new ValueType1()),
new ComponentWithNamedCascadingParam());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Null(result);
}
[Fact]
public void FindCascadingParameters_MismatchingNameButMatchingType_ReturnsNull()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent(new ValueType1(), "MismatchName"),
new ComponentWithNamedCascadingParam());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Null(result);
}
[Fact]
public void FindCascadingParameters_MatchingNameButMismatchingType_ReturnsNull()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent(new ValueType2(), "MatchOnName"),
new ComponentWithNamedCascadingParam());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Null(result);
}
[Fact]
public void FindCascadingParameters_MatchingNameAndType_ReturnsMatches()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent(new ValueType1(), "matchonNAME"), // To show it's case-insensitive
new ComponentWithNamedCascadingParam());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Collection(result, match => {
Assert.Equal(nameof(ComponentWithNamedCascadingParam.SomeLocalName), match.LocalValueName);
Assert.Same(states[0].Component, match.ValueSupplier);
});
}
[Fact]
public void FindCascadingParameters_MultipleMatchingAncestors_ReturnsClosestMatches()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent(new ValueType1()),
CreateCascadingValueComponent(new ValueType2()),
CreateCascadingValueComponent(new ValueType1()),
CreateCascadingValueComponent(new ValueType2()),
new ComponentWithCascadingParams());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Collection(result.OrderBy(x => x.LocalValueName),
match => {
Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam1), match.LocalValueName);
Assert.Same(states[2].Component, match.ValueSupplier);
},
match => {
Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam2), match.LocalValueName);
Assert.Same(states[3].Component, match.ValueSupplier);
});
}
[Fact]
public void FindCascadingParameters_CanOverrideNonNullValueWithNull()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent(new ValueType1()),
CreateCascadingValueComponent((ValueType1)null),
new ComponentWithCascadingParams());
// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
// Assert
Assert.Collection(result.OrderBy(x => x.LocalValueName),
match => {
Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam1), match.LocalValueName);
Assert.Same(states[1].Component, match.ValueSupplier);
Assert.Null(match.ValueSupplier.CurrentValue);
});
}
static ComponentState[] CreateAncestry(params IComponent[] components)
{
var result = new ComponentState[components.Length];
for (var i = 0; i < components.Length; i++)
{
result[i] = CreateComponentState(
components[i],
i == 0 ? null : result[i - 1]);
}
return result;
}
static ComponentState CreateComponentState(
IComponent component, ComponentState parentComponentState = null)
{
return new ComponentState(new TestRenderer(), 0, component, parentComponentState);
}
static CascadingValue<T> CreateCascadingValueComponent<T>(T value, string name = null)
{
var supplier = new CascadingValue<T>();
supplier.Init(new RenderHandle(new TestRenderer(), 0));
var supplierParams = new Dictionary<string, object>
{
{ "Value", value }
};
if (name != null)
{
supplierParams.Add("Name", name);
}
supplier.SetParameters(supplierParams);
return supplier;
}
class ComponentWithNoParams : TestComponentBase
{
}
class ComponentWithNoCascadingParams : TestComponentBase
{
[Parameter] bool SomeRegularParameter { get; set; }
}
class ComponentWithCascadingParams : TestComponentBase
{
[Parameter] bool RegularParam { get; set; }
[CascadingParameter] internal ValueType1 CascadingParam1 { get; set; }
[CascadingParameter] internal ValueType2 CascadingParam2 { get; set; }
}
class ComponentWithInheritedCascadingParams : ComponentWithCascadingParams
{
[CascadingParameter] internal ValueType3 CascadingParam3 { get; set; }
}
class ComponentWithGenericCascadingParam<T> : TestComponentBase
{
[CascadingParameter] internal T LocalName { get; set; }
}
class ComponentWithNamedCascadingParam : TestComponentBase
{
[CascadingParameter(Name = "MatchOnName")]
internal ValueType1 SomeLocalName { get; set; }
}
class TestComponentBase : IComponent
{
public void Init(RenderHandle renderHandle)
=> throw new NotImplementedException();
public void SetParameters(ParameterCollection parameters)
=> throw new NotImplementedException();
}
class ValueType1 { }
class ValueType2 { }
class ValueType3 { }
class CascadingValueTypeBaseClass { }
class CascadingValueTypeDerivedClass : CascadingValueTypeBaseClass, ICascadingValueTypeDerivedClassInterface { }
interface ICascadingValueTypeDerivedClassInterface { }
}
}

View File

@ -0,0 +1,282 @@
// 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.Components;
using Microsoft.AspNetCore.Blazor.RenderTree;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
using System.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Test
{
public class CascadingParameterTest
{
[Fact]
public void PassesCascadingParametersToNestedComponents()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<string>>(0);
builder.AddAttribute(1, "Value", "Hello");
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder =>
{
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
childBuilder.AddAttribute(1, "RegularParameter", "Goodbye");
childBuilder.CloseComponent();
}));
builder.CloseComponent();
});
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var batch = renderer.Batches.Single();
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(batch, out var nestedComponentId);
var nestedComponentDiff = batch.DiffsByComponentId[nestedComponentId].Single();
// The nested component was rendered with the correct parameters
Assert.Collection(nestedComponentDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"CascadingParameter=Hello; RegularParameter=Goodbye");
});
Assert.Equal(1, nestedComponent.NumRenders);
}
[Fact]
public void RetainsCascadingParametersWhenUpdatingDirectParameters()
{
// Arrange
var renderer = new TestRenderer();
var regularParameterValue = "Initial value";
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<string>>(0);
builder.AddAttribute(1, "Value", "Hello");
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder =>
{
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
childBuilder.AddAttribute(1, "RegularParameter", regularParameterValue);
childBuilder.CloseComponent();
}));
builder.CloseComponent();
});
// Act 1: Render in initial state
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Capture the nested component so we can verify the update later
var firstBatch = renderer.Batches.Single();
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
Assert.Equal(1, nestedComponent.NumRenders);
// Act 2: Render again with updated regular parameter
regularParameterValue = "Changed value";
component.TriggerRender();
// Assert
Assert.Equal(2, renderer.Batches.Count);
var secondBatch = renderer.Batches[1];
var nestedComponentDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single();
// The nested component was rendered with the correct parameters
Assert.Collection(nestedComponentDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex); // This is the only change
AssertFrame.Text(secondBatch.ReferenceFrames[0], "CascadingParameter=Hello; RegularParameter=Changed value");
});
Assert.Equal(2, nestedComponent.NumRenders);
}
[Fact]
public void NotifiesDescendantsOfUpdatedCascadingParameterValuesAndPreservesDirectParameters()
{
// Arrange
var providedValue = "Initial value";
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<string>>(0);
builder.AddAttribute(1, "Value", providedValue);
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder =>
{
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
childBuilder.AddAttribute(1, "RegularParameter", "Goodbye");
childBuilder.CloseComponent();
}));
builder.CloseComponent();
});
// Act 1: Initial render; capture nested component ID
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var firstBatch = renderer.Batches.Single();
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
Assert.Equal(1, nestedComponent.NumRenders);
// Act 2: Re-render CascadingValue with new value
providedValue = "Updated value";
component.TriggerRender();
// Assert: We re-rendered CascadingParameterConsumerComponent
Assert.Equal(2, renderer.Batches.Count);
var secondBatch = renderer.Batches[1];
var nestedComponentDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single();
// The nested component was rendered with the correct parameters
Assert.Collection(nestedComponentDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex); // This is the only change
AssertFrame.Text(secondBatch.ReferenceFrames[0], "CascadingParameter=Updated value; RegularParameter=Goodbye");
});
Assert.Equal(2, nestedComponent.NumRenders);
}
[Fact]
public void DoesNotNotifyDescendantsIfCascadingParameterValuesAreImmutableAndUnchanged()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<string>>(0);
builder.AddAttribute(1, "Value", "Unchanging value");
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder =>
{
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
childBuilder.AddAttribute(1, "RegularParameter", "Goodbye");
childBuilder.CloseComponent();
}));
builder.CloseComponent();
});
// Act 1: Initial render
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var firstBatch = renderer.Batches.Single();
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out _);
Assert.Equal(3, firstBatch.DiffsByComponentId.Count); // Root + CascadingValue + nested
Assert.Equal(1, nestedComponent.NumRenders);
// Act/Assert: Re-render the CascadingValue; observe nested component wasn't re-rendered
component.TriggerRender();
// Assert: We did not re-render CascadingParameterConsumerComponent
Assert.Equal(2, renderer.Batches.Count);
var secondBatch = renderer.Batches[1];
Assert.Equal(2, secondBatch.DiffsByComponentId.Count); // Root + CascadingValue, but not nested one
Assert.Equal(1, nestedComponent.NumRenders);
}
[Fact]
public void StopsNotifyingDescendantsIfTheyAreRemoved()
{
// Arrange
var providedValue = "Initial value";
var displayNestedComponent = true;
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<string>>(0);
builder.AddAttribute(1, "Value", providedValue);
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder =>
{
if (displayNestedComponent)
{
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
childBuilder.AddAttribute(1, "RegularParameter", "Goodbye");
childBuilder.CloseComponent();
}
}));
builder.CloseComponent();
});
// Act 1: Initial render; capture nested component ID
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var firstBatch = renderer.Batches.Single();
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
Assert.Equal(1, nestedComponent.NumSetParametersCalls);
Assert.Equal(1, nestedComponent.NumRenders);
// Act/Assert 2: Re-render the CascadingValue; observe nested component wasn't re-rendered
providedValue = "Updated value";
displayNestedComponent = false; // Remove the nested componet
component.TriggerRender();
// Assert: We did not render the nested component now it's been removed
Assert.Equal(2, renderer.Batches.Count);
var secondBatch = renderer.Batches[1];
Assert.Equal(1, nestedComponent.NumRenders);
Assert.Equal(2, secondBatch.DiffsByComponentId.Count); // Root + CascadingValue, but not nested one
// We *did* send updated params during the first render where it was removed,
// because the params are sent before the disposal logic runs. We could avoid
// this by moving the notifications into the OnAfterRender phase, but then we'd
// often render descendants twice (once because they are descendants and some
// direct parameter might have changed, then once because a cascading parameter
// changed). We can't have it both ways, so optimize for the case when the
// nested component *hasn't* just been removed.
Assert.Equal(2, nestedComponent.NumSetParametersCalls);
// Act 3: However, after disposal, the subscription is removed, so we won't send
// updated params on subsequent CascadingValue renders.
providedValue = "Updated value 2";
component.TriggerRender();
Assert.Equal(2, nestedComponent.NumSetParametersCalls);
}
private static T FindComponent<T>(CapturedBatch batch, out int componentId)
{
var componentFrame = batch.ReferenceFrames.Single(
frame => frame.FrameType == RenderTreeFrameType.Component
&& frame.Component is T);
componentId = componentFrame.ComponentId;
return (T)componentFrame.Component;
}
class TestComponent : AutoRenderComponent
{
private readonly RenderFragment _renderFragment;
public TestComponent(RenderFragment renderFragment)
{
_renderFragment = renderFragment;
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
=> _renderFragment(builder);
}
class CascadingParameterConsumerComponent<T> : AutoRenderComponent
{
public int NumSetParametersCalls { get; private set; }
public int NumRenders { get; private set; }
[CascadingParameter] T CascadingParameter { get; set; }
[Parameter] string RegularParameter { get; set; }
public override void SetParameters(ParameterCollection parameters)
{
NumSetParametersCalls++;
base.SetParameters(parameters);
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
NumRenders++;
builder.AddContent(0, $"CascadingParameter={CascadingParameter}; RegularParameter={RegularParameter}");
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -133,7 +133,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(default, target.IntProp); Assert.Equal(default, target.IntProp);
Assert.Equal( Assert.Equal(
$"Object of type '{typeof(HasPropertyWithoutParameterAttribute).FullName}' has a property matching the name '{nameof(HasPropertyWithoutParameterAttribute.IntProp)}', " + $"Object of type '{typeof(HasPropertyWithoutParameterAttribute).FullName}' has a property matching the name '{nameof(HasPropertyWithoutParameterAttribute.IntProp)}', " +
$"but it does not have [{nameof(ParameterAttribute)}] applied.", $"but it does not have [{nameof(ParameterAttribute)}] or [{nameof(CascadingParameterAttribute)}] applied.",
ex.Message); ex.Message);
} }

View File

@ -1,7 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.AspNetCore.Blazor.RenderTree; using Microsoft.AspNetCore.Blazor.RenderTree;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -85,6 +86,30 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertParameter("attribute 2", attribute2Value)); AssertParameter("attribute 2", attribute2Value));
} }
[Fact]
public void EnumerationIncludesCascadingParameters()
{
// Arrange
var attribute1Value = new object();
var attribute2Value = new object();
var attribute3Value = new object();
var parameterCollection = new ParameterCollection(new[]
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
RenderTreeFrame.Attribute(1, "attribute 1", attribute1Value)
}, 0).WithCascadingParameters(new List<CascadingParameterState>
{
new CascadingParameterState("attribute 2", new TestCascadingValue(attribute2Value)),
new CascadingParameterState("attribute 3", new TestCascadingValue(attribute3Value)),
});
// Assert
Assert.Collection(ToEnumerable(parameterCollection),
AssertParameter("attribute 1", attribute1Value),
AssertParameter("attribute 2", attribute2Value),
AssertParameter("attribute 3", attribute3Value));
}
[Fact] [Fact]
public void CanTryGetNonExistingValue() public void CanTryGetNonExistingValue()
{ {
@ -140,6 +165,25 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Same(myEntryValue, result); Assert.Same(myEntryValue, result);
} }
[Fact]
public void CanGetValueOrDefault_WithMultipleMatchingValues()
{
// Arrange
var myEntryValue = new object();
var parameterCollection = new ParameterCollection(new[]
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(3),
RenderTreeFrame.Attribute(1, "my entry", myEntryValue),
RenderTreeFrame.Attribute(1, "my entry", new object()),
}, 0);
// Act
var result = parameterCollection.GetValueOrDefault<object>("my entry");
// Assert: Picks first match
Assert.Same(myEntryValue, result);
}
[Fact] [Fact]
public void CanGetValueOrDefault_WithNonExistingValue() public void CanGetValueOrDefault_WithNonExistingValue()
{ {
@ -148,7 +192,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
{ {
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2), RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
RenderTreeFrame.Attribute(1, "some other entry", new object()) RenderTreeFrame.Attribute(1, "some other entry", new object())
}, 0); }, 0).WithCascadingParameters(new List<CascadingParameterState>
{
new CascadingParameterState("another entry", new TestCascadingValue(null))
});
// Act // Act
var result = parameterCollection.GetValueOrDefault<DateTime>("nonexisting entry"); var result = parameterCollection.GetValueOrDefault<DateTime>("nonexisting entry");
@ -221,6 +268,29 @@ namespace Microsoft.AspNetCore.Blazor.Test
}); });
} }
[Fact]
public void CanGetValueOrDefault_WithMatchingCascadingParameter()
{
// Arrange
var myEntryValue = new object();
var parameterCollection = new ParameterCollection(new[]
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
RenderTreeFrame.Attribute(1, "unrelated value", new object())
}, 0).WithCascadingParameters(new List<CascadingParameterState>
{
new CascadingParameterState("unrelated value 2", new TestCascadingValue(null)),
new CascadingParameterState("my entry", new TestCascadingValue(myEntryValue)),
new CascadingParameterState("unrelated value 3", new TestCascadingValue(null)),
});
// Act
var result = parameterCollection.GetValueOrDefault<object>("my entry");
// Assert
Assert.Same(myEntryValue, result);
}
private Action<Parameter> AssertParameter(string expectedName, object expectedValue) private Action<Parameter> AssertParameter(string expectedName, object expectedValue)
{ {
return parameter => return parameter =>
@ -246,5 +316,24 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void SetParameters(ParameterCollection parameters) public void SetParameters(ParameterCollection parameters)
=> throw new NotImplementedException(); => throw new NotImplementedException();
} }
private class TestCascadingValue : ICascadingValueComponent
{
public TestCascadingValue(object value)
{
CurrentValue = value;
}
public object CurrentValue { get; }
public bool CanSupplyValue(Type valueType, string valueName)
=> throw new NotImplementedException();
public void Subscribe(ComponentState subscriber)
=> throw new NotImplementedException();
public void Unsubscribe(ComponentState subscriber)
=> throw new NotImplementedException();
}
} }
} }

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Rendering; using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.AspNetCore.Blazor.RenderTree; using Microsoft.AspNetCore.Blazor.RenderTree;
@ -83,39 +82,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
}); });
} }
[Fact]
public void CanWalkTheAncestorHierarchy()
{
// TODO: Instead of testing this directly, once there's some other functionaly
// that relies on the ancestor hierarchy (e.g., deep params), test that instead
// and remove the otherwise unnecessary TemporaryGetParentComponentIdForTest API.
// Arrange
var renderer = new TestRenderer();
var rootComponent = new TestComponent(builder =>
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AncestryComponent>(0);
builder.AddAttribute(1, nameof(AncestryComponent.NumDescendants), 2);
builder.CloseComponent();
});
// Act
var componentId = renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
// Assert
var batch = renderer.Batches.Single();
var componentIds = batch.ReferenceFrames
.Where(frame => frame.FrameType == RenderTreeFrameType.Component)
.Select(f => f.ComponentId);
Assert.Equal(new[] { 1, 2, 3 }, componentIds);
Assert.Equal(2, renderer.TemporaryGetParentComponentIdForTest(3));
Assert.Equal(1, renderer.TemporaryGetParentComponentIdForTest(2));
Assert.Equal(0, renderer.TemporaryGetParentComponentIdForTest(1));
Assert.Null(renderer.TemporaryGetParentComponentIdForTest(0));
}
[Fact] [Fact]
public void CanReRenderTopLevelComponents() public void CanReRenderTopLevelComponents()
{ {
@ -1363,25 +1329,5 @@ namespace Microsoft.AspNetCore.Blazor.Test
{ {
} }
} }
private class AncestryComponent : AutoRenderComponent
{
[Parameter] public int NumDescendants { get; private set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
// Recursively renders more of the same until NumDescendants == 0
if (NumDescendants > 0)
{
builder.OpenComponent<AncestryComponent>(0);
builder.AddAttribute(1, nameof(NumDescendants), NumDescendants - 1);
builder.CloseComponent();
}
else
{
builder.AddContent(1, "I'm the final descendant");
}
}
}
} }
} }

View File

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Rendering; using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.AspNetCore.Blazor.RenderTree; using Microsoft.AspNetCore.Blazor.RenderTree;
using Microsoft.AspNetCore.Blazor.Server.Circuits; using Microsoft.AspNetCore.Blazor.Server.Circuits;
using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -188,6 +189,7 @@ namespace Microsoft.AspNetCore.Blazor.Server
public void CanIncludeReferenceFrames() public void CanIncludeReferenceFrames()
{ {
// Arrange/Act // Arrange/Act
var renderer = new FakeRenderer();
var bytes = Serialize(new RenderBatch( var bytes = Serialize(new RenderBatch(
default, default,
new ArrayRange<RenderTreeFrame>(new[] { new ArrayRange<RenderTreeFrame>(new[] {
@ -197,7 +199,7 @@ namespace Microsoft.AspNetCore.Blazor.Server
.WithAttributeEventHandlerId(789), .WithAttributeEventHandlerId(789),
RenderTreeFrame.ChildComponent(126, typeof(object)) RenderTreeFrame.ChildComponent(126, typeof(object))
.WithComponentSubtreeLength(5678) .WithComponentSubtreeLength(5678)
.WithComponentInstance(2000, new FakeComponent()), .WithComponent(new ComponentState(renderer, 2000, new FakeComponent(), null)),
RenderTreeFrame.ComponentReferenceCapture(127, value => { }, 1001), RenderTreeFrame.ComponentReferenceCapture(127, value => { }, 1001),
RenderTreeFrame.Element(128, "Some element") RenderTreeFrame.Element(128, "Some element")
.WithElementSubtreeLength(1234), .WithElementSubtreeLength(1234),
@ -366,5 +368,16 @@ namespace Microsoft.AspNetCore.Blazor.Server
public void SetParameters(ParameterCollection parameters) public void SetParameters(ParameterCollection parameters)
=> throw new NotImplementedException(); => throw new NotImplementedException();
} }
class FakeRenderer : Renderer
{
public FakeRenderer()
: base(new ServiceCollection().BuildServiceProvider())
{
}
protected override void UpdateDisplay(in RenderBatch renderBatch)
=> throw new NotImplementedException();
}
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Components;
@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers
_renderHandle = renderHandle; _renderHandle = renderHandle;
} }
public void SetParameters(ParameterCollection parameters) public virtual void SetParameters(ParameterCollection parameters)
{ {
parameters.AssignToProperties(this); parameters.AssignToProperties(this);
TriggerRender(); TriggerRender();