1
1
// Licensed to the .NET Foundation under one or more agreements.
2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
+ using System . Diagnostics . CodeAnalysis ;
5
+ using System . Linq ;
4
6
using System . Reflection . Metadata ;
5
7
using Microsoft . AspNetCore . Components . Binding ;
6
8
using Microsoft . AspNetCore . Components . Rendering ;
@@ -11,12 +13,13 @@ namespace Microsoft.AspNetCore.Components;
11
13
/// <summary>
12
14
/// Defines the binding context for data bound from external sources.
13
15
/// </summary>
14
- public sealed class CascadingModelBinder : IComponent , ICascadingValueComponent , IDisposable
16
+ public sealed class CascadingModelBinder : IComponent , ICascadingValueSupplier , IDisposable
15
17
{
18
+ private readonly Dictionary < Type , CascadingModelBindingProvider ? > _providersByCascadingParameterAttributeType = new ( ) ;
19
+
16
20
private RenderHandle _handle ;
17
21
private ModelBindingContext ? _bindingContext ;
18
22
private bool _hasPendingQueuedRender ;
19
- private BindingInfo ? _bindingInfo ;
20
23
21
24
/// <summary>
22
25
/// The binding context name.
@@ -40,7 +43,9 @@ public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent,
40
43
41
44
[ Inject ] internal NavigationManager Navigation { get ; set ; } = null ! ;
42
45
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 ;
44
49
45
50
void IComponent . Attach ( RenderHandle renderHandle )
46
51
{
@@ -110,85 +115,118 @@ internal void UpdateBindingInformation(string url)
110
115
// BindingContextId = <<base-relative-uri>>((<<existing-query>>&)|?)handler=my-handler
111
116
var name = ModelBindingContext . Combine ( ParentContext , Name ) ;
112
117
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 ) ;
113
122
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 )
121
124
{
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
+ }
129
134
130
- _bindingContext = bindingContext ;
135
+ _bindingContext = new ModelBindingContext ( name , bindingId , CanBind ) ;
136
+ }
131
137
132
138
string GenerateBindingContextId ( string name )
133
139
{
134
140
var bindingId = Navigation . ToBaseRelativePath ( Navigation . GetUriWithQueryParameter ( "handler" , name ) ) ;
135
141
var hashIndex = bindingId . IndexOf ( '#' ) ;
136
142
return hashIndex == - 1 ? bindingId : new string ( bindingId . AsSpan ( 0 , hashIndex ) ) ;
137
143
}
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
+ }
138
157
}
139
158
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 )
141
164
{
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
+ }
143
172
}
144
173
145
- bool ICascadingValueComponent . CanSupplyValue ( Type valueType , string ? valueName )
174
+ void ICascadingValueSupplier . Unsubscribe ( ComponentState subscriber , in CascadingParameterInfo parameterInfo )
146
175
{
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 ) ;
150
178
151
- if ( _bindingInfo != null &&
152
- string . Equals ( _bindingInfo . FormName , formName , StringComparison . Ordinal ) &&
153
- _bindingInfo . ValueType . Equals ( valueType ) )
179
+ if ( ! provider . AreValuesFixed )
154
180
{
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 ) ;
159
182
}
183
+ }
160
184
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 ;
170
189
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 } '.") ;
172
195
}
173
196
174
- return false ;
197
+ return provider ;
175
198
}
176
199
177
- void ICascadingValueComponent . Subscribe ( ComponentState subscriber )
200
+ private bool TryGetProvider ( in CascadingParameterInfo parameterInfo , [ NotNullWhen ( true ) ] out CascadingModelBindingProvider ? result )
178
201
{
179
- throw new InvalidOperationException ( "Form values are always fixed." ) ;
180
- }
202
+ var attributeType = parameterInfo . Attribute . GetType ( ) ;
181
203
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
+ }
186
208
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 ;
190
213
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
+ }
192
227
193
- private record BindingInfo ( string ? FormName , Type ValueType , bool BindingResult , object ? BoundValue ) ;
228
+ void IDisposable . Dispose ( )
229
+ {
230
+ Navigation . LocationChanged -= HandleLocationChanged ;
231
+ }
194
232
}
0 commit comments