Skip to content

Commit 883f06c

Browse files
[Blazor] Take cascading parameter attribute type into account when supplying cascading values (#48554)
1 parent c53f18a commit 883f06c

39 files changed

+1045
-845
lines changed

src/Components/Authorization/test/AuthorizeRouteViewTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ public AuthorizeRouteViewTest()
3434
serviceCollection.AddSingleton<IAuthorizationPolicyProvider, TestAuthorizationPolicyProvider>();
3535
serviceCollection.AddSingleton<IAuthorizationService>(_testAuthorizationService);
3636
serviceCollection.AddSingleton<NavigationManager, TestNavigationManager>();
37-
serviceCollection.AddSingleton<IFormValueSupplier, TestFormValueSupplier>();
3837

3938
var services = serviceCollection.BuildServiceProvider();
4039
_renderer = new TestRenderer(services);

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

Lines changed: 92 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
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;
5+
using System.Linq;
46
using System.Reflection.Metadata;
57
using Microsoft.AspNetCore.Components.Binding;
68
using Microsoft.AspNetCore.Components.Rendering;
@@ -11,12 +13,13 @@ namespace Microsoft.AspNetCore.Components;
1113
/// <summary>
1214
/// Defines the binding context for data bound from external sources.
1315
/// </summary>
14-
public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent, IDisposable
16+
public sealed class CascadingModelBinder : IComponent, ICascadingValueSupplier, IDisposable
1517
{
18+
private readonly Dictionary<Type, CascadingModelBindingProvider?> _providersByCascadingParameterAttributeType = new();
19+
1620
private RenderHandle _handle;
1721
private ModelBindingContext? _bindingContext;
1822
private bool _hasPendingQueuedRender;
19-
private BindingInfo? _bindingInfo;
2023

2124
/// <summary>
2225
/// The binding context name.
@@ -40,7 +43,9 @@ public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent,
4043

4144
[Inject] internal NavigationManager Navigation { get; set; } = null!;
4245

43-
[Inject] internal IFormValueSupplier FormValueSupplier { get; set; } = null!;
46+
[Inject] internal IEnumerable<CascadingModelBindingProvider> ModelBindingProviders { get; set; } = Enumerable.Empty<CascadingModelBindingProvider>();
47+
48+
internal ModelBindingContext? BindingContext => _bindingContext;
4449

4550
void IComponent.Attach(RenderHandle renderHandle)
4651
{
@@ -110,85 +115,118 @@ internal void UpdateBindingInformation(string url)
110115
// BindingContextId = <<base-relative-uri>>((<<existing-query>>&)|?)handler=my-handler
111116
var name = ModelBindingContext.Combine(ParentContext, Name);
112117
var bindingId = string.IsNullOrEmpty(name) ? "" : GenerateBindingContextId(name);
118+
var bindingContextDidChange =
119+
_bindingContext is null ||
120+
!string.Equals(_bindingContext.Name, name, StringComparison.Ordinal) ||
121+
!string.Equals(_bindingContext.BindingContextId, bindingId, StringComparison.Ordinal);
113122

114-
var bindingContext = _bindingContext != null &&
115-
string.Equals(_bindingContext.Name, name, StringComparison.Ordinal) &&
116-
string.Equals(_bindingContext.BindingContextId, bindingId, StringComparison.Ordinal) ?
117-
_bindingContext : new ModelBindingContext(name, bindingId, FormValueSupplier.CanConvertSingleValue);
118-
119-
// It doesn't matter that we don't check IsFixed, since the CascadingValue we are setting up will throw if the app changes.
120-
if (IsFixed && _bindingContext != null && _bindingContext != bindingContext)
123+
if (bindingContextDidChange)
121124
{
122-
// Throw an exception if either the Name or the BindingContextId changed. Once a CascadingModelBinder has been initialized
123-
// as fixed, it can't change it's name nor its BindingContextId. This can happen in several situations:
124-
// * Component ParentContext hierarchy changes.
125-
// * Technically, the component won't be retained in this case and will be destroyed instead.
126-
// * A parent changes Name.
127-
throw new InvalidOperationException($"'{nameof(CascadingModelBinder)}' 'Name' can't change after initialized.");
128-
}
125+
if (IsFixed && _bindingContext is not null)
126+
{
127+
// Throw an exception if either the Name or the BindingContextId changed. Once a CascadingModelBinder has been initialized
128+
// as fixed, it can't change it's name nor its BindingContextId. This can happen in several situations:
129+
// * Component ParentContext hierarchy changes.
130+
// * Technically, the component won't be retained in this case and will be destroyed instead.
131+
// * A parent changes Name.
132+
throw new InvalidOperationException($"'{nameof(CascadingModelBinder)}' 'Name' can't change after initialized.");
133+
}
129134

130-
_bindingContext = bindingContext;
135+
_bindingContext = new ModelBindingContext(name, bindingId, CanBind);
136+
}
131137

132138
string GenerateBindingContextId(string name)
133139
{
134140
var bindingId = Navigation.ToBaseRelativePath(Navigation.GetUriWithQueryParameter("handler", name));
135141
var hashIndex = bindingId.IndexOf('#');
136142
return hashIndex == -1 ? bindingId : new string(bindingId.AsSpan(0, hashIndex));
137143
}
144+
145+
bool CanBind(Type type)
146+
{
147+
foreach (var provider in ModelBindingProviders)
148+
{
149+
if (provider.SupportsParameterType(type))
150+
{
151+
return true;
152+
}
153+
}
154+
155+
return false;
156+
}
138157
}
139158

140-
void IDisposable.Dispose()
159+
bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo)
160+
=> TryGetProvider(in parameterInfo, out var provider)
161+
&& provider.CanSupplyValue(_bindingContext, parameterInfo);
162+
163+
void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
141164
{
142-
Navigation.LocationChanged -= HandleLocationChanged;
165+
// We expect there to always be a provider at this point, because CanSupplyValue must have returned true.
166+
var provider = GetProviderOrThrow(parameterInfo);
167+
168+
if (!provider.AreValuesFixed)
169+
{
170+
provider.Subscribe(subscriber);
171+
}
143172
}
144173

145-
bool ICascadingValueComponent.CanSupplyValue(Type valueType, string? valueName)
174+
void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
146175
{
147-
var formName = string.IsNullOrEmpty(valueName) ?
148-
(_bindingContext?.Name) :
149-
ModelBindingContext.Combine(_bindingContext, valueName);
176+
// We expect there to always be a provider at this point, because CanSupplyValue must have returned true.
177+
var provider = GetProviderOrThrow(parameterInfo);
150178

151-
if (_bindingInfo != null &&
152-
string.Equals(_bindingInfo.FormName, formName, StringComparison.Ordinal) &&
153-
_bindingInfo.ValueType.Equals(valueType))
179+
if (!provider.AreValuesFixed)
154180
{
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;
181+
provider.Unsubscribe(subscriber);
159182
}
183+
}
160184

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-
}
185+
object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo)
186+
=> TryGetProvider(in parameterInfo, out var provider)
187+
? provider.GetCurrentValue(_bindingContext, parameterInfo)
188+
: null;
170189

171-
return true;
190+
private CascadingModelBindingProvider GetProviderOrThrow(in CascadingParameterInfo parameterInfo)
191+
{
192+
if (!TryGetProvider(parameterInfo, out var provider))
193+
{
194+
throw new InvalidOperationException($"No model binding provider could be found for parameter '{parameterInfo.PropertyName}'.");
172195
}
173196

174-
return false;
197+
return provider;
175198
}
176199

177-
void ICascadingValueComponent.Subscribe(ComponentState subscriber)
200+
private bool TryGetProvider(in CascadingParameterInfo parameterInfo, [NotNullWhen(true)] out CascadingModelBindingProvider? result)
178201
{
179-
throw new InvalidOperationException("Form values are always fixed.");
180-
}
202+
var attributeType = parameterInfo.Attribute.GetType();
181203

182-
void ICascadingValueComponent.Unsubscribe(ComponentState subscriber)
183-
{
184-
throw new InvalidOperationException("Form values are always fixed.");
185-
}
204+
if (_providersByCascadingParameterAttributeType.TryGetValue(attributeType, out result))
205+
{
206+
return result is not null;
207+
}
186208

187-
object? ICascadingValueComponent.CurrentValue => _bindingInfo == null ?
188-
throw new InvalidOperationException("Tried to access form value before it was bound.") :
189-
_bindingInfo.BoundValue;
209+
// We deliberately cache 'null' results to avoid searching for the same attribute type multiple times.
210+
result = FindProviderForAttributeType(attributeType);
211+
_providersByCascadingParameterAttributeType[attributeType] = result;
212+
return result is not null;
190213

191-
bool ICascadingValueComponent.CurrentValueIsFixed => true;
214+
CascadingModelBindingProvider? FindProviderForAttributeType(Type attributeType)
215+
{
216+
foreach (var provider in ModelBindingProviders)
217+
{
218+
if (provider.SupportsCascadingParameterAttributeType(attributeType))
219+
{
220+
return provider;
221+
}
222+
}
223+
224+
return null;
225+
}
226+
}
192227

193-
private record BindingInfo(string? FormName, Type ValueType, bool BindingResult, object? BoundValue);
228+
void IDisposable.Dispose()
229+
{
230+
Navigation.LocationChanged -= HandleLocationChanged;
231+
}
194232
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 Microsoft.AspNetCore.Components.Rendering;
5+
6+
namespace Microsoft.AspNetCore.Components.Binding;
7+
8+
/// <summary>
9+
/// Provides values that get supplied to cascading parameters with <see cref="CascadingModelBinder"/>.
10+
/// </summary>
11+
public abstract class CascadingModelBindingProvider
12+
{
13+
/// <summary>
14+
/// Gets whether values supplied by this instance will not change.
15+
/// </summary>
16+
protected internal abstract bool AreValuesFixed { get; }
17+
18+
/// <summary>
19+
/// Determines whether this instance can provide values for parameters annotated with the specified attribute type.
20+
/// </summary>
21+
/// <param name="attributeType">The attribute type.</param>
22+
/// <returns><c>true</c> if this instance can provide values for parameters annotated with the specified attribute type, otherwise <c>false</c>.</returns>
23+
protected internal abstract bool SupportsCascadingParameterAttributeType(Type attributeType);
24+
25+
/// <summary>
26+
/// Determines whether this instance can provide values to parameters with the specified type.
27+
/// </summary>
28+
/// <param name="parameterType">The parameter type.</param>
29+
/// <returns><c>true</c> if this instance can provide values to parameters with the specified type, otherwise <c>false</c>.</returns>
30+
protected internal abstract bool SupportsParameterType(Type parameterType);
31+
32+
/// <summary>
33+
/// Determines whether this instance can supply a value for the specified parameter.
34+
/// </summary>
35+
/// <param name="bindingContext">The current <see cref="ModelBindingContext"/>.</param>
36+
/// <param name="parameterInfo">The <see cref="CascadingParameterInfo"/> for the component parameter.</param>
37+
/// <returns><c>true</c> if a value can be supplied, otherwise <c>false</c>.</returns>
38+
protected internal abstract bool CanSupplyValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo);
39+
40+
/// <summary>
41+
/// Gets the value for the specified parameter.
42+
/// </summary>
43+
/// <param name="bindingContext">The current <see cref="ModelBindingContext"/>.</param>
44+
/// <param name="parameterInfo">The <see cref="CascadingParameterInfo"/> for the component parameter.</param>
45+
/// <returns>The value to supply to the parameter.</returns>
46+
protected internal abstract object? GetCurrentValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo);
47+
48+
/// <summary>
49+
/// Subscribes to changes in supplied values, if they can change.
50+
/// </summary>
51+
/// <remarks>
52+
/// This method must be implemented if <see cref="AreValuesFixed"/> is <c>false</c>.
53+
/// </remarks>
54+
/// <param name="subscriber">The <see cref="ComponentState"/> for the subscribing component.</param>
55+
protected internal virtual void Subscribe(ComponentState subscriber)
56+
=> throw new InvalidOperationException(
57+
$"'{nameof(CascadingModelBindingProvider)}' instances that have non-fixed values must provide an implementation for '{nameof(Subscribe)}'.");
58+
59+
/// <summary>
60+
/// Unsubscribes from changes in supplied values, if they can change.
61+
/// </summary>
62+
/// <remarks>
63+
/// This method must be implemented if <see cref="AreValuesFixed"/> is <c>false</c>.
64+
/// </remarks>
65+
/// <param name="subscriber">The <see cref="ComponentState"/> for the unsubscribing component.</param>
66+
protected internal virtual void Unsubscribe(ComponentState subscriber)
67+
=> throw new InvalidOperationException(
68+
$"'{nameof(CascadingModelBindingProvider)}' instances that have non-fixed values must provide an implementation for '{nameof(Unsubscribe)}'.");
69+
}

0 commit comments

Comments
 (0)