Skip to content

Commit

Permalink
[Blazor] Take cascading parameter attribute type into account when su…
Browse files Browse the repository at this point in the history
…pplying cascading values (#48554)
  • Loading branch information
MackinnonBuck authored Jun 16, 2023
1 parent c53f18a commit 883f06c
Show file tree
Hide file tree
Showing 39 changed files with 1,045 additions and 845 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ public AuthorizeRouteViewTest()
serviceCollection.AddSingleton<IAuthorizationPolicyProvider, TestAuthorizationPolicyProvider>();
serviceCollection.AddSingleton<IAuthorizationService>(_testAuthorizationService);
serviceCollection.AddSingleton<NavigationManager, TestNavigationManager>();
serviceCollection.AddSingleton<IFormValueSupplier, TestFormValueSupplier>();

var services = serviceCollection.BuildServiceProvider();
_renderer = new TestRenderer(services);
Expand Down
146 changes: 92 additions & 54 deletions src/Components/Components/src/Binding/CascadingModelBinder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection.Metadata;
using Microsoft.AspNetCore.Components.Binding;
using Microsoft.AspNetCore.Components.Rendering;
Expand All @@ -11,12 +13,13 @@ namespace Microsoft.AspNetCore.Components;
/// <summary>
/// Defines the binding context for data bound from external sources.
/// </summary>
public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent, IDisposable
public sealed class CascadingModelBinder : IComponent, ICascadingValueSupplier, IDisposable
{
private readonly Dictionary<Type, CascadingModelBindingProvider?> _providersByCascadingParameterAttributeType = new();

private RenderHandle _handle;
private ModelBindingContext? _bindingContext;
private bool _hasPendingQueuedRender;
private BindingInfo? _bindingInfo;

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

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

[Inject] internal IFormValueSupplier FormValueSupplier { get; set; } = null!;
[Inject] internal IEnumerable<CascadingModelBindingProvider> ModelBindingProviders { get; set; } = Enumerable.Empty<CascadingModelBindingProvider>();

internal ModelBindingContext? BindingContext => _bindingContext;

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

var bindingContext = _bindingContext != null &&
string.Equals(_bindingContext.Name, name, StringComparison.Ordinal) &&
string.Equals(_bindingContext.BindingContextId, bindingId, StringComparison.Ordinal) ?
_bindingContext : new ModelBindingContext(name, bindingId, FormValueSupplier.CanConvertSingleValue);

// It doesn't matter that we don't check IsFixed, since the CascadingValue we are setting up will throw if the app changes.
if (IsFixed && _bindingContext != null && _bindingContext != bindingContext)
if (bindingContextDidChange)
{
// Throw an exception if either the Name or the BindingContextId changed. Once a CascadingModelBinder has been initialized
// as fixed, it can't change it's name nor its BindingContextId. This can happen in several situations:
// * Component ParentContext hierarchy changes.
// * Technically, the component won't be retained in this case and will be destroyed instead.
// * A parent changes Name.
throw new InvalidOperationException($"'{nameof(CascadingModelBinder)}' 'Name' can't change after initialized.");
}
if (IsFixed && _bindingContext is not null)
{
// Throw an exception if either the Name or the BindingContextId changed. Once a CascadingModelBinder has been initialized
// as fixed, it can't change it's name nor its BindingContextId. This can happen in several situations:
// * Component ParentContext hierarchy changes.
// * Technically, the component won't be retained in this case and will be destroyed instead.
// * A parent changes Name.
throw new InvalidOperationException($"'{nameof(CascadingModelBinder)}' 'Name' can't change after initialized.");
}

_bindingContext = bindingContext;
_bindingContext = new ModelBindingContext(name, bindingId, CanBind);
}

string GenerateBindingContextId(string name)
{
var bindingId = Navigation.ToBaseRelativePath(Navigation.GetUriWithQueryParameter("handler", name));
var hashIndex = bindingId.IndexOf('#');
return hashIndex == -1 ? bindingId : new string(bindingId.AsSpan(0, hashIndex));
}

bool CanBind(Type type)
{
foreach (var provider in ModelBindingProviders)
{
if (provider.SupportsParameterType(type))
{
return true;
}
}

return false;
}
}

void IDisposable.Dispose()
bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo)
=> TryGetProvider(in parameterInfo, out var provider)
&& provider.CanSupplyValue(_bindingContext, parameterInfo);

void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
{
Navigation.LocationChanged -= HandleLocationChanged;
// We expect there to always be a provider at this point, because CanSupplyValue must have returned true.
var provider = GetProviderOrThrow(parameterInfo);

if (!provider.AreValuesFixed)
{
provider.Subscribe(subscriber);
}
}

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

if (_bindingInfo != null &&
string.Equals(_bindingInfo.FormName, formName, StringComparison.Ordinal) &&
_bindingInfo.ValueType.Equals(valueType))
if (!provider.AreValuesFixed)
{
// We already bound the value, but some component might have been destroyed and
// re-created. If the type and name of the value that we bound are the same,
// we can provide the value that we bound.
return true;
provider.Unsubscribe(subscriber);
}
}

// Can't supply the value if this context is for a form with a different name.
if (FormValueSupplier.CanBind(formName!, valueType))
{
var bindingSucceeded = FormValueSupplier.TryBind(formName!, valueType, out var boundValue);
_bindingInfo = new BindingInfo(formName, valueType, bindingSucceeded, boundValue);
if (!bindingSucceeded)
{
// Report errors
}
object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo)
=> TryGetProvider(in parameterInfo, out var provider)
? provider.GetCurrentValue(_bindingContext, parameterInfo)
: null;

return true;
private CascadingModelBindingProvider GetProviderOrThrow(in CascadingParameterInfo parameterInfo)
{
if (!TryGetProvider(parameterInfo, out var provider))
{
throw new InvalidOperationException($"No model binding provider could be found for parameter '{parameterInfo.PropertyName}'.");
}

return false;
return provider;
}

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

void ICascadingValueComponent.Unsubscribe(ComponentState subscriber)
{
throw new InvalidOperationException("Form values are always fixed.");
}
if (_providersByCascadingParameterAttributeType.TryGetValue(attributeType, out result))
{
return result is not null;
}

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

bool ICascadingValueComponent.CurrentValueIsFixed => true;
CascadingModelBindingProvider? FindProviderForAttributeType(Type attributeType)
{
foreach (var provider in ModelBindingProviders)
{
if (provider.SupportsCascadingParameterAttributeType(attributeType))
{
return provider;
}
}

return null;
}
}

private record BindingInfo(string? FormName, Type ValueType, bool BindingResult, object? BoundValue);
void IDisposable.Dispose()
{
Navigation.LocationChanged -= HandleLocationChanged;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.Rendering;

namespace Microsoft.AspNetCore.Components.Binding;

/// <summary>
/// Provides values that get supplied to cascading parameters with <see cref="CascadingModelBinder"/>.
/// </summary>
public abstract class CascadingModelBindingProvider
{
/// <summary>
/// Gets whether values supplied by this instance will not change.
/// </summary>
protected internal abstract bool AreValuesFixed { get; }

/// <summary>
/// Determines whether this instance can provide values for parameters annotated with the specified attribute type.
/// </summary>
/// <param name="attributeType">The attribute type.</param>
/// <returns><c>true</c> if this instance can provide values for parameters annotated with the specified attribute type, otherwise <c>false</c>.</returns>
protected internal abstract bool SupportsCascadingParameterAttributeType(Type attributeType);

/// <summary>
/// Determines whether this instance can provide values to parameters with the specified type.
/// </summary>
/// <param name="parameterType">The parameter type.</param>
/// <returns><c>true</c> if this instance can provide values to parameters with the specified type, otherwise <c>false</c>.</returns>
protected internal abstract bool SupportsParameterType(Type parameterType);

/// <summary>
/// Determines whether this instance can supply a value for the specified parameter.
/// </summary>
/// <param name="bindingContext">The current <see cref="ModelBindingContext"/>.</param>
/// <param name="parameterInfo">The <see cref="CascadingParameterInfo"/> for the component parameter.</param>
/// <returns><c>true</c> if a value can be supplied, otherwise <c>false</c>.</returns>
protected internal abstract bool CanSupplyValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo);

/// <summary>
/// Gets the value for the specified parameter.
/// </summary>
/// <param name="bindingContext">The current <see cref="ModelBindingContext"/>.</param>
/// <param name="parameterInfo">The <see cref="CascadingParameterInfo"/> for the component parameter.</param>
/// <returns>The value to supply to the parameter.</returns>
protected internal abstract object? GetCurrentValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo);

/// <summary>
/// Subscribes to changes in supplied values, if they can change.
/// </summary>
/// <remarks>
/// This method must be implemented if <see cref="AreValuesFixed"/> is <c>false</c>.
/// </remarks>
/// <param name="subscriber">The <see cref="ComponentState"/> for the subscribing component.</param>
protected internal virtual void Subscribe(ComponentState subscriber)
=> throw new InvalidOperationException(
$"'{nameof(CascadingModelBindingProvider)}' instances that have non-fixed values must provide an implementation for '{nameof(Subscribe)}'.");

/// <summary>
/// Unsubscribes from changes in supplied values, if they can change.
/// </summary>
/// <remarks>
/// This method must be implemented if <see cref="AreValuesFixed"/> is <c>false</c>.
/// </remarks>
/// <param name="subscriber">The <see cref="ComponentState"/> for the unsubscribing component.</param>
protected internal virtual void Unsubscribe(ComponentState subscriber)
=> throw new InvalidOperationException(
$"'{nameof(CascadingModelBindingProvider)}' instances that have non-fixed values must provide an implementation for '{nameof(Unsubscribe)}'.");
}
Loading

0 comments on commit 883f06c

Please sign in to comment.