Skip to content

Commit e81e73c

Browse files
Allow components to mutate [SupplyParameterFromForm] data (#49489)
1 parent c9ac964 commit e81e73c

File tree

11 files changed

+375
-27
lines changed

11 files changed

+375
-27
lines changed

src/Components/Components/src/CascadingParameterAttributeBase.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,10 @@ public abstract class CascadingParameterAttributeBase : Attribute
1313
/// of a cascading value.
1414
/// </summary>
1515
public abstract string? Name { get; set; }
16+
17+
/// <summary>
18+
/// Gets a flag indicating whether the cascading parameter should
19+
/// be supplied only once per component.
20+
/// </summary>
21+
internal virtual bool SingleDelivery => false;
1622
}

src/Components/Components/src/CascadingParameterState.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ public CascadingParameterState(in CascadingParameterInfo parameterInfo, ICascadi
2525
ValueSupplier = valueSupplier;
2626
}
2727

28-
public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(ComponentState componentState)
28+
public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(ComponentState componentState, out bool hasSingleDeliveryParameters)
2929
{
3030
var componentType = componentState.Component.GetType();
3131
var infos = GetCascadingParameterInfos(componentType);
32+
hasSingleDeliveryParameters = false;
3233

3334
// For components known not to have any cascading parameters, bail out early
3435
if (infos.Length == 0)
@@ -50,6 +51,18 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
5051
// Although not all parameters might be matched, we know the maximum number
5152
resultStates ??= new List<CascadingParameterState>(infos.Length - infoIndex);
5253
resultStates.Add(new CascadingParameterState(info, supplier));
54+
55+
if (info.Attribute.SingleDelivery)
56+
{
57+
hasSingleDeliveryParameters = true;
58+
if (!supplier.IsFixed)
59+
{
60+
// We don't have a use case for IsFixed=false with SingleDelivery=true. To avoid complications about
61+
// subscribing/unsubscribing in this case, just disallow it. It shouldn't be possible for this to
62+
// occur unless someone creates their own CascadingParameterAttributeBase subclass.
63+
throw new InvalidOperationException($"'{info.Attribute.GetType()}' is flagged with SingleDelivery, but the selected supplier '{supplier.GetType()}' is not flagged with {nameof(ICascadingValueSupplier.IsFixed)}");
64+
}
65+
}
5366
}
5467
}
5568

src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
<argument>ILLink</argument>
3636
<argument>IL2072</argument>
3737
<property name="Scope">member</property>
38-
<property name="Target">M:Microsoft.AspNetCore.Components.CascadingParameterState.FindCascadingParameters(Microsoft.AspNetCore.Components.Rendering.ComponentState)</property>
38+
<property name="Target">M:Microsoft.AspNetCore.Components.CascadingParameterState.FindCascadingParameters(Microsoft.AspNetCore.Components.Rendering.ComponentState,System.Boolean@)</property>
3939
</attribute>
4040
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
4141
<argument>ILLink</argument>

src/Components/Components/src/Rendering/ComponentState.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ namespace Microsoft.AspNetCore.Components.Rendering;
1616
public class ComponentState : IAsyncDisposable
1717
{
1818
private readonly Renderer _renderer;
19-
private readonly IReadOnlyList<CascadingParameterState> _cascadingParameters;
20-
private readonly bool _hasCascadingParameters;
2119
private readonly bool _hasAnyCascadingParameterSubscriptions;
20+
private IReadOnlyList<CascadingParameterState> _cascadingParameters;
21+
private bool _hasCascadingParameters;
22+
private bool _hasSingleDeliveryCascadingParameters;
2223
private RenderTreeBuilder _nextRenderTree;
2324
private ArrayBuilder<RenderTreeFrame>? _latestDirectParametersSnapshot; // Lazily instantiated
2425
private bool _componentWasDisposed;
@@ -39,7 +40,7 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
3940
? (GetSectionOutletLogicalParent(renderer, (SectionOutlet)parentComponentState!.Component) ?? parentComponentState)
4041
: parentComponentState;
4142
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
42-
_cascadingParameters = CascadingParameterState.FindCascadingParameters(this);
43+
_cascadingParameters = CascadingParameterState.FindCascadingParameters(this, out _hasSingleDeliveryCascadingParameters);
4344
CurrentRenderTree = new RenderTreeBuilder();
4445
_nextRenderTree = new RenderTreeBuilder();
4546

@@ -174,11 +175,37 @@ internal void SetDirectParameters(ParameterView parameters)
174175
if (_hasCascadingParameters)
175176
{
176177
parameters = parameters.WithCascadingParameters(_cascadingParameters);
178+
if (_hasSingleDeliveryCascadingParameters)
179+
{
180+
StopSupplyingSingleDeliveryCascadingParameters();
181+
}
177182
}
178183

179184
SupplyCombinedParameters(parameters);
180185
}
181186

187+
private void StopSupplyingSingleDeliveryCascadingParameters()
188+
{
189+
// We're optimizing for the case where there are no single-delivery parameters, or if there were, we already
190+
// removed them. In those cases _cascadingParameters is already up-to-date and gets used as-is without any filtering.
191+
// In the unusual case were there are single-delivery parameters and we haven't yet removed them, it's OK to
192+
// go through the extra work and allocation of creating a new list.
193+
List<CascadingParameterState>? remainingCascadingParameters = null;
194+
foreach (var param in _cascadingParameters)
195+
{
196+
if (!param.ParameterInfo.Attribute.SingleDelivery)
197+
{
198+
remainingCascadingParameters ??= new(_cascadingParameters.Count /* upper bound on capacity needed */);
199+
remainingCascadingParameters.Add(param);
200+
}
201+
}
202+
203+
// Now update all the tracking state to match the filtered set
204+
_hasCascadingParameters = remainingCascadingParameters is not null;
205+
_cascadingParameters = (IReadOnlyList<CascadingParameterState>?)remainingCascadingParameters ?? Array.Empty<CascadingParameterState>();
206+
_hasSingleDeliveryCascadingParameters = false;
207+
}
208+
182209
internal void NotifyCascadingValueChanged(in ParameterViewLifetime lifetime)
183210
{
184211
// If the component was already disposed, we must not try to supply new parameters. Among other reasons,

src/Components/Components/test/CascadingParameterStateTest.cs

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public void FindCascadingParameters_IfHasNoParameters_ReturnsEmpty()
1616
var componentState = CreateComponentState(new ComponentWithNoParams());
1717

1818
// Act
19-
var result = CascadingParameterState.FindCascadingParameters(componentState);
19+
var result = CascadingParameterState.FindCascadingParameters(componentState, out _);
2020

2121
// Assert
2222
Assert.Empty(result);
@@ -29,7 +29,7 @@ public void FindCascadingParameters_IfHasNoCascadingParameters_ReturnsEmpty()
2929
var componentState = CreateComponentState(new ComponentWithNoCascadingParams());
3030

3131
// Act
32-
var result = CascadingParameterState.FindCascadingParameters(componentState);
32+
var result = CascadingParameterState.FindCascadingParameters(componentState, out _);
3333

3434
// Assert
3535
Assert.Empty(result);
@@ -42,7 +42,7 @@ public void FindCascadingParameters_IfHasNoAncestors_ReturnsEmpty()
4242
var componentState = CreateComponentState(new ComponentWithCascadingParams());
4343

4444
// Act
45-
var result = CascadingParameterState.FindCascadingParameters(componentState);
45+
var result = CascadingParameterState.FindCascadingParameters(componentState, out _);
4646

4747
// Assert
4848
Assert.Empty(result);
@@ -59,7 +59,7 @@ public void FindCascadingParameters_IfHasNoMatchesInAncestors_ReturnsEmpty()
5959
new ComponentWithCascadingParams());
6060

6161
// Act
62-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
62+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
6363

6464
// Assert
6565
Assert.Empty(result);
@@ -76,7 +76,7 @@ public void FindCascadingParameters_IfHasPartialMatchesInAncestors_ReturnsMatche
7676
new ComponentWithCascadingParams());
7777

7878
// Act
79-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
79+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
8080

8181
// Assert
8282
Assert.Collection(result, match =>
@@ -98,7 +98,7 @@ public void FindCascadingParameters_IfHasMultipleMatchesInAncestors_ReturnsMatch
9898
new ComponentWithCascadingParams());
9999

100100
// Act
101-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
101+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
102102

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

126126
// Act
127-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
127+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
128128

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

151151
// Act
152-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
152+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
153153

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

170170
// Act
171-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
171+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
172172

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

189189
// Act
190-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
190+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
191191

192192
// Assert
193193
Assert.Empty(result);
@@ -202,7 +202,7 @@ public void FindCascadingParameters_TypeAssignmentIsValidForNullValue_ReturnsMat
202202
new ComponentWithGenericCascadingParam<CascadingValueTypeBaseClass>());
203203

204204
// Act
205-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
205+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
206206

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

223223
// Act
224-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
224+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
225225

226226
// Assert
227227
Assert.Empty(result);
@@ -236,7 +236,7 @@ public void FindCascadingParameters_SupplierSpecifiesNameButConsumerDoesNot_Retu
236236
new ComponentWithCascadingParams());
237237

238238
// Act
239-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
239+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
240240

241241
// Assert
242242
Assert.Empty(result);
@@ -251,7 +251,7 @@ public void FindCascadingParameters_ConsumerSpecifiesNameButSupplierDoesNot_Retu
251251
new ComponentWithNamedCascadingParam());
252252

253253
// Act
254-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
254+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
255255

256256
// Assert
257257
Assert.Empty(result);
@@ -266,7 +266,7 @@ public void FindCascadingParameters_MismatchingNameButMatchingType_ReturnsEmpty(
266266
new ComponentWithNamedCascadingParam());
267267

268268
// Act
269-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
269+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
270270

271271
// Assert
272272
Assert.Empty(result);
@@ -281,7 +281,7 @@ public void FindCascadingParameters_MatchingNameButMismatchingType_ReturnsEmpty(
281281
new ComponentWithNamedCascadingParam());
282282

283283
// Act
284-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
284+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
285285

286286
// Assert
287287
Assert.Empty(result);
@@ -296,7 +296,7 @@ public void FindCascadingParameters_MatchingNameAndType_ReturnsMatches()
296296
new ComponentWithNamedCascadingParam());
297297

298298
// Act
299-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
299+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
300300

301301
// Assert
302302
Assert.Collection(result, match =>
@@ -318,7 +318,7 @@ public void FindCascadingParameters_MultipleMatchingAncestors_ReturnsClosestMatc
318318
new ComponentWithCascadingParams());
319319

320320
// Act
321-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
321+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
322322

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

346346
// Act
347-
var result = CascadingParameterState.FindCascadingParameters(states.Last());
347+
var result = CascadingParameterState.FindCascadingParameters(states.Last(), out _);
348348

349349
// Assert
350350
Assert.Collection(result.OrderBy(x => x.ParameterInfo.PropertyName),
@@ -356,7 +356,49 @@ public void FindCascadingParameters_CanOverrideNonNullValueWithNull()
356356
});
357357
}
358358

359-
359+
[Fact]
360+
public void FindCascadingParameters_WithoutSingleDelivery()
361+
{
362+
// Even though ComponentWithCascadingParams itself declares a [SupplyParameterAsSingleDelivery],
363+
// none of the suppliers match it, so we'll get hasSingleDeliveryParameters = false
364+
365+
// Arrange
366+
var states = CreateAncestry(
367+
CreateCascadingValueComponent(new ValueType1()),
368+
new ComponentWithCascadingParams());
369+
370+
// Act
371+
_ = CascadingParameterState.FindCascadingParameters(states.Last(), out var hasSingleDeliveryParameters);
372+
373+
// Assert
374+
Assert.False(hasSingleDeliveryParameters);
375+
}
376+
377+
[Fact]
378+
public void FindCascadingParameters_WithSingleDelivery()
379+
{
380+
// Arrange
381+
var states = CreateAncestry(
382+
CreateCascadingValueComponent(new ValueType1()),
383+
new SupplyParameterWithSingleDeliveryComponent(isFixed: true),
384+
new ComponentWithCascadingParams());
385+
386+
// Act
387+
_ = CascadingParameterState.FindCascadingParameters(states.Last(), out var hasSingleDeliveryParameters);
388+
389+
// Assert
390+
Assert.True(hasSingleDeliveryParameters);
391+
}
392+
393+
[Fact]
394+
public void FindCascadingParameters_DisallowsSingleDeliveryWhenIsFixedIsFalse()
395+
{
396+
var ex = Assert.Throws<InvalidOperationException>(() => CreateAncestry(
397+
new SupplyParameterWithSingleDeliveryComponent(isFixed: false),
398+
new ComponentWithCascadingParams()));
399+
400+
Assert.StartsWith($"'{typeof(SupplyParameterWithSingleDeliveryAttribute)}' is flagged with SingleDelivery", ex.Message);
401+
}
360402

361403
static ComponentState[] CreateAncestry(params IComponent[] components)
362404
{
@@ -412,6 +454,8 @@ class ComponentWithCascadingParams : TestComponentBase
412454
[Parameter] public bool RegularParam { get; set; }
413455
[CascadingParameter] internal ValueType1 CascadingParam1 { get; set; }
414456
[CascadingParameter] internal ValueType2 CascadingParam2 { get; set; }
457+
458+
[SupplyParameterWithSingleDelivery] internal ValueType3 SingleDeliveryCascadingParam { get; set; }
415459
}
416460

417461
class ComponentWithInheritedCascadingParams : ComponentWithCascadingParams
@@ -430,6 +474,30 @@ class ComponentWithNamedCascadingParam : TestComponentBase
430474
internal ValueType1 SomeLocalName { get; set; }
431475
}
432476

477+
class SupplyParameterWithSingleDeliveryAttribute : CascadingParameterAttributeBase
478+
{
479+
public override string Name { get; set; }
480+
481+
internal override bool SingleDelivery => true;
482+
}
483+
484+
class SupplyParameterWithSingleDeliveryComponent(bool isFixed) : ComponentBase, ICascadingValueSupplier
485+
{
486+
public bool IsFixed => isFixed;
487+
488+
public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
489+
=> parameterInfo.Attribute is SupplyParameterWithSingleDeliveryAttribute;
490+
491+
public object GetCurrentValue(in CascadingParameterInfo parameterInfo)
492+
=> throw new NotImplementedException();
493+
494+
public void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
495+
=> throw new NotImplementedException();
496+
497+
public void Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
498+
=> throw new NotImplementedException();
499+
}
500+
433501
class TestComponentBase : IComponent
434502
{
435503
public void Attach(RenderHandle renderHandle)

0 commit comments

Comments
 (0)