Skip to content

Commit 815d19d

Browse files
[Blazor] Allow properties marked with both [Parameter] and [SupplyParameterFromQuery] to receive values directly (#48954) (#49038)
1 parent 417ce3b commit 815d19d

File tree

4 files changed

+108
-14
lines changed

4 files changed

+108
-14
lines changed

src/Components/Components/src/ParameterView.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public readonly struct ParameterView
1616
{
1717
private static readonly RenderTreeFrame[] _emptyFrames = new RenderTreeFrame[]
1818
{
19-
RenderTreeFrame.Element(0, string.Empty).WithComponentSubtreeLength(1)
19+
RenderTreeFrame.Element(0, string.Empty).WithComponentSubtreeLength(1)
2020
};
2121

2222
private static readonly ParameterView _empty = new ParameterView(ParameterViewLifetime.Unbound, _emptyFrames, 0, Array.Empty<CascadingParameterState>());
@@ -131,6 +131,20 @@ internal ParameterView Clone()
131131
internal ParameterView WithCascadingParameters(IReadOnlyList<CascadingParameterState> cascadingParameters)
132132
=> new ParameterView(_lifetime, _frames, _ownerIndex, cascadingParameters);
133133

134+
internal bool HasDirectParameter(string parameterName)
135+
{
136+
var directParameterEnumerator = new RenderTreeFrameParameterEnumerator(_frames, _ownerIndex);
137+
while (directParameterEnumerator.MoveNext())
138+
{
139+
if (string.Equals(directParameterEnumerator.Current.Name, parameterName, StringComparison.Ordinal))
140+
{
141+
return true;
142+
}
143+
}
144+
145+
return false;
146+
}
147+
134148
// It's internal because there isn't a known use case for user code comparing
135149
// ParameterView instances, and even if there was, it's unlikely it should
136150
// use these equality rules which are designed for their effect on rendering.

src/Components/Components/src/Reflection/ComponentProperties.cs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ public static void SetProperties(in ParameterView parameters, object target)
4545
ThrowForUnknownIncomingParameterName(targetType, parameterName);
4646
throw null; // Unreachable
4747
}
48-
else if (writer.Cascading && !parameter.Cascading)
48+
else if (!writer.AcceptsDirectParameters && !parameter.Cascading)
4949
{
50-
// We don't allow you to set a cascading parameter with a non-cascading value. Put another way:
50+
// We don't allow you to set a cascading parameter with a non-cascading (direct) value. Put another way:
5151
// cascading parameters are not part of the public API of a component, so it's not reasonable
5252
// for someone to set it directly.
5353
//
@@ -56,13 +56,23 @@ public static void SetProperties(in ParameterView parameters, object target)
5656
ThrowForSettingCascadingParameterWithNonCascadingValue(targetType, parameterName);
5757
throw null; // Unreachable
5858
}
59-
else if (!writer.Cascading && parameter.Cascading)
59+
else if (!writer.AcceptsCascadingParameters && parameter.Cascading)
6060
{
6161
// We're giving a more specific error here because trying to set a non-cascading parameter
6262
// with a cascading value is likely deliberate (but not supported), or is a bug in our code.
6363
ThrowForSettingParameterWithCascadingValue(targetType, parameterName);
6464
throw null; // Unreachable
6565
}
66+
else if (parameter.Cascading && writer.AcceptsDirectParameters && writer.AcceptsCascadingParameters)
67+
{
68+
// Today, the only case where this is possible is when a property is annotated with both
69+
// ParameterAttribute and SupplyParameterFromQueryAttribute. If that happens, we want to
70+
// prefer the directly supplied value over the cascading value.
71+
if (parameters.HasDirectParameter(parameterName))
72+
{
73+
continue;
74+
}
75+
}
6676

6777
SetProperty(target, writer, parameterName, parameter.Value);
6878
}
@@ -82,7 +92,7 @@ public static void SetProperties(in ParameterView parameters, object target)
8292

8393
if (writers.TryGetValue(parameterName, out var writer))
8494
{
85-
if (!writer.Cascading && parameter.Cascading)
95+
if (!writer.AcceptsCascadingParameters && parameter.Cascading)
8696
{
8797
// Don't allow an "extra" cascading value to be collected - or don't allow a non-cascading
8898
// parameter to be set with a cascading value.
@@ -91,7 +101,7 @@ public static void SetProperties(in ParameterView parameters, object target)
91101
ThrowForSettingParameterWithCascadingValue(targetType, parameterName);
92102
throw null; // Unreachable
93103
}
94-
else if (writer.Cascading && !parameter.Cascading)
104+
else if (writer.AcceptsCascadingParameters && !parameter.Cascading)
95105
{
96106
// Allow unmatched parameters to collide with the names of cascading parameters. This is
97107
// valid because cascading parameter names are not part of the public API. There's no
@@ -196,8 +206,8 @@ private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMem
196206
private static void ThrowForSettingCascadingParameterWithNonCascadingValue(Type targetType, string parameterName)
197207
{
198208
throw new InvalidOperationException(
199-
$"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " +
200-
$"but it does not have [{nameof(ParameterAttribute)}] applied.");
209+
$"The property '{parameterName}' on component type '{targetType.FullName}' cannot be set " +
210+
$"explicitly because it only accepts cascading values.");
201211
}
202212

203213
[DoesNotReturn]
@@ -279,8 +289,12 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType)
279289
}
280290
}
281291

282-
var isParameter = parameterAttribute != null || cascadingParameterAttribute != null;
283-
if (!isParameter)
292+
// A property cannot accept direct parameters if it's annotated with a cascading value attribute, unless it's a
293+
// SupplyParameterFromQueryAttribute. This is to retain backwards compatibility with previous versions of the
294+
// SupplyParameterFromQuery feature that did not utilize cascading values, and thus did not have this limitation.
295+
var acceptsDirectParameters = parameterAttribute is not null && cascadingParameterAttribute is null or SupplyParameterFromQueryAttribute;
296+
var acceptsCascadingParameters = cascadingParameterAttribute is not null;
297+
if (!acceptsDirectParameters && !acceptsCascadingParameters)
284298
{
285299
continue;
286300
}
@@ -294,7 +308,8 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType)
294308

295309
var propertySetter = new PropertySetter(targetType, propertyInfo)
296310
{
297-
Cascading = cascadingParameterAttribute != null,
311+
AcceptsDirectParameters = acceptsDirectParameters,
312+
AcceptsCascadingParameters = acceptsCascadingParameters,
298313
};
299314

300315
if (_underlyingWriters.ContainsKey(propertyName))

src/Components/Components/src/Reflection/PropertySetter.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ public PropertySetter(Type targetType, PropertyInfo property)
3636
callPropertySetterClosedGenericMethod.CreateDelegate(typeof(Action<object, object>), propertySetterAsAction);
3737
}
3838

39-
public bool Cascading { get; init; }
39+
public bool AcceptsDirectParameters { get; init; }
40+
41+
public bool AcceptsCascadingParameters { get; init; }
4042

4143
public void SetValue(object target, object value) => _setterDelegate(target, value);
4244

src/Components/Components/test/ParameterViewTest.Assignment.cs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,8 @@ public void IncomingNonCascadingValueMatchesCascadingParameter_Throws()
237237

238238
// Assert
239239
Assert.Equal(
240-
$"Object of type '{typeof(HasCascadingParameter).FullName}' has a property matching the name '{nameof(HasCascadingParameter.Cascading)}', " +
241-
$"but it does not have [{nameof(ParameterAttribute)}] applied.",
240+
$"The property '{nameof(HasCascadingParameter.Cascading)}' on component type '{typeof(HasCascadingParameter).FullName}' " +
241+
$"cannot be set explicitly because it only accepts cascading values.",
242242
ex.Message);
243243
}
244244

@@ -261,6 +261,59 @@ public void IncomingCascadingValueMatchesNonCascadingParameter_Throws()
261261
ex.Message);
262262
}
263263

264+
[Fact]
265+
public void IncomingNonCascadingValueMatchesParameterThatIsBothCascadingAndNonCascading_Throws()
266+
{
267+
// Arrange
268+
var target = new HasPropertyWithParameterAndCascadingParameterAttributes();
269+
var builder = new ParameterViewBuilder();
270+
builder.Add(nameof(HasPropertyWithParameterAndCascadingParameterAttributes.Parameter), "Hello", cascading: false);
271+
var parameters = builder.Build();
272+
273+
// Act
274+
var ex = Assert.Throws<InvalidOperationException>(() => parameters.SetParameterProperties(target));
275+
276+
// Assert
277+
Assert.Equal(
278+
$"The property '{nameof(HasPropertyWithParameterAndCascadingParameterAttributes.Parameter)}' on component type " +
279+
$"'{typeof(HasPropertyWithParameterAndCascadingParameterAttributes).FullName}' cannot be set explicitly because it " +
280+
$"only accepts cascading values.",
281+
ex.Message);
282+
}
283+
284+
[Fact]
285+
public void IncomingCascadingValueMatchesParameterThatIsBothCascadingAndNonCascading_Works()
286+
{
287+
// Arrange
288+
var target = new HasPropertyWithParameterAndCascadingParameterAttributes();
289+
var builder = new ParameterViewBuilder();
290+
builder.Add(nameof(HasPropertyWithParameterAndCascadingParameterAttributes.Parameter), "Hello", cascading: true);
291+
var parameters = builder.Build();
292+
293+
// Act
294+
parameters.SetParameterProperties(target);
295+
296+
// Assert
297+
Assert.Equal("Hello", target.Parameter);
298+
}
299+
300+
[Fact]
301+
public void ParameterThatCanBeSuppliedFromQueryOrNonCascadingValue_PrefersNonCascadingValue()
302+
{
303+
// Arrange
304+
var target = new HasPropertyWithParameterAndSupplyParameterFromQueryAttributes();
305+
var builder = new ParameterViewBuilder();
306+
builder.Add(nameof(HasPropertyWithParameterAndSupplyParameterFromQueryAttributes.Parameter), "Non-cascading", cascading: false);
307+
builder.Add(nameof(HasPropertyWithParameterAndSupplyParameterFromQueryAttributes.Parameter), "Cascading", cascading: true);
308+
var parameters = builder.Build();
309+
310+
// Act
311+
parameters.SetParameterProperties(target);
312+
313+
// Assert
314+
Assert.Equal("Non-cascading", target.Parameter);
315+
}
316+
264317
[Fact]
265318
public void SettingCaptureUnmatchedValuesParameterExplicitlyWorks()
266319
{
@@ -644,6 +697,16 @@ class HasNonPublicCascadingParameter
644697
public string GetCascadingValue() => Cascading;
645698
}
646699

700+
class HasPropertyWithParameterAndCascadingParameterAttributes
701+
{
702+
[Parameter, CascadingParameter] public string Parameter { get; set; }
703+
}
704+
705+
class HasPropertyWithParameterAndSupplyParameterFromQueryAttributes
706+
{
707+
[Parameter, SupplyParameterFromQuery] public string Parameter { get; set; }
708+
}
709+
647710
class ParameterViewBuilder : IEnumerable
648711
{
649712
private readonly List<(string Name, object Value, bool Cascading)> _parameters = new();

0 commit comments

Comments
 (0)