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:
parent
fa2b61773a
commit
211ad636fd
|
|
@ -74,7 +74,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
|
|||
domElementSelector,
|
||||
componentId);
|
||||
|
||||
component.SetParameters(ParameterCollection.Empty);
|
||||
RenderRootComponent(componentId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
|
|||
componentId);
|
||||
CaptureAsyncExceptions(attachComponentTask);
|
||||
|
||||
component.SetParameters(ParameterCollection.Empty);
|
||||
RenderRootComponent(componentId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -11,31 +9,20 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
|||
/// </summary>
|
||||
public readonly struct Parameter
|
||||
{
|
||||
private readonly RenderTreeFrame[] _frames;
|
||||
private readonly int _frameIndex;
|
||||
|
||||
internal Parameter(RenderTreeFrame[] frames, int currentIndex)
|
||||
{
|
||||
_frames = frames;
|
||||
_frameIndex = currentIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the parameter.
|
||||
/// </summary>
|
||||
public string Name
|
||||
=> _frames[_frameIndex].AttributeName;
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the parameter.
|
||||
/// Gets the value being supplied for the parameter.
|
||||
/// </summary>
|
||||
public object Value
|
||||
=> _frames[_frameIndex].AttributeValue;
|
||||
public object Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="RenderTreeFrame" /> that holds the parameter name and value.
|
||||
/// </summary>
|
||||
internal ref RenderTreeFrame Frame
|
||||
=> ref _frames[_frameIndex];
|
||||
internal Parameter(string name, object value)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<CascadingParameterState> _cascadingParametersOrNull;
|
||||
|
||||
internal ParameterCollection(RenderTreeFrame[] frames, int ownerIndex)
|
||||
: this(frames, ownerIndex, null)
|
||||
{
|
||||
}
|
||||
|
||||
private ParameterCollection(RenderTreeFrame[] frames, int ownerIndex, IReadOnlyList<CascadingParameterState> cascadingParametersOrNull)
|
||||
{
|
||||
_frames = frames;
|
||||
_ownerIndex = ownerIndex;
|
||||
_cascadingParametersOrNull = cascadingParametersOrNull;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -40,7 +47,7 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
|||
/// </summary>
|
||||
/// <returns>The enumerator.</returns>
|
||||
public ParameterEnumerator GetEnumerator()
|
||||
=> new ParameterEnumerator(_frames, _ownerIndex);
|
||||
=> new ParameterEnumerator(_frames, _ownerIndex, _cascadingParametersOrNull);
|
||||
|
||||
/// <summary>
|
||||
/// 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<CascadingParameterState> 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<RenderTreeFrame> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Type, IDictionary<string, WriteParameterAction>> _cachedParameterWriters
|
||||
= new ConcurrentDictionary<Type, IDictionary<string, WriteParameterAction>>();
|
||||
|
|
@ -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<PropertyInfo> GetCandidateBindableProperties(Type targetType)
|
||||
=> MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags);
|
||||
|
||||
private static IDictionary<string, WriteParameterAction> CreateParameterWriters(Type targetType)
|
||||
{
|
||||
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 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<PropertyInfo> 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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// </summary>
|
||||
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<CascadingParameterState> cascadingParameters)
|
||||
{
|
||||
_frames = frames;
|
||||
_ownerIndex = ownerIndex;
|
||||
_ownerDescendantsEndIndexExcl = ownerIndex + _frames[ownerIndex].ElementSubtreeLength;
|
||||
_currentIndex = ownerIndex;
|
||||
_directParamsEnumerator = new RenderTreeFrameParameterEnumerator(frames, ownerIndex);
|
||||
_cascadingParameterEnumerator = new CascadingParameterEnumerator(cascadingParameters);
|
||||
_isEnumeratingDirectParams = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of the enumerator.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Instructs the enumerator to move to the next value in the sequence.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>A flag to indicate whether or not there is a next value.</returns>
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// </summary>
|
||||
[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>
|
||||
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Component"/>,
|
||||
/// gets the child component instance. Otherwise, the value is undefined.
|
||||
/// </summary>
|
||||
[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);
|
||||
|
|
|
|||
|
|
@ -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<CascadingParameterState> _cascadingParameters;
|
||||
private RenderTreeBuilder _renderTreeBuilderCurrent;
|
||||
private RenderTreeBuilder _renderTreeBuilderPrevious;
|
||||
private ArrayBuilder<RenderTreeFrame> _latestDirectParametersSnapshot; // Lazily instantiated
|
||||
private bool _componentWasDisposed;
|
||||
|
||||
public int ComponentId => _componentId;
|
||||
public IComponent Component => _component;
|
||||
public ComponentState ParentComponentState => _parentComponentState;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="ComponentState"/>.
|
||||
/// </summary>
|
||||
|
|
@ -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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,16 +49,28 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
|||
/// <param name="component">The component.</param>
|
||||
/// <returns>The component's assigned identifier.</returns>
|
||||
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 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;
|
||||
}
|
||||
|
||||
/// <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));
|
||||
}
|
||||
|
||||
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
|
|||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
=> _componentStateById.TryGetValue(componentId, out var componentState)
|
||||
? componentState
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 { }
|
||||
}
|
||||
}
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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]
|
||||
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<object>("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<CascadingParameterState>
|
||||
{
|
||||
new CascadingParameterState("another entry", new TestCascadingValue(null))
|
||||
});
|
||||
|
||||
// Act
|
||||
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)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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]
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<RenderTreeFrame>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue