Skip to content

Commit 3432083

Browse files
Replace AssignToProperties with SetParameterProperties, which also clears unspecified parameter properties (imported from Blazor PR 1108) (#4797)
1 parent 3757908 commit 3432083

File tree

17 files changed

+138
-83
lines changed

17 files changed

+138
-83
lines changed

src/Components/blazor/samples/StandaloneApp/Pages/FetchData.cshtml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@page "/fetchdata"
1+
@page "/fetchdata"
22
@page "/fetchdata/{StartDate:datetime}"
33
@inject HttpClient Http
44

@@ -48,14 +48,14 @@ else
4848

4949
WeatherForecast[] forecasts;
5050

51-
public override void SetParameters(ParameterCollection parameters)
52-
{
53-
StartDate = DateTime.Now;
54-
base.SetParameters(parameters);
55-
}
56-
5751
protected override async Task OnParametersSetAsync()
5852
{
53+
// If no value was given in the URL for StartDate, apply a default
54+
if (StartDate == default)
55+
{
56+
StartDate = DateTime.Now;
57+
}
58+
5959
forecasts = await Http.GetJsonAsync<WeatherForecast[]>(
6060
$"sample-data/weather.json?date={StartDate.ToString("yyyy-MM-dd")}");
6161

src/Components/samples/ComponentsApp.App/Pages/FetchData.cshtml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ else
4848

4949
WeatherForecast[] forecasts;
5050

51-
public override void SetParameters(ParameterCollection parameters)
52-
{
53-
StartDate = DateTime.Now;
54-
base.SetParameters(parameters);
55-
}
56-
5751
protected override async Task OnParametersSetAsync()
5852
{
53+
// If no value was given in the URL for StartDate, apply a default
54+
if (StartDate == default)
55+
{
56+
StartDate = DateTime.Now;
57+
}
58+
5959
forecasts = await ForecastService.GetForecastAsync(StartDate);
6060
}
6161
}

src/Components/src/Microsoft.AspNetCore.Components/CascadingValue.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public void Init(RenderHandle renderHandle)
5858
public void SetParameters(ParameterCollection parameters)
5959
{
6060
// Implementing the parameter binding manually, instead of just calling
61-
// parameters.AssignToProperties(this), is just a very slight perf optimization
61+
// parameters.SetParameterProperties(this), is just a very slight perf optimization
6262
// and makes it simpler impose rules about the params being required or not.
6363

6464
var hasSuppliedValue = false;

src/Components/src/Microsoft.AspNetCore.Components/ComponentBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ void IComponent.Init(RenderHandle renderHandle)
151151
/// <param name="parameters">The parameters to apply.</param>
152152
public virtual void SetParameters(ParameterCollection parameters)
153153
{
154-
parameters.AssignToProperties(this);
154+
parameters.SetParameterProperties(this);
155155

156156
if (!_hasCalledInit)
157157
{

src/Components/src/Microsoft.AspNetCore.Components/Layouts/LayoutDisplay.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -42,7 +42,7 @@ public void Init(RenderHandle renderHandle)
4242
/// <inheritdoc />
4343
public void SetParameters(ParameterCollection parameters)
4444
{
45-
parameters.AssignToProperties(this);
45+
parameters.SetParameterProperties(this);
4646
Render();
4747
}
4848

src/Components/src/Microsoft.AspNetCore.Components/Microsoft.AspNetCore.Components.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
55
<Description>Components feature for ASP.NET Core.</Description>
6+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
67
</PropertyGroup>
78

89
<ItemGroup>

src/Components/src/Microsoft.AspNetCore.Components/ParameterCollectionExtensions.cs

Lines changed: 77 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,16 @@ public static class ParameterCollectionExtensions
1616
{
1717
private const BindingFlags _bindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase;
1818

19-
private delegate void WriteParameterAction(object target, object parameterValue);
20-
21-
private readonly static IDictionary<Type, IDictionary<string, WriteParameterAction>> _cachedParameterWriters
22-
= new ConcurrentDictionary<Type, IDictionary<string, WriteParameterAction>>();
19+
private readonly static ConcurrentDictionary<Type, WritersForType> _cachedWritersByType
20+
= new ConcurrentDictionary<Type, WritersForType>();
2321

2422
/// <summary>
25-
/// Iterates through the <see cref="ParameterCollection"/>, assigning each parameter
26-
/// to a property of the same name on <paramref name="target"/>.
23+
/// For each parameter property on <paramref name="target"/>, updates its value to
24+
/// match the corresponding entry in the <see cref="ParameterCollection"/>.
2725
/// </summary>
2826
/// <param name="parameterCollection">The <see cref="ParameterCollection"/>.</param>
2927
/// <param name="target">An object that has a public writable property matching each parameter's name and type.</param>
30-
public static void AssignToProperties(
28+
public unsafe static void SetParameterProperties(
3129
in this ParameterCollection parameterCollection,
3230
object target)
3331
{
@@ -37,23 +35,36 @@ public static void AssignToProperties(
3735
}
3836

3937
var targetType = target.GetType();
40-
if (!_cachedParameterWriters.TryGetValue(targetType, out var parameterWriters))
38+
if (!_cachedWritersByType.TryGetValue(targetType, out var writers))
4139
{
42-
parameterWriters = CreateParameterWriters(targetType);
43-
_cachedParameterWriters[targetType] = parameterWriters;
40+
writers = new WritersForType(targetType);
41+
_cachedWritersByType[targetType] = writers;
4442
}
4543

44+
// We only want to iterate through the parameterCollection once, and by the end of it,
45+
// need to have tracked which of the parameter properties haven't yet been written.
46+
// To avoid allocating any list/dictionary to track that, here we stackalloc an array
47+
// of flags and set them based on the indices of the writers we use.
48+
var numWriters = writers.WritersByIndex.Count;
49+
var numUsedWriters = 0;
50+
51+
// TODO: Once we're able to move to netstandard2.1, this can be changed to be
52+
// a Span<bool> and then the enclosing method no longer needs to be 'unsafe'
53+
bool* usageFlags = stackalloc bool[numWriters];
54+
4655
foreach (var parameter in parameterCollection)
4756
{
4857
var parameterName = parameter.Name;
49-
if (!parameterWriters.TryGetValue(parameterName, out var parameterWriter))
58+
if (!writers.WritersByName.TryGetValue(parameterName, out var writerWithIndex))
5059
{
5160
ThrowForUnknownIncomingParameterName(targetType, parameterName);
5261
}
5362

5463
try
5564
{
56-
parameterWriter(target, parameter.Value);
65+
writerWithIndex.Writer.SetValue(target, parameter.Value);
66+
usageFlags[writerWithIndex.Index] = true;
67+
numUsedWriters++;
5768
}
5869
catch (Exception ex)
5970
{
@@ -62,43 +73,28 @@ public static void AssignToProperties(
6273
$"type '{target.GetType().FullName}'. The error was: {ex.Message}", ex);
6374
}
6475
}
65-
}
66-
67-
internal static IEnumerable<PropertyInfo> GetCandidateBindableProperties(Type targetType)
68-
=> MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags);
6976

70-
private static IDictionary<string, WriteParameterAction> CreateParameterWriters(Type targetType)
71-
{
72-
var result = new Dictionary<string, WriteParameterAction>(StringComparer.OrdinalIgnoreCase);
73-
74-
foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
77+
// Now we can determine whether any writers have not been used, and if there are
78+
// some unused ones, find them.
79+
for (var index = 0; numUsedWriters < numWriters; index++)
7580
{
76-
var shouldCreateWriter = propertyInfo.IsDefined(typeof(ParameterAttribute))
77-
|| propertyInfo.IsDefined(typeof(CascadingParameterAttribute));
78-
if (!shouldCreateWriter)
81+
if (index >= numWriters)
7982
{
80-
continue;
83+
// This should not be possible
84+
throw new InvalidOperationException("Ran out of writers before marking them all as used.");
8185
}
8286

83-
var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo);
84-
85-
var propertyName = propertyInfo.Name;
86-
if (result.ContainsKey(propertyName))
87+
if (!usageFlags[index])
8788
{
88-
throw new InvalidOperationException(
89-
$"The type '{targetType.FullName}' declares more than one parameter matching the " +
90-
$"name '{propertyName.ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique.");
89+
writers.WritersByIndex[index].SetDefaultValue(target);
90+
numUsedWriters++;
9191
}
92-
93-
result.Add(propertyName, (object target, object parameterValue) =>
94-
{
95-
propertySetter.SetValue(target, parameterValue);
96-
});
9792
}
98-
99-
return result;
10093
}
10194

95+
internal static IEnumerable<PropertyInfo> GetCandidateBindableProperties(Type targetType)
96+
=> MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags);
97+
10298
private static void ThrowForUnknownIncomingParameterName(Type targetType, string parameterName)
10399
{
104100
// We know we're going to throw by this stage, so it doesn't matter that the following
@@ -126,5 +122,47 @@ private static void ThrowForUnknownIncomingParameterName(Type targetType, string
126122
$"matching the name '{parameterName}'.");
127123
}
128124
}
125+
126+
class WritersForType
127+
{
128+
public Dictionary<string, (int Index, IPropertySetter Writer)> WritersByName { get; }
129+
public List<IPropertySetter> WritersByIndex { get; }
130+
131+
public WritersForType(Type targetType)
132+
{
133+
var propertySettersByName = new Dictionary<string, IPropertySetter>(StringComparer.OrdinalIgnoreCase);
134+
foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
135+
{
136+
var shouldCreateWriter = propertyInfo.IsDefined(typeof(ParameterAttribute))
137+
|| propertyInfo.IsDefined(typeof(CascadingParameterAttribute));
138+
if (!shouldCreateWriter)
139+
{
140+
continue;
141+
}
142+
143+
var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo);
144+
145+
var propertyName = propertyInfo.Name;
146+
if (propertySettersByName.ContainsKey(propertyName))
147+
{
148+
throw new InvalidOperationException(
149+
$"The type '{targetType.FullName}' declares more than one parameter matching the " +
150+
$"name '{propertyName.ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique.");
151+
}
152+
153+
propertySettersByName.Add(propertyName, propertySetter);
154+
}
155+
156+
// Now we know all the entries, construct the resulting list/dictionary
157+
// with well-defined indices
158+
WritersByIndex = new List<IPropertySetter>();
159+
WritersByName = new Dictionary<string, (int, IPropertySetter)>(StringComparer.OrdinalIgnoreCase);
160+
foreach (var pair in propertySettersByName)
161+
{
162+
WritersByName.Add(pair.Key, (WritersByIndex.Count, pair.Value));
163+
WritersByIndex.Add(pair.Value);
164+
}
165+
}
166+
}
129167
}
130168
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System;
5-
64
namespace Microsoft.AspNetCore.Components.Reflection
75
{
86
internal interface IPropertySetter
97
{
108
void SetValue(object target, object value);
9+
10+
void SetDefaultValue(object target);
1111
}
1212
}

src/Components/src/Microsoft.AspNetCore.Components/Reflection/MemberAssignment.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -48,10 +48,14 @@ public PropertySetter(MethodInfo setMethod)
4848
{
4949
_setterDelegate = (Action<TTarget, TValue>)Delegate.CreateDelegate(
5050
typeof(Action<TTarget, TValue>), setMethod);
51+
var propertyType = typeof(TValue);
5152
}
5253

5354
public void SetValue(object target, object value)
5455
=> _setterDelegate((TTarget)target, (TValue)value);
56+
57+
public void SetDefaultValue(object target)
58+
=> _setterDelegate((TTarget)target, default);
5559
}
5660
}
5761
}

src/Components/src/Microsoft.AspNetCore.Components/Routing/Router.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public void Init(RenderHandle renderHandle)
4949
/// <inheritdoc />
5050
public void SetParameters(ParameterCollection parameters)
5151
{
52-
parameters.AssignToProperties(this);
52+
parameters.SetParameterProperties(this);
5353
var types = ComponentResolver.ResolveComponents(AppAssembly);
5454
Routes = RouteTable.Create(types);
5555
Refresh();

src/Components/test/Microsoft.AspNetCore.Components.E2ETest/Tests/RoutingTest.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,17 @@ public void CanFollowLinkToPageWithParameters()
212212

213213
var app = MountTestComponent<TestRouter>();
214214
app.FindElement(By.LinkText("With parameters")).Click();
215+
WaitAssert.Equal("Your full name is Abc .", () => app.FindElement(By.Id("test-info")).Text);
216+
AssertHighlightedLinks("With parameters");
217+
218+
// Can add more parameters while remaining on same page
219+
app.FindElement(By.LinkText("With more parameters")).Click();
215220
WaitAssert.Equal("Your full name is Abc McDef.", () => app.FindElement(By.Id("test-info")).Text);
221+
AssertHighlightedLinks("With parameters", "With more parameters");
222+
223+
// Can remove parameters while remaining on same page
224+
app.FindElement(By.LinkText("With parameters")).Click();
225+
WaitAssert.Equal("Your full name is Abc .", () => app.FindElement(By.Id("test-info")).Text);
216226
AssertHighlightedLinks("With parameters");
217227
}
218228

0 commit comments

Comments
 (0)