diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs index f1aede68c9..5c48a80a8f 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs @@ -74,7 +74,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering domElementSelector, componentId); - component.SetParameters(ParameterCollection.Empty); + RenderRootComponent(componentId); } /// diff --git a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RemoteRenderer.cs b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RemoteRenderer.cs index 256a51fa4d..c50152685f 100644 --- a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RemoteRenderer.cs +++ b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RemoteRenderer.cs @@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering componentId); CaptureAsyncExceptions(attachComponentTask); - component.SetParameters(ParameterCollection.Empty); + RenderRootComponent(componentId); } /// diff --git a/src/Microsoft.AspNetCore.Blazor/Components/CascadingParameterAttribute.cs b/src/Microsoft.AspNetCore.Blazor/Components/CascadingParameterAttribute.cs new file mode 100644 index 0000000000..db3cb4ec79 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor/Components/CascadingParameterAttribute.cs @@ -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 +{ + /// + /// Denotes the target member as a cascading component parameter. Its value will be + /// supplied by the closest ancestor component that + /// supplies values with a compatible type and name. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public sealed class CascadingParameterAttribute : Attribute + { + /// + /// If specified, the parameter value will be supplied by the closest + /// ancestor that supplies a value with + /// this name. + /// + /// If null, the parameter value will be supplied by the closest ancestor + /// that supplies a value with a compatible + /// type. + /// + public string Name { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor/Components/CascadingParameterState.cs b/src/Microsoft.AspNetCore.Blazor/Components/CascadingParameterState.cs new file mode 100644 index 0000000000..680e38c618 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor/Components/CascadingParameterState.cs @@ -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 _cachedInfos + = new ConcurrentDictionary(); + + public string LocalValueName { get; } + public ICascadingValueComponent ValueSupplier { get; } + + public CascadingParameterState(string localValueName, ICascadingValueComponent valueSupplier) + { + LocalValueName = localValueName; + ValueSupplier = valueSupplier; + } + + public static IReadOnlyList 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 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(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 result = null; + var candidateProps = ParameterCollectionExtensions.GetCandidateBindableProperties(componentType); + foreach (var prop in candidateProps) + { + var attribute = prop.GetCustomAttribute(); + if (attribute != null) + { + if (result == null) + { + result = new List(); + } + + 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; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor/Components/CascadingValue.cs b/src/Microsoft.AspNetCore.Blazor/Components/CascadingValue.cs new file mode 100644 index 0000000000..5c0ac51950 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor/Components/CascadingValue.cs @@ -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 +{ + /// + /// A component that provides a cascading value to all descendant components. + /// + public class CascadingValue : ICascadingValueComponent, IComponent + { + private RenderHandle _renderHandle; + private HashSet _subscribers; // Lazily instantiated + + /// + /// The content to which the value should be provided. + /// + [Parameter] private RenderFragment ChildContent { get; set; } + + /// + /// The value to be provided. + /// + [Parameter] private T Value { get; set; } + + /// + /// 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. + /// + [Parameter] private string Name { get; set; } + + object ICascadingValueComponent.CurrentValue => Value; + + /// + public void Init(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + /// + 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)}' does not allow null or empty values."); + } + } + else + { + throw new ArgumentException($"The component '{nameof(CascadingValue)}' 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 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(); + } + + _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); + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor/Components/ChangeDetection.cs b/src/Microsoft.AspNetCore.Blazor/Components/ChangeDetection.cs new file mode 100644 index 0000000000..9ed115cae8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor/Components/ChangeDetection.cs @@ -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 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); + } +} diff --git a/src/Microsoft.AspNetCore.Blazor/Components/ICascadingValueComponent.cs b/src/Microsoft.AspNetCore.Blazor/Components/ICascadingValueComponent.cs new file mode 100644 index 0000000000..2cf5349dd6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor/Components/ICascadingValueComponent.cs @@ -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 types regardless of T. + + bool CanSupplyValue(Type valueType, string valueName); + + object CurrentValue { get; } + + void Subscribe(ComponentState subscriber); + + void Unsubscribe(ComponentState subscriber); + } +} diff --git a/src/Microsoft.AspNetCore.Blazor/Components/Parameter.cs b/src/Microsoft.AspNetCore.Blazor/Components/Parameter.cs index 6c4e54e02f..09c806f79f 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/Parameter.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/Parameter.cs @@ -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. -using Microsoft.AspNetCore.Blazor.RenderTree; - namespace Microsoft.AspNetCore.Blazor.Components { /// @@ -11,31 +9,20 @@ namespace Microsoft.AspNetCore.Blazor.Components /// public readonly struct Parameter { - private readonly RenderTreeFrame[] _frames; - private readonly int _frameIndex; - - internal Parameter(RenderTreeFrame[] frames, int currentIndex) - { - _frames = frames; - _frameIndex = currentIndex; - } - /// /// Gets the name of the parameter. /// - public string Name - => _frames[_frameIndex].AttributeName; + public string Name { get; } /// - /// Gets the value of the parameter. + /// Gets the value being supplied for the parameter. /// - public object Value - => _frames[_frameIndex].AttributeValue; + public object Value { get; } - /// - /// Gets the that holds the parameter name and value. - /// - internal ref RenderTreeFrame Frame - => ref _frames[_frameIndex]; + internal Parameter(string name, object value) + { + Name = name; + Value = value; + } } } diff --git a/src/Microsoft.AspNetCore.Blazor/Components/ParameterAttribute.cs b/src/Microsoft.AspNetCore.Blazor/Components/ParameterAttribute.cs index c92f41fbd6..51ff9e0979 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/ParameterAttribute.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/ParameterAttribute.cs @@ -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. using System; diff --git a/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollection.cs b/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollection.cs index 162518b524..4a51a95db0 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollection.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollection.cs @@ -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. using System; @@ -19,15 +19,22 @@ namespace Microsoft.AspNetCore.Blazor.Components }; private static readonly ParameterCollection _emptyCollection - = new ParameterCollection(_emptyCollectionFrames, 0); + = new ParameterCollection(_emptyCollectionFrames, 0, null); private readonly RenderTreeFrame[] _frames; private readonly int _ownerIndex; + private readonly IReadOnlyList _cascadingParametersOrNull; internal ParameterCollection(RenderTreeFrame[] frames, int ownerIndex) + : this(frames, ownerIndex, null) + { + } + + private ParameterCollection(RenderTreeFrame[] frames, int ownerIndex, IReadOnlyList cascadingParametersOrNull) { _frames = frames; _ownerIndex = ownerIndex; + _cascadingParametersOrNull = cascadingParametersOrNull; } /// @@ -40,7 +47,7 @@ namespace Microsoft.AspNetCore.Blazor.Components /// /// The enumerator. public ParameterEnumerator GetEnumerator() - => new ParameterEnumerator(_frames, _ownerIndex); + => new ParameterEnumerator(_frames, _ownerIndex, _cascadingParametersOrNull); /// /// Gets the value of the parameter with the specified name. @@ -99,6 +106,9 @@ namespace Microsoft.AspNetCore.Blazor.Components return result; } + internal ParameterCollection WithCascadingParameters(IReadOnlyList cascadingParameters) + => new ParameterCollection(_frames, _ownerIndex, cascadingParameters); + // 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 // 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 newValue = newFrame.AttributeValue; - var oldIsNotNull = oldValue != null; - var newIsNotNull = newValue != null; - if (oldIsNotNull != newIsNotNull) + if (ChangeDetection.MayHaveChanged(oldValue, newValue)) { - return false; // 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 false; - } - } - else - { - // Both null, hence equal, so continue + 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 bool IsKnownImmutableType(Type type) - => type.IsPrimitive - || type == typeof(string) - || type == typeof(DateTime) - || type == typeof(decimal); + internal void CaptureSnapshot(ArrayBuilder builder) + { + builder.Clear(); + + var numEntries = 0; + foreach (var entry in this) + { + numEntries++; + } + + // 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); + } + } } } diff --git a/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollectionExtensions.cs b/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollectionExtensions.cs index a0e85defbf..389a332a5f 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollectionExtensions.cs @@ -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. using Microsoft.AspNetCore.Blazor.Reflection; -using Microsoft.AspNetCore.Blazor.RenderTree; using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using System.Reflection; 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 delegate void WriteParameterAction(ref RenderTreeFrame frame, object target); + private delegate void WriteParameterAction(object target, object parameterValue); private readonly static IDictionary> _cachedParameterWriters = new ConcurrentDictionary>(); @@ -55,7 +53,7 @@ namespace Microsoft.AspNetCore.Blazor.Components try { - parameterWriter(ref parameter.Frame, target); + parameterWriter(target, parameter.Value); } catch (Exception ex) { @@ -66,12 +64,22 @@ namespace Microsoft.AspNetCore.Blazor.Components } } + internal static IEnumerable GetCandidateBindableProperties(Type targetType) + => MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags); + private static IDictionary CreateParameterWriters(Type targetType) { var result = new Dictionary(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 propertyName = propertyInfo.Name; @@ -82,19 +90,15 @@ namespace Microsoft.AspNetCore.Blazor.Components $"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; } - private static IEnumerable GetBindableProperties(Type targetType) - => MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags) - .Where(property => property.IsDefined(typeof(ParameterAttribute))); - 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 @@ -102,11 +106,11 @@ namespace Microsoft.AspNetCore.Blazor.Components var propertyInfo = targetType.GetProperty(parameterName, _bindablePropertyFlags); if (propertyInfo != null) { - if (!propertyInfo.IsDefined(typeof(ParameterAttribute))) + if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) && !propertyInfo.IsDefined(typeof(CascadingParameterAttribute))) { throw new InvalidOperationException( $"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 { diff --git a/src/Microsoft.AspNetCore.Blazor/Components/ParameterEnumerator.cs b/src/Microsoft.AspNetCore.Blazor/Components/ParameterEnumerator.cs index b232403887..09806dfa56 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/ParameterEnumerator.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/ParameterEnumerator.cs @@ -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. using Microsoft.AspNetCore.Blazor.RenderTree; using System; +using System.Collections.Generic; namespace Microsoft.AspNetCore.Blazor.Components { @@ -11,49 +12,126 @@ namespace Microsoft.AspNetCore.Blazor.Components /// public struct ParameterEnumerator { - private readonly RenderTreeFrame[] _frames; - private readonly int _ownerIndex; - private readonly int _ownerDescendantsEndIndexExcl; - private int _currentIndex; + private RenderTreeFrameParameterEnumerator _directParamsEnumerator; + private CascadingParameterEnumerator _cascadingParameterEnumerator; + private bool _isEnumeratingDirectParams; - internal ParameterEnumerator(RenderTreeFrame[] frames, int ownerIndex) + internal ParameterEnumerator(RenderTreeFrame[] frames, int ownerIndex, IReadOnlyList cascadingParameters) { - _frames = frames; - _ownerIndex = ownerIndex; - _ownerDescendantsEndIndexExcl = ownerIndex + _frames[ownerIndex].ElementSubtreeLength; - _currentIndex = ownerIndex; + _directParamsEnumerator = new RenderTreeFrameParameterEnumerator(frames, ownerIndex); + _cascadingParameterEnumerator = new CascadingParameterEnumerator(cascadingParameters); + _isEnumeratingDirectParams = true; } /// /// Gets the current value of the enumerator. /// - public Parameter Current - => _currentIndex > _ownerIndex - ? new Parameter(_frames, _currentIndex) - : throw new InvalidOperationException("Iteration has not yet started."); + public Parameter Current => _isEnumeratingDirectParams + ? _directParamsEnumerator.Current + : _cascadingParameterEnumerator.Current; /// /// Instructs the enumerator to move to the next value in the sequence. /// - /// + /// A flag to indicate whether or not there is a next value. public bool MoveNext() { - // Stop iteration if you get to the end of the owner's descendants... - var nextIndex = _currentIndex + 1; - if (nextIndex == _ownerDescendantsEndIndexExcl) + if (_isEnumeratingDirectParams) { - return false; + if (_directParamsEnumerator.MoveNext()) + { + return true; + } + else + { + _isEnumeratingDirectParams = 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 _cascadingParameterEnumerator.MoveNext(); + } + + 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; - return true; + public Parameter Current => _current; + + 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 _cascadingParameters; + private int _currentIndex; + private Parameter _current; + + public CascadingParameterEnumerator(IReadOnlyList 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; + } + } } } } diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs index 4e6e51d60d..56c1e7b987 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs @@ -267,11 +267,10 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree var newTree = diffContext.NewTree; ref var oldComponentFrame = ref oldTree[oldComponentIndex]; ref var newComponentFrame = ref newTree[newComponentIndex]; - var componentId = oldComponentFrame.ComponentId; - var componentInstance = oldComponentFrame.Component; + var componentState = oldComponentFrame.ComponentState; // Preserve the actual componentInstance - newComponentFrame = newComponentFrame.WithComponentInstance(componentId, componentInstance); + newComponentFrame = newComponentFrame.WithComponent(componentState); // As an important rendering optimization, we want to skip parameter update // 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); if (!newParameters.DefinitelyEquals(oldParameters)) { - componentInstance.SetParameters(newParameters); + componentState.SetDirectParameters(newParameters); } } @@ -633,18 +632,18 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree var frames = diffContext.NewTree; ref var frame = ref frames[frameIndex]; - if (frame.Component != null) + if (frame.ComponentState != null) { throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}"); } var parentComponentId = diffContext.ComponentId; diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId); - var childComponentInstance = frame.Component; + var childComponentState = frame.ComponentState; // Set initial parameters var initialParameters = new ParameterCollection(frames, frameIndex); - childComponentInstance.SetParameters(initialParameters); + childComponentState.SetDirectParameters(initialParameters); } 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++) { 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); } diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs index 91a9a0c8f1..c9f624fba7 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Blazor.Components; +using Microsoft.AspNetCore.Blazor.Rendering; namespace Microsoft.AspNetCore.Blazor.RenderTree { @@ -112,11 +113,17 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree /// [FieldOffset(16)] public readonly Type ComponentType; + /// + /// If the property equals , + /// gets the child component state object. Otherwise, the value is undefined. + /// + [FieldOffset(24)] internal readonly ComponentState ComponentState; + /// /// If the property equals , /// gets the child component instance. Otherwise, the value is undefined. /// - [FieldOffset(24)] public readonly IComponent Component; + public IComponent Component => ComponentState?.Component; // -------------------------------------------------------------------------------- // RenderTreeFrameType.Region @@ -195,11 +202,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree 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) { - ComponentId = componentId; - Component = component; + ComponentId = componentState.ComponentId; + ComponentState = componentState; } private RenderTreeFrame(int sequence, string textContent) @@ -283,6 +290,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree internal static RenderTreeFrame ChildComponent(int sequence, Type componentType) => new RenderTreeFrame(sequence, componentType, 0); + internal static RenderTreeFrame PlaceholderChildComponentWithSubtreeLength(int subtreeLength) + => new RenderTreeFrame(0, typeof(IComponent), subtreeLength); + internal static RenderTreeFrame Region(int sequence) => new RenderTreeFrame(sequence, regionSubtreeLength: 0); @@ -301,8 +311,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree internal RenderTreeFrame WithAttributeSequence(int sequence) => new RenderTreeFrame(sequence, attributeName: AttributeName, attributeValue: AttributeValue); - internal RenderTreeFrame WithComponentInstance(int componentId, IComponent component) - => new RenderTreeFrame(Sequence, ComponentType, ComponentSubtreeLength, componentId, component); + internal RenderTreeFrame WithComponent(ComponentState componentState) + => new RenderTreeFrame(Sequence, ComponentType, ComponentSubtreeLength, componentState); internal RenderTreeFrame WithAttributeEventHandlerId(int eventHandlerId) => new RenderTreeFrame(Sequence, AttributeName, AttributeValue, eventHandlerId); diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs index b6e5537cd6..10f069f71c 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.RenderTree; @@ -18,10 +19,16 @@ namespace Microsoft.AspNetCore.Blazor.Rendering private readonly ComponentState _parentComponentState; private readonly IComponent _component; private readonly Renderer _renderer; + private readonly IReadOnlyList _cascadingParameters; private RenderTreeBuilder _renderTreeBuilderCurrent; private RenderTreeBuilder _renderTreeBuilderPrevious; + private ArrayBuilder _latestDirectParametersSnapshot; // Lazily instantiated private bool _componentWasDisposed; + public int ComponentId => _componentId; + public IComponent Component => _component; + public ComponentState ParentComponentState => _parentComponentState; + /// /// Constructs an instance of . /// @@ -35,8 +42,14 @@ namespace Microsoft.AspNetCore.Blazor.Rendering _parentComponentState = parentComponentState; _component = component ?? throw new ArgumentNullException(nameof(component)); _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); + _cascadingParameters = CascadingParameterState.FindCascadingParameters(this); _renderTreeBuilderCurrent = new RenderTreeBuilder(renderer); _renderTreeBuilderPrevious = new RenderTreeBuilder(renderer); + + if (_cascadingParameters != null) + { + AddCascadingParameterSubscriptions(); + } } public void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment) @@ -74,6 +87,11 @@ namespace Microsoft.AspNetCore.Blazor.Rendering } RenderTreeDiffBuilder.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames()); + + if (_cascadingParameters != null) + { + RemoveCascadingParameterSubscriptions(); + } } public void DispatchEvent(EventHandlerInvoker binding, UIEventArgs eventArgs) @@ -93,9 +111,61 @@ namespace Microsoft.AspNetCore.Blazor.Rendering public void NotifyRenderCompleted() => (_component as IHandleAfterRender)?.OnAfterRender(); - // TODO: Remove this once we can remove TemporaryGetParentComponentIdForTest - // from Renderer.cs and corresponding unit test. - public int? TemporaryParentComponentIdForTests - => _parentComponentState?._componentId; + public void SetDirectParameters(ParameterCollection parameters) + { + // Note: We should be careful to ensure that the framework never calls + // 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(); + } + 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); + } + } } } diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs index 7567f6afbb..c15db2bb2e 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs @@ -49,16 +49,28 @@ namespace Microsoft.AspNetCore.Blazor.Rendering /// The component. /// The component's assigned identifier. protected int AssignRootComponentId(IComponent component) - => AssignComponentId(component, -1); + => AttachAndInitComponent(component, -1).ComponentId; - private int AssignComponentId(IComponent component, int parentComponentId) + /// + /// 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. + /// + /// The ID returned by . + protected void RenderRootComponent(int componentId) + { + GetRequiredComponentState(componentId) + .SetDirectParameters(ParameterCollection.Empty); + } + + private ComponentState AttachAndInitComponent(IComponent component, int parentComponentId) { var componentId = _nextComponentId++; var parentComponentState = GetOptionalComponentState(parentComponentId); var componentState = new ComponentState(this, componentId, component, parentComponentState); _componentStateById.Add(componentId, componentState); component.Init(new RenderHandle(this, componentId)); - return componentId; + return componentState; } /// @@ -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)); } - if (frame.Component != null) + if (frame.ComponentState != null) { throw new ArgumentException($"The frame already has a non-null component instance", nameof(frame)); } var newComponent = InstantiateComponent(frame.ComponentType); - var newComponentId = AssignComponentId(newComponent, parentComponentId); - frame = frame.WithComponentInstance(newComponentId, newComponent); + var newComponentState = AttachAndInitComponent(newComponent, parentComponentId); + frame = frame.WithComponent(newComponentState); } internal void AssignEventHandlerId(ref RenderTreeFrame frame) @@ -144,15 +156,6 @@ namespace Microsoft.AspNetCore.Blazor.Rendering } } - /// - /// This only needs to exist until there's some other unit-testable functionality - /// that makes use of walking the ancestor hierarchy. - /// - /// The component ID. - /// The parent component's ID, or null if the component was at the root. - internal int? TemporaryGetParentComponentIdForTest(int componentId) - => GetRequiredComponentState(componentId).TemporaryParentComponentIdForTests; - private ComponentState GetRequiredComponentState(int componentId) => _componentStateById.TryGetValue(componentId, out var componentState) ? componentState diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs index 8451014691..22cec14956 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs @@ -116,7 +116,7 @@ namespace Test // Assert Assert.Equal( "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); } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/CascadingParameterStateTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/CascadingParameterStateTest.cs new file mode 100644 index 0000000000..7b2fb35a00 --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Test/CascadingParameterStateTest.cs @@ -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()); + + // Act + var result = CascadingParameterState.FindCascadingParameters(states.Last()); + + // Assert + Assert.Collection(result, match => { + Assert.Equal(nameof(ComponentWithGenericCascadingParam.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()); + + // Act + var result = CascadingParameterState.FindCascadingParameters(states.Last()); + + // Assert + Assert.Collection(result, match => { + Assert.Equal(nameof(ComponentWithGenericCascadingParam.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()); + + // 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()); + + // Act + var result = CascadingParameterState.FindCascadingParameters(states.Last()); + + // Assert + Assert.Collection(result, match => { + Assert.Equal(nameof(ComponentWithGenericCascadingParam.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()); + + // 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 CreateCascadingValueComponent(T value, string name = null) + { + var supplier = new CascadingValue(); + supplier.Init(new RenderHandle(new TestRenderer(), 0)); + + var supplierParams = new Dictionary + { + { "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 : 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 { } + } +} diff --git a/test/Microsoft.AspNetCore.Blazor.Test/CascadingParameterTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/CascadingParameterTest.cs new file mode 100644 index 0000000000..d4b2a6f40f --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Test/CascadingParameterTest.cs @@ -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>(0); + builder.AddAttribute(1, "Value", "Hello"); + builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder => + { + childBuilder.OpenComponent>(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>(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>(0); + builder.AddAttribute(1, "Value", "Hello"); + builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder => + { + childBuilder.OpenComponent>(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>(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>(0); + builder.AddAttribute(1, "Value", providedValue); + builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder => + { + childBuilder.OpenComponent>(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>(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>(0); + builder.AddAttribute(1, "Value", "Unchanging value"); + builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder => + { + childBuilder.OpenComponent>(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>(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>(0); + builder.AddAttribute(1, "Value", providedValue); + builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder => + { + if (displayNestedComponent) + { + childBuilder.OpenComponent>(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>(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(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 : 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}"); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionAssignmentExtensionsTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionAssignmentExtensionsTest.cs index b99c42993b..b0c46404c4 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionAssignmentExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionAssignmentExtensionsTest.cs @@ -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. using System; @@ -133,7 +133,7 @@ namespace Microsoft.AspNetCore.Blazor.Test Assert.Equal(default, target.IntProp); Assert.Equal( $"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); } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionTest.cs index 769c7dfa4e..4a7213bf4d 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionTest.cs @@ -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. using Microsoft.AspNetCore.Blazor.Components; +using Microsoft.AspNetCore.Blazor.Rendering; using Microsoft.AspNetCore.Blazor.RenderTree; using System; using System.Collections.Generic; @@ -85,6 +86,30 @@ namespace Microsoft.AspNetCore.Blazor.Test 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 + { + 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] public void CanTryGetNonExistingValue() { @@ -140,6 +165,25 @@ namespace Microsoft.AspNetCore.Blazor.Test 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("my entry"); + + // Assert: Picks first match + Assert.Same(myEntryValue, result); + } + [Fact] public void CanGetValueOrDefault_WithNonExistingValue() { @@ -148,7 +192,10 @@ namespace Microsoft.AspNetCore.Blazor.Test { RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2), RenderTreeFrame.Attribute(1, "some other entry", new object()) - }, 0); + }, 0).WithCascadingParameters(new List + { + new CascadingParameterState("another entry", new TestCascadingValue(null)) + }); // Act var result = parameterCollection.GetValueOrDefault("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 + { + 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("my entry"); + + // Assert + Assert.Same(myEntryValue, result); + } + private Action AssertParameter(string expectedName, object expectedValue) { return parameter => @@ -246,5 +316,24 @@ namespace Microsoft.AspNetCore.Blazor.Test public void SetParameters(ParameterCollection parameters) => 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(); + } } } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs index 5d33b11f9b..808c8dd1e2 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Rendering; 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(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] 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(0); - builder.AddAttribute(1, nameof(NumDescendants), NumDescendants - 1); - builder.CloseComponent(); - } - else - { - builder.AddContent(1, "I'm the final descendant"); - } - } - } } } diff --git a/test/Microsoft.AspnetCore.Blazor.Server.Test/Circuits/RenderBatchWriterTest.cs b/test/Microsoft.AspnetCore.Blazor.Server.Test/Circuits/RenderBatchWriterTest.cs index 6c0081f21e..cbdb156fe3 100644 --- a/test/Microsoft.AspnetCore.Blazor.Server.Test/Circuits/RenderBatchWriterTest.cs +++ b/test/Microsoft.AspnetCore.Blazor.Server.Test/Circuits/RenderBatchWriterTest.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Rendering; using Microsoft.AspNetCore.Blazor.RenderTree; using Microsoft.AspNetCore.Blazor.Server.Circuits; +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.IO; @@ -188,6 +189,7 @@ namespace Microsoft.AspNetCore.Blazor.Server public void CanIncludeReferenceFrames() { // Arrange/Act + var renderer = new FakeRenderer(); var bytes = Serialize(new RenderBatch( default, new ArrayRange(new[] { @@ -197,7 +199,7 @@ namespace Microsoft.AspNetCore.Blazor.Server .WithAttributeEventHandlerId(789), RenderTreeFrame.ChildComponent(126, typeof(object)) .WithComponentSubtreeLength(5678) - .WithComponentInstance(2000, new FakeComponent()), + .WithComponent(new ComponentState(renderer, 2000, new FakeComponent(), null)), RenderTreeFrame.ComponentReferenceCapture(127, value => { }, 1001), RenderTreeFrame.Element(128, "Some element") .WithElementSubtreeLength(1234), @@ -366,5 +368,16 @@ namespace Microsoft.AspNetCore.Blazor.Server public void SetParameters(ParameterCollection parameters) => throw new NotImplementedException(); } + + class FakeRenderer : Renderer + { + public FakeRenderer() + : base(new ServiceCollection().BuildServiceProvider()) + { + } + + protected override void UpdateDisplay(in RenderBatch renderBatch) + => throw new NotImplementedException(); + } } } diff --git a/test/shared/AutoRenderComponent.cs b/test/shared/AutoRenderComponent.cs index f30e450e18..8ab9f568f4 100644 --- a/test/shared/AutoRenderComponent.cs +++ b/test/shared/AutoRenderComponent.cs @@ -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. using Microsoft.AspNetCore.Blazor.Components; @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers _renderHandle = renderHandle; } - public void SetParameters(ParameterCollection parameters) + public virtual void SetParameters(ParameterCollection parameters) { parameters.AssignToProperties(this); TriggerRender();