Skip to content

Commit 61286fd

Browse files
authored
[Blazor] Initial support for [SupplyParameterFromForm] (#48412)
* Adds support for binding string data to parameters using [SupplyParameterFromForm]
1 parent 2c4a51e commit 61286fd

29 files changed

+595
-40
lines changed

src/Components/Authorization/test/AuthorizeRouteViewTest.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using System.Security.Claims;
56
using Microsoft.AspNetCore.Authorization;
7+
using Microsoft.AspNetCore.Components.Binding;
68
using Microsoft.AspNetCore.Components.Rendering;
79
using Microsoft.AspNetCore.Components.RenderTree;
810
using Microsoft.AspNetCore.Components.Test.Helpers;
@@ -32,6 +34,7 @@ public AuthorizeRouteViewTest()
3234
serviceCollection.AddSingleton<IAuthorizationPolicyProvider, TestAuthorizationPolicyProvider>();
3335
serviceCollection.AddSingleton<IAuthorizationService>(_testAuthorizationService);
3436
serviceCollection.AddSingleton<NavigationManager, TestNavigationManager>();
37+
serviceCollection.AddSingleton<IFormValueSupplier, TestFormValueSupplier>();
3538

3639
var services = serviceCollection.BuildServiceProvider();
3740
_renderer = new TestRenderer(services);
@@ -467,4 +470,18 @@ public TestNavigationManager()
467470
Initialize("https://localhost:85/subdir/", "https://localhost:85/subdir/path?query=value#hash");
468471
}
469472
}
473+
474+
private class TestFormValueSupplier : IFormValueSupplier
475+
{
476+
public bool CanBind(string formName, Type valueType)
477+
{
478+
return false;
479+
}
480+
481+
public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue)
482+
{
483+
boundValue = null;
484+
return false;
485+
}
486+
}
470487
}

src/Components/Components/src/Binding/CascadingModelBinder.cs

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Reflection.Metadata;
5+
using Microsoft.AspNetCore.Components.Binding;
6+
using Microsoft.AspNetCore.Components.Rendering;
57
using Microsoft.AspNetCore.Components.Routing;
68

79
namespace Microsoft.AspNetCore.Components;
810

911
/// <summary>
1012
/// Defines the binding context for data bound from external sources.
1113
/// </summary>
12-
public sealed class CascadingModelBinder : IComponent, IDisposable
14+
public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent, IDisposable
1315
{
1416
private RenderHandle _handle;
1517
private ModelBindingContext? _bindingContext;
1618
private bool _hasPendingQueuedRender;
19+
private BindingInfo? _bindingInfo;
1720

1821
/// <summary>
1922
/// The binding context name.
@@ -35,7 +38,9 @@ public sealed class CascadingModelBinder : IComponent, IDisposable
3538

3639
[CascadingParameter] ModelBindingContext? ParentContext { get; set; }
3740

38-
[Inject] private NavigationManager Navigation { get; set; } = null!;
41+
[Inject] internal NavigationManager Navigation { get; set; } = null!;
42+
43+
[Inject] internal IFormValueSupplier FormValueSupplier { get; set; } = null!;
3944

4045
void IComponent.Attach(RenderHandle renderHandle)
4146
{
@@ -87,7 +92,7 @@ private void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
8792
Render();
8893
}
8994

90-
private void UpdateBindingInformation(string url)
95+
internal void UpdateBindingInformation(string url)
9196
{
9297
// BindingContextId: action parameter used to define the handler
9398
// Name: form name and context used to bind
@@ -103,11 +108,11 @@ private void UpdateBindingInformation(string url)
103108
// 3) Parent has a name "parent-name"
104109
// Name = "parent-name.my-handler";
105110
// BindingContextId = <<base-relative-uri>>((<<existing-query>>&)|?)handler=my-handler
106-
var name = string.IsNullOrEmpty(ParentContext?.Name) ? Name : $"{ParentContext.Name}.{Name}";
111+
var name = ModelBindingContext.Combine(ParentContext, Name);
107112
var bindingId = string.IsNullOrEmpty(name) ? "" : GenerateBindingContextId(name);
108113

109114
var bindingContext = _bindingContext != null &&
110-
string.Equals(_bindingContext.Name, Name, StringComparison.Ordinal) &&
115+
string.Equals(_bindingContext.Name, name, StringComparison.Ordinal) &&
111116
string.Equals(_bindingContext.BindingContextId, bindingId, StringComparison.Ordinal) ?
112117
_bindingContext : new ModelBindingContext(name, bindingId);
113118

@@ -136,4 +141,54 @@ void IDisposable.Dispose()
136141
{
137142
Navigation.LocationChanged -= HandleLocationChanged;
138143
}
144+
145+
bool ICascadingValueComponent.CanSupplyValue(Type valueType, string? valueName)
146+
{
147+
var formName = string.IsNullOrEmpty(valueName) ?
148+
(_bindingContext?.Name) :
149+
ModelBindingContext.Combine(_bindingContext, valueName);
150+
151+
if (_bindingInfo != null &&
152+
string.Equals(_bindingInfo.FormName, formName, StringComparison.Ordinal) &&
153+
_bindingInfo.ValueType.Equals(valueType))
154+
{
155+
// We already bound the value, but some component might have been destroyed and
156+
// re-created. If the type and name of the value that we bound are the same,
157+
// we can provide the value that we bound.
158+
return true;
159+
}
160+
161+
// Can't supply the value if this context is for a form with a different name.
162+
if (FormValueSupplier.CanBind(formName!, valueType))
163+
{
164+
var bindingSucceeded = FormValueSupplier.TryBind(formName!, valueType, out var boundValue);
165+
_bindingInfo = new BindingInfo(formName, valueType, bindingSucceeded, boundValue);
166+
if (!bindingSucceeded)
167+
{
168+
// Report errors
169+
}
170+
171+
return true;
172+
}
173+
174+
return false;
175+
}
176+
177+
void ICascadingValueComponent.Subscribe(ComponentState subscriber)
178+
{
179+
throw new InvalidOperationException("Form values are always fixed.");
180+
}
181+
182+
void ICascadingValueComponent.Unsubscribe(ComponentState subscriber)
183+
{
184+
throw new InvalidOperationException("Form values are always fixed.");
185+
}
186+
187+
object? ICascadingValueComponent.CurrentValue => _bindingInfo == null ?
188+
throw new InvalidOperationException("Tried to access form value before it was bound.") :
189+
_bindingInfo.BoundValue;
190+
191+
bool ICascadingValueComponent.CurrentValueIsFixed => true;
192+
193+
private record BindingInfo(string? FormName, Type ValueType, bool BindingResult, object? BoundValue);
139194
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace Microsoft.AspNetCore.Components.Binding;
7+
8+
/// <summary>
9+
/// Binds form data valuesto a model.
10+
/// </summary>
11+
public interface IFormValueSupplier
12+
{
13+
/// <summary>
14+
/// Determines whether the specified value type can be bound.
15+
/// </summary>
16+
/// <param name="formName">The form name to bind data from.</param>
17+
/// <param name="valueType">The <see cref="Type"/> for the value to bind.</param>
18+
/// <returns><c>true</c> if the value type can be bound; otherwise, <c>false</c>.</returns>
19+
bool CanBind(string formName, Type valueType);
20+
21+
/// <summary>
22+
/// Tries to bind the form with the specified name to a value of the specified type.
23+
/// </summary>
24+
/// <param name="formName">The form name to bind data from.</param>
25+
/// <param name="valueType">The <see cref="Type"/> for the value to bind.</param>
26+
/// <param name="boundValue">The bound value if succeeded.</param>
27+
/// <returns><c>true</c> if the form was bound successfully; otherwise, <c>false</c>.</returns>
28+
bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue);
29+
}

src/Components/Components/src/Binding/ModelBindingContext.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,7 @@ internal ModelBindingContext(string name, string bindingContextId)
3434
/// The computed identifier used to determine what parts of the app can bind data.
3535
/// </summary>
3636
public string BindingContextId { get; }
37+
38+
internal static string Combine(ModelBindingContext? parentContext, string name) =>
39+
string.IsNullOrEmpty(parentContext?.Name) ? name : $"{parentContext.Name}.{name}";
3740
}

src/Components/Components/src/CascadingParameterState.cs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections.Concurrent;
55
using System.Diagnostics.CodeAnalysis;
6+
using System.Linq;
67
using System.Reflection;
78
using Microsoft.AspNetCore.Components.Reflection;
89
using Microsoft.AspNetCore.Components.Rendering;
@@ -45,11 +46,8 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
4546
var supplier = GetMatchingCascadingValueSupplier(info, componentState);
4647
if (supplier != null)
4748
{
48-
if (resultStates == null)
49-
{
50-
// Although not all parameters might be matched, we know the maximum number
51-
resultStates = new List<CascadingParameterState>(infos.Length - infoIndex);
52-
}
49+
// Although not all parameters might be matched, we know the maximum number
50+
resultStates ??= new List<CascadingParameterState>(infos.Length - infoIndex);
5351

5452
resultStates.Add(new CascadingParameterState(info.ConsumerValueName, supplier));
5553
}
@@ -98,16 +96,25 @@ private static ReflectedCascadingParameterInfo[] CreateReflectedCascadingParamet
9896
var attribute = prop.GetCustomAttribute<CascadingParameterAttribute>();
9997
if (attribute != null)
10098
{
101-
if (result == null)
102-
{
103-
result = new List<ReflectedCascadingParameterInfo>();
104-
}
99+
result ??= new List<ReflectedCascadingParameterInfo>();
105100

106101
result.Add(new ReflectedCascadingParameterInfo(
107102
prop.Name,
108103
prop.PropertyType,
109104
attribute.Name));
110105
}
106+
107+
var hostParameterAttribute = prop.GetCustomAttributes()
108+
.OfType<IHostEnvironmentCascadingParameter>().SingleOrDefault();
109+
if (hostParameterAttribute != null)
110+
{
111+
result ??= new List<ReflectedCascadingParameterInfo>();
112+
113+
result.Add(new ReflectedCascadingParameterInfo(
114+
prop.Name,
115+
prop.PropertyType,
116+
hostParameterAttribute.Name));
117+
}
111118
}
112119

113120
return result?.ToArray() ?? Array.Empty<ReflectedCascadingParameterInfo>();
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components;
5+
6+
// Marks a cascading parameter that can be offered via an attribute that is not
7+
// directly defined in the Components assembly. For example [SupplyParameterFromForm].
8+
internal interface IHostEnvironmentCascadingParameter
9+
{
10+
public string? Name { get; set; }
11+
}

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#nullable enable
22
abstract Microsoft.AspNetCore.Components.RenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode!
3+
Microsoft.AspNetCore.Components.Binding.IFormValueSupplier
4+
Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.CanBind(string! formName, System.Type! valueType) -> bool
5+
Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.TryBind(string! formName, System.Type! valueType, out object? boundValue) -> bool
36
Microsoft.AspNetCore.Components.CascadingModelBinder
47
Microsoft.AspNetCore.Components.CascadingModelBinder.CascadingModelBinder() -> void
58
Microsoft.AspNetCore.Components.CascadingModelBinder.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.ModelBindingContext!>!

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

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections.Concurrent;
55
using System.Diagnostics.CodeAnalysis;
6+
using System.Linq;
67
using System.Reflection;
78
using static Microsoft.AspNetCore.Internal.LinkerFlags;
89

@@ -168,11 +169,14 @@ private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMem
168169
var propertyInfo = targetType.GetProperty(parameterName, BindablePropertyFlags);
169170
if (propertyInfo != null)
170171
{
171-
if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) && !propertyInfo.IsDefined(typeof(CascadingParameterAttribute)))
172+
if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) &&
173+
!propertyInfo.IsDefined(typeof(CascadingParameterAttribute)) &&
174+
!propertyInfo.GetCustomAttributes().OfType<IHostEnvironmentCascadingParameter>().Any())
172175
{
173176
throw new InvalidOperationException(
174177
$"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " +
175-
$"but it does not have [{nameof(ParameterAttribute)}] or [{nameof(CascadingParameterAttribute)}] applied.");
178+
$"but it does not have [{nameof(ParameterAttribute)}], [{nameof(CascadingParameterAttribute)}] or " +
179+
$"[SupplyParameterFromFormAttribute] applied.");
176180
}
177181
else
178182
{
@@ -257,9 +261,30 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType)
257261

258262
foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
259263
{
260-
var parameterAttribute = propertyInfo.GetCustomAttribute<ParameterAttribute>();
261-
var cascadingParameterAttribute = propertyInfo.GetCustomAttribute<CascadingParameterAttribute>();
262-
var isParameter = parameterAttribute != null || cascadingParameterAttribute != null;
264+
ParameterAttribute? parameterAttribute = null;
265+
CascadingParameterAttribute? cascadingParameterAttribute = null;
266+
IHostEnvironmentCascadingParameter? hostEnvironmentCascadingParameter = null;
267+
268+
var attributes = propertyInfo.GetCustomAttributes();
269+
foreach (var attribute in attributes)
270+
{
271+
switch (attribute)
272+
{
273+
case ParameterAttribute parameter:
274+
parameterAttribute = parameter;
275+
break;
276+
case CascadingParameterAttribute cascadingParameter:
277+
cascadingParameterAttribute = cascadingParameter;
278+
break;
279+
case IHostEnvironmentCascadingParameter hostEnvironmentAttribute:
280+
hostEnvironmentCascadingParameter = hostEnvironmentAttribute;
281+
break;
282+
default:
283+
break;
284+
}
285+
}
286+
287+
var isParameter = parameterAttribute != null || cascadingParameterAttribute != null || hostEnvironmentCascadingParameter != null;
263288
if (!isParameter)
264289
{
265290
continue;
@@ -274,7 +299,7 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType)
274299

275300
var propertySetter = new PropertySetter(targetType, propertyInfo)
276301
{
277-
Cascading = cascadingParameterAttribute != null,
302+
Cascading = cascadingParameterAttribute != null || hostEnvironmentCascadingParameter != null,
278303
};
279304

280305
if (_underlyingWriters.ContainsKey(propertyName))

src/Components/Components/test/CascadingModelBinderTest.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics.CodeAnalysis;
5+
using Microsoft.AspNetCore.Components.Binding;
56
using Microsoft.AspNetCore.Components.Rendering;
67
using Microsoft.AspNetCore.Components.Test.Helpers;
78
using Microsoft.Extensions.DependencyInjection;
@@ -18,6 +19,7 @@ public CascadingModelBinderTest()
1819
var serviceCollection = new ServiceCollection();
1920
_navigationManager = new TestNavigationManager();
2021
serviceCollection.AddSingleton<NavigationManager>(_navigationManager);
22+
serviceCollection.AddSingleton<IFormValueSupplier, TestFormValueSupplier>();
2123
var services = serviceCollection.BuildServiceProvider();
2224
_renderer = new TestRenderer(services);
2325
}
@@ -328,4 +330,18 @@ public TestComponent(RenderFragment renderFragment)
328330
protected override void BuildRenderTree(RenderTreeBuilder builder)
329331
=> _renderFragment(builder);
330332
}
333+
334+
private class TestFormValueSupplier : IFormValueSupplier
335+
{
336+
public bool CanBind(string formName, Type valueType)
337+
{
338+
return false;
339+
}
340+
341+
public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue)
342+
{
343+
boundValue = null;
344+
return false;
345+
}
346+
}
331347
}

0 commit comments

Comments
 (0)