Skip to content

Commit

Permalink
Allow components to mutate [SupplyParameterFromForm] data (#49489)
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveSandersonMS authored Jul 18, 2023
1 parent c9ac964 commit e81e73c
Show file tree
Hide file tree
Showing 11 changed files with 375 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@ public abstract class CascadingParameterAttributeBase : Attribute
/// of a cascading value.
/// </summary>
public abstract string? Name { get; set; }

/// <summary>
/// Gets a flag indicating whether the cascading parameter should
/// be supplied only once per component.
/// </summary>
internal virtual bool SingleDelivery => false;
}
15 changes: 14 additions & 1 deletion src/Components/Components/src/CascadingParameterState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ public CascadingParameterState(in CascadingParameterInfo parameterInfo, ICascadi
ValueSupplier = valueSupplier;
}

public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(ComponentState componentState)
public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(ComponentState componentState, out bool hasSingleDeliveryParameters)
{
var componentType = componentState.Component.GetType();
var infos = GetCascadingParameterInfos(componentType);
hasSingleDeliveryParameters = false;

// For components known not to have any cascading parameters, bail out early
if (infos.Length == 0)
Expand All @@ -50,6 +51,18 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
// Although not all parameters might be matched, we know the maximum number
resultStates ??= new List<CascadingParameterState>(infos.Length - infoIndex);
resultStates.Add(new CascadingParameterState(info, supplier));

if (info.Attribute.SingleDelivery)
{
hasSingleDeliveryParameters = true;
if (!supplier.IsFixed)
{
// We don't have a use case for IsFixed=false with SingleDelivery=true. To avoid complications about
// subscribing/unsubscribing in this case, just disallow it. It shouldn't be possible for this to
// occur unless someone creates their own CascadingParameterAttributeBase subclass.
throw new InvalidOperationException($"'{info.Attribute.GetType()}' is flagged with SingleDelivery, but the selected supplier '{supplier.GetType()}' is not flagged with {nameof(ICascadingValueSupplier.IsFixed)}");
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<argument>ILLink</argument>
<argument>IL2072</argument>
<property name="Scope">member</property>
<property name="Target">M:Microsoft.AspNetCore.Components.CascadingParameterState.FindCascadingParameters(Microsoft.AspNetCore.Components.Rendering.ComponentState)</property>
<property name="Target">M:Microsoft.AspNetCore.Components.CascadingParameterState.FindCascadingParameters(Microsoft.AspNetCore.Components.Rendering.ComponentState,System.Boolean@)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
Expand Down
33 changes: 30 additions & 3 deletions src/Components/Components/src/Rendering/ComponentState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ namespace Microsoft.AspNetCore.Components.Rendering;
public class ComponentState : IAsyncDisposable
{
private readonly Renderer _renderer;
private readonly IReadOnlyList<CascadingParameterState> _cascadingParameters;
private readonly bool _hasCascadingParameters;
private readonly bool _hasAnyCascadingParameterSubscriptions;
private IReadOnlyList<CascadingParameterState> _cascadingParameters;
private bool _hasCascadingParameters;
private bool _hasSingleDeliveryCascadingParameters;
private RenderTreeBuilder _nextRenderTree;
private ArrayBuilder<RenderTreeFrame>? _latestDirectParametersSnapshot; // Lazily instantiated
private bool _componentWasDisposed;
Expand All @@ -39,7 +40,7 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
? (GetSectionOutletLogicalParent(renderer, (SectionOutlet)parentComponentState!.Component) ?? parentComponentState)
: parentComponentState;
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
_cascadingParameters = CascadingParameterState.FindCascadingParameters(this);
_cascadingParameters = CascadingParameterState.FindCascadingParameters(this, out _hasSingleDeliveryCascadingParameters);
CurrentRenderTree = new RenderTreeBuilder();
_nextRenderTree = new RenderTreeBuilder();

Expand Down Expand Up @@ -174,11 +175,37 @@ internal void SetDirectParameters(ParameterView parameters)
if (_hasCascadingParameters)
{
parameters = parameters.WithCascadingParameters(_cascadingParameters);
if (_hasSingleDeliveryCascadingParameters)
{
StopSupplyingSingleDeliveryCascadingParameters();
}
}

SupplyCombinedParameters(parameters);
}

private void StopSupplyingSingleDeliveryCascadingParameters()
{
// We're optimizing for the case where there are no single-delivery parameters, or if there were, we already
// removed them. In those cases _cascadingParameters is already up-to-date and gets used as-is without any filtering.
// In the unusual case were there are single-delivery parameters and we haven't yet removed them, it's OK to
// go through the extra work and allocation of creating a new list.
List<CascadingParameterState>? remainingCascadingParameters = null;
foreach (var param in _cascadingParameters)
{
if (!param.ParameterInfo.Attribute.SingleDelivery)
{
remainingCascadingParameters ??= new(_cascadingParameters.Count /* upper bound on capacity needed */);
remainingCascadingParameters.Add(param);
}
}

// Now update all the tracking state to match the filtered set
_hasCascadingParameters = remainingCascadingParameters is not null;
_cascadingParameters = (IReadOnlyList<CascadingParameterState>?)remainingCascadingParameters ?? Array.Empty<CascadingParameterState>();
_hasSingleDeliveryCascadingParameters = false;
}

internal void NotifyCascadingValueChanged(in ParameterViewLifetime lifetime)
{
// If the component was already disposed, we must not try to supply new parameters. Among other reasons,
Expand Down
108 changes: 88 additions & 20 deletions src/Components/Components/test/CascadingParameterStateTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public void FindCascadingParameters_IfHasNoParameters_ReturnsEmpty()
var componentState = CreateComponentState(new ComponentWithNoParams());

// Act
var result = CascadingParameterState.FindCascadingParameters(componentState);
var result = CascadingParameterState.FindCascadingParameters(componentState, out _);

// Assert
Assert.Empty(result);
Expand All @@ -29,7 +29,7 @@ public void FindCascadingParameters_IfHasNoCascadingParameters_ReturnsEmpty()
var componentState = CreateComponentState(new ComponentWithNoCascadingParams());

// Act
var result = CascadingParameterState.FindCascadingParameters(componentState);
var result = CascadingParameterState.FindCascadingParameters(componentState, out _);

// Assert
Assert.Empty(result);
Expand All @@ -42,7 +42,7 @@ public void FindCascadingParameters_IfHasNoAncestors_ReturnsEmpty()
var componentState = CreateComponentState(new ComponentWithCascadingParams());

// Act
var result = CascadingParameterState.FindCascadingParameters(componentState);
var result = CascadingParameterState.FindCascadingParameters(componentState, out _);

// Assert
Assert.Empty(result);
Expand All @@ -59,7 +59,7 @@ public void FindCascadingParameters_IfHasNoMatchesInAncestors_ReturnsEmpty()
new ComponentWithCascadingParams());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Empty(result);
Expand All @@ -76,7 +76,7 @@ public void FindCascadingParameters_IfHasPartialMatchesInAncestors_ReturnsMatche
new ComponentWithCascadingParams());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Collection(result, match =>
Expand All @@ -98,7 +98,7 @@ public void FindCascadingParameters_IfHasMultipleMatchesInAncestors_ReturnsMatch
new ComponentWithCascadingParams());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Collection(result.OrderBy(x => x.ParameterInfo.PropertyName),
Expand All @@ -124,7 +124,7 @@ public void FindCascadingParameters_InheritedParameters_ReturnsMatches()
new ComponentWithInheritedCascadingParams());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Collection(result.OrderBy(x => x.ParameterInfo.PropertyName),
Expand All @@ -149,7 +149,7 @@ public void FindCascadingParameters_ComponentRequestsBaseType_ReturnsMatches()
new ComponentWithGenericCascadingParam<CascadingValueTypeBaseClass>());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Collection(result, match =>
Expand All @@ -168,7 +168,7 @@ public void FindCascadingParameters_ComponentRequestsImplementedInterface_Return
new ComponentWithGenericCascadingParam<ICascadingValueTypeDerivedClassInterface>());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Collection(result, match =>
Expand All @@ -187,7 +187,7 @@ public void FindCascadingParameters_ComponentRequestsDerivedType_ReturnsEmpty()
new ComponentWithGenericCascadingParam<CascadingValueTypeDerivedClass>());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Empty(result);
Expand All @@ -202,7 +202,7 @@ public void FindCascadingParameters_TypeAssignmentIsValidForNullValue_ReturnsMat
new ComponentWithGenericCascadingParam<CascadingValueTypeBaseClass>());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Collection(result, match =>
Expand All @@ -221,7 +221,7 @@ public void FindCascadingParameters_TypeAssignmentIsInvalidForNullValue_ReturnsE
new ComponentWithGenericCascadingParam<ValueType1>());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Empty(result);
Expand All @@ -236,7 +236,7 @@ public void FindCascadingParameters_SupplierSpecifiesNameButConsumerDoesNot_Retu
new ComponentWithCascadingParams());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Empty(result);
Expand All @@ -251,7 +251,7 @@ public void FindCascadingParameters_ConsumerSpecifiesNameButSupplierDoesNot_Retu
new ComponentWithNamedCascadingParam());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Empty(result);
Expand All @@ -266,7 +266,7 @@ public void FindCascadingParameters_MismatchingNameButMatchingType_ReturnsEmpty(
new ComponentWithNamedCascadingParam());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Empty(result);
Expand All @@ -281,7 +281,7 @@ public void FindCascadingParameters_MatchingNameButMismatchingType_ReturnsEmpty(
new ComponentWithNamedCascadingParam());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Empty(result);
Expand All @@ -296,7 +296,7 @@ public void FindCascadingParameters_MatchingNameAndType_ReturnsMatches()
new ComponentWithNamedCascadingParam());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Collection(result, match =>
Expand All @@ -318,7 +318,7 @@ public void FindCascadingParameters_MultipleMatchingAncestors_ReturnsClosestMatc
new ComponentWithCascadingParams());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Collection(result.OrderBy(x => x.ParameterInfo.PropertyName),
Expand All @@ -344,7 +344,7 @@ public void FindCascadingParameters_CanOverrideNonNullValueWithNull()
new ComponentWithCascadingParams());

// Act
var result = CascadingParameterState.FindCascadingParameters(states.Last());
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);

// Assert
Assert.Collection(result.OrderBy(x => x.ParameterInfo.PropertyName),
Expand All @@ -356,7 +356,49 @@ public void FindCascadingParameters_CanOverrideNonNullValueWithNull()
});
}


[Fact]
public void FindCascadingParameters_WithoutSingleDelivery()
{
// Even though ComponentWithCascadingParams itself declares a [SupplyParameterAsSingleDelivery],
// none of the suppliers match it, so we'll get hasSingleDeliveryParameters = false

// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent(new ValueType1()),
new ComponentWithCascadingParams());

// Act
_ = CascadingParameterState.FindCascadingParameters(states.Last(), out var hasSingleDeliveryParameters);

// Assert
Assert.False(hasSingleDeliveryParameters);
}

[Fact]
public void FindCascadingParameters_WithSingleDelivery()
{
// Arrange
var states = CreateAncestry(
CreateCascadingValueComponent(new ValueType1()),
new SupplyParameterWithSingleDeliveryComponent(isFixed: true),
new ComponentWithCascadingParams());

// Act
_ = CascadingParameterState.FindCascadingParameters(states.Last(), out var hasSingleDeliveryParameters);

// Assert
Assert.True(hasSingleDeliveryParameters);
}

[Fact]
public void FindCascadingParameters_DisallowsSingleDeliveryWhenIsFixedIsFalse()
{
var ex = Assert.Throws<InvalidOperationException>(() => CreateAncestry(
new SupplyParameterWithSingleDeliveryComponent(isFixed: false),
new ComponentWithCascadingParams()));

Assert.StartsWith($"'{typeof(SupplyParameterWithSingleDeliveryAttribute)}' is flagged with SingleDelivery", ex.Message);
}

static ComponentState[] CreateAncestry(params IComponent[] components)
{
Expand Down Expand Up @@ -412,6 +454,8 @@ class ComponentWithCascadingParams : TestComponentBase
[Parameter] public bool RegularParam { get; set; }
[CascadingParameter] internal ValueType1 CascadingParam1 { get; set; }
[CascadingParameter] internal ValueType2 CascadingParam2 { get; set; }

[SupplyParameterWithSingleDelivery] internal ValueType3 SingleDeliveryCascadingParam { get; set; }
}

class ComponentWithInheritedCascadingParams : ComponentWithCascadingParams
Expand All @@ -430,6 +474,30 @@ class ComponentWithNamedCascadingParam : TestComponentBase
internal ValueType1 SomeLocalName { get; set; }
}

class SupplyParameterWithSingleDeliveryAttribute : CascadingParameterAttributeBase
{
public override string Name { get; set; }

internal override bool SingleDelivery => true;
}

class SupplyParameterWithSingleDeliveryComponent(bool isFixed) : ComponentBase, ICascadingValueSupplier
{
public bool IsFixed => isFixed;

public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
=> parameterInfo.Attribute is SupplyParameterWithSingleDeliveryAttribute;

public object GetCurrentValue(in CascadingParameterInfo parameterInfo)
=> throw new NotImplementedException();

public void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
=> throw new NotImplementedException();

public void Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
=> throw new NotImplementedException();
}

class TestComponentBase : IComponent
{
public void Attach(RenderHandle renderHandle)
Expand Down
Loading

0 comments on commit e81e73c

Please sign in to comment.