From a9b17cd5cc77b9a4fa697ea3c511488395ffd8db Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 31 May 2023 16:05:00 -0700 Subject: [PATCH 01/13] Filter cascading parameters by attribute type --- .../src/Binding/CascadingModelBinder.cs | 24 +++- .../IFormValueCascadingParameterAttribute.cs | 8 ++ .../src/CascadingParameterAttribute.cs | 2 +- .../Components/src/CascadingParameterState.cs | 42 +++--- .../Components/src/CascadingValue.cs | 30 +++-- .../src/ICascadingParameterAttribute.cs | 12 ++ ...omponent.cs => ICascadingValueSupplier.cs} | 4 +- .../src/ICascadingValueSupplierFactory.cs | 11 ++ .../src/IHostEnvironmentCascadingParameter.cs | 11 -- .../src/Reflection/ComponentProperties.cs | 15 +-- .../src/SupplyParameterFromQueryAttribute.cs | 2 +- .../test/CascadingParameterStateTest.cs | 2 +- .../Components/test/CascadingParameterTest.cs | 127 ++++++++++++++++++ .../test/ParameterViewTest.Assignment.cs | 2 +- .../Components/test/ParameterViewTest.cs | 2 +- .../src/SupplyParameterFromFormAttribute.cs | 4 +- 16 files changed, 229 insertions(+), 69 deletions(-) create mode 100644 src/Components/Components/src/Binding/IFormValueCascadingParameterAttribute.cs create mode 100644 src/Components/Components/src/ICascadingParameterAttribute.cs rename src/Components/Components/src/{ICascadingValueComponent.cs => ICascadingValueSupplier.cs} (83%) create mode 100644 src/Components/Components/src/ICascadingValueSupplierFactory.cs delete mode 100644 src/Components/Components/src/IHostEnvironmentCascadingParameter.cs diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index cdf6d4d2e755..5d16d48a9940 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -1,6 +1,7 @@ // 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.Reflection.Metadata; using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; @@ -11,7 +12,7 @@ namespace Microsoft.AspNetCore.Components; /// /// Defines the binding context for data bound from external sources. /// -public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent, IDisposable +public sealed class CascadingModelBinder : IComponent, ICascadingValueSupplierFactory, ICascadingValueSupplier, IDisposable { private RenderHandle _handle; private ModelBindingContext? _bindingContext; @@ -42,6 +43,8 @@ public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent, [Inject] internal IFormValueSupplier FormValueSupplier { get; set; } = null!; + internal ModelBindingContext? BindingContext => _bindingContext; + void IComponent.Attach(RenderHandle renderHandle) { _handle = renderHandle; @@ -142,8 +145,15 @@ void IDisposable.Dispose() Navigation.LocationChanged -= HandleLocationChanged; } - bool ICascadingValueComponent.CanSupplyValue(Type valueType, string? valueName) + bool ICascadingValueSupplierFactory.TryGetValueSupplier(object propertyAttribute, Type valueType, string? valueName, [NotNullWhen(true)] out ICascadingValueSupplier? result) { + result = default; + + if (propertyAttribute is not IFormValueCascadingParameterAttribute) + { + return false; + } + var formName = string.IsNullOrEmpty(valueName) ? (_bindingContext?.Name) : ModelBindingContext.Combine(_bindingContext, valueName); @@ -155,6 +165,7 @@ bool ICascadingValueComponent.CanSupplyValue(Type valueType, string? valueName) // 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. + result = this; return true; } @@ -168,27 +179,28 @@ bool ICascadingValueComponent.CanSupplyValue(Type valueType, string? valueName) // Report errors } + result = this; return true; } return false; } - void ICascadingValueComponent.Subscribe(ComponentState subscriber) + void ICascadingValueSupplier.Subscribe(ComponentState subscriber) { throw new InvalidOperationException("Form values are always fixed."); } - void ICascadingValueComponent.Unsubscribe(ComponentState subscriber) + void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber) { throw new InvalidOperationException("Form values are always fixed."); } - object? ICascadingValueComponent.CurrentValue => _bindingInfo == null ? + object? ICascadingValueSupplier.CurrentValue => _bindingInfo == null ? throw new InvalidOperationException("Tried to access form value before it was bound.") : _bindingInfo.BoundValue; - bool ICascadingValueComponent.CurrentValueIsFixed => true; + bool ICascadingValueSupplier.CurrentValueIsFixed => true; private record BindingInfo(string? FormName, Type ValueType, bool BindingResult, object? BoundValue); } diff --git a/src/Components/Components/src/Binding/IFormValueCascadingParameterAttribute.cs b/src/Components/Components/src/Binding/IFormValueCascadingParameterAttribute.cs new file mode 100644 index 000000000000..b5cf2b40f936 --- /dev/null +++ b/src/Components/Components/src/Binding/IFormValueCascadingParameterAttribute.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Binding; + +internal interface IFormValueCascadingParameterAttribute : ICascadingParameterAttribute +{ +} diff --git a/src/Components/Components/src/CascadingParameterAttribute.cs b/src/Components/Components/src/CascadingParameterAttribute.cs index 70cb5998ff72..a0e7135c9241 100644 --- a/src/Components/Components/src/CascadingParameterAttribute.cs +++ b/src/Components/Components/src/CascadingParameterAttribute.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Components; /// supplies values with a compatible type and name. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class CascadingParameterAttribute : Attribute +public sealed class CascadingParameterAttribute : Attribute, ICascadingParameterAttribute { /// /// If specified, the parameter value will be supplied by the closest diff --git a/src/Components/Components/src/CascadingParameterState.cs b/src/Components/Components/src/CascadingParameterState.cs index 7af4dc1b9cce..e409e652560f 100644 --- a/src/Components/Components/src/CascadingParameterState.cs +++ b/src/Components/Components/src/CascadingParameterState.cs @@ -16,9 +16,9 @@ internal readonly struct CascadingParameterState private static readonly ConcurrentDictionary _cachedInfos = new(); public string LocalValueName { get; } - public ICascadingValueComponent ValueSupplier { get; } + public ICascadingValueSupplier ValueSupplier { get; } - public CascadingParameterState(string localValueName, ICascadingValueComponent valueSupplier) + public CascadingParameterState(string localValueName, ICascadingValueSupplier valueSupplier) { LocalValueName = localValueName; ValueSupplier = valueSupplier; @@ -48,7 +48,6 @@ public static IReadOnlyList FindCascadingParameters(Com { // Although not all parameters might be matched, we know the maximum number resultStates ??= new List(infos.Length - infoIndex); - resultStates.Add(new CascadingParameterState(info.ConsumerValueName, supplier)); } } @@ -56,15 +55,16 @@ public static IReadOnlyList FindCascadingParameters(Com return resultStates ?? (IReadOnlyList)Array.Empty(); } - private static ICascadingValueComponent? GetMatchingCascadingValueSupplier(in ReflectedCascadingParameterInfo info, ComponentState componentState) + private static ICascadingValueSupplier? GetMatchingCascadingValueSupplier(in ReflectedCascadingParameterInfo info, ComponentState componentState) { var candidate = componentState; do { - if (candidate.Component is ICascadingValueComponent candidateSupplier - && candidateSupplier.CanSupplyValue(info.ValueType, info.SupplierValueName)) + var candidateComponent = candidate.Component; + if (candidateComponent is ICascadingValueSupplierFactory valueSupplierFactory && + valueSupplierFactory.TryGetValueSupplier(info.PropertyAttribute, info.ValueType, info.SupplierValueName, out var valueSupplier)) { - return candidateSupplier; + return valueSupplier; } candidate = candidate.ParentComponentState; @@ -93,27 +93,16 @@ private static ReflectedCascadingParameterInfo[] CreateReflectedCascadingParamet var candidateProps = ComponentProperties.GetCandidateBindableProperties(componentType); foreach (var prop in candidateProps) { - var attribute = prop.GetCustomAttribute(); - if (attribute != null) - { - result ??= new List(); - - result.Add(new ReflectedCascadingParameterInfo( - prop.Name, - prop.PropertyType, - attribute.Name)); - } - - var hostParameterAttribute = prop.GetCustomAttributes() - .OfType().SingleOrDefault(); - if (hostParameterAttribute != null) + var cascadingParameterAttribute = prop.GetCustomAttributes() + .OfType().SingleOrDefault(); + if (cascadingParameterAttribute != null) { result ??= new List(); - result.Add(new ReflectedCascadingParameterInfo( + cascadingParameterAttribute, prop.Name, prop.PropertyType, - hostParameterAttribute.Name)); + cascadingParameterAttribute.Name)); } } @@ -122,13 +111,18 @@ private static ReflectedCascadingParameterInfo[] CreateReflectedCascadingParamet readonly struct ReflectedCascadingParameterInfo { + public object PropertyAttribute { get; } public string ConsumerValueName { get; } public string? SupplierValueName { get; } public Type ValueType { get; } public ReflectedCascadingParameterInfo( - string consumerValueName, Type valueType, string? supplierValueName) + object propertyAttribute, + string consumerValueName, + Type valueType, + string? supplierValueName) { + PropertyAttribute = propertyAttribute; ConsumerValueName = consumerValueName; SupplierValueName = supplierValueName; ValueType = valueType; diff --git a/src/Components/Components/src/CascadingValue.cs b/src/Components/Components/src/CascadingValue.cs index facb9821e415..d3e882acc0ba 100644 --- a/src/Components/Components/src/CascadingValue.cs +++ b/src/Components/Components/src/CascadingValue.cs @@ -1,6 +1,7 @@ // 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 Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components; @@ -8,7 +9,7 @@ namespace Microsoft.AspNetCore.Components; /// /// A component that provides a cascading value to all descendant components. /// -public class CascadingValue : ICascadingValueComponent, IComponent +public class CascadingValue : ICascadingValueSupplierFactory, ICascadingValueSupplier, IComponent { private RenderHandle _renderHandle; private HashSet? _subscribers; // Lazily instantiated @@ -41,9 +42,9 @@ public class CascadingValue : ICascadingValueComponent, IComponent /// [Parameter] public bool IsFixed { get; set; } - object? ICascadingValueComponent.CurrentValue => Value; + object? ICascadingValueSupplier.CurrentValue => Value; - bool ICascadingValueComponent.CurrentValueIsFixed => IsFixed; + bool ICascadingValueSupplier.CurrentValueIsFixed => IsFixed; /// public void Attach(RenderHandle renderHandle) @@ -130,18 +131,29 @@ public Task SetParametersAsync(ParameterView parameters) return Task.CompletedTask; } - bool ICascadingValueComponent.CanSupplyValue(Type requestedType, string? requestedName) + bool ICascadingValueSupplierFactory.TryGetValueSupplier(object propertyAttribute, Type requestedType, string? requestedName, [NotNullWhen(true)] out ICascadingValueSupplier? result) { - if (!requestedType.IsAssignableFrom(typeof(TValue))) + result = default; + + if (propertyAttribute is not CascadingParameterAttribute || !requestedType.IsAssignableFrom(typeof(TValue))) + { + return false; + } + + var isMatch = + (requestedName == null && Name == null) || // Match on type alone + string.Equals(requestedName, Name, StringComparison.OrdinalIgnoreCase); // Also match on name + + if (!isMatch) { return false; } - return (requestedName == null && Name == null) // Match on type alone - || string.Equals(requestedName, Name, StringComparison.OrdinalIgnoreCase); // Also match on name + result = this; + return true; } - void ICascadingValueComponent.Subscribe(ComponentState subscriber) + void ICascadingValueSupplier.Subscribe(ComponentState subscriber) { #if DEBUG if (IsFixed) @@ -160,7 +172,7 @@ void ICascadingValueComponent.Subscribe(ComponentState subscriber) _subscribers.Add(subscriber); } - void ICascadingValueComponent.Unsubscribe(ComponentState subscriber) + void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber) { _subscribers?.Remove(subscriber); } diff --git a/src/Components/Components/src/ICascadingParameterAttribute.cs b/src/Components/Components/src/ICascadingParameterAttribute.cs new file mode 100644 index 000000000000..b4a1d16fe90f --- /dev/null +++ b/src/Components/Components/src/ICascadingParameterAttribute.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components; + +// Represents an attribute marking a parameter to be set by a cascading value. +// This exists so cascading parameter attributes can be defined outside the Components assembly. +// For example: [SupplyParameterFromForm]. +internal interface ICascadingParameterAttribute +{ + public string? Name { get; set; } +} diff --git a/src/Components/Components/src/ICascadingValueComponent.cs b/src/Components/Components/src/ICascadingValueSupplier.cs similarity index 83% rename from src/Components/Components/src/ICascadingValueComponent.cs rename to src/Components/Components/src/ICascadingValueSupplier.cs index b18735c86a9e..6899afa8035b 100644 --- a/src/Components/Components/src/ICascadingValueComponent.cs +++ b/src/Components/Components/src/ICascadingValueSupplier.cs @@ -5,13 +5,11 @@ namespace Microsoft.AspNetCore.Components; -internal interface ICascadingValueComponent +internal interface ICascadingValueSupplier { // This interface exists only so that CascadingParameterState has a way // to work with all CascadingValue types regardless of T. - bool CanSupplyValue(Type valueType, string? valueName); - object? CurrentValue { get; } bool CurrentValueIsFixed { get; } diff --git a/src/Components/Components/src/ICascadingValueSupplierFactory.cs b/src/Components/Components/src/ICascadingValueSupplierFactory.cs new file mode 100644 index 000000000000..5d8f83299bff --- /dev/null +++ b/src/Components/Components/src/ICascadingValueSupplierFactory.cs @@ -0,0 +1,11 @@ +// 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; + +namespace Microsoft.AspNetCore.Components; + +internal interface ICascadingValueSupplierFactory +{ + bool TryGetValueSupplier(object propertyAttribute, Type valueType, string? valueName, [NotNullWhen(true)] out ICascadingValueSupplier? result); +} diff --git a/src/Components/Components/src/IHostEnvironmentCascadingParameter.cs b/src/Components/Components/src/IHostEnvironmentCascadingParameter.cs deleted file mode 100644 index 8f407e0cdd5e..000000000000 --- a/src/Components/Components/src/IHostEnvironmentCascadingParameter.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Components; - -// Marks a cascading parameter that can be offered via an attribute that is not -// directly defined in the Components assembly. For example [SupplyParameterFromForm]. -internal interface IHostEnvironmentCascadingParameter -{ - public string? Name { get; set; } -} diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index 9569e8e6a84e..5ca7489fadbf 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -170,8 +170,7 @@ private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMem if (propertyInfo != null) { if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) && - !propertyInfo.IsDefined(typeof(CascadingParameterAttribute)) && - !propertyInfo.GetCustomAttributes().OfType().Any()) + !propertyInfo.GetCustomAttributes().OfType().Any()) { throw new InvalidOperationException( $"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " + @@ -262,8 +261,7 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType) foreach (var propertyInfo in GetCandidateBindableProperties(targetType)) { ParameterAttribute? parameterAttribute = null; - CascadingParameterAttribute? cascadingParameterAttribute = null; - IHostEnvironmentCascadingParameter? hostEnvironmentCascadingParameter = null; + ICascadingParameterAttribute? cascadingParameterAttribute = null; var attributes = propertyInfo.GetCustomAttributes(); foreach (var attribute in attributes) @@ -273,18 +271,15 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType) case ParameterAttribute parameter: parameterAttribute = parameter; break; - case CascadingParameterAttribute cascadingParameter: + case ICascadingParameterAttribute cascadingParameter: cascadingParameterAttribute = cascadingParameter; break; - case IHostEnvironmentCascadingParameter hostEnvironmentAttribute: - hostEnvironmentCascadingParameter = hostEnvironmentAttribute; - break; default: break; } } - var isParameter = parameterAttribute != null || cascadingParameterAttribute != null || hostEnvironmentCascadingParameter != null; + var isParameter = parameterAttribute != null || cascadingParameterAttribute != null; if (!isParameter) { continue; @@ -299,7 +294,7 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType) var propertySetter = new PropertySetter(targetType, propertyInfo) { - Cascading = cascadingParameterAttribute != null || hostEnvironmentCascadingParameter != null, + Cascading = cascadingParameterAttribute != null, }; if (_underlyingWriters.ContainsKey(propertyName)) diff --git a/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs b/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs index f88fe737c598..2e8ef9b5a7ac 100644 --- a/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs +++ b/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components; /// current URL querystring. They may also supply further values if the URL querystring changes. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class SupplyParameterFromQueryAttribute : Attribute +public sealed class SupplyParameterFromQueryAttribute : Attribute, ICascadingParameterAttribute { /// /// Gets or sets the name of the querystring parameter. If null, the querystring diff --git a/src/Components/Components/test/CascadingParameterStateTest.cs b/src/Components/Components/test/CascadingParameterStateTest.cs index 95f1201137db..fac6ed5c676a 100644 --- a/src/Components/Components/test/CascadingParameterStateTest.cs +++ b/src/Components/Components/test/CascadingParameterStateTest.cs @@ -552,7 +552,7 @@ public TestNavigationManager() } [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class SupplyParameterFromFormAttribute : Attribute, IHostEnvironmentCascadingParameter +public sealed class SupplyParameterFromFormAttribute : Attribute, ICascadingParameterAttribute { /// /// Gets or sets the name for the parameter. The name is used to match diff --git a/src/Components/Components/test/CascadingParameterTest.cs b/src/Components/Components/test/CascadingParameterTest.cs index 0330b92fede7..d75ae9baab7a 100644 --- a/src/Components/Components/test/CascadingParameterTest.cs +++ b/src/Components/Components/test/CascadingParameterTest.cs @@ -384,6 +384,59 @@ public void ParameterViewSuppliedWithCascadingParametersCannotBeUsedAfterSynchro Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message); } + [Fact] + public void CanSupplyCascadingValuesForSpecificCascadingParameterAttributeType() + { + // Arrange + var renderer = new TestRenderer(); + var component = new TestComponent(builder => + { + builder.OpenComponent>(0); + builder.AddComponentParameter(1, "Value", "Hello 1"); + builder.AddComponentParameter(2, "ChildContent", new RenderFragment(builder => + { + builder.OpenComponent>(0); + builder.AddComponentParameter(1, "Value", "Hello 2"); + builder.AddComponentParameter(2, "ChildContent", new RenderFragment(builder => + { + builder.OpenComponent(0); + builder.CloseComponent(); + builder.OpenComponent(1); + builder.CloseComponent(); + })); + builder.CloseComponent(); + })); + builder.CloseComponent(); + }); + + // Act/Assert + var componentId = renderer.AssignRootComponentId(component); + component.TriggerRender(); + var batch = renderer.Batches.Single(); + var nestedComponent1 = FindComponent(batch, out var nestedComponentId1); + var nestedComponent2 = FindComponent(batch, out var nestedComponentId2); + var nestedComponentDiff1 = batch.DiffsByComponentId[nestedComponentId1].Single(); + var nestedComponentDiff2 = batch.DiffsByComponentId[nestedComponentId2].Single(); + + // The nested components were rendered with the correct parameters + Assert.Collection(nestedComponentDiff1.Edits, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "Value 1 is 'Hello 1'."); + }); + Assert.Collection(nestedComponentDiff2.Edits, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "Value 2 is 'Hello 2'."); + }); + } + private static T FindComponent(CapturedBatch batch, out int componentId) { var componentFrame = batch.ReferenceFrames.Single( @@ -441,4 +494,78 @@ class SecondCascadingParameterConsumerComponent : CascadingParameterCons { [CascadingParameter] T2 SecondCascadingParameter { get; set; } } + + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + class CustomCascadingParameter1Attribute : Attribute, ICascadingParameterAttribute + { + public string Name { get; set; } + } + + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + class CustomCascadingParameter2Attribute : Attribute, ICascadingParameterAttribute + { + public string Name { get; set; } + } + + class CustomCascadingValueProducer : AutoRenderComponent, ICascadingValueSupplierFactory, ICascadingValueSupplier + { + [Parameter] public object Value { get; set; } + + [Parameter] public RenderFragment ChildContent { get; set; } + + object ICascadingValueSupplier.CurrentValue => Value; + + bool ICascadingValueSupplier.CurrentValueIsFixed => true; + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, ChildContent); + } + + bool ICascadingValueSupplierFactory.TryGetValueSupplier(object propertyAttribute, Type valueType, string valueName, out ICascadingValueSupplier result) + { + if (propertyAttribute is not TAttribute || + valueType != typeof(object) || + valueName != nameof(Value)) + { + result = default; + return false; + } + + result = this; + return true; + } + + void ICascadingValueSupplier.Subscribe(ComponentState subscriber) + { + throw new NotImplementedException(); + } + + void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber) + { + throw new NotImplementedException(); + } + } + + class CustomCascadingValueConsumer1 : AutoRenderComponent + { + [CustomCascadingParameter1(Name = nameof(Value))] + public object Value { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, $"Value 1 is '{Value}'."); + } + } + + class CustomCascadingValueConsumer2 : AutoRenderComponent + { + [CustomCascadingParameter2(Name = nameof(Value))] + public object Value { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, $"Value 2 is '{Value}'."); + } + } } diff --git a/src/Components/Components/test/ParameterViewTest.Assignment.cs b/src/Components/Components/test/ParameterViewTest.Assignment.cs index bcc032c7e080..c7ebabf9cb1a 100644 --- a/src/Components/Components/test/ParameterViewTest.Assignment.cs +++ b/src/Components/Components/test/ParameterViewTest.Assignment.cs @@ -684,7 +684,7 @@ public ParameterView Build() } } - private class TestCascadingValueProvider : ICascadingValueComponent + private class TestCascadingValueProvider : ICascadingValueSupplier { public TestCascadingValueProvider(object value) { diff --git a/src/Components/Components/test/ParameterViewTest.cs b/src/Components/Components/test/ParameterViewTest.cs index 740359e5cb0a..67e479872f80 100644 --- a/src/Components/Components/test/ParameterViewTest.cs +++ b/src/Components/Components/test/ParameterViewTest.cs @@ -595,7 +595,7 @@ public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException(); } - private class TestCascadingValue : ICascadingValueComponent + private class TestCascadingValue : ICascadingValueSupplier { public TestCascadingValue(object value) { diff --git a/src/Components/Web/src/SupplyParameterFromFormAttribute.cs b/src/Components/Web/src/SupplyParameterFromFormAttribute.cs index 2ae657c95583..b6e8c00914b5 100644 --- a/src/Components/Web/src/SupplyParameterFromFormAttribute.cs +++ b/src/Components/Web/src/SupplyParameterFromFormAttribute.cs @@ -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 Microsoft.AspNetCore.Components.Binding; + namespace Microsoft.AspNetCore.Components; /// @@ -8,7 +10,7 @@ namespace Microsoft.AspNetCore.Components; /// the form data for the form with the specified name. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class SupplyParameterFromFormAttribute : Attribute, IHostEnvironmentCascadingParameter +public sealed class SupplyParameterFromFormAttribute : Attribute, IFormValueCascadingParameterAttribute { /// /// Gets or sets the name for the parameter. The name is used to match From 4c294a4da463966a05280f8e75bc2bf0c0a38a44 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 2 Jun 2023 16:50:12 -0700 Subject: [PATCH 02/13] Use cascading values for query-supplied parameters --- .../Components/src/CascadingParameterState.cs | 28 +- .../src/CascadingQueryValueProvider.cs | 224 ++++++++ .../Components/src/CascadingValue.cs | 11 +- .../src/ICascadingValueSupplierFactory.cs | 2 +- .../Components/src/RenderTree/Renderer.cs | 1 - src/Components/Components/src/RouteView.cs | 26 +- .../Routing/QueryParameterValueSupplier.cs | 189 ------- .../QueryParameterValueSupplierTest.cs | 508 ------------------ 8 files changed, 251 insertions(+), 738 deletions(-) create mode 100644 src/Components/Components/src/CascadingQueryValueProvider.cs delete mode 100644 src/Components/Components/src/Routing/QueryParameterValueSupplier.cs delete mode 100644 src/Components/Components/test/Routing/QueryParameterValueSupplierTest.cs diff --git a/src/Components/Components/src/CascadingParameterState.cs b/src/Components/Components/src/CascadingParameterState.cs index e409e652560f..05907cb3de99 100644 --- a/src/Components/Components/src/CascadingParameterState.cs +++ b/src/Components/Components/src/CascadingParameterState.cs @@ -48,7 +48,7 @@ public static IReadOnlyList FindCascadingParameters(Com { // Although not all parameters might be matched, we know the maximum number resultStates ??= new List(infos.Length - infoIndex); - resultStates.Add(new CascadingParameterState(info.ConsumerValueName, supplier)); + resultStates.Add(new CascadingParameterState(info.PropertyName, supplier)); } } @@ -62,7 +62,7 @@ public static IReadOnlyList FindCascadingParameters(Com { var candidateComponent = candidate.Component; if (candidateComponent is ICascadingValueSupplierFactory valueSupplierFactory && - valueSupplierFactory.TryGetValueSupplier(info.PropertyAttribute, info.ValueType, info.SupplierValueName, out var valueSupplier)) + valueSupplierFactory.TryGetValueSupplier(info.Attribute, info.PropertyType, info.PropertyName, out var valueSupplier)) { return valueSupplier; } @@ -101,8 +101,7 @@ private static ReflectedCascadingParameterInfo[] CreateReflectedCascadingParamet result.Add(new ReflectedCascadingParameterInfo( cascadingParameterAttribute, prop.Name, - prop.PropertyType, - cascadingParameterAttribute.Name)); + prop.PropertyType)); } } @@ -111,21 +110,18 @@ private static ReflectedCascadingParameterInfo[] CreateReflectedCascadingParamet readonly struct ReflectedCascadingParameterInfo { - public object PropertyAttribute { get; } - public string ConsumerValueName { get; } - public string? SupplierValueName { get; } - public Type ValueType { get; } + public object Attribute { get; } + public string PropertyName { get; } + public Type PropertyType { get; } public ReflectedCascadingParameterInfo( - object propertyAttribute, - string consumerValueName, - Type valueType, - string? supplierValueName) + object attribute, + string propertyName, + Type propertyType) { - PropertyAttribute = propertyAttribute; - ConsumerValueName = consumerValueName; - SupplierValueName = supplierValueName; - ValueType = valueType; + Attribute = attribute; + PropertyName = propertyName; + PropertyType = propertyType; } } } diff --git a/src/Components/Components/src/CascadingQueryValueProvider.cs b/src/Components/Components/src/CascadingQueryValueProvider.cs new file mode 100644 index 000000000000..6d79a7aa524f --- /dev/null +++ b/src/Components/Components/src/CascadingQueryValueProvider.cs @@ -0,0 +1,224 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Internal; + +namespace Microsoft.AspNetCore.Components; + +internal sealed class CascadingQueryValueProvider : IComponent, ICascadingValueSupplierFactory, IDisposable +{ + private readonly Dictionary _cachedSuppliers = new(); + private readonly Dictionary, StringSegmentAccumulator> _queryParameterValuesByName = new(QueryParameterNameComparer.Instance); + + private RenderHandle _handle; + private bool _hasSetInitialParameters; + + [Inject] private NavigationManager Navigation { get; set; } = default!; + + [Parameter] public RenderFragment? ChildContent { get; set; } + + void IComponent.Attach(RenderHandle renderHandle) + { + _handle = renderHandle; + } + + Task IComponent.SetParametersAsync(ParameterView parameters) + { + if (!_hasSetInitialParameters) + { + _hasSetInitialParameters = true; + + UpdateQueryParameterValues(); + UpdateAllSuppliers(); + + Navigation.LocationChanged += HandleLocationChanged; + } + + parameters.SetParameterProperties(this); + + _handle.Render(Render); + + return Task.CompletedTask; + } + + private void HandleLocationChanged(object? sender, LocationChangedEventArgs e) + { + UpdateQueryParameterValues(); + UpdateAllSuppliers(); + } + + private void UpdateQueryParameterValues() + { + _queryParameterValuesByName.Clear(); + + var url = Navigation.Uri; + var queryStartPos = url.IndexOf('?'); + + if (queryStartPos < 0) + { + return; + } + + var queryEndPos = url.IndexOf('#', queryStartPos); + var query = url.AsMemory(queryStartPos..(queryEndPos < 0 ? url.Length : queryEndPos)); + var queryStringEnumerable = new QueryStringEnumerable(query); + + foreach (var suppliedPair in queryStringEnumerable) + { + var decodedName = suppliedPair.DecodeName(); + var decodedValue = suppliedPair.DecodeValue(); + + // This is safe because we don't mutate the dictionary while the ref local is in scope. + ref var values = ref CollectionsMarshal.GetValueRefOrAddDefault(_queryParameterValuesByName, decodedName, out _); + values.Add(decodedValue); + } + } + + private void UpdateAllSuppliers() + { + foreach (var supplier in _cachedSuppliers.Values) + { + UpdateSupplier(supplier); + } + } + + private void UpdateSupplier(QueryValueSupplier supplier) + { + // This is safe because we don't mutate the dictionary while the ref local is in scope. + ref var existingValues = ref CollectionsMarshal.GetValueRefOrNullRef(_queryParameterValuesByName, supplier.ValueName); + + if (!Unsafe.IsNullRef(ref existingValues)) + { + supplier.UpdateValues(ref existingValues); + } + else + { + var emptyValues = default(StringSegmentAccumulator); + supplier.UpdateValues(ref emptyValues); + } + } + + private void Render(RenderTreeBuilder builder) + { + builder.AddContent(0, ChildContent); + } + + bool ICascadingValueSupplierFactory.TryGetValueSupplier(object attribute, Type parameterType, string parameterName, [NotNullWhen(true)] out ICascadingValueSupplier? result) + { + result = default; + + if (attribute is not SupplyParameterFromQueryAttribute supplyFromQueryAttribute) + { + return false; + } + + var specifiedName = supplyFromQueryAttribute.Name; + var valueName = string.IsNullOrEmpty(specifiedName) ? parameterName : specifiedName; + var key = new SupplierKey(parameterType, valueName.AsMemory()); + + if (!_cachedSuppliers.TryGetValue(key, out var supplier)) + { + supplier = QueryValueSupplier.Create(parameterType, valueName); + + // Supply an initial value if possible. + UpdateSupplier(supplier); + + _cachedSuppliers.Add(key, supplier); + } + + result = supplier; + return true; + } + + void IDisposable.Dispose() + { + Navigation.LocationChanged -= HandleLocationChanged; + } + + private readonly struct SupplierKey(Type targetType, ReadOnlyMemory valueName) : IEquatable + { + private readonly Type _targetType = targetType; + private readonly ReadOnlyMemory _valueName = valueName; + + public bool Equals(SupplierKey other) + => _targetType.Equals(other._targetType) && QueryParameterNameComparer.Instance.Equals(_valueName, other._valueName); + + public override bool Equals(object? obj) + => obj is SupplierKey other && Equals(other); + + public override int GetHashCode() + => HashCode.Combine(_targetType, QueryParameterNameComparer.Instance.GetHashCode(_valueName)); + } + + private sealed class QueryValueSupplier : ICascadingValueSupplier + { + private readonly UrlValueConstraint _parser; + private readonly bool _isArray; + private readonly string _valueName; + + private HashSet? _subscribers; + private object? _currentValue; + + public ReadOnlyMemory ValueName => _valueName.AsMemory(); + + public static QueryValueSupplier Create(Type targetType, string valueName) + { + var isArray = targetType.IsArray; + var elementType = isArray ? targetType.GetElementType()! : targetType; + + if (!UrlValueConstraint.TryGetByTargetType(elementType, out var parser)) + { + throw new NotSupportedException($"Querystring values cannot be parsed as type '{elementType}'."); + } + + return new(isArray, valueName, parser); + } + + private QueryValueSupplier(bool isArray, string valueName, UrlValueConstraint parser) + { + _isArray = isArray; + _valueName = valueName; + _parser = parser; + } + + public void UpdateValues(ref StringSegmentAccumulator values) + { + var oldValue = _currentValue; + _currentValue = _isArray + ? _parser.ParseMultiple(values, _valueName) + : values.Count == 0 + ? default + : _parser.Parse(values[0].Span, _valueName); + + if (_subscribers is null || !ChangeDetection.MayHaveChanged(oldValue, _currentValue)) + { + return; + } + + foreach (var subscriber in _subscribers) + { + subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); + } + } + + object? ICascadingValueSupplier.CurrentValue => _currentValue; + + bool ICascadingValueSupplier.CurrentValueIsFixed => false; + + void ICascadingValueSupplier.Subscribe(ComponentState subscriber) + { + _subscribers ??= new(); + _subscribers.Add(subscriber); + } + + void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber) + { + _subscribers?.Remove(subscriber); + } + } +} diff --git a/src/Components/Components/src/CascadingValue.cs b/src/Components/Components/src/CascadingValue.cs index d3e882acc0ba..d6d4c659eea8 100644 --- a/src/Components/Components/src/CascadingValue.cs +++ b/src/Components/Components/src/CascadingValue.cs @@ -131,18 +131,21 @@ public Task SetParametersAsync(ParameterView parameters) return Task.CompletedTask; } - bool ICascadingValueSupplierFactory.TryGetValueSupplier(object propertyAttribute, Type requestedType, string? requestedName, [NotNullWhen(true)] out ICascadingValueSupplier? result) + bool ICascadingValueSupplierFactory.TryGetValueSupplier(object attribute, Type parameterType, string parameterName, [NotNullWhen(true)] out ICascadingValueSupplier? result) { result = default; - if (propertyAttribute is not CascadingParameterAttribute || !requestedType.IsAssignableFrom(typeof(TValue))) + if (attribute is not CascadingParameterAttribute cascadingParameterAttribute || !parameterType.IsAssignableFrom(typeof(TValue))) { return false; } + // We only consider explicitly specified names, not the property name. + var parameterSpecifiedName = cascadingParameterAttribute.Name; + var isMatch = - (requestedName == null && Name == null) || // Match on type alone - string.Equals(requestedName, Name, StringComparison.OrdinalIgnoreCase); // Also match on name + (parameterSpecifiedName == null && Name == null) || // Match on type alone + string.Equals(parameterSpecifiedName, Name, StringComparison.OrdinalIgnoreCase); // Also match on name if (!isMatch) { diff --git a/src/Components/Components/src/ICascadingValueSupplierFactory.cs b/src/Components/Components/src/ICascadingValueSupplierFactory.cs index 5d8f83299bff..db34ceacb063 100644 --- a/src/Components/Components/src/ICascadingValueSupplierFactory.cs +++ b/src/Components/Components/src/ICascadingValueSupplierFactory.cs @@ -7,5 +7,5 @@ namespace Microsoft.AspNetCore.Components; internal interface ICascadingValueSupplierFactory { - bool TryGetValueSupplier(object propertyAttribute, Type valueType, string? valueName, [NotNullWhen(true)] out ICascadingValueSupplier? result); + bool TryGetValueSupplier(object attribute, Type parameterType, string parameterName, [NotNullWhen(true)] out ICascadingValueSupplier? result); } diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 187ffb6d8f79..9262cb7423c4 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -131,7 +131,6 @@ private async void RenderRootComponentsOnHotReload() // Before re-rendering the root component, also clear any well-known caches in the framework ComponentFactory.ClearCache(); ComponentProperties.ClearCache(); - Routing.QueryParameterValueSupplier.ClearCache(); await Dispatcher.InvokeAsync(() => { diff --git a/src/Components/Components/src/RouteView.cs b/src/Components/Components/src/RouteView.cs index e3c9f6b4a54e..72a3382b3eb8 100644 --- a/src/Components/Components/src/RouteView.cs +++ b/src/Components/Components/src/RouteView.cs @@ -84,30 +84,18 @@ private void RenderPageWithParameters(RenderTreeBuilder builder) void RenderPageCore(RenderTreeBuilder builder) { - builder.OpenComponent(0, RouteData.PageType); - - foreach (var kvp in RouteData.RouteValues) + builder.OpenComponent(0); + builder.AddComponentParameter(1, nameof(CascadingQueryValueProvider.ChildContent), (RenderFragment)(builder => { - builder.AddComponentParameter(1, kvp.Key, kvp.Value); - } + builder.OpenComponent(0, RouteData.PageType); - var queryParameterSupplier = QueryParameterValueSupplier.ForType(RouteData.PageType); - if (queryParameterSupplier is not null) - { - // Since this component does accept some parameters from query, we must supply values for all of them, - // even if the querystring in the URI is empty. So don't skip the following logic. - var relativeUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); - var url = NavigationManager.Uri; - ReadOnlyMemory query = default; - var queryStartPos = url.IndexOf('?'); - if (queryStartPos >= 0) + foreach (var kvp in RouteData.RouteValues) { - var queryEndPos = url.IndexOf('#', queryStartPos); - query = url.AsMemory(queryStartPos..(queryEndPos < 0 ? url.Length : queryEndPos)); + builder.AddComponentParameter(1, kvp.Key, kvp.Value); } - queryParameterSupplier.RenderParametersFromQueryString(builder, query); - } + builder.CloseComponent(); + })); builder.CloseComponent(); } } diff --git a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs b/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs deleted file mode 100644 index b217d09878ff..000000000000 --- a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs +++ /dev/null @@ -1,189 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Buffers; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using Microsoft.AspNetCore.Components.Reflection; -using Microsoft.AspNetCore.Components.Rendering; -using Microsoft.AspNetCore.Internal; -using static Microsoft.AspNetCore.Internal.LinkerFlags; - -namespace Microsoft.AspNetCore.Components.Routing; - -internal sealed class QueryParameterValueSupplier -{ - public static void ClearCache() => _cacheByType.Clear(); - - private static readonly ConcurrentDictionary _cacheByType = new(); - - // These two arrays contain the same number of entries, and their corresponding positions refer to each other. - // Holding the info like this means we can use Array.BinarySearch with less custom implementation. - private readonly ReadOnlyMemory[] _queryParameterNames; - private readonly QueryParameterDestination[] _destinations; - - public static QueryParameterValueSupplier? ForType([DynamicallyAccessedMembers(Component)] Type componentType) - { - if (!_cacheByType.TryGetValue(componentType, out var instanceOrNull)) - { - // If the component doesn't have any query parameters, store a null value for it - // so we know the upstream code can't try to render query parameter frames for it. - var sortedMappings = GetSortedMappings(componentType); - instanceOrNull = sortedMappings == null ? null : new QueryParameterValueSupplier(sortedMappings); - _cacheByType.TryAdd(componentType, instanceOrNull); - } - - return instanceOrNull; - } - - private QueryParameterValueSupplier(QueryParameterMapping[] sortedMappings) - { - _queryParameterNames = new ReadOnlyMemory[sortedMappings.Length]; - _destinations = new QueryParameterDestination[sortedMappings.Length]; - for (var i = 0; i < sortedMappings.Length; i++) - { - ref var mapping = ref sortedMappings[i]; - _queryParameterNames[i] = mapping.QueryParameterName; - _destinations[i] = mapping.Destination; - } - } - - public void RenderParametersFromQueryString(RenderTreeBuilder builder, ReadOnlyMemory queryString) - { - // If there's no querystring contents, we can skip renting from the pool - if (queryString.IsEmpty) - { - for (var destinationIndex = 0; destinationIndex < _destinations.Length; destinationIndex++) - { - ref var destination = ref _destinations[destinationIndex]; - var blankValue = destination.IsArray ? destination.Parser.ParseMultiple(default, string.Empty) : null; - builder.AddComponentParameter(0, destination.ComponentParameterName, blankValue); - } - return; - } - - // Temporary workspace in which we accumulate the data while walking the querystring. - var valuesByMapping = ArrayPool.Shared.Rent(_destinations.Length); - - try - { - // Capture values by destination in a single pass through the querystring - var queryStringEnumerable = new QueryStringEnumerable(queryString); - foreach (var suppliedPair in queryStringEnumerable) - { - var decodedName = suppliedPair.DecodeName(); - var mappingIndex = Array.BinarySearch(_queryParameterNames, decodedName, QueryParameterNameComparer.Instance); - if (mappingIndex >= 0) - { - var decodedValue = suppliedPair.DecodeValue(); - - if (_destinations[mappingIndex].IsArray) - { - valuesByMapping[mappingIndex].Add(decodedValue); - } - else - { - valuesByMapping[mappingIndex].SetSingle(decodedValue); - } - } - } - - // Finally, emit the parameter attributes by parsing all the string segments and building arrays - for (var mappingIndex = 0; mappingIndex < _destinations.Length; mappingIndex++) - { - ref var destination = ref _destinations[mappingIndex]; - ref var values = ref valuesByMapping[mappingIndex]; - - var parsedValue = destination.IsArray - ? destination.Parser.ParseMultiple(values, destination.ComponentParameterName) - : values.Count == 0 - ? default - : destination.Parser.Parse(values[0].Span, destination.ComponentParameterName); - - builder.AddComponentParameter(0, destination.ComponentParameterName, parsedValue); - } - } - finally - { - ArrayPool.Shared.Return(valuesByMapping, true); - } - } - - private static QueryParameterMapping[]? GetSortedMappings([DynamicallyAccessedMembers(Component)] Type componentType) - { - var candidateProperties = MemberAssignment.GetPropertiesIncludingInherited(componentType, ComponentProperties.BindablePropertyFlags); - HashSet>? usedQueryParameterNames = null; - List? mappings = null; - - foreach (var propertyInfo in candidateProperties) - { - if (!propertyInfo.IsDefined(typeof(ParameterAttribute))) - { - continue; - } - - var fromQueryAttribute = propertyInfo.GetCustomAttribute(); - if (fromQueryAttribute is not null) - { - // Found a parameter that's assignable from querystring - var componentParameterName = propertyInfo.Name; - var queryParameterName = (string.IsNullOrEmpty(fromQueryAttribute.Name) - ? componentParameterName - : fromQueryAttribute.Name).AsMemory(); - - // If it's an array type, capture that info and prepare to parse the element type - Type effectiveType = propertyInfo.PropertyType; - var isArray = false; - if (effectiveType.IsArray) - { - isArray = true; - effectiveType = effectiveType.GetElementType()!; - } - - if (!UrlValueConstraint.TryGetByTargetType(effectiveType, out var parser)) - { - throw new NotSupportedException($"Querystring values cannot be parsed as type '{propertyInfo.PropertyType}'."); - } - - // Add the destination for this component parameter name - usedQueryParameterNames ??= new(QueryParameterNameComparer.Instance); - if (usedQueryParameterNames.Contains(queryParameterName)) - { - throw new InvalidOperationException($"The component '{componentType}' declares more than one mapping for the query parameter '{queryParameterName}'."); - } - usedQueryParameterNames.Add(queryParameterName); - - mappings ??= new(); - mappings.Add(new QueryParameterMapping - { - QueryParameterName = queryParameterName, - Destination = new QueryParameterDestination(componentParameterName, parser, isArray) - }); - } - } - - mappings?.Sort((a, b) => QueryParameterNameComparer.Instance.Compare(a.QueryParameterName, b.QueryParameterName)); - return mappings?.ToArray(); - } - - private readonly struct QueryParameterMapping - { - public ReadOnlyMemory QueryParameterName { get; init; } - public QueryParameterDestination Destination { get; init; } - } - - private readonly struct QueryParameterDestination - { - public readonly string ComponentParameterName; - public readonly UrlValueConstraint Parser; - public readonly bool IsArray; - - public QueryParameterDestination(string componentParameterName, UrlValueConstraint parser, bool isArray) - { - ComponentParameterName = componentParameterName; - Parser = parser; - IsArray = isArray; - } - } -} diff --git a/src/Components/Components/test/Routing/QueryParameterValueSupplierTest.cs b/src/Components/Components/test/Routing/QueryParameterValueSupplierTest.cs deleted file mode 100644 index e96bd3a9deab..000000000000 --- a/src/Components/Components/test/Routing/QueryParameterValueSupplierTest.cs +++ /dev/null @@ -1,508 +0,0 @@ -// 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.Routing; - -public class QueryParameterValueSupplierTest -{ - private class NoQueryParameters : ComponentBase { } - - [Fact] - public void ComponentWithNoQueryParametersHasNoSupplier() - { - Assert.Null(QueryParameterValueSupplier.ForType(typeof(NoQueryParameters))); - } - - private class IgnorableProperties : ComponentBase - { - [Parameter] public string Invalid1 { get; set; } - [SupplyParameterFromQuery] public string Invalid2 { get; set; } - [Parameter, SupplyParameterFromQuery] public string Valid { get; set; } - [Parameter] public object InvalidAndUnsupportedType { get; set; } - } - - [Fact] - public void SuppliesParametersOnlyForPropertiesWithMatchingAttributes() - { - var query = $"?{nameof(IgnorableProperties.Invalid1)}=a&{nameof(IgnorableProperties.Invalid2)}=b&{nameof(IgnorableProperties.Valid)}=c"; - Assert.Collection(GetSuppliedParameters(query), - AssertKeyValuePair(nameof(IgnorableProperties.Valid), "c")); - } - - private class ValidTypes : ComponentBase - { - [Parameter, SupplyParameterFromQuery] public bool BoolVal { get; set; } - [Parameter, SupplyParameterFromQuery] public DateTime DateTimeVal { get; set; } - [Parameter, SupplyParameterFromQuery] public decimal DecimalVal { get; set; } - [Parameter, SupplyParameterFromQuery] public double DoubleVal { get; set; } - [Parameter, SupplyParameterFromQuery] public float FloatVal { get; set; } - [Parameter, SupplyParameterFromQuery] public Guid GuidVal { get; set; } - [Parameter, SupplyParameterFromQuery] public int IntVal { get; set; } - [Parameter, SupplyParameterFromQuery] public long LongVal { get; set; } - [Parameter, SupplyParameterFromQuery] public string StringVal { get; set; } - - [Parameter, SupplyParameterFromQuery] public bool? NullableBoolVal { get; set; } - [Parameter, SupplyParameterFromQuery] public DateTime? NullableDateTimeVal { get; set; } - [Parameter, SupplyParameterFromQuery] public decimal? NullableDecimalVal { get; set; } - [Parameter, SupplyParameterFromQuery] public double? NullableDoubleVal { get; set; } - [Parameter, SupplyParameterFromQuery] public float? NullableFloatVal { get; set; } - [Parameter, SupplyParameterFromQuery] public Guid? NullableGuidVal { get; set; } - [Parameter, SupplyParameterFromQuery] public int? NullableIntVal { get; set; } - [Parameter, SupplyParameterFromQuery] public long? NullableLongVal { get; set; } - } - - [Fact] - public void SupportsExpectedValueTypes() - { - var query = - $"{nameof(ValidTypes.BoolVal)}=true&" + - $"{nameof(ValidTypes.DateTimeVal)}=2020-01-02+03:04:05.678-09:00&" + - $"{nameof(ValidTypes.DecimalVal)}=-1.234&" + - $"{nameof(ValidTypes.DoubleVal)}=-2.345&" + - $"{nameof(ValidTypes.FloatVal)}=-3.456&" + - $"{nameof(ValidTypes.GuidVal)}=9e7257ad-03aa-42c7-9819-be08b177fef9&" + - $"{nameof(ValidTypes.IntVal)}=-54321&" + - $"{nameof(ValidTypes.LongVal)}=-99987654321&" + - $"{nameof(ValidTypes.StringVal)}=Some+string+%26+more&" + - $"{nameof(ValidTypes.NullableBoolVal)}=true&" + - $"{nameof(ValidTypes.NullableDateTimeVal)}=2021-01-02+03:04:05.678Z&" + - $"{nameof(ValidTypes.NullableDecimalVal)}=1.234&" + - $"{nameof(ValidTypes.NullableDoubleVal)}=2.345&" + - $"{nameof(ValidTypes.NullableFloatVal)}=3.456&" + - $"{nameof(ValidTypes.NullableGuidVal)}=1e7257ad-03aa-42c7-9819-be08b177fef9&" + - $"{nameof(ValidTypes.NullableIntVal)}=54321&" + - $"{nameof(ValidTypes.NullableLongVal)}=99987654321&"; - - Assert.Collection(GetSuppliedParameters(query), - AssertKeyValuePair(nameof(ValidTypes.BoolVal), true), - AssertKeyValuePair(nameof(ValidTypes.DateTimeVal), new DateTimeOffset(2020, 1, 2, 3, 4, 5, 678, TimeSpan.FromHours(-9)).LocalDateTime), - AssertKeyValuePair(nameof(ValidTypes.DecimalVal), -1.234m), - AssertKeyValuePair(nameof(ValidTypes.DoubleVal), -2.345), - AssertKeyValuePair(nameof(ValidTypes.FloatVal), -3.456f), - AssertKeyValuePair(nameof(ValidTypes.GuidVal), new Guid("9e7257ad-03aa-42c7-9819-be08b177fef9")), - AssertKeyValuePair(nameof(ValidTypes.IntVal), -54321), - AssertKeyValuePair(nameof(ValidTypes.LongVal), -99987654321), - AssertKeyValuePair(nameof(ValidTypes.NullableBoolVal), true), - AssertKeyValuePair(nameof(ValidTypes.NullableDateTimeVal), new DateTime(2021, 1, 2, 3, 4, 5, 678, DateTimeKind.Utc).ToLocalTime()), - AssertKeyValuePair(nameof(ValidTypes.NullableDecimalVal), 1.234m), - AssertKeyValuePair(nameof(ValidTypes.NullableDoubleVal), 2.345), - AssertKeyValuePair(nameof(ValidTypes.NullableFloatVal), 3.456f), - AssertKeyValuePair(nameof(ValidTypes.NullableGuidVal), new Guid("1e7257ad-03aa-42c7-9819-be08b177fef9")), - AssertKeyValuePair(nameof(ValidTypes.NullableIntVal), 54321), - AssertKeyValuePair(nameof(ValidTypes.NullableLongVal), 99987654321), - AssertKeyValuePair(nameof(ValidTypes.StringVal), "Some string & more")); - } - - [Theory] - [InlineData("")] - [InlineData("?")] - [InlineData("?unrelated=123")] - public void SuppliesNullForValueTypesIfNotSpecified(string query) - { - // Although we could supply default(T) for missing values, there's precedent in the routing - // system for supplying null for missing route parameters. The component is then responsible - // for interpreting null as a blank value for the parameter, regardless of its type. To keep - // the rules aligned, we do the same thing for querystring parameters. - Assert.Collection(GetSuppliedParameters(query), - AssertKeyValuePair(nameof(ValidTypes.BoolVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.DateTimeVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.DecimalVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.DoubleVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.FloatVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.GuidVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.IntVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.LongVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableBoolVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableDateTimeVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableDecimalVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableDoubleVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableFloatVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableGuidVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableIntVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableLongVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.StringVal), (object)null)); - } - - private class ValidArrayTypes : ComponentBase - { - [Parameter, SupplyParameterFromQuery] public bool[] BoolVals { get; set; } - [Parameter, SupplyParameterFromQuery] public DateTime[] DateTimeVals { get; set; } - [Parameter, SupplyParameterFromQuery] public decimal[] DecimalVals { get; set; } - [Parameter, SupplyParameterFromQuery] public double[] DoubleVals { get; set; } - [Parameter, SupplyParameterFromQuery] public float[] FloatVals { get; set; } - [Parameter, SupplyParameterFromQuery] public Guid[] GuidVals { get; set; } - [Parameter, SupplyParameterFromQuery] public int[] IntVals { get; set; } - [Parameter, SupplyParameterFromQuery] public long[] LongVals { get; set; } - [Parameter, SupplyParameterFromQuery] public string[] StringVals { get; set; } - - [Parameter, SupplyParameterFromQuery] public bool?[] NullableBoolVals { get; set; } - [Parameter, SupplyParameterFromQuery] public DateTime?[] NullableDateTimeVals { get; set; } - [Parameter, SupplyParameterFromQuery] public decimal?[] NullableDecimalVals { get; set; } - [Parameter, SupplyParameterFromQuery] public double?[] NullableDoubleVals { get; set; } - [Parameter, SupplyParameterFromQuery] public float?[] NullableFloatVals { get; set; } - [Parameter, SupplyParameterFromQuery] public Guid?[] NullableGuidVals { get; set; } - [Parameter, SupplyParameterFromQuery] public int?[] NullableIntVals { get; set; } - [Parameter, SupplyParameterFromQuery] public long?[] NullableLongVals { get; set; } - } - - [Fact] - public void SupportsExpectedArrayTypes() - { - var query = - $"{nameof(ValidArrayTypes.BoolVals)}=true&" + - $"{nameof(ValidArrayTypes.DateTimeVals)}=2020-01-02+03:04:05.678Z&" + - $"{nameof(ValidArrayTypes.DecimalVals)}=-1.234&" + - $"{nameof(ValidArrayTypes.DoubleVals)}=-2.345&" + - $"{nameof(ValidArrayTypes.FloatVals)}=-3.456&" + - $"{nameof(ValidArrayTypes.GuidVals)}=9e7257ad-03aa-42c7-9819-be08b177fef9&" + - $"{nameof(ValidArrayTypes.IntVals)}=-54321&" + - $"{nameof(ValidArrayTypes.LongVals)}=-99987654321&" + - $"{nameof(ValidArrayTypes.StringVals)}=Some+string+%26+more&" + - $"{nameof(ValidArrayTypes.NullableBoolVals)}=true&" + - $"{nameof(ValidArrayTypes.NullableDateTimeVals)}=2021-01-02+03:04:05.678Z&" + - $"{nameof(ValidArrayTypes.NullableDecimalVals)}=1.234&" + - $"{nameof(ValidArrayTypes.NullableDoubleVals)}=2.345&" + - $"{nameof(ValidArrayTypes.NullableFloatVals)}=3.456&" + - $"{nameof(ValidArrayTypes.NullableGuidVals)}=1e7257ad-03aa-42c7-9819-be08b177fef9&" + - $"{nameof(ValidArrayTypes.NullableIntVals)}=54321&" + - $"{nameof(ValidArrayTypes.NullableLongVals)}=99987654321&"; - - Assert.Collection(GetSuppliedParameters(query), - AssertKeyValuePair(nameof(ValidArrayTypes.BoolVals), new[] { true }), - AssertKeyValuePair(nameof(ValidArrayTypes.DateTimeVals), new[] { new DateTime(2020, 1, 2, 3, 4, 5, 678, DateTimeKind.Utc).ToLocalTime() }), - AssertKeyValuePair(nameof(ValidArrayTypes.DecimalVals), new[] { -1.234m }), - AssertKeyValuePair(nameof(ValidArrayTypes.DoubleVals), new[] { -2.345 }), - AssertKeyValuePair(nameof(ValidArrayTypes.FloatVals), new[] { -3.456f }), - AssertKeyValuePair(nameof(ValidArrayTypes.GuidVals), new[] { new Guid("9e7257ad-03aa-42c7-9819-be08b177fef9") }), - AssertKeyValuePair(nameof(ValidArrayTypes.IntVals), new[] { -54321 }), - AssertKeyValuePair(nameof(ValidArrayTypes.LongVals), new[] { -99987654321 }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableBoolVals), new[] { true }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableDateTimeVals), new[] { new DateTime(2021, 1, 2, 3, 4, 5, 678, DateTimeKind.Utc).ToLocalTime() }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableDecimalVals), new[] { 1.234m }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableDoubleVals), new[] { 2.345 }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableFloatVals), new[] { 3.456f }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableGuidVals), new[] { new Guid("1e7257ad-03aa-42c7-9819-be08b177fef9") }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableIntVals), new[] { 54321 }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableLongVals), new[] { 99987654321 }), - AssertKeyValuePair(nameof(ValidArrayTypes.StringVals), new[] { "Some string & more" })); - } - - [Theory] - [InlineData("")] - [InlineData("?")] - [InlineData("?unrelated=123")] - public void SuppliesEmptyArrayForArrayTypesIfNotSpecified(string query) - { - Assert.Collection(GetSuppliedParameters(query), - AssertKeyValuePair(nameof(ValidArrayTypes.BoolVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.DateTimeVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.DecimalVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.DoubleVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.FloatVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.GuidVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.IntVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.LongVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableBoolVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableDateTimeVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableDecimalVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableDoubleVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableFloatVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableGuidVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableIntVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableLongVals), Array.Empty()), - AssertKeyValuePair(nameof(ValidArrayTypes.StringVals), Array.Empty())); - } - - class OverrideParameterName : ComponentBase - { - [Parameter, SupplyParameterFromQuery(Name = "anothername1")] public string Value1 { get; set; } - [Parameter, SupplyParameterFromQuery(Name = "anothername2")] public string Value2 { get; set; } - } - - [Fact] - public void CanOverrideParameterName() - { - var query = $"anothername1=Some+value+1&Value2=Some+value+2"; - Assert.Collection(GetSuppliedParameters(query), - // Because we specified the mapped name, we receive the value - AssertKeyValuePair(nameof(OverrideParameterName.Value1), "Some value 1"), - // If we specify the component parameter name directly, we do not receive the value - AssertKeyValuePair(nameof(OverrideParameterName.Value2), (object)null)); - } - - class MapSingleQueryParameterToMultipleProperties : ComponentBase - { - [Parameter, SupplyParameterFromQuery(Name = "a")] public int ValueAsInt { get; set; } - [Parameter, SupplyParameterFromQuery(Name = "b")] public DateTime ValueAsDateTime { get; set; } - [Parameter, SupplyParameterFromQuery(Name = "A")] public long ValueAsLong { get; set; } - } - - [Fact] - public void CannotMapSingleQueryParameterToMultipleProperties() - { - var ex = Assert.Throws( - () => QueryParameterValueSupplier.ForType(typeof(MapSingleQueryParameterToMultipleProperties))); - Assert.Contains("declares more than one mapping for the query parameter 'a'.", ex.Message, StringComparison.OrdinalIgnoreCase); - } - - class UnsupportedType : ComponentBase - { - [Parameter, SupplyParameterFromQuery] public int IntValid { get; set; } - [Parameter, SupplyParameterFromQuery] public object ObjectValue { get; set; } - } - - [Fact] - public void RejectsUnsupportedType() - { - var ex = Assert.Throws( - () => QueryParameterValueSupplier.ForType(typeof(UnsupportedType))); - Assert.Equal("Querystring values cannot be parsed as type 'System.Object'.", ex.Message); - } - - [Theory] - [InlineData(nameof(ValidTypes.BoolVal), "abc", typeof(bool))] - [InlineData(nameof(ValidTypes.DateTimeVal), "2020-02-31", typeof(DateTime))] - [InlineData(nameof(ValidTypes.DecimalVal), "1.2.3", typeof(decimal))] - [InlineData(nameof(ValidTypes.DoubleVal), "1x", typeof(double))] - [InlineData(nameof(ValidTypes.FloatVal), "1e1000", typeof(float))] - [InlineData(nameof(ValidTypes.GuidVal), "123456-789-0", typeof(Guid))] - [InlineData(nameof(ValidTypes.IntVal), "5000000000", typeof(int))] - [InlineData(nameof(ValidTypes.LongVal), "this+is+a+long+value", typeof(long))] - [InlineData(nameof(ValidTypes.NullableBoolVal), "abc", typeof(bool?))] - [InlineData(nameof(ValidTypes.NullableDateTimeVal), "2020-02-31", typeof(DateTime?))] - [InlineData(nameof(ValidTypes.NullableDecimalVal), "1.2.3", typeof(decimal?))] - [InlineData(nameof(ValidTypes.NullableDoubleVal), "1x", typeof(double?))] - [InlineData(nameof(ValidTypes.NullableFloatVal), "1e1000", typeof(float?))] - [InlineData(nameof(ValidTypes.NullableGuidVal), "123456-789-0", typeof(Guid?))] - [InlineData(nameof(ValidTypes.NullableIntVal), "5000000000", typeof(int?))] - [InlineData(nameof(ValidTypes.NullableLongVal), "this+is+a+long+value", typeof(long?))] - public void RejectsUnparseableValues(string key, string value, Type targetType) - { - var ex = Assert.Throws( - () => GetSuppliedParameters($"?{key}={value}")); - Assert.Equal($"Cannot parse the value '{value.Replace('+', ' ')}' as type '{targetType}' for '{key}'.", ex.Message); - } - - [Theory] - [InlineData(nameof(ValidArrayTypes.BoolVals), "true", "abc", typeof(bool))] - [InlineData(nameof(ValidArrayTypes.DateTimeVals), "2020-02-28", "2020-02-31", typeof(DateTime))] - [InlineData(nameof(ValidArrayTypes.DecimalVals), "1.23", "1.2.3", typeof(decimal))] - [InlineData(nameof(ValidArrayTypes.DoubleVals), "1", "1x", typeof(double))] - [InlineData(nameof(ValidArrayTypes.FloatVals), "1000", "1e1000", typeof(float))] - [InlineData(nameof(ValidArrayTypes.GuidVals), "9e7257ad-03aa-42c7-9819-be08b177fef9", "123456-789-0", typeof(Guid))] - [InlineData(nameof(ValidArrayTypes.IntVals), "5000000", "5000000000", typeof(int))] - [InlineData(nameof(ValidArrayTypes.LongVals), "-1234", "this+is+a+long+value", typeof(long))] - [InlineData(nameof(ValidArrayTypes.NullableBoolVals), "true", "abc", typeof(bool?))] - [InlineData(nameof(ValidArrayTypes.NullableDateTimeVals), "2020-02-28", "2020-02-31", typeof(DateTime?))] - [InlineData(nameof(ValidArrayTypes.NullableDecimalVals), "1.23", "1.2.3", typeof(decimal?))] - [InlineData(nameof(ValidArrayTypes.NullableDoubleVals), "1", "1x", typeof(double?))] - [InlineData(nameof(ValidArrayTypes.NullableFloatVals), "1000", "1e1000", typeof(float?))] - [InlineData(nameof(ValidArrayTypes.NullableGuidVals), "9e7257ad-03aa-42c7-9819-be08b177fef9", "123456-789-0", typeof(Guid?))] - [InlineData(nameof(ValidArrayTypes.NullableIntVals), "5000000", "5000000000", typeof(int?))] - [InlineData(nameof(ValidArrayTypes.NullableLongVals), "-1234", "this+is+a+long+value", typeof(long?))] - public void RejectsUnparseableArrayEntries(string key, string validValue, string invalidValue, Type targetType) - { - var ex = Assert.Throws( - () => GetSuppliedParameters($"?{key}={validValue}&{key}={invalidValue}")); - Assert.Equal($"Cannot parse the value '{invalidValue.Replace('+', ' ')}' as type '{targetType}' for '{key}'.", ex.Message); - } - - [Theory] - [InlineData(nameof(ValidTypes.BoolVal), typeof(bool))] - [InlineData(nameof(ValidTypes.DateTimeVal), typeof(DateTime))] - [InlineData(nameof(ValidTypes.DecimalVal), typeof(decimal))] - [InlineData(nameof(ValidTypes.DoubleVal), typeof(double))] - [InlineData(nameof(ValidTypes.FloatVal), typeof(float))] - [InlineData(nameof(ValidTypes.GuidVal), typeof(Guid))] - [InlineData(nameof(ValidTypes.IntVal), typeof(int))] - [InlineData(nameof(ValidTypes.LongVal), typeof(long))] - public void RejectsBlankValuesWhenNotNullable(string key, Type targetType) - { - var ex = Assert.Throws( - () => GetSuppliedParameters($"?{nameof(ValidTypes.StringVal)}=somevalue&{key}=")); - Assert.Equal($"Cannot parse the value '' as type '{targetType}' for '{key}'.", ex.Message); - } - - [Fact] - public void AcceptsBlankValuesWhenNullable() - { - var query = - $"{nameof(ValidTypes.NullableBoolVal)}=&" + - $"{nameof(ValidTypes.NullableDateTimeVal)}=&" + - $"{nameof(ValidTypes.NullableDecimalVal)}=&" + - $"{nameof(ValidTypes.NullableDoubleVal)}=&" + - $"{nameof(ValidTypes.NullableFloatVal)}=&" + - $"{nameof(ValidTypes.NullableGuidVal)}=&" + - $"{nameof(ValidTypes.NullableIntVal)}=&" + - $"{nameof(ValidTypes.NullableLongVal)}=&"; - Assert.Collection(GetSuppliedParameters(query).Where(pair => pair.key.StartsWith("Nullable", StringComparison.Ordinal)), - AssertKeyValuePair(nameof(ValidTypes.NullableBoolVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableDateTimeVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableDecimalVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableDoubleVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableFloatVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableGuidVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableIntVal), (object)null), - AssertKeyValuePair(nameof(ValidTypes.NullableLongVal), (object)null)); - } - - [Theory] - [InlineData("")] - [InlineData("=")] - public void EmptyStringValuesAreSuppliedAsEmptyString(string queryPart) - { - var query = $"?{nameof(ValidTypes.StringVal)}{queryPart}"; - var suppliedParameters = GetSuppliedParameters(query).ToDictionary(x => x.key, x => x.value); - Assert.Equal(string.Empty, suppliedParameters[nameof(ValidTypes.StringVal)]); - } - - [Fact] - public void EmptyStringArrayValuesAreSuppliedAsEmptyStrings() - { - var query = $"?{nameof(ValidArrayTypes.StringVals)}=a&" + - $"{nameof(ValidArrayTypes.StringVals)}&" + - $"{nameof(ValidArrayTypes.StringVals)}=&" + - $"{nameof(ValidArrayTypes.StringVals)}=b"; - var suppliedParameters = GetSuppliedParameters(query).ToDictionary(x => x.key, x => x.value); - Assert.Equal(new[] { "a", string.Empty, string.Empty, "b" }, suppliedParameters[nameof(ValidArrayTypes.StringVals)]); - } - - [Theory] - [InlineData(nameof(ValidArrayTypes.BoolVals), typeof(bool))] - [InlineData(nameof(ValidArrayTypes.DateTimeVals), typeof(DateTime))] - [InlineData(nameof(ValidArrayTypes.DecimalVals), typeof(decimal))] - [InlineData(nameof(ValidArrayTypes.DoubleVals), typeof(double))] - [InlineData(nameof(ValidArrayTypes.FloatVals), typeof(float))] - [InlineData(nameof(ValidArrayTypes.GuidVals), typeof(Guid))] - [InlineData(nameof(ValidArrayTypes.IntVals), typeof(int))] - [InlineData(nameof(ValidArrayTypes.LongVals), typeof(long))] - public void RejectsBlankArrayEntriesWhenNotNullable(string key, Type targetType) - { - var ex = Assert.Throws( - () => GetSuppliedParameters($"?{nameof(ValidTypes.StringVal)}=somevalue&{key}=")); - Assert.Equal($"Cannot parse the value '' as type '{targetType}' for '{key}'.", ex.Message); - } - - [Fact] - public void AcceptsBlankArrayEntriesWhenNullable() - { - var query = - $"{nameof(ValidArrayTypes.NullableBoolVals)}=&" + - $"{nameof(ValidArrayTypes.NullableDateTimeVals)}=&" + - $"{nameof(ValidArrayTypes.NullableDecimalVals)}=&" + - $"{nameof(ValidArrayTypes.NullableDoubleVals)}=&" + - $"{nameof(ValidArrayTypes.NullableFloatVals)}=&" + - $"{nameof(ValidArrayTypes.NullableGuidVals)}=&" + - $"{nameof(ValidArrayTypes.NullableIntVals)}=&" + - $"{nameof(ValidArrayTypes.NullableLongVals)}=&"; - Assert.Collection(GetSuppliedParameters(query).Where(pair => pair.key.StartsWith("Nullable", StringComparison.Ordinal)), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableBoolVals), new bool?[] { null }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableDateTimeVals), new DateTime?[] { null }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableDecimalVals), new decimal?[] { null }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableDoubleVals), new double?[] { null }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableFloatVals), new float?[] { null }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableGuidVals), new Guid?[] { null }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableIntVals), new int?[] { null }), - AssertKeyValuePair(nameof(ValidArrayTypes.NullableLongVals), new long?[] { null })); - } - - private class SpecialQueryParameterName : ComponentBase - { - public const string NameThatLooksEncoded = "name+that+looks+%5Bencoded%5D"; - [Parameter, SupplyParameterFromQuery(Name = NameThatLooksEncoded)] public string Key { get; set; } - } - - [Fact] - public void DecodesKeysAndValues() - { - var encodedName = Uri.EscapeDataString(SpecialQueryParameterName.NameThatLooksEncoded); - var query = $"?{encodedName}=Some+%5Bencoded%5D+value"; - Assert.Collection(GetSuppliedParameters(query), - AssertKeyValuePair(nameof(SpecialQueryParameterName.Key), "Some [encoded] value")); - } - - private class KeyCaseMatching : ComponentBase - { - [Parameter, SupplyParameterFromQuery] public int KeyOne { get; set; } - [Parameter, SupplyParameterFromQuery(Name = "keytwo")] public int KeyTwo { get; set; } - } - - [Fact] - public void MatchesKeysCaseInsensitively() - { - var query = $"?KEYONE=1&KEYTWO=2"; - Assert.Collection(GetSuppliedParameters(query), - AssertKeyValuePair(nameof(KeyCaseMatching.KeyOne), 1), - AssertKeyValuePair(nameof(KeyCaseMatching.KeyTwo), 2)); - } - - private class KeysWithNonAsciiChars : ComponentBase - { - [Parameter, SupplyParameterFromQuery] public string Имя_моей_собственности { get; set; } - [Parameter, SupplyParameterFromQuery(Name = "خاصية_أخرى")] public string AnotherProperty { get; set; } - } - - [Fact] - public void MatchesKeysWithNonAsciiChars() - { - var query = $"?{nameof(KeysWithNonAsciiChars.Имя_моей_собственности)}=first&خاصية_أخرى=second"; - var result = GetSuppliedParameters(query); - Assert.Collection(result, - AssertKeyValuePair(nameof(KeysWithNonAsciiChars.AnotherProperty), "second"), - AssertKeyValuePair(nameof(KeysWithNonAsciiChars.Имя_моей_собственности), "first")); - } - - private class SingleValueOverwriting : ComponentBase - { - [Parameter, SupplyParameterFromQuery] public int Age { get; set; } - [Parameter, SupplyParameterFromQuery] public int? Id { get; set; } - [Parameter, SupplyParameterFromQuery] public string Name { get; set; } - } - - [Fact] - public void ForNonArrayValuesOnlyOneValueIsSupplied() - { - // For simplicity and speed, the value assignment logic doesn't check if the a single-valued destination is - // already populated, and just overwrites in a left-to-right manner. For nullable values it's possible to - // overwrite a value with null, or a string with empty. - Assert.Collection(GetSuppliedParameters($"?age=123&age=456&age=789&id=1&id&name=Bobbins&name"), - AssertKeyValuePair(nameof(SingleValueOverwriting.Age), 789), - AssertKeyValuePair(nameof(SingleValueOverwriting.Id), (int?)null), - AssertKeyValuePair(nameof(SingleValueOverwriting.Name), string.Empty)); - } - - private static IEnumerable<(string key, object value)> GetSuppliedParameters(string query) where TComponent : IComponent - { - var supplier = QueryParameterValueSupplier.ForType(typeof(TComponent)); - using var builder = new RenderTreeBuilder(); - builder.OpenComponent(0); - supplier.RenderParametersFromQueryString(builder, query.AsMemory()); - builder.CloseComponent(); - - var frames = builder.GetFrames(); - return frames.Array.Take(frames.Count) - .Where(frame => frame.FrameType == RenderTree.RenderTreeFrameType.Attribute) - .Select(frame => (frame.AttributeName, frame.AttributeValue)) - .OrderBy(pair => pair.AttributeName) // The order isn't defined, so use alphabetical for tests - .ToList(); - } - - private Action<(string key, object value)> AssertKeyValuePair(string expectedKey, T expectedValue) - { - return pair => - { - Assert.Equal(expectedKey, pair.key); - if (expectedValue is null) - { - Assert.Null(pair.value); - } - else - { - Assert.IsType(expectedValue); - Assert.Equal(expectedValue, pair.value); - } - }; - } -} From 10601ddbbf7f1ccda9ca614b1a1da8a155fd412c Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 2 Jun 2023 16:55:36 -0700 Subject: [PATCH 03/13] Update RouteView.cs --- src/Components/Components/src/RouteView.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/Components/src/RouteView.cs b/src/Components/Components/src/RouteView.cs index 72a3382b3eb8..271e87d070fc 100644 --- a/src/Components/Components/src/RouteView.cs +++ b/src/Components/Components/src/RouteView.cs @@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.AspNetCore.Components.Rendering; -using Microsoft.AspNetCore.Components.Routing; namespace Microsoft.AspNetCore.Components; From fa7d9c58de330a6c6992a49442d069f768e51e11 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Sun, 4 Jun 2023 15:04:35 -0700 Subject: [PATCH 04/13] Update tests --- .../Components/src/Binding/CascadingModelBinder.cs | 13 +++++++------ .../Components/src/CascadingQueryValueProvider.cs | 4 ++-- src/Components/test/E2ETest/Tests/RoutingTest.cs | 14 ++++++++++++++ .../RouterTest/NestedQueryParameters.razor | 12 ++++++++++++ .../RouterTest/WithQueryParameters.razor | 2 ++ 5 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 src/Components/test/testassets/BasicTestApp/RouterTest/NestedQueryParameters.razor diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index 5d16d48a9940..24b48bba7347 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -145,22 +145,23 @@ void IDisposable.Dispose() Navigation.LocationChanged -= HandleLocationChanged; } - bool ICascadingValueSupplierFactory.TryGetValueSupplier(object propertyAttribute, Type valueType, string? valueName, [NotNullWhen(true)] out ICascadingValueSupplier? result) + bool ICascadingValueSupplierFactory.TryGetValueSupplier(object propertyAttribute, Type parameterType, string? parameterName, [NotNullWhen(true)] out ICascadingValueSupplier? result) { result = default; - if (propertyAttribute is not IFormValueCascadingParameterAttribute) + if (propertyAttribute is not IFormValueCascadingParameterAttribute formCascadingParameterAttribute) { return false; } + var valueName = formCascadingParameterAttribute.Name; var formName = string.IsNullOrEmpty(valueName) ? (_bindingContext?.Name) : ModelBindingContext.Combine(_bindingContext, valueName); if (_bindingInfo != null && string.Equals(_bindingInfo.FormName, formName, StringComparison.Ordinal) && - _bindingInfo.ValueType.Equals(valueType)) + _bindingInfo.ValueType.Equals(parameterType)) { // 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, @@ -170,10 +171,10 @@ bool ICascadingValueSupplierFactory.TryGetValueSupplier(object propertyAttribute } // Can't supply the value if this context is for a form with a different name. - if (FormValueSupplier.CanBind(formName!, valueType)) + if (FormValueSupplier.CanBind(formName!, parameterType)) { - var bindingSucceeded = FormValueSupplier.TryBind(formName!, valueType, out var boundValue); - _bindingInfo = new BindingInfo(formName, valueType, bindingSucceeded, boundValue); + var bindingSucceeded = FormValueSupplier.TryBind(formName!, parameterType, out var boundValue); + _bindingInfo = new BindingInfo(formName, parameterType, bindingSucceeded, boundValue); if (!bindingSucceeded) { // Report errors diff --git a/src/Components/Components/src/CascadingQueryValueProvider.cs b/src/Components/Components/src/CascadingQueryValueProvider.cs index 6d79a7aa524f..d84d415b94bc 100644 --- a/src/Components/Components/src/CascadingQueryValueProvider.cs +++ b/src/Components/Components/src/CascadingQueryValueProvider.cs @@ -117,8 +117,8 @@ bool ICascadingValueSupplierFactory.TryGetValueSupplier(object attribute, Type p return false; } - var specifiedName = supplyFromQueryAttribute.Name; - var valueName = string.IsNullOrEmpty(specifiedName) ? parameterName : specifiedName; + var parameterSpecifiedName = supplyFromQueryAttribute.Name; + var valueName = string.IsNullOrEmpty(parameterSpecifiedName) ? parameterName : parameterSpecifiedName; var key = new SupplierKey(parameterType, valueName.AsMemory()); if (!_cachedSuppliers.TryGetValue(key, out var supplier)) diff --git a/src/Components/test/E2ETest/Tests/RoutingTest.cs b/src/Components/test/E2ETest/Tests/RoutingTest.cs index a2fc4373048c..b161d9365472 100644 --- a/src/Components/test/E2ETest/Tests/RoutingTest.cs +++ b/src/Components/test/E2ETest/Tests/RoutingTest.cs @@ -1437,11 +1437,13 @@ public void CanArriveAtQueryStringPageWithNoQuery() var app = Browser.MountTestComponent(); Assert.Equal("Hello Abc .", app.FindElement(By.Id("test-info")).Text); Assert.Equal("0", app.FindElement(By.Id("value-QueryInt")).Text); + Assert.Equal("0", app.FindElement(By.Id("value-nested-QueryInt")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateTimeValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateOnlyValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableTimeOnlyValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-StringValue")).Text); Assert.Equal("0 values ()", app.FindElement(By.Id("value-LongValues")).Text); + Assert.Equal("0 values ()", app.FindElement(By.Id("value-nested-LongValues")).Text); AssertHighlightedLinks("With query parameters (none)"); } @@ -1454,11 +1456,13 @@ public void CanArriveAtQueryStringPageWithStringQuery() var app = Browser.MountTestComponent(); Assert.Equal("Hello Abc .", app.FindElement(By.Id("test-info")).Text); Assert.Equal("0", app.FindElement(By.Id("value-QueryInt")).Text); + Assert.Equal("0", app.FindElement(By.Id("value-nested-QueryInt")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateTimeValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateOnlyValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableTimeOnlyValue")).Text); Assert.Equal("Hello there", app.FindElement(By.Id("value-StringValue")).Text); Assert.Equal("0 values ()", app.FindElement(By.Id("value-LongValues")).Text); + Assert.Equal("0 values ()", app.FindElement(By.Id("value-nested-LongValues")).Text); AssertHighlightedLinks("With query parameters (none)", "With query parameters (passing string value)"); } @@ -1474,11 +1478,13 @@ public void CanArriveAtQueryStringPageWithDateTimeQuery() var app = Browser.MountTestComponent(); Assert.Equal("Hello Abc .", app.FindElement(By.Id("test-info")).Text); Assert.Equal("0", app.FindElement(By.Id("value-QueryInt")).Text); + Assert.Equal("0", app.FindElement(By.Id("value-nested-QueryInt")).Text); Assert.Equal(dateTime.ToString("hh:mm:ss on yyyy-MM-dd", CultureInfo.InvariantCulture), app.FindElement(By.Id("value-NullableDateTimeValue")).Text); Assert.Equal(dateOnly.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), app.FindElement(By.Id("value-NullableDateOnlyValue")).Text); Assert.Equal(timeOnly.ToString("hh:mm:ss", CultureInfo.InvariantCulture), app.FindElement(By.Id("value-NullableTimeOnlyValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-StringValue")).Text); Assert.Equal("0 values ()", app.FindElement(By.Id("value-LongValues")).Text); + Assert.Equal("0 values ()", app.FindElement(By.Id("value-nested-LongValues")).Text); AssertHighlightedLinks("With query parameters (none)", "With query parameters (passing Date Time values)"); } @@ -1493,11 +1499,13 @@ public void CanNavigateToQueryStringPageWithNoQuery() Assert.Equal("Hello Abc .", app.FindElement(By.Id("test-info")).Text); Assert.Equal("0", app.FindElement(By.Id("value-QueryInt")).Text); + Assert.Equal("0", app.FindElement(By.Id("value-nested-QueryInt")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateTimeValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateOnlyValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableTimeOnlyValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-StringValue")).Text); Assert.Equal("0 values ()", app.FindElement(By.Id("value-LongValues")).Text); + Assert.Equal("0 values ()", app.FindElement(By.Id("value-nested-LongValues")).Text); AssertHighlightedLinks("With query parameters (none)"); } @@ -1513,11 +1521,13 @@ public void CanNavigateBetweenPagesWithQueryStrings() Browser.Equal("Hello Abc .", () => app.FindElement(By.Id("test-info")).Text); Assert.Equal("0", app.FindElement(By.Id("value-QueryInt")).Text); + Assert.Equal("0", app.FindElement(By.Id("value-nested-QueryInt")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateTimeValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateOnlyValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableTimeOnlyValue")).Text); Assert.Equal("Hello there", app.FindElement(By.Id("value-StringValue")).Text); Assert.Equal("0 values ()", app.FindElement(By.Id("value-LongValues")).Text); + Assert.Equal("0 values ()", app.FindElement(By.Id("value-nested-LongValues")).Text); var instanceId = app.FindElement(By.Id("instance-id")).Text; Assert.True(!string.IsNullOrWhiteSpace(instanceId)); @@ -1526,22 +1536,26 @@ public void CanNavigateBetweenPagesWithQueryStrings() // We can also navigate to a different query while retaining the same component instance app.FindElement(By.LinkText("With IntValue and LongValues")).Click(); Browser.Equal("123", () => app.FindElement(By.Id("value-QueryInt")).Text); + Browser.Equal("123", () => app.FindElement(By.Id("value-nested-QueryInt")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateTimeValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateOnlyValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableTimeOnlyValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-StringValue")).Text); Assert.Equal("3 values (50, 100, -20)", app.FindElement(By.Id("value-LongValues")).Text); + Assert.Equal("3 values (50, 100, -20)", app.FindElement(By.Id("value-nested-LongValues")).Text); Assert.Equal(instanceId, app.FindElement(By.Id("instance-id")).Text); AssertHighlightedLinks("With query parameters (none)"); // We can also click back to go the preceding query while retaining the same component instance Browser.Navigate().Back(); Browser.Equal("0", () => app.FindElement(By.Id("value-QueryInt")).Text); + Browser.Equal("0", () => app.FindElement(By.Id("value-nested-QueryInt")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateTimeValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableDateOnlyValue")).Text); Assert.Equal(string.Empty, app.FindElement(By.Id("value-NullableTimeOnlyValue")).Text); Assert.Equal("Hello there", app.FindElement(By.Id("value-StringValue")).Text); Assert.Equal("0 values ()", app.FindElement(By.Id("value-LongValues")).Text); + Assert.Equal("0 values ()", app.FindElement(By.Id("value-nested-LongValues")).Text); Assert.Equal(instanceId, app.FindElement(By.Id("instance-id")).Text); AssertHighlightedLinks("With query parameters (none)", "With query parameters (passing string value)"); } diff --git a/src/Components/test/testassets/BasicTestApp/RouterTest/NestedQueryParameters.razor b/src/Components/test/testassets/BasicTestApp/RouterTest/NestedQueryParameters.razor new file mode 100644 index 000000000000..9f14f51fe401 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/RouterTest/NestedQueryParameters.razor @@ -0,0 +1,12 @@ +
+ Nested query parameters: +

Nested IntValue: @IntValue

+

Nested LongValues: @LongValues.Length values (@string.Join(", ", LongValues.Select(x => x.ToString()).ToArray()))

+
+ +@code +{ + [Parameter, SupplyParameterFromQuery] public int IntValue { get ; set; } + + [Parameter, SupplyParameterFromQuery(Name = "l")] public long[] LongValues { get ; set; } +} diff --git a/src/Components/test/testassets/BasicTestApp/RouterTest/WithQueryParameters.razor b/src/Components/test/testassets/BasicTestApp/RouterTest/WithQueryParameters.razor index 46a19b816bcc..30cc95a04f10 100644 --- a/src/Components/test/testassets/BasicTestApp/RouterTest/WithQueryParameters.razor +++ b/src/Components/test/testassets/BasicTestApp/RouterTest/WithQueryParameters.razor @@ -7,6 +7,8 @@

StringValue: @StringValue

LongValues: @LongValues.Length values (@string.Join(", ", LongValues.Select(x => x.ToString()).ToArray()))

+ +

Instance ID: @instanceId

From 640ec5f3ad8381a3871968a70894bbc8cbcd50cd Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 5 Jun 2023 15:31:16 -0700 Subject: [PATCH 05/13] Merged ICascadingValueSupplier{Factory} together --- .../src/Binding/CascadingModelBinder.cs | 14 +- .../Components/src/CascadingParameterInfo.cs | 11 ++ .../Components/src/CascadingParameterState.cs | 48 ++---- .../src/CascadingQueryValueProvider.cs | 151 +++++------------- .../Components/src/CascadingValue.cs | 25 +-- .../Components/src/ICascadingValueSupplier.cs | 4 +- .../src/ICascadingValueSupplierFactory.cs | 11 -- .../Components/src/ParameterView.cs | 3 +- .../test/CascadingParameterStateTest.cs | 36 ++--- .../Components/test/CascadingParameterTest.cs | 19 +-- .../test/ParameterViewTest.Assignment.cs | 30 ++-- .../Components/test/ParameterViewTest.cs | 23 +-- 12 files changed, 138 insertions(+), 237 deletions(-) create mode 100644 src/Components/Components/src/CascadingParameterInfo.cs delete mode 100644 src/Components/Components/src/ICascadingValueSupplierFactory.cs diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index 24b48bba7347..0473e06afe53 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -1,7 +1,6 @@ // 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.Reflection.Metadata; using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; @@ -12,7 +11,7 @@ namespace Microsoft.AspNetCore.Components; ///

/// Defines the binding context for data bound from external sources. /// -public sealed class CascadingModelBinder : IComponent, ICascadingValueSupplierFactory, ICascadingValueSupplier, IDisposable +public sealed class CascadingModelBinder : IComponent, ICascadingValueSupplier, IDisposable { private RenderHandle _handle; private ModelBindingContext? _bindingContext; @@ -145,15 +144,14 @@ void IDisposable.Dispose() Navigation.LocationChanged -= HandleLocationChanged; } - bool ICascadingValueSupplierFactory.TryGetValueSupplier(object propertyAttribute, Type parameterType, string? parameterName, [NotNullWhen(true)] out ICascadingValueSupplier? result) + bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo) { - result = default; - - if (propertyAttribute is not IFormValueCascadingParameterAttribute formCascadingParameterAttribute) + if (parameterInfo.Attribute is not IFormValueCascadingParameterAttribute formCascadingParameterAttribute) { return false; } + var parameterType = parameterInfo.PropertyType; var valueName = formCascadingParameterAttribute.Name; var formName = string.IsNullOrEmpty(valueName) ? (_bindingContext?.Name) : @@ -166,7 +164,6 @@ bool ICascadingValueSupplierFactory.TryGetValueSupplier(object propertyAttribute // 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. - result = this; return true; } @@ -180,7 +177,6 @@ bool ICascadingValueSupplierFactory.TryGetValueSupplier(object propertyAttribute // Report errors } - result = this; return true; } @@ -197,7 +193,7 @@ void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber) throw new InvalidOperationException("Form values are always fixed."); } - object? ICascadingValueSupplier.CurrentValue => _bindingInfo == null ? + object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo) => _bindingInfo == null ? throw new InvalidOperationException("Tried to access form value before it was bound.") : _bindingInfo.BoundValue; diff --git a/src/Components/Components/src/CascadingParameterInfo.cs b/src/Components/Components/src/CascadingParameterInfo.cs new file mode 100644 index 000000000000..d77554a27551 --- /dev/null +++ b/src/Components/Components/src/CascadingParameterInfo.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components; + +internal readonly struct CascadingParameterInfo(ICascadingParameterAttribute attribute, string propertyName, Type propertyType) +{ + public ICascadingParameterAttribute Attribute { get; } = attribute; + public string PropertyName { get; } = propertyName; + public Type PropertyType { get; } = propertyType; +} diff --git a/src/Components/Components/src/CascadingParameterState.cs b/src/Components/Components/src/CascadingParameterState.cs index 05907cb3de99..71e9fdcd6cbe 100644 --- a/src/Components/Components/src/CascadingParameterState.cs +++ b/src/Components/Components/src/CascadingParameterState.cs @@ -13,21 +13,21 @@ namespace Microsoft.AspNetCore.Components; internal readonly struct CascadingParameterState { - private static readonly ConcurrentDictionary _cachedInfos = new(); + private static readonly ConcurrentDictionary _cachedInfos = new(); - public string LocalValueName { get; } + public CascadingParameterInfo ParameterInfo { get; } public ICascadingValueSupplier ValueSupplier { get; } - public CascadingParameterState(string localValueName, ICascadingValueSupplier valueSupplier) + public CascadingParameterState(in CascadingParameterInfo parameterInfo, ICascadingValueSupplier valueSupplier) { - LocalValueName = localValueName; + ParameterInfo = parameterInfo; ValueSupplier = valueSupplier; } public static IReadOnlyList FindCascadingParameters(ComponentState componentState) { var componentType = componentState.Component.GetType(); - var infos = GetReflectedCascadingParameterInfos(componentType); + var infos = GetCascadingParameterInfos(componentType); // For components known not to have any cascading parameters, bail out early if (infos.Length == 0) @@ -48,21 +48,20 @@ public static IReadOnlyList FindCascadingParameters(Com { // Although not all parameters might be matched, we know the maximum number resultStates ??= new List(infos.Length - infoIndex); - resultStates.Add(new CascadingParameterState(info.PropertyName, supplier)); + resultStates.Add(new CascadingParameterState(info, supplier)); } } return resultStates ?? (IReadOnlyList)Array.Empty(); } - private static ICascadingValueSupplier? GetMatchingCascadingValueSupplier(in ReflectedCascadingParameterInfo info, ComponentState componentState) + private static ICascadingValueSupplier? GetMatchingCascadingValueSupplier(in CascadingParameterInfo info, ComponentState componentState) { var candidate = componentState; do { var candidateComponent = candidate.Component; - if (candidateComponent is ICascadingValueSupplierFactory valueSupplierFactory && - valueSupplierFactory.TryGetValueSupplier(info.Attribute, info.PropertyType, info.PropertyName, out var valueSupplier)) + if (candidateComponent is ICascadingValueSupplier valueSupplier && valueSupplier.CanSupplyValue(info)) { return valueSupplier; } @@ -74,22 +73,22 @@ public static IReadOnlyList FindCascadingParameters(Com return null; } - private static ReflectedCascadingParameterInfo[] GetReflectedCascadingParameterInfos( + private static CascadingParameterInfo[] GetCascadingParameterInfos( [DynamicallyAccessedMembers(Component)] Type componentType) { if (!_cachedInfos.TryGetValue(componentType, out var infos)) { - infos = CreateReflectedCascadingParameterInfos(componentType); + infos = CreateCascadingParameterInfos(componentType); _cachedInfos[componentType] = infos; } return infos; } - private static ReflectedCascadingParameterInfo[] CreateReflectedCascadingParameterInfos( + private static CascadingParameterInfo[] CreateCascadingParameterInfos( [DynamicallyAccessedMembers(Component)] Type componentType) { - List? result = null; + List? result = null; var candidateProps = ComponentProperties.GetCandidateBindableProperties(componentType); foreach (var prop in candidateProps) { @@ -97,31 +96,14 @@ private static ReflectedCascadingParameterInfo[] CreateReflectedCascadingParamet .OfType().SingleOrDefault(); if (cascadingParameterAttribute != null) { - result ??= new List(); - result.Add(new ReflectedCascadingParameterInfo( + result ??= new List(); + result.Add(new CascadingParameterInfo( cascadingParameterAttribute, prop.Name, prop.PropertyType)); } } - return result?.ToArray() ?? Array.Empty(); - } - - readonly struct ReflectedCascadingParameterInfo - { - public object Attribute { get; } - public string PropertyName { get; } - public Type PropertyType { get; } - - public ReflectedCascadingParameterInfo( - object attribute, - string propertyName, - Type propertyType) - { - Attribute = attribute; - PropertyName = propertyName; - PropertyType = propertyType; - } + return result?.ToArray() ?? Array.Empty(); } } diff --git a/src/Components/Components/src/CascadingQueryValueProvider.cs b/src/Components/Components/src/CascadingQueryValueProvider.cs index d84d415b94bc..73adfa24393a 100644 --- a/src/Components/Components/src/CascadingQueryValueProvider.cs +++ b/src/Components/Components/src/CascadingQueryValueProvider.cs @@ -1,8 +1,6 @@ // 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.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Routing; @@ -10,11 +8,11 @@ namespace Microsoft.AspNetCore.Components; -internal sealed class CascadingQueryValueProvider : IComponent, ICascadingValueSupplierFactory, IDisposable +internal sealed class CascadingQueryValueProvider : IComponent, ICascadingValueSupplier, IDisposable { - private readonly Dictionary _cachedSuppliers = new(); private readonly Dictionary, StringSegmentAccumulator> _queryParameterValuesByName = new(QueryParameterNameComparer.Instance); + private HashSet? _subscribers; private RenderHandle _handle; private bool _hasSetInitialParameters; @@ -34,7 +32,7 @@ Task IComponent.SetParametersAsync(ParameterView parameters) _hasSetInitialParameters = true; UpdateQueryParameterValues(); - UpdateAllSuppliers(); + UpdateAllSubscribers(); Navigation.LocationChanged += HandleLocationChanged; } @@ -49,7 +47,7 @@ Task IComponent.SetParametersAsync(ParameterView parameters) private void HandleLocationChanged(object? sender, LocationChangedEventArgs e) { UpdateQueryParameterValues(); - UpdateAllSuppliers(); + UpdateAllSubscribers(); } private void UpdateQueryParameterValues() @@ -79,27 +77,16 @@ private void UpdateQueryParameterValues() } } - private void UpdateAllSuppliers() + private void UpdateAllSubscribers() { - foreach (var supplier in _cachedSuppliers.Values) + if (_subscribers is null) { - UpdateSupplier(supplier); + return; } - } - private void UpdateSupplier(QueryValueSupplier supplier) - { - // This is safe because we don't mutate the dictionary while the ref local is in scope. - ref var existingValues = ref CollectionsMarshal.GetValueRefOrNullRef(_queryParameterValuesByName, supplier.ValueName); - - if (!Unsafe.IsNullRef(ref existingValues)) - { - supplier.UpdateValues(ref existingValues); - } - else + foreach (var subscriber in _subscribers) { - var emptyValues = default(StringSegmentAccumulator); - supplier.UpdateValues(ref emptyValues); + subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); } } @@ -108,117 +95,51 @@ private void Render(RenderTreeBuilder builder) builder.AddContent(0, ChildContent); } - bool ICascadingValueSupplierFactory.TryGetValueSupplier(object attribute, Type parameterType, string parameterName, [NotNullWhen(true)] out ICascadingValueSupplier? result) + bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo) + => parameterInfo.Attribute is SupplyParameterFromQueryAttribute; + + bool ICascadingValueSupplier.CurrentValueIsFixed => false; + + object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo) { - result = default; + var expectedValueType = parameterInfo.PropertyType; + var isArray = expectedValueType.IsArray; + var elementType = isArray ? expectedValueType.GetElementType()! : expectedValueType; - if (attribute is not SupplyParameterFromQueryAttribute supplyFromQueryAttribute) + if (!UrlValueConstraint.TryGetByTargetType(elementType, out var parser)) { - return false; + throw new InvalidOperationException($"Querystring values cannot be parsed as type '{elementType}'."); } - var parameterSpecifiedName = supplyFromQueryAttribute.Name; - var valueName = string.IsNullOrEmpty(parameterSpecifiedName) ? parameterName : parameterSpecifiedName; - var key = new SupplierKey(parameterType, valueName.AsMemory()); + var valueName = parameterInfo.Attribute.Name ?? parameterInfo.PropertyName; + var values = _queryParameterValuesByName.GetValueOrDefault(valueName.AsMemory()); - if (!_cachedSuppliers.TryGetValue(key, out var supplier)) + if (isArray) { - supplier = QueryValueSupplier.Create(parameterType, valueName); - - // Supply an initial value if possible. - UpdateSupplier(supplier); + return parser.ParseMultiple(values, valueName); + } - _cachedSuppliers.Add(key, supplier); + if (values.Count > 0) + { + return parser.Parse(values[0].Span, valueName); } - result = supplier; - return true; + return default; } - void IDisposable.Dispose() + void ICascadingValueSupplier.Subscribe(ComponentState subscriber) { - Navigation.LocationChanged -= HandleLocationChanged; + _subscribers ??= new(); + _subscribers.Add(subscriber); } - private readonly struct SupplierKey(Type targetType, ReadOnlyMemory valueName) : IEquatable + void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber) { - private readonly Type _targetType = targetType; - private readonly ReadOnlyMemory _valueName = valueName; - - public bool Equals(SupplierKey other) - => _targetType.Equals(other._targetType) && QueryParameterNameComparer.Instance.Equals(_valueName, other._valueName); - - public override bool Equals(object? obj) - => obj is SupplierKey other && Equals(other); - - public override int GetHashCode() - => HashCode.Combine(_targetType, QueryParameterNameComparer.Instance.GetHashCode(_valueName)); + _subscribers?.Remove(subscriber); } - private sealed class QueryValueSupplier : ICascadingValueSupplier + void IDisposable.Dispose() { - private readonly UrlValueConstraint _parser; - private readonly bool _isArray; - private readonly string _valueName; - - private HashSet? _subscribers; - private object? _currentValue; - - public ReadOnlyMemory ValueName => _valueName.AsMemory(); - - public static QueryValueSupplier Create(Type targetType, string valueName) - { - var isArray = targetType.IsArray; - var elementType = isArray ? targetType.GetElementType()! : targetType; - - if (!UrlValueConstraint.TryGetByTargetType(elementType, out var parser)) - { - throw new NotSupportedException($"Querystring values cannot be parsed as type '{elementType}'."); - } - - return new(isArray, valueName, parser); - } - - private QueryValueSupplier(bool isArray, string valueName, UrlValueConstraint parser) - { - _isArray = isArray; - _valueName = valueName; - _parser = parser; - } - - public void UpdateValues(ref StringSegmentAccumulator values) - { - var oldValue = _currentValue; - _currentValue = _isArray - ? _parser.ParseMultiple(values, _valueName) - : values.Count == 0 - ? default - : _parser.Parse(values[0].Span, _valueName); - - if (_subscribers is null || !ChangeDetection.MayHaveChanged(oldValue, _currentValue)) - { - return; - } - - foreach (var subscriber in _subscribers) - { - subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); - } - } - - object? ICascadingValueSupplier.CurrentValue => _currentValue; - - bool ICascadingValueSupplier.CurrentValueIsFixed => false; - - void ICascadingValueSupplier.Subscribe(ComponentState subscriber) - { - _subscribers ??= new(); - _subscribers.Add(subscriber); - } - - void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber) - { - _subscribers?.Remove(subscriber); - } + Navigation.LocationChanged -= HandleLocationChanged; } } diff --git a/src/Components/Components/src/CascadingValue.cs b/src/Components/Components/src/CascadingValue.cs index d6d4c659eea8..b0fd1390a83e 100644 --- a/src/Components/Components/src/CascadingValue.cs +++ b/src/Components/Components/src/CascadingValue.cs @@ -1,7 +1,6 @@ // 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 Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components; @@ -9,7 +8,7 @@ namespace Microsoft.AspNetCore.Components; /// /// A component that provides a cascading value to all descendant components. /// -public class CascadingValue : ICascadingValueSupplierFactory, ICascadingValueSupplier, IComponent +public class CascadingValue : ICascadingValueSupplier, IComponent { private RenderHandle _renderHandle; private HashSet? _subscribers; // Lazily instantiated @@ -42,8 +41,6 @@ public class CascadingValue : ICascadingValueSupplierFactory, ICascading ///
[Parameter] public bool IsFixed { get; set; } - object? ICascadingValueSupplier.CurrentValue => Value; - bool ICascadingValueSupplier.CurrentValueIsFixed => IsFixed; /// @@ -131,11 +128,9 @@ public Task SetParametersAsync(ParameterView parameters) return Task.CompletedTask; } - bool ICascadingValueSupplierFactory.TryGetValueSupplier(object attribute, Type parameterType, string parameterName, [NotNullWhen(true)] out ICascadingValueSupplier? result) + bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo) { - result = default; - - if (attribute is not CascadingParameterAttribute cascadingParameterAttribute || !parameterType.IsAssignableFrom(typeof(TValue))) + if (parameterInfo.Attribute is not CascadingParameterAttribute cascadingParameterAttribute || !parameterInfo.PropertyType.IsAssignableFrom(typeof(TValue))) { return false; } @@ -143,17 +138,13 @@ bool ICascadingValueSupplierFactory.TryGetValueSupplier(object attribute, Type p // We only consider explicitly specified names, not the property name. var parameterSpecifiedName = cascadingParameterAttribute.Name; - var isMatch = - (parameterSpecifiedName == null && Name == null) || // Match on type alone + return (parameterSpecifiedName == null && Name == null) || // Match on type alone string.Equals(parameterSpecifiedName, Name, StringComparison.OrdinalIgnoreCase); // Also match on name + } - if (!isMatch) - { - return false; - } - - result = this; - return true; + object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo) + { + return Value; } void ICascadingValueSupplier.Subscribe(ComponentState subscriber) diff --git a/src/Components/Components/src/ICascadingValueSupplier.cs b/src/Components/Components/src/ICascadingValueSupplier.cs index 6899afa8035b..580a5b782866 100644 --- a/src/Components/Components/src/ICascadingValueSupplier.cs +++ b/src/Components/Components/src/ICascadingValueSupplier.cs @@ -10,7 +10,9 @@ internal interface ICascadingValueSupplier // This interface exists only so that CascadingParameterState has a way // to work with all CascadingValue types regardless of T. - object? CurrentValue { get; } + bool CanSupplyValue(in CascadingParameterInfo parameterInfo); + + object? GetCurrentValue(in CascadingParameterInfo parameterInfo); bool CurrentValueIsFixed { get; } diff --git a/src/Components/Components/src/ICascadingValueSupplierFactory.cs b/src/Components/Components/src/ICascadingValueSupplierFactory.cs deleted file mode 100644 index db34ceacb063..000000000000 --- a/src/Components/Components/src/ICascadingValueSupplierFactory.cs +++ /dev/null @@ -1,11 +0,0 @@ -// 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; - -namespace Microsoft.AspNetCore.Components; - -internal interface ICascadingValueSupplierFactory -{ - bool TryGetValueSupplier(object attribute, Type parameterType, string parameterName, [NotNullWhen(true)] out ICascadingValueSupplier? result); -} diff --git a/src/Components/Components/src/ParameterView.cs b/src/Components/Components/src/ParameterView.cs index 37ba9c2dfbcd..1b3a25d635d1 100644 --- a/src/Components/Components/src/ParameterView.cs +++ b/src/Components/Components/src/ParameterView.cs @@ -423,7 +423,8 @@ public bool MoveNext() _currentIndex = nextIndex; var state = _cascadingParameters[_currentIndex]; - _current = new ParameterValue(state.LocalValueName, state.ValueSupplier.CurrentValue!, true); + var currentValue = state.ValueSupplier.GetCurrentValue(state.ParameterInfo); + _current = new ParameterValue(state.ParameterInfo.PropertyName, currentValue!, true); return true; } else diff --git a/src/Components/Components/test/CascadingParameterStateTest.cs b/src/Components/Components/test/CascadingParameterStateTest.cs index fac6ed5c676a..27a45654991b 100644 --- a/src/Components/Components/test/CascadingParameterStateTest.cs +++ b/src/Components/Components/test/CascadingParameterStateTest.cs @@ -83,7 +83,7 @@ public void FindCascadingParameters_IfHasPartialMatchesInAncestors_ReturnsMatche // Assert Assert.Collection(result, match => { - Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam2), match.LocalValueName); + Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam2), match.ParameterInfo.PropertyName); Assert.Same(states[1].Component, match.ValueSupplier); }); } @@ -103,15 +103,15 @@ public void FindCascadingParameters_IfHasMultipleMatchesInAncestors_ReturnsMatch var result = CascadingParameterState.FindCascadingParameters(states.Last()); // Assert - Assert.Collection(result.OrderBy(x => x.LocalValueName), + Assert.Collection(result.OrderBy(x => x.ParameterInfo.PropertyName), match => { - Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam1), match.LocalValueName); + Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam1), match.ParameterInfo.PropertyName); Assert.Same(states[3].Component, match.ValueSupplier); }, match => { - Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam2), match.LocalValueName); + Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam2), match.ParameterInfo.PropertyName); Assert.Same(states[1].Component, match.ValueSupplier); }); } @@ -129,15 +129,15 @@ public void FindCascadingParameters_InheritedParameters_ReturnsMatches() var result = CascadingParameterState.FindCascadingParameters(states.Last()); // Assert - Assert.Collection(result.OrderBy(x => x.LocalValueName), + Assert.Collection(result.OrderBy(x => x.ParameterInfo.PropertyName), match => { - Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam1), match.LocalValueName); + Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam1), match.ParameterInfo.PropertyName); Assert.Same(states[0].Component, match.ValueSupplier); }, match => { - Assert.Equal(nameof(ComponentWithInheritedCascadingParams.CascadingParam3), match.LocalValueName); + Assert.Equal(nameof(ComponentWithInheritedCascadingParams.CascadingParam3), match.ParameterInfo.PropertyName); Assert.Same(states[1].Component, match.ValueSupplier); }); } @@ -156,7 +156,7 @@ public void FindCascadingParameters_ComponentRequestsBaseType_ReturnsMatches() // Assert Assert.Collection(result, match => { - Assert.Equal(nameof(ComponentWithGenericCascadingParam.LocalName), match.LocalValueName); + Assert.Equal(nameof(ComponentWithGenericCascadingParam.LocalName), match.ParameterInfo.PropertyName); Assert.Same(states[0].Component, match.ValueSupplier); }); } @@ -175,7 +175,7 @@ public void FindCascadingParameters_ComponentRequestsImplementedInterface_Return // Assert Assert.Collection(result, match => { - Assert.Equal(nameof(ComponentWithGenericCascadingParam.LocalName), match.LocalValueName); + Assert.Equal(nameof(ComponentWithGenericCascadingParam.LocalName), match.ParameterInfo.PropertyName); Assert.Same(states[0].Component, match.ValueSupplier); }); } @@ -209,7 +209,7 @@ public void FindCascadingParameters_TypeAssignmentIsValidForNullValue_ReturnsMat // Assert Assert.Collection(result, match => { - Assert.Equal(nameof(ComponentWithGenericCascadingParam.LocalName), match.LocalValueName); + Assert.Equal(nameof(ComponentWithGenericCascadingParam.LocalName), match.ParameterInfo.PropertyName); Assert.Same(states[0].Component, match.ValueSupplier); }); } @@ -303,7 +303,7 @@ public void FindCascadingParameters_MatchingNameAndType_ReturnsMatches() // Assert Assert.Collection(result, match => { - Assert.Equal(nameof(ComponentWithNamedCascadingParam.SomeLocalName), match.LocalValueName); + Assert.Equal(nameof(ComponentWithNamedCascadingParam.SomeLocalName), match.ParameterInfo.PropertyName); Assert.Same(states[0].Component, match.ValueSupplier); }); } @@ -323,15 +323,15 @@ public void FindCascadingParameters_MultipleMatchingAncestors_ReturnsClosestMatc var result = CascadingParameterState.FindCascadingParameters(states.Last()); // Assert - Assert.Collection(result.OrderBy(x => x.LocalValueName), + Assert.Collection(result.OrderBy(x => x.ParameterInfo.PropertyName), match => { - Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam1), match.LocalValueName); + Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam1), match.ParameterInfo.PropertyName); Assert.Same(states[2].Component, match.ValueSupplier); }, match => { - Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam2), match.LocalValueName); + Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam2), match.ParameterInfo.PropertyName); Assert.Same(states[3].Component, match.ValueSupplier); }); } @@ -349,12 +349,12 @@ public void FindCascadingParameters_CanOverrideNonNullValueWithNull() var result = CascadingParameterState.FindCascadingParameters(states.Last()); // Assert - Assert.Collection(result.OrderBy(x => x.LocalValueName), + Assert.Collection(result.OrderBy(x => x.ParameterInfo.PropertyName), match => { - Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam1), match.LocalValueName); + Assert.Equal(nameof(ComponentWithCascadingParams.CascadingParam1), match.ParameterInfo.PropertyName); Assert.Same(states[1].Component, match.ValueSupplier); - Assert.Null(match.ValueSupplier.CurrentValue); + Assert.Null(match.ValueSupplier.GetCurrentValue(match.ParameterInfo)); }); } @@ -552,7 +552,7 @@ public TestNavigationManager() } [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class SupplyParameterFromFormAttribute : Attribute, ICascadingParameterAttribute +public sealed class SupplyParameterFromFormAttribute : Attribute, IFormValueCascadingParameterAttribute { /// /// Gets or sets the name for the parameter. The name is used to match diff --git a/src/Components/Components/test/CascadingParameterTest.cs b/src/Components/Components/test/CascadingParameterTest.cs index d75ae9baab7a..cd6cc5328d43 100644 --- a/src/Components/Components/test/CascadingParameterTest.cs +++ b/src/Components/Components/test/CascadingParameterTest.cs @@ -507,14 +507,12 @@ class CustomCascadingParameter2Attribute : Attribute, ICascadingParameterAttribu public string Name { get; set; } } - class CustomCascadingValueProducer : AutoRenderComponent, ICascadingValueSupplierFactory, ICascadingValueSupplier + class CustomCascadingValueProducer : AutoRenderComponent, ICascadingValueSupplier { [Parameter] public object Value { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } - object ICascadingValueSupplier.CurrentValue => Value; - bool ICascadingValueSupplier.CurrentValueIsFixed => true; protected override void BuildRenderTree(RenderTreeBuilder builder) @@ -522,20 +520,23 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddContent(0, ChildContent); } - bool ICascadingValueSupplierFactory.TryGetValueSupplier(object propertyAttribute, Type valueType, string valueName, out ICascadingValueSupplier result) + bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo) { - if (propertyAttribute is not TAttribute || - valueType != typeof(object) || - valueName != nameof(Value)) + if (parameterInfo.Attribute is not TAttribute || + parameterInfo.PropertyType != typeof(object) || + parameterInfo.PropertyName != nameof(Value)) { - result = default; return false; } - result = this; return true; } + object ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo cascadingParameterState) + { + return Value; + } + void ICascadingValueSupplier.Subscribe(ComponentState subscriber) { throw new NotImplementedException(); diff --git a/src/Components/Components/test/ParameterViewTest.Assignment.cs b/src/Components/Components/test/ParameterViewTest.Assignment.cs index c7ebabf9cb1a..afb2e7dd0548 100644 --- a/src/Components/Components/test/ParameterViewTest.Assignment.cs +++ b/src/Components/Components/test/ParameterViewTest.Assignment.cs @@ -646,11 +646,10 @@ class HasNonPublicCascadingParameter class ParameterViewBuilder : IEnumerable { - private readonly List<(string Name, object Value, bool Cascading)> _keyValuePairs - = new List<(string, object, bool)>(); + private readonly List<(string Name, object Value, bool Cascading)> _parameters = new(); public void Add(string name, object value, bool cascading = false) - => _keyValuePairs.Add((name, value, cascading)); + => _parameters.Add((name, value, cascading)); public IEnumerator GetEnumerator() => throw new NotImplementedException(); @@ -660,11 +659,11 @@ public ParameterView Build() var builder = new RenderTreeBuilder(); builder.OpenComponent(0); - foreach (var kvp in _keyValuePairs) + foreach (var (name, value, cascading) in _parameters) { - if (!kvp.Cascading) + if (!cascading) { - builder.AddComponentParameter(1, kvp.Name, kvp.Value); + builder.AddComponentParameter(1, name, value); } } builder.CloseComponent(); @@ -672,11 +671,11 @@ public ParameterView Build() var view = new ParameterView(ParameterViewLifetime.Unbound, builder.GetFrames().Array, ownerIndex: 0); var cascadingParameters = new List(); - foreach (var kvp in _keyValuePairs) + foreach (var (name, value, cascading) in _parameters) { - if (kvp.Cascading) + if (cascading) { - cascadingParameters.Add(new CascadingParameterState(kvp.Name, new TestCascadingValueProvider(kvp.Value))); + cascadingParameters.Add(new CascadingParameterState(new(null, name, value.GetType()), new TestCascadingValueProvider(value))); } } @@ -686,20 +685,25 @@ public ParameterView Build() private class TestCascadingValueProvider : ICascadingValueSupplier { + private readonly object _value; + public TestCascadingValueProvider(object value) { - CurrentValue = value; + _value = value; } - public object CurrentValue { get; } - public bool CurrentValueIsFixed => throw new NotImplementedException(); - public bool CanSupplyValue(Type valueType, string valueName) + public bool CanSupplyValue(in CascadingParameterInfo parameterInfo) { throw new NotImplementedException(); } + public object GetCurrentValue(in CascadingParameterInfo parameterInfo) + { + return _value; + } + public void Subscribe(ComponentState subscriber) { throw new NotImplementedException(); diff --git a/src/Components/Components/test/ParameterViewTest.cs b/src/Components/Components/test/ParameterViewTest.cs index 67e479872f80..60c3a693cd22 100644 --- a/src/Components/Components/test/ParameterViewTest.cs +++ b/src/Components/Components/test/ParameterViewTest.cs @@ -95,8 +95,8 @@ public void EnumerationIncludesCascadingParameters() RenderTreeFrame.Attribute(1, "attribute 1", attribute1Value) }, 0).WithCascadingParameters(new List { - new CascadingParameterState("attribute 2", new TestCascadingValue(attribute2Value)), - new CascadingParameterState("attribute 3", new TestCascadingValue(attribute3Value)), + new CascadingParameterState(new(null, "attribute 2", attribute2Value.GetType()), new TestCascadingValue(attribute2Value)), + new CascadingParameterState(new(null, "attribute 3", attribute3Value.GetType()), new TestCascadingValue(attribute3Value)), }); // Assert @@ -190,7 +190,7 @@ public void CanGetValueOrDefault_WithNonExistingValue() RenderTreeFrame.Attribute(1, "some other entry", new object()) }, 0).WithCascadingParameters(new List { - new CascadingParameterState("another entry", new TestCascadingValue(null)) + new CascadingParameterState(new(null, "another entry", typeof(object)), new TestCascadingValue(null)) }); // Act @@ -305,9 +305,9 @@ public void CanGetValueOrDefault_WithMatchingCascadingParameter() RenderTreeFrame.Attribute(1, "unrelated value", new object()) }, 0).WithCascadingParameters(new List { - new CascadingParameterState("unrelated value 2", new TestCascadingValue(null)), - new CascadingParameterState("my entry", new TestCascadingValue(myEntryValue)), - new CascadingParameterState("unrelated value 3", new TestCascadingValue(null)), + new CascadingParameterState(new(null, "unrelated value 2", typeof(object)), new TestCascadingValue(null)), + new CascadingParameterState(new(null, "my entry", myEntryValue.GetType()), new TestCascadingValue(myEntryValue)), + new CascadingParameterState(new(null, "unrelated value 3", typeof(object)), new TestCascadingValue(null)), }); // Act @@ -597,18 +597,21 @@ public Task SetParametersAsync(ParameterView parameters) private class TestCascadingValue : ICascadingValueSupplier { + private readonly object _value; + public TestCascadingValue(object value) { - CurrentValue = value; + _value = value; } - public object CurrentValue { get; } - public bool CurrentValueIsFixed => false; - public bool CanSupplyValue(Type valueType, string valueName) + public bool CanSupplyValue(in CascadingParameterInfo parameterInfo) => throw new NotImplementedException(); + public object GetCurrentValue(in CascadingParameterInfo parameterInfo) + => _value; + public void Subscribe(ComponentState subscriber) => throw new NotImplementedException(); From 1868b540859ef3c5c89c13be26ca80d8ba9e3548 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 5 Jun 2023 16:51:57 -0700 Subject: [PATCH 06/13] Some cleanup --- .../src/Binding/CascadingModelBinder.cs | 10 +-- .../Components/src/CascadingParameterState.cs | 3 +- .../src/CascadingQueryValueProvider.cs | 73 +++++-------------- .../Routing/QueryParameterValueSupplier.cs | 54 ++++++++++++++ 4 files changed, 78 insertions(+), 62 deletions(-) create mode 100644 src/Components/Components/src/Routing/QueryParameterValueSupplier.cs diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index 0473e06afe53..e9d19813b528 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -151,7 +151,7 @@ bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterI return false; } - var parameterType = parameterInfo.PropertyType; + var valueType = parameterInfo.PropertyType; var valueName = formCascadingParameterAttribute.Name; var formName = string.IsNullOrEmpty(valueName) ? (_bindingContext?.Name) : @@ -159,7 +159,7 @@ bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterI if (_bindingInfo != null && string.Equals(_bindingInfo.FormName, formName, StringComparison.Ordinal) && - _bindingInfo.ValueType.Equals(parameterType)) + _bindingInfo.ValueType.Equals(valueType)) { // 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, @@ -168,10 +168,10 @@ bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterI } // Can't supply the value if this context is for a form with a different name. - if (FormValueSupplier.CanBind(formName!, parameterType)) + if (FormValueSupplier.CanBind(formName!, valueType)) { - var bindingSucceeded = FormValueSupplier.TryBind(formName!, parameterType, out var boundValue); - _bindingInfo = new BindingInfo(formName, parameterType, bindingSucceeded, boundValue); + var bindingSucceeded = FormValueSupplier.TryBind(formName!, valueType, out var boundValue); + _bindingInfo = new BindingInfo(formName, valueType, bindingSucceeded, boundValue); if (!bindingSucceeded) { // Report errors diff --git a/src/Components/Components/src/CascadingParameterState.cs b/src/Components/Components/src/CascadingParameterState.cs index 71e9fdcd6cbe..c1e346c08673 100644 --- a/src/Components/Components/src/CascadingParameterState.cs +++ b/src/Components/Components/src/CascadingParameterState.cs @@ -60,8 +60,7 @@ public static IReadOnlyList FindCascadingParameters(Com var candidate = componentState; do { - var candidateComponent = candidate.Component; - if (candidateComponent is ICascadingValueSupplier valueSupplier && valueSupplier.CanSupplyValue(info)) + if (candidate.Component is ICascadingValueSupplier valueSupplier && valueSupplier.CanSupplyValue(info)) { return valueSupplier; } diff --git a/src/Components/Components/src/CascadingQueryValueProvider.cs b/src/Components/Components/src/CascadingQueryValueProvider.cs index 73adfa24393a..62f0d0f0539d 100644 --- a/src/Components/Components/src/CascadingQueryValueProvider.cs +++ b/src/Components/Components/src/CascadingQueryValueProvider.cs @@ -1,16 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.InteropServices; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Routing; -using Microsoft.AspNetCore.Internal; namespace Microsoft.AspNetCore.Components; internal sealed class CascadingQueryValueProvider : IComponent, ICascadingValueSupplier, IDisposable { - private readonly Dictionary, StringSegmentAccumulator> _queryParameterValuesByName = new(QueryParameterNameComparer.Instance); + private readonly QueryParameterValueSupplier _queryParameterValueSupplier = new(); private HashSet? _subscribers; private RenderHandle _handle; @@ -32,7 +30,6 @@ Task IComponent.SetParametersAsync(ParameterView parameters) _hasSetInitialParameters = true; UpdateQueryParameterValues(); - UpdateAllSubscribers(); Navigation.LocationChanged += HandleLocationChanged; } @@ -45,48 +42,35 @@ Task IComponent.SetParametersAsync(ParameterView parameters) } private void HandleLocationChanged(object? sender, LocationChangedEventArgs e) - { - UpdateQueryParameterValues(); - UpdateAllSubscribers(); - } + => UpdateQueryParameterValues(); private void UpdateQueryParameterValues() { - _queryParameterValuesByName.Clear(); + var query = GetQueryString(Navigation.Uri); - var url = Navigation.Uri; - var queryStartPos = url.IndexOf('?'); + _queryParameterValueSupplier.ReadParametersFromQuery(query); - if (queryStartPos < 0) + if (_subscribers is null) { return; } - var queryEndPos = url.IndexOf('#', queryStartPos); - var query = url.AsMemory(queryStartPos..(queryEndPos < 0 ? url.Length : queryEndPos)); - var queryStringEnumerable = new QueryStringEnumerable(query); - - foreach (var suppliedPair in queryStringEnumerable) + foreach (var subscriber in _subscribers) { - var decodedName = suppliedPair.DecodeName(); - var decodedValue = suppliedPair.DecodeValue(); - - // This is safe because we don't mutate the dictionary while the ref local is in scope. - ref var values = ref CollectionsMarshal.GetValueRefOrAddDefault(_queryParameterValuesByName, decodedName, out _); - values.Add(decodedValue); + subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); } - } - private void UpdateAllSubscribers() - { - if (_subscribers is null) + static ReadOnlyMemory GetQueryString(string url) { - return; - } + var queryStartPos = url.IndexOf('?'); - foreach (var subscriber in _subscribers) - { - subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); + if (queryStartPos < 0) + { + return default; + } + + var queryEndPos = url.IndexOf('#', queryStartPos); + return url.AsMemory(queryStartPos..(queryEndPos < 0 ? url.Length : queryEndPos)); } } @@ -102,29 +86,8 @@ bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterI object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo) { - var expectedValueType = parameterInfo.PropertyType; - var isArray = expectedValueType.IsArray; - var elementType = isArray ? expectedValueType.GetElementType()! : expectedValueType; - - if (!UrlValueConstraint.TryGetByTargetType(elementType, out var parser)) - { - throw new InvalidOperationException($"Querystring values cannot be parsed as type '{elementType}'."); - } - - var valueName = parameterInfo.Attribute.Name ?? parameterInfo.PropertyName; - var values = _queryParameterValuesByName.GetValueOrDefault(valueName.AsMemory()); - - if (isArray) - { - return parser.ParseMultiple(values, valueName); - } - - if (values.Count > 0) - { - return parser.Parse(values[0].Span, valueName); - } - - return default; + var queryParameterName = parameterInfo.Attribute.Name ?? parameterInfo.PropertyName; + return _queryParameterValueSupplier.GetQueryParameterValue(parameterInfo.PropertyType, queryParameterName); } void ICascadingValueSupplier.Subscribe(ComponentState subscriber) diff --git a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs b/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs new file mode 100644 index 000000000000..d5e75c9003f6 --- /dev/null +++ b/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Internal; + +namespace Microsoft.AspNetCore.Components.Routing; + +internal sealed class QueryParameterValueSupplier +{ + private readonly Dictionary, StringSegmentAccumulator> _queryParameterValuesByName = new(QueryParameterNameComparer.Instance); + + public void ReadParametersFromQuery(ReadOnlyMemory query) + { + _queryParameterValuesByName.Clear(); + + var queryStringEnumerable = new QueryStringEnumerable(query); + + foreach (var suppliedPair in queryStringEnumerable) + { + var decodedName = suppliedPair.DecodeName(); + var decodedValue = suppliedPair.DecodeValue(); + + // This is safe because we don't mutate the dictionary while the ref local is in scope. + ref var values = ref CollectionsMarshal.GetValueRefOrAddDefault(_queryParameterValuesByName, decodedName, out _); + values.Add(decodedValue); + } + } + + public object? GetQueryParameterValue(Type targetType, string queryParameterName) + { + var isArray = targetType.IsArray; + var elementType = isArray ? targetType.GetElementType()! : targetType; + + if (!UrlValueConstraint.TryGetByTargetType(elementType, out var parser)) + { + throw new InvalidOperationException($"Querystring values cannot be parsed as type '{elementType}'."); + } + + var values = _queryParameterValuesByName.GetValueOrDefault(queryParameterName.AsMemory()); + + if (isArray) + { + return parser.ParseMultiple(values, queryParameterName); + } + + if (values.Count > 0) + { + return parser.Parse(values[0].Span, queryParameterName); + } + + return default; + } +} From 09bae24ae8d2c142d36f59422e67a000c6b37255 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 6 Jun 2023 15:54:59 -0700 Subject: [PATCH 07/13] Create QueryParameterValueSupplierTest.cs --- .../QueryParameterValueSupplierTest.cs | 352 ++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 src/Components/Components/test/Routing/QueryParameterValueSupplierTest.cs diff --git a/src/Components/Components/test/Routing/QueryParameterValueSupplierTest.cs b/src/Components/Components/test/Routing/QueryParameterValueSupplierTest.cs new file mode 100644 index 000000000000..631b35b76ddd --- /dev/null +++ b/src/Components/Components/test/Routing/QueryParameterValueSupplierTest.cs @@ -0,0 +1,352 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Routing; + +public class QueryParameterValueSupplierTest +{ + private readonly QueryParameterValueSupplier _supplier = new(); + + [Fact] + public void SupportsExpectedValueTypes() + { + var query = + $"BoolVal=true&" + + $"DateTimeVal=2020-01-02+03:04:05.678-09:00&" + + $"DecimalVal=-1.234&" + + $"DoubleVal=-2.345&" + + $"FloatVal=-3.456&" + + $"GuidVal=9e7257ad-03aa-42c7-9819-be08b177fef9&" + + $"IntVal=-54321&" + + $"LongVal=-99987654321&" + + $"StringVal=Some+string+%26+more&" + + $"NullableBoolVal=true&" + + $"NullableDateTimeVal=2021-01-02+03:04:05.678Z&" + + $"NullableDecimalVal=1.234&" + + $"NullableDoubleVal=2.345&" + + $"NullableFloatVal=3.456&" + + $"NullableGuidVal=1e7257ad-03aa-42c7-9819-be08b177fef9&" + + $"NullableIntVal=54321&" + + $"NullableLongVal=99987654321&"; + + ReadQuery(query); + + AssertKeyValuePair("BoolVal", true); + AssertKeyValuePair("DateTimeVal", new DateTimeOffset(2020, 1, 2, 3, 4, 5, 678, TimeSpan.FromHours(-9)).LocalDateTime); + AssertKeyValuePair("DecimalVal", -1.234m); + AssertKeyValuePair("DoubleVal", -2.345); + AssertKeyValuePair("FloatVal", -3.456f); + AssertKeyValuePair("GuidVal", new Guid("9e7257ad-03aa-42c7-9819-be08b177fef9")); + AssertKeyValuePair("IntVal", -54321); + AssertKeyValuePair("LongVal", -99987654321); + AssertKeyValuePair("NullableBoolVal", true); + AssertKeyValuePair("NullableDateTimeVal", new DateTime(2021, 1, 2, 3, 4, 5, 678, DateTimeKind.Utc).ToLocalTime()); + AssertKeyValuePair("NullableDecimalVal", 1.234m); + AssertKeyValuePair("NullableDoubleVal", 2.345); + AssertKeyValuePair("NullableFloatVal", 3.456f); + AssertKeyValuePair("NullableGuidVal", new Guid("1e7257ad-03aa-42c7-9819-be08b177fef9")); + AssertKeyValuePair("NullableIntVal", 54321); + AssertKeyValuePair("NullableLongVal", 99987654321); + AssertKeyValuePair("StringVal", "Some string & more"); + } + + [Theory] + [InlineData("")] + [InlineData("?")] + [InlineData("?unrelated=123")] + public void SuppliesNullForValueTypesIfNotSpecified(string query) + { + ReadQuery(query); + + // Although we could supply default(T) for missing values, there's precedent in the routing + // system for supplying null for missing route parameters. The component is then responsible + // for interpreting null as a blank value for the parameter, regardless of its type. To keep + // the rules aligned, we do the same thing for querystring parameters. + AssertKeyValuePair("BoolVal", null); + AssertKeyValuePair("DateTimeVal", null); + AssertKeyValuePair("DecimalVal", null); + AssertKeyValuePair("DoubleVal", null); + AssertKeyValuePair("FloatVal", null); + AssertKeyValuePair("GuidVal", null); + AssertKeyValuePair("IntVal", null); + AssertKeyValuePair("LongVal", null); + AssertKeyValuePair("NullableBoolVal", null); + AssertKeyValuePair("NullableDateTimeVal", null); + AssertKeyValuePair("NullableDecimalVal", null); + AssertKeyValuePair("NullableDoubleVal", null); + AssertKeyValuePair("NullableFloatVal", null); + AssertKeyValuePair("NullableGuidVal", null); + AssertKeyValuePair("NullableIntVal", null); + AssertKeyValuePair("NullableLongVal", null); + AssertKeyValuePair("StringVal", null); + } + + [Fact] + public void SupportsExpectedArrayTypes() + { + var query = + $"BoolVals=true&" + + $"DateTimeVals=2020-01-02+03:04:05.678Z&" + + $"DecimalVals=-1.234&" + + $"DoubleVals=-2.345&" + + $"FloatVals=-3.456&" + + $"GuidVals=9e7257ad-03aa-42c7-9819-be08b177fef9&" + + $"IntVals=-54321&" + + $"LongVals=-99987654321&" + + $"StringVals=Some+string+%26+more&" + + $"NullableBoolVals=true&" + + $"NullableDateTimeVals=2021-01-02+03:04:05.678Z&" + + $"NullableDecimalVals=1.234&" + + $"NullableDoubleVals=2.345&" + + $"NullableFloatVals=3.456&" + + $"NullableGuidVals=1e7257ad-03aa-42c7-9819-be08b177fef9&" + + $"NullableIntVals=54321&" + + $"NullableLongVals=99987654321&"; + + ReadQuery(query); + + AssertKeyValuePair("BoolVals", new[] { true }); + AssertKeyValuePair("DateTimeVals", new[] { new DateTime(2020, 1, 2, 3, 4, 5, 678, DateTimeKind.Utc).ToLocalTime() }); + AssertKeyValuePair("DecimalVals", new[] { -1.234m }); + AssertKeyValuePair("DoubleVals", new[] { -2.345 }); + AssertKeyValuePair("FloatVals", new[] { -3.456f }); + AssertKeyValuePair("GuidVals", new[] { new Guid("9e7257ad-03aa-42c7-9819-be08b177fef9") }); + AssertKeyValuePair("IntVals", new[] { -54321 }); + AssertKeyValuePair("LongVals", new[] { -99987654321 }); + AssertKeyValuePair("NullableBoolVals", new[] { true }); + AssertKeyValuePair("NullableDateTimeVals", new[] { new DateTime(2021, 1, 2, 3, 4, 5, 678, DateTimeKind.Utc).ToLocalTime() }); + AssertKeyValuePair("NullableDecimalVals", new[] { 1.234m }); + AssertKeyValuePair("NullableDoubleVals", new[] { 2.345 }); + AssertKeyValuePair("NullableFloatVals", new[] { 3.456f }); + AssertKeyValuePair("NullableGuidVals", new[] { new Guid("1e7257ad-03aa-42c7-9819-be08b177fef9") }); + AssertKeyValuePair("NullableIntVals", new[] { 54321 }); + AssertKeyValuePair("NullableLongVals", new[] { 99987654321 }); + AssertKeyValuePair("StringVals", new[] { "Some string & more" }); + } + + [Theory] + [InlineData("")] + [InlineData("?")] + [InlineData("?unrelated=123")] + public void SuppliesEmptyArrayForArrayTypesIfNotSpecified(string query) + { + ReadQuery(query); + + AssertKeyValuePair("BoolVals", Array.Empty()); + AssertKeyValuePair("DateTimeVals", Array.Empty()); + AssertKeyValuePair("DecimalVals", Array.Empty()); + AssertKeyValuePair("DoubleVals", Array.Empty()); + AssertKeyValuePair("FloatVals", Array.Empty()); + AssertKeyValuePair("GuidVals", Array.Empty()); + AssertKeyValuePair("IntVals", Array.Empty()); + AssertKeyValuePair("LongVals", Array.Empty()); + AssertKeyValuePair("NullableBoolVals", Array.Empty()); + AssertKeyValuePair("NullableDateTimeVals", Array.Empty()); + AssertKeyValuePair("NullableDecimalVals", Array.Empty()); + AssertKeyValuePair("NullableDoubleVals", Array.Empty()); + AssertKeyValuePair("NullableFloatVals", Array.Empty()); + AssertKeyValuePair("NullableGuidVals", Array.Empty()); + AssertKeyValuePair("NullableIntVals", Array.Empty()); + AssertKeyValuePair("NullableLongVals", Array.Empty()); + AssertKeyValuePair("StringVals", Array.Empty()); + } + + [Theory] + [InlineData("BoolVal", "abc", typeof(bool))] + [InlineData("DateTimeVal", "2020-02-31", typeof(DateTime))] + [InlineData("DecimalVal", "1.2.3", typeof(decimal))] + [InlineData("DoubleVal", "1x", typeof(double))] + [InlineData("FloatVal", "1e1000", typeof(float))] + [InlineData("GuidVal", "123456-789-0", typeof(Guid))] + [InlineData("IntVal", "5000000000", typeof(int))] + [InlineData("LongVal", "this+is+a+long+value", typeof(long))] + [InlineData("NullableBoolVal", "abc", typeof(bool?))] + [InlineData("NullableDateTimeVal", "2020-02-31", typeof(DateTime?))] + [InlineData("NullableDecimalVal", "1.2.3", typeof(decimal?))] + [InlineData("NullableDoubleVal", "1x", typeof(double?))] + [InlineData("NullableFloatVal", "1e1000", typeof(float?))] + [InlineData("NullableGuidVal", "123456-789-0", typeof(Guid?))] + [InlineData("NullableIntVal", "5000000000", typeof(int?))] + [InlineData("NullableLongVal", "this+is+a+long+value", typeof(long?))] + public void RejectsUnparseableValues(string key, string value, Type targetType) + { + ReadQuery($"?{key}={value}"); + + var ex = Assert.Throws(() => _supplier.GetQueryParameterValue(targetType, key)); + Assert.Equal($"Cannot parse the value '{value.Replace('+', ' ')}' as type '{targetType}' for '{key}'.", ex.Message); + } + + [Theory] + [InlineData("BoolVals", "true", "abc", typeof(bool))] + [InlineData("DateTimeVals", "2020-02-28", "2020-02-31", typeof(DateTime))] + [InlineData("DecimalVals", "1.23", "1.2.3", typeof(decimal))] + [InlineData("DoubleVals", "1", "1x", typeof(double))] + [InlineData("FloatVals", "1000", "1e1000", typeof(float))] + [InlineData("GuidVals", "9e7257ad-03aa-42c7-9819-be08b177fef9", "123456-789-0", typeof(Guid))] + [InlineData("IntVals", "5000000", "5000000000", typeof(int))] + [InlineData("LongVals", "-1234", "this+is+a+long+value", typeof(long))] + [InlineData("NullableBoolVals", "true", "abc", typeof(bool?))] + [InlineData("NullableDateTimeVals", "2020-02-28", "2020-02-31", typeof(DateTime?))] + [InlineData("NullableDecimalVals", "1.23", "1.2.3", typeof(decimal?))] + [InlineData("NullableDoubleVals", "1", "1x", typeof(double?))] + [InlineData("NullableFloatVals", "1000", "1e1000", typeof(float?))] + [InlineData("NullableGuidVals", "9e7257ad-03aa-42c7-9819-be08b177fef9", "123456-789-0", typeof(Guid?))] + [InlineData("NullableIntVals", "5000000", "5000000000", typeof(int?))] + [InlineData("NullableLongVals", "-1234", "this+is+a+long+value", typeof(long?))] + public void RejectsUnparseableArrayEntries(string key, string validValue, string invalidValue, Type targetType) + { + ReadQuery($"?{key}={validValue}&{key}={invalidValue}"); + + var ex = Assert.Throws(() => _supplier.GetQueryParameterValue(targetType.MakeArrayType(), key)); + Assert.Equal($"Cannot parse the value '{invalidValue.Replace('+', ' ')}' as type '{targetType}' for '{key}'.", ex.Message); + } + + [Theory] + [InlineData("BoolVal", typeof(bool))] + [InlineData("DateTimeVal", typeof(DateTime))] + [InlineData("DecimalVal", typeof(decimal))] + [InlineData("DoubleVal", typeof(double))] + [InlineData("FloatVal", typeof(float))] + [InlineData("GuidVal", typeof(Guid))] + [InlineData("IntVal", typeof(int))] + [InlineData("LongVal", typeof(long))] + public void RejectsBlankValuesWhenNotNullable(string key, Type targetType) + { + ReadQuery($"?StringVal=somevalue&{key}="); + + var ex = Assert.Throws(() => _supplier.GetQueryParameterValue(targetType, key)); + Assert.Equal($"Cannot parse the value '' as type '{targetType}' for '{key}'.", ex.Message); + } + + [Fact] + public void AcceptsBlankValuesWhenNullable() + { + var query = + $"NullableBoolVal=&" + + $"NullableDateTimeVal=&" + + $"NullableDecimalVal=&" + + $"NullableDoubleVal=&" + + $"NullableFloatVal=&" + + $"NullableGuidVal=&" + + $"NullableIntVal=&" + + $"NullableLongVal=&"; + + ReadQuery(query); + + AssertKeyValuePair("NullableBoolVal", null); + AssertKeyValuePair("NullableDateTimeVal", null); + AssertKeyValuePair("NullableDecimalVal", null); + AssertKeyValuePair("NullableDoubleVal", null); + AssertKeyValuePair("NullableFloatVal", null); + AssertKeyValuePair("NullableGuidVal", null); + AssertKeyValuePair("NullableIntVal", null); + AssertKeyValuePair("NullableLongVal", null); + } + + [Theory] + [InlineData("")] + [InlineData("=")] + public void EmptyStringValuesAreSuppliedAsEmptyString(string queryPart) + { + ReadQuery($"?StringVal{queryPart}"); + + Assert.Equal(string.Empty, _supplier.GetQueryParameterValue(typeof(string), "StringVal")); + } + + [Fact] + public void EmptyStringArrayValuesAreSuppliedAsEmptyStrings() + { + var query = $"?StringVals=a&" + + $"StringVals&" + + $"StringVals=&" + + $"StringVals=b"; + + ReadQuery(query); + + Assert.Equal(new[] { "a", string.Empty, string.Empty, "b" }, _supplier.GetQueryParameterValue(typeof(string[]), "StringVals")); + } + + [Theory] + [InlineData("BoolVals", typeof(bool))] + [InlineData("DateTimeVals", typeof(DateTime))] + [InlineData("DecimalVals", typeof(decimal))] + [InlineData("DoubleVals", typeof(double))] + [InlineData("FloatVals", typeof(float))] + [InlineData("GuidVals", typeof(Guid))] + [InlineData("IntVals", typeof(int))] + [InlineData("LongVals", typeof(long))] + public void RejectsBlankArrayEntriesWhenNotNullable(string key, Type targetType) + { + ReadQuery($"?StringVal=somevalue&{key}="); + + var ex = Assert.Throws( + () => _supplier.GetQueryParameterValue(targetType, key)); + Assert.Equal($"Cannot parse the value '' as type '{targetType}' for '{key}'.", ex.Message); + } + + [Fact] + public void AcceptsBlankArrayEntriesWhenNullable() + { + var query = + $"NullableBoolVals=&" + + $"NullableDateTimeVals=&" + + $"NullableDecimalVals=&" + + $"NullableDoubleVals=&" + + $"NullableFloatVals=&" + + $"NullableGuidVals=&" + + $"NullableIntVals=&" + + $"NullableLongVals=&"; + + ReadQuery(query); + + AssertKeyValuePair("NullableBoolVals", new bool?[] { null }); + AssertKeyValuePair("NullableDateTimeVals", new DateTime?[] { null }); + AssertKeyValuePair("NullableDecimalVals", new decimal?[] { null }); + AssertKeyValuePair("NullableDoubleVals", new double?[] { null }); + AssertKeyValuePair("NullableFloatVals", new float?[] { null }); + AssertKeyValuePair("NullableGuidVals", new Guid?[] { null }); + AssertKeyValuePair("NullableIntVals", new int?[] { null }); + AssertKeyValuePair("NullableLongVals", new long?[] { null }); + } + + [Fact] + public void DecodesKeysAndValues() + { + var nameThatLooksEncoded = "name+that+looks+%5Bencoded%5D"; + var encodedName = Uri.EscapeDataString(nameThatLooksEncoded); + var query = $"?{encodedName}=Some+%5Bencoded%5D+value"; + + ReadQuery(query); + + AssertKeyValuePair(nameThatLooksEncoded, "Some [encoded] value"); + } + + [Fact] + public void MatchesKeysCaseInsensitively() + { + ReadQuery($"?KEYONE=1&KEYTWO=2"); + + AssertKeyValuePair("KeyOne", 1); + AssertKeyValuePair("KeyTwo", 2); + } + + [Fact] + public void MatchesKeysWithNonAsciiChars() + { + ReadQuery($"?Имя_моей_собственности=first&خاصية_أخرى=second"); + + AssertKeyValuePair("خاصية_أخرى", "second"); + AssertKeyValuePair("Имя_моей_собственности", "first"); + } + + private void ReadQuery(string query) + { + _supplier.ReadParametersFromQuery(query.AsMemory()); + } + + private void AssertKeyValuePair(string key, object expectedValue) + { + var actualValue = _supplier.GetQueryParameterValue(typeof(T), key); + Assert.Equal(expectedValue, actualValue); + } +} From 05d5bcb379eb49c72cabcc32a8c341de5438c784 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 12 Jun 2023 17:18:22 -0700 Subject: [PATCH 08/13] More PR feedback --- .../src/Binding/CascadingModelBinder.cs | 133 ++++++++++-------- .../Binding/CascadingModelBindingProvider.cs | 77 ++++++++++ .../CascadingQueryModelBindingProvider.cs | 93 ++++++++++++ .../IFormValueCascadingParameterAttribute.cs | 8 -- .../src/Binding/IFormValueSupplier.cs | 2 +- .../src/CascadingParameterAttribute.cs | 4 +- .../src/CascadingParameterAttributeBase.cs | 16 +++ .../Components/src/CascadingParameterInfo.cs | 29 +++- .../Components/src/CascadingParameterState.cs | 2 +- .../src/CascadingQueryValueProvider.cs | 108 -------------- .../Components/src/CascadingValue.cs | 12 +- .../src/ICascadingParameterAttribute.cs | 12 -- .../Components/src/ICascadingValueSupplier.cs | 6 +- .../Components/src/PublicAPI.Shipped.txt | 4 - .../Components/src/PublicAPI.Unshipped.txt | 25 ++++ .../src/Reflection/ComponentProperties.cs | 6 +- .../src/Rendering/ComponentState.cs | 6 +- src/Components/Components/src/RouteView.cs | 15 +- .../Routing/QueryParameterValueSupplier.cs | 6 + .../src/SupplyParameterFromQueryAttribute.cs | 4 +- .../test/CascadingModelBinderTest.cs | 1 + .../test/CascadingParameterStateTest.cs | 64 ++++----- .../Components/test/CascadingParameterTest.cs | 12 +- .../test/ParameterViewTest.Assignment.cs | 4 +- .../Components/test/ParameterViewTest.cs | 4 +- .../Components/test/RouteViewTest.cs | 1 + ...orComponentsServiceCollectionExtensions.cs | 2 + .../CascadingFormModelBindingProvider.cs | 64 +++++++++ .../Web/src/PublicAPI.Unshipped.txt | 6 +- .../src/SupplyParameterFromFormAttribute.cs | 6 +- src/Components/Web/test/Forms/EditFormTest.cs | 2 + .../src/Hosting/WebAssemblyHostBuilder.cs | 2 + ...nentsWebViewServiceCollectionExtensions.cs | 2 + 33 files changed, 457 insertions(+), 281 deletions(-) create mode 100644 src/Components/Components/src/Binding/CascadingModelBindingProvider.cs create mode 100644 src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs delete mode 100644 src/Components/Components/src/Binding/IFormValueCascadingParameterAttribute.cs create mode 100644 src/Components/Components/src/CascadingParameterAttributeBase.cs delete mode 100644 src/Components/Components/src/CascadingQueryValueProvider.cs delete mode 100644 src/Components/Components/src/ICascadingParameterAttribute.cs create mode 100644 src/Components/Web/src/Binding/CascadingFormModelBindingProvider.cs diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index 7c49c129eb6e..ab767d4d946f 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -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; @@ -13,10 +15,11 @@ namespace Microsoft.AspNetCore.Components; /// public sealed class CascadingModelBinder : IComponent, ICascadingValueSupplier, IDisposable { + private readonly Dictionary _providersByCascadingParameterAttributeType = new(); + private RenderHandle _handle; private ModelBindingContext? _bindingContext; private bool _hasPendingQueuedRender; - private BindingInfo? _bindingInfo; /// /// The binding context name. @@ -40,7 +43,7 @@ public sealed class CascadingModelBinder : IComponent, ICascadingValueSupplier, [Inject] internal NavigationManager Navigation { get; set; } = null!; - [Inject] internal IFormValueSupplier FormValueSupplier { get; set; } = null!; + [Inject] internal IEnumerable ModelBindingProviders { get; set; } = Enumerable.Empty(); internal ModelBindingContext? BindingContext => _bindingContext; @@ -112,24 +115,30 @@ internal void UpdateBindingInformation(string url) // BindingContextId = <>((<>&)|?)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 = new ModelBindingContext(name, bindingId, CanBind); } - _bindingContext = bindingContext; + foreach (var provider in ModelBindingProviders) + { + provider?.OnBindingContextUpdated(_bindingContext); + } string GenerateBindingContextId(string name) { @@ -137,67 +146,77 @@ string GenerateBindingContextId(string name) var hashIndex = bindingId.IndexOf('#'); return hashIndex == -1 ? bindingId : new string(bindingId.AsSpan(0, hashIndex)); } - } - void IDisposable.Dispose() - { - Navigation.LocationChanged -= HandleLocationChanged; + bool CanBind(Type type) + => ModelBindingProviders.Any(provider => provider.SupportsParameterType(type)); } bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo) + => TryGetProvider(in parameterInfo, out var provider) + && provider.CanSupplyValue(_bindingContext, parameterInfo); + + void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) { - if (parameterInfo.Attribute is not IFormValueCascadingParameterAttribute formCascadingParameterAttribute) + if (!TryGetProvider(in parameterInfo, out var provider)) { - return false; + // This should never happen because this method only gets called after CanSupplyValue returns true. + // But we want to know if this ever does happen somehow. + throw new InvalidOperationException( + $"Cannot subscribe to changes in values that '{nameof(CascadingModelBinder)}' cannot supply."); } - var valueType = parameterInfo.PropertyType; - var valueName = formCascadingParameterAttribute.Name; - var formName = string.IsNullOrEmpty(valueName) ? - (_bindingContext?.Name) : - ModelBindingContext.Combine(_bindingContext, valueName); - - 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.Subscribe(subscriber); } + } - // Can't supply the value if this context is for a form with a different name. - if (FormValueSupplier.CanBind(formName!, valueType)) + void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber) + { + foreach (var provider in ModelBindingProviders) { - var bindingSucceeded = FormValueSupplier.TryBind(formName!, valueType, out var boundValue); - _bindingInfo = new BindingInfo(formName, valueType, bindingSucceeded, boundValue); - if (!bindingSucceeded) + if (!provider.AreValuesFixed) { - // Report errors + provider.Unsubscribe(subscriber); } - - return true; } - - return false; } - void ICascadingValueSupplier.Subscribe(ComponentState subscriber) - { - throw new InvalidOperationException("Form values are always fixed."); - } + object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo) + => TryGetProvider(in parameterInfo, out var provider) + ? provider.GetCurrentValue(_bindingContext, parameterInfo) + : null; - void ICascadingValueSupplier.Unsubscribe(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(); - object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo) => _bindingInfo == null ? - throw new InvalidOperationException("Tried to access form value before it was bound.") : - _bindingInfo.BoundValue; + if (_providersByCascadingParameterAttributeType.TryGetValue(attributeType, out result)) + { + return result is not null; + } - bool ICascadingValueSupplier.CurrentValueIsFixed => true; + // 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; - private record BindingInfo(string? FormName, Type ValueType, bool BindingResult, object? BoundValue); + CascadingModelBindingProvider? FindProviderForAttributeType(Type attributeType) + { + foreach (var provider in ModelBindingProviders) + { + if (provider.SupportsCascadingParameterAttributeType(attributeType)) + { + return provider; + } + } + + return null; + } + } + + void IDisposable.Dispose() + { + Navigation.LocationChanged -= HandleLocationChanged; + } } diff --git a/src/Components/Components/src/Binding/CascadingModelBindingProvider.cs b/src/Components/Components/src/Binding/CascadingModelBindingProvider.cs new file mode 100644 index 000000000000..641b738584f8 --- /dev/null +++ b/src/Components/Components/src/Binding/CascadingModelBindingProvider.cs @@ -0,0 +1,77 @@ +// 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; + +/// +/// Provides values that get supplied to cascading parameters with . +/// +public abstract class CascadingModelBindingProvider +{ + /// + /// Gets whether values supplied by this instance will not change. + /// + protected internal abstract bool AreValuesFixed { get; } + + /// + /// Determines whether this instance can provide values for parameters annotated with the specified attribute type. + /// + /// The attribute type. + /// true if this instance can provide values for parameters annotated with the specified attribute type, otherwise false. + protected internal abstract bool SupportsCascadingParameterAttributeType(Type attributeType); + + /// + /// Determines whether this instance can provide values to parameters with the specified type. + /// + /// The parameter type. + /// true if this instance can provide values to parameters with the specified type, otherwise false. + protected internal abstract bool SupportsParameterType(Type parameterType); + + /// + /// Determines whether this instance can supply a value for the specified parameter. + /// + /// The current . + /// The for the component parameter. + /// true if a value can be supplied, otherwise false. + protected internal abstract bool CanSupplyValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo); + + /// + /// Gets the value for the specified parameter. + /// + /// The current . + /// The for the component parameter. + /// The value to supply to the parameter. + protected internal abstract object? GetCurrentValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo); + + /// + /// Invoked when the current gets updated. + /// + /// The updated . + protected internal virtual void OnBindingContextUpdated(ModelBindingContext? bindingContext) + { + } + + /// + /// Subscribes to changes in supplied values, if they can change. + /// + /// + /// This method must be implemented if is false. + /// + /// The for the subscribing component. + 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)}'."); + + /// + /// Unsubscribes from changes in supplied values, if they can change. + /// + /// + /// This method must be implemented if is false. + /// + /// The for the unsubscribing component. + 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)}'."); +} diff --git a/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs b/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs new file mode 100644 index 000000000000..448c43c151d4 --- /dev/null +++ b/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs @@ -0,0 +1,93 @@ +// 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; +using Microsoft.AspNetCore.Components.Routing; + +namespace Microsoft.AspNetCore.Components.Binding; + +/// +/// Enables component parameters to be supplied from the query string with . +/// +public sealed class CascadingQueryModelBindingProvider : CascadingModelBindingProvider +{ + private readonly QueryParameterValueSupplier _queryParameterValueSupplier = new(); + private readonly NavigationManager _navigationManager; + + private HashSet? _subscribers; + + /// + protected internal override bool AreValuesFixed => false; + + /// + /// Constructs a new instance of . + /// + public CascadingQueryModelBindingProvider(NavigationManager navigationManager) + { + _navigationManager = navigationManager; + } + + /// + protected internal override bool SupportsCascadingParameterAttributeType(Type attributeType) + => attributeType == typeof(SupplyParameterFromQueryAttribute); + + /// + protected internal override bool SupportsParameterType(Type type) + => QueryParameterValueSupplier.CanSupplyValueForType(type); + + /// + protected internal override bool CanSupplyValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo) + // We can always supply a value; it'll just be null if there's no match. + => true; + + /// + protected internal override object? GetCurrentValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo) + { + var queryParameterName = parameterInfo.Attribute.Name ?? parameterInfo.PropertyName; + return _queryParameterValueSupplier.GetQueryParameterValue(parameterInfo.PropertyType, queryParameterName); + } + + /// + protected internal override void OnBindingContextUpdated(ModelBindingContext? bindingContext) + { + var query = GetQueryString(_navigationManager.Uri); + + _queryParameterValueSupplier.ReadParametersFromQuery(query); + + if (_subscribers is null) + { + return; + } + + foreach (var subscriber in _subscribers) + { + subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); + } + + static ReadOnlyMemory GetQueryString(string url) + { + var queryStartPos = url.IndexOf('?'); + + if (queryStartPos < 0) + { + return default; + } + + var queryEndPos = url.IndexOf('#', queryStartPos); + return url.AsMemory(queryStartPos..(queryEndPos < 0 ? url.Length : queryEndPos)); + } + } + + /// + protected internal override void Subscribe(ComponentState subscriber) + { + _subscribers ??= new(); + _subscribers.Add(subscriber); + } + + /// + protected internal override void Unsubscribe(ComponentState subscriber) + { + _subscribers?.Remove(subscriber); + } +} diff --git a/src/Components/Components/src/Binding/IFormValueCascadingParameterAttribute.cs b/src/Components/Components/src/Binding/IFormValueCascadingParameterAttribute.cs deleted file mode 100644 index b5cf2b40f936..000000000000 --- a/src/Components/Components/src/Binding/IFormValueCascadingParameterAttribute.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Components.Binding; - -internal interface IFormValueCascadingParameterAttribute : ICascadingParameterAttribute -{ -} diff --git a/src/Components/Components/src/Binding/IFormValueSupplier.cs b/src/Components/Components/src/Binding/IFormValueSupplier.cs index b8696a224339..4b5403222409 100644 --- a/src/Components/Components/src/Binding/IFormValueSupplier.cs +++ b/src/Components/Components/src/Binding/IFormValueSupplier.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Components.Binding; /// -/// Binds form data valuesto a model. +/// Binds form data values to a model. /// public interface IFormValueSupplier { diff --git a/src/Components/Components/src/CascadingParameterAttribute.cs b/src/Components/Components/src/CascadingParameterAttribute.cs index a0e7135c9241..bb9be43a5b08 100644 --- a/src/Components/Components/src/CascadingParameterAttribute.cs +++ b/src/Components/Components/src/CascadingParameterAttribute.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Components; /// supplies values with a compatible type and name. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class CascadingParameterAttribute : Attribute, ICascadingParameterAttribute +public sealed class CascadingParameterAttribute : CascadingParameterAttributeBase { /// /// If specified, the parameter value will be supplied by the closest @@ -20,5 +20,5 @@ public sealed class CascadingParameterAttribute : Attribute, ICascadingParameter /// that supplies a value with a compatible /// type. /// - public string? Name { get; set; } + public override string? Name { get; set; } } diff --git a/src/Components/Components/src/CascadingParameterAttributeBase.cs b/src/Components/Components/src/CascadingParameterAttributeBase.cs new file mode 100644 index 000000000000..f85fbaa6cffc --- /dev/null +++ b/src/Components/Components/src/CascadingParameterAttributeBase.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components; + +/// +/// Represents a parameter whose value cascades down the component hierarchy. +/// +public abstract class CascadingParameterAttributeBase : Attribute +{ + /// + /// Gets or sets the name for the parameter, which correlates to the name + /// of a cascading value. + /// + public abstract string? Name { get; set; } +} diff --git a/src/Components/Components/src/CascadingParameterInfo.cs b/src/Components/Components/src/CascadingParameterInfo.cs index d77554a27551..2d8493ff70f4 100644 --- a/src/Components/Components/src/CascadingParameterInfo.cs +++ b/src/Components/Components/src/CascadingParameterInfo.cs @@ -3,9 +3,30 @@ namespace Microsoft.AspNetCore.Components; -internal readonly struct CascadingParameterInfo(ICascadingParameterAttribute attribute, string propertyName, Type propertyType) +/// +/// Contains information about a cascading parameter. +/// +public readonly struct CascadingParameterInfo { - public ICascadingParameterAttribute Attribute { get; } = attribute; - public string PropertyName { get; } = propertyName; - public Type PropertyType { get; } = propertyType; + /// + /// Gets the property's attribute. + /// + public CascadingParameterAttributeBase Attribute { get; } + + /// + /// Gets the name of the parameter's property. + /// + public string PropertyName { get; } + + /// + /// Gets the type of the parameter's property. + /// + public Type PropertyType { get; } + + internal CascadingParameterInfo(CascadingParameterAttributeBase attribute, string propertyName, Type propertyType) + { + Attribute = attribute; + PropertyName = propertyName; + PropertyType = propertyType; + } } diff --git a/src/Components/Components/src/CascadingParameterState.cs b/src/Components/Components/src/CascadingParameterState.cs index c1e346c08673..1c9b95b720b4 100644 --- a/src/Components/Components/src/CascadingParameterState.cs +++ b/src/Components/Components/src/CascadingParameterState.cs @@ -92,7 +92,7 @@ private static CascadingParameterInfo[] CreateCascadingParameterInfos( foreach (var prop in candidateProps) { var cascadingParameterAttribute = prop.GetCustomAttributes() - .OfType().SingleOrDefault(); + .OfType().SingleOrDefault(); if (cascadingParameterAttribute != null) { result ??= new List(); diff --git a/src/Components/Components/src/CascadingQueryValueProvider.cs b/src/Components/Components/src/CascadingQueryValueProvider.cs deleted file mode 100644 index 62f0d0f0539d..000000000000 --- a/src/Components/Components/src/CascadingQueryValueProvider.cs +++ /dev/null @@ -1,108 +0,0 @@ -// 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; -using Microsoft.AspNetCore.Components.Routing; - -namespace Microsoft.AspNetCore.Components; - -internal sealed class CascadingQueryValueProvider : IComponent, ICascadingValueSupplier, IDisposable -{ - private readonly QueryParameterValueSupplier _queryParameterValueSupplier = new(); - - private HashSet? _subscribers; - private RenderHandle _handle; - private bool _hasSetInitialParameters; - - [Inject] private NavigationManager Navigation { get; set; } = default!; - - [Parameter] public RenderFragment? ChildContent { get; set; } - - void IComponent.Attach(RenderHandle renderHandle) - { - _handle = renderHandle; - } - - Task IComponent.SetParametersAsync(ParameterView parameters) - { - if (!_hasSetInitialParameters) - { - _hasSetInitialParameters = true; - - UpdateQueryParameterValues(); - - Navigation.LocationChanged += HandleLocationChanged; - } - - parameters.SetParameterProperties(this); - - _handle.Render(Render); - - return Task.CompletedTask; - } - - private void HandleLocationChanged(object? sender, LocationChangedEventArgs e) - => UpdateQueryParameterValues(); - - private void UpdateQueryParameterValues() - { - var query = GetQueryString(Navigation.Uri); - - _queryParameterValueSupplier.ReadParametersFromQuery(query); - - if (_subscribers is null) - { - return; - } - - foreach (var subscriber in _subscribers) - { - subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); - } - - static ReadOnlyMemory GetQueryString(string url) - { - var queryStartPos = url.IndexOf('?'); - - if (queryStartPos < 0) - { - return default; - } - - var queryEndPos = url.IndexOf('#', queryStartPos); - return url.AsMemory(queryStartPos..(queryEndPos < 0 ? url.Length : queryEndPos)); - } - } - - private void Render(RenderTreeBuilder builder) - { - builder.AddContent(0, ChildContent); - } - - bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo) - => parameterInfo.Attribute is SupplyParameterFromQueryAttribute; - - bool ICascadingValueSupplier.CurrentValueIsFixed => false; - - object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo) - { - var queryParameterName = parameterInfo.Attribute.Name ?? parameterInfo.PropertyName; - return _queryParameterValueSupplier.GetQueryParameterValue(parameterInfo.PropertyType, queryParameterName); - } - - void ICascadingValueSupplier.Subscribe(ComponentState subscriber) - { - _subscribers ??= new(); - _subscribers.Add(subscriber); - } - - void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber) - { - _subscribers?.Remove(subscriber); - } - - void IDisposable.Dispose() - { - Navigation.LocationChanged -= HandleLocationChanged; - } -} diff --git a/src/Components/Components/src/CascadingValue.cs b/src/Components/Components/src/CascadingValue.cs index b0fd1390a83e..c8362331f34d 100644 --- a/src/Components/Components/src/CascadingValue.cs +++ b/src/Components/Components/src/CascadingValue.cs @@ -41,8 +41,6 @@ public class CascadingValue : ICascadingValueSupplier, IComponent /// [Parameter] public bool IsFixed { get; set; } - bool ICascadingValueSupplier.CurrentValueIsFixed => IsFixed; - /// public void Attach(RenderHandle renderHandle) { @@ -147,22 +145,16 @@ bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterI return Value; } - void ICascadingValueSupplier.Subscribe(ComponentState subscriber) + void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) { -#if DEBUG if (IsFixed) { // Should not be possible. User code cannot trigger this. // Checking only to catch possible future framework bugs. throw new InvalidOperationException($"Cannot subscribe to a {typeof(CascadingValue<>).Name} when {nameof(IsFixed)} is true."); } -#endif - - if (_subscribers == null) - { - _subscribers = new HashSet(); - } + _subscribers ??= new HashSet(); _subscribers.Add(subscriber); } diff --git a/src/Components/Components/src/ICascadingParameterAttribute.cs b/src/Components/Components/src/ICascadingParameterAttribute.cs deleted file mode 100644 index b4a1d16fe90f..000000000000 --- a/src/Components/Components/src/ICascadingParameterAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Components; - -// Represents an attribute marking a parameter to be set by a cascading value. -// This exists so cascading parameter attributes can be defined outside the Components assembly. -// For example: [SupplyParameterFromForm]. -internal interface ICascadingParameterAttribute -{ - public string? Name { get; set; } -} diff --git a/src/Components/Components/src/ICascadingValueSupplier.cs b/src/Components/Components/src/ICascadingValueSupplier.cs index 580a5b782866..fef930467d43 100644 --- a/src/Components/Components/src/ICascadingValueSupplier.cs +++ b/src/Components/Components/src/ICascadingValueSupplier.cs @@ -10,13 +10,13 @@ internal interface ICascadingValueSupplier // This interface exists only so that CascadingParameterState has a way // to work with all CascadingValue types regardless of T. + bool IsFixed { get; } + bool CanSupplyValue(in CascadingParameterInfo parameterInfo); object? GetCurrentValue(in CascadingParameterInfo parameterInfo); - bool CurrentValueIsFixed { get; } - - void Subscribe(ComponentState subscriber); + void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo); void Unsubscribe(ComponentState subscriber); } diff --git a/src/Components/Components/src/PublicAPI.Shipped.txt b/src/Components/Components/src/PublicAPI.Shipped.txt index 859bce795398..bada6db3efa1 100644 --- a/src/Components/Components/src/PublicAPI.Shipped.txt +++ b/src/Components/Components/src/PublicAPI.Shipped.txt @@ -31,8 +31,6 @@ Microsoft.AspNetCore.Components.BindElementAttribute.Suffix.get -> string? Microsoft.AspNetCore.Components.BindElementAttribute.ValueAttribute.get -> string! Microsoft.AspNetCore.Components.CascadingParameterAttribute Microsoft.AspNetCore.Components.CascadingParameterAttribute.CascadingParameterAttribute() -> void -Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string? -Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.CascadingTypeParameterAttribute(string! name) -> void Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.Name.get -> string! @@ -406,8 +404,6 @@ Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.set -> void Microsoft.AspNetCore.Components.Routing.Router.Router() -> void Microsoft.AspNetCore.Components.Routing.Router.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute -Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string? -Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.SupplyParameterFromQueryAttribute() -> void override Microsoft.AspNetCore.Components.LayoutComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! override Microsoft.AspNetCore.Components.MarkupString.ToString() -> string! diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 625c0e05e595..96969f37414f 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,5 +1,16 @@ #nullable enable +abstract Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.AreValuesFixed.get -> bool +abstract Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.CanSupplyValue(Microsoft.AspNetCore.Components.ModelBindingContext? bindingContext, in Microsoft.AspNetCore.Components.CascadingParameterInfo parameterInfo) -> bool +abstract Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.GetCurrentValue(Microsoft.AspNetCore.Components.ModelBindingContext? bindingContext, in Microsoft.AspNetCore.Components.CascadingParameterInfo parameterInfo) -> object? +abstract Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.SupportsCascadingParameterAttributeType(System.Type! attributeType) -> bool +abstract Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.SupportsParameterType(System.Type! parameterType) -> bool +abstract Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.Name.get -> string? +abstract Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.Name.set -> void abstract Microsoft.AspNetCore.Components.RenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! +Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider +Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.CascadingModelBindingProvider() -> void +Microsoft.AspNetCore.Components.Binding.CascadingQueryModelBindingProvider +Microsoft.AspNetCore.Components.Binding.CascadingQueryModelBindingProvider.CascadingQueryModelBindingProvider(Microsoft.AspNetCore.Components.NavigationManager! navigationManager) -> void Microsoft.AspNetCore.Components.Binding.IFormValueSupplier Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.CanBind(string! formName, System.Type! valueType) -> bool Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.CanConvertSingleValue(System.Type! type) -> bool @@ -12,6 +23,13 @@ Microsoft.AspNetCore.Components.CascadingModelBinder.IsFixed.get -> bool Microsoft.AspNetCore.Components.CascadingModelBinder.IsFixed.set -> void Microsoft.AspNetCore.Components.CascadingModelBinder.Name.get -> string! Microsoft.AspNetCore.Components.CascadingModelBinder.Name.set -> void +Microsoft.AspNetCore.Components.CascadingParameterAttributeBase +Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.CascadingParameterAttributeBase() -> void +Microsoft.AspNetCore.Components.CascadingParameterInfo +Microsoft.AspNetCore.Components.CascadingParameterInfo.Attribute.get -> Microsoft.AspNetCore.Components.CascadingParameterAttributeBase! +Microsoft.AspNetCore.Components.CascadingParameterInfo.CascadingParameterInfo() -> void +Microsoft.AspNetCore.Components.CascadingParameterInfo.PropertyName.get -> string! +Microsoft.AspNetCore.Components.CascadingParameterInfo.PropertyType.get -> System.Type! Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.IComponentRenderMode Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task! @@ -60,10 +78,17 @@ Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParamete Microsoft.AspNetCore.Components.StreamRenderingAttribute Microsoft.AspNetCore.Components.StreamRenderingAttribute.Enabled.get -> bool Microsoft.AspNetCore.Components.StreamRenderingAttribute.StreamRenderingAttribute(bool enabled) -> void +override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string? +override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool +override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string? +override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void +virtual Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.OnBindingContextUpdated(Microsoft.AspNetCore.Components.ModelBindingContext? bindingContext) -> void +virtual Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.Subscribe(Microsoft.AspNetCore.Components.Rendering.ComponentState! subscriber) -> void +virtual Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.Unsubscribe(Microsoft.AspNetCore.Components.Rendering.ComponentState! subscriber) -> void virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.CreateComponentState(int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> Microsoft.AspNetCore.Components.Rendering.ComponentState! diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index 5ca7489fadbf..507d5bcd353a 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -170,7 +170,7 @@ private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMem if (propertyInfo != null) { if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) && - !propertyInfo.GetCustomAttributes().OfType().Any()) + !propertyInfo.GetCustomAttributes().OfType().Any()) { throw new InvalidOperationException( $"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " + @@ -261,7 +261,7 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType) foreach (var propertyInfo in GetCandidateBindableProperties(targetType)) { ParameterAttribute? parameterAttribute = null; - ICascadingParameterAttribute? cascadingParameterAttribute = null; + CascadingParameterAttributeBase? cascadingParameterAttribute = null; var attributes = propertyInfo.GetCustomAttributes(); foreach (var attribute in attributes) @@ -271,7 +271,7 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType) case ParameterAttribute parameter: parameterAttribute = parameter; break; - case ICascadingParameterAttribute cascadingParameter: + case CascadingParameterAttributeBase cascadingParameter: cascadingParameterAttribute = cascadingParameter; break; default: diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index f7de9f106215..5dcce2af8197 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -194,9 +194,9 @@ private bool AddCascadingParameterSubscriptions() for (var i = 0; i < numCascadingParameters; i++) { var valueSupplier = _cascadingParameters[i].ValueSupplier; - if (!valueSupplier.CurrentValueIsFixed) + if (!valueSupplier.IsFixed) { - valueSupplier.Subscribe(this); + valueSupplier.Subscribe(this, _cascadingParameters[i].ParameterInfo); hasSubscription = true; } } @@ -210,7 +210,7 @@ private void RemoveCascadingParameterSubscriptions() for (var i = 0; i < numCascadingParameters; i++) { var supplier = _cascadingParameters[i].ValueSupplier; - if (!supplier.CurrentValueIsFixed) + if (!supplier.IsFixed) { supplier.Unsubscribe(this); } diff --git a/src/Components/Components/src/RouteView.cs b/src/Components/Components/src/RouteView.cs index 82840e0c1fc0..bb778771a09f 100644 --- a/src/Components/Components/src/RouteView.cs +++ b/src/Components/Components/src/RouteView.cs @@ -95,18 +95,13 @@ private void RenderPageWithParameters(RenderTreeBuilder builder) void RenderPageCore(RenderTreeBuilder builder) { - builder.OpenComponent(0); - builder.AddComponentParameter(1, nameof(CascadingQueryValueProvider.ChildContent), (RenderFragment)(builder => - { - builder.OpenComponent(0, RouteData.PageType); + builder.OpenComponent(0, RouteData.PageType); - foreach (var kvp in RouteData.RouteValues) - { - builder.AddComponentParameter(1, kvp.Key, kvp.Value); - } + foreach (var kvp in RouteData.RouteValues) + { + builder.AddComponentParameter(1, kvp.Key, kvp.Value); + } - builder.CloseComponent(); - })); builder.CloseComponent(); } } diff --git a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs b/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs index d5e75c9003f6..967169ee0191 100644 --- a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs +++ b/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs @@ -51,4 +51,10 @@ public void ReadParametersFromQuery(ReadOnlyMemory query) return default; } + + public static bool CanSupplyValueForType(Type targetType) + { + var elementType = targetType.IsArray ? targetType.GetElementType()! : targetType; + return UrlValueConstraint.TryGetByTargetType(elementType, out _); + } } diff --git a/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs b/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs index 2e8ef9b5a7ac..ffae75576ff7 100644 --- a/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs +++ b/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs @@ -8,11 +8,11 @@ namespace Microsoft.AspNetCore.Components; /// current URL querystring. They may also supply further values if the URL querystring changes. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class SupplyParameterFromQueryAttribute : Attribute, ICascadingParameterAttribute +public sealed class SupplyParameterFromQueryAttribute : CascadingParameterAttributeBase { /// /// Gets or sets the name of the querystring parameter. If null, the querystring /// parameter is assumed to have the same name as the associated property. /// - public string? Name { get; set; } + public override string? Name { get; set; } } diff --git a/src/Components/Components/test/CascadingModelBinderTest.cs b/src/Components/Components/test/CascadingModelBinderTest.cs index c20703b51d8f..48a6f5ecff15 100644 --- a/src/Components/Components/test/CascadingModelBinderTest.cs +++ b/src/Components/Components/test/CascadingModelBinderTest.cs @@ -20,6 +20,7 @@ public CascadingModelBinderTest() _navigationManager = new TestNavigationManager(); serviceCollection.AddSingleton(_navigationManager); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); var services = serviceCollection.BuildServiceProvider(); _renderer = new TestRenderer(services); } diff --git a/src/Components/Components/test/CascadingParameterStateTest.cs b/src/Components/Components/test/CascadingParameterStateTest.cs index 4db81ba18ef2..89a1a0148402 100644 --- a/src/Components/Components/test/CascadingParameterStateTest.cs +++ b/src/Components/Components/test/CascadingParameterStateTest.cs @@ -1,7 +1,6 @@ // 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 Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Test.Helpers; @@ -362,15 +361,14 @@ public void FindCascadingParameters_CanOverrideNonNullValueWithNull() public void FindCascadingParameters_HandlesSupplyParameterFromFormValues() { // Arrange + var provider = new TestCascadingFormModelBindingProvider + { + FormName = "", + CurrentValue = "some value", + }; var cascadingModelBinder = new CascadingModelBinder { - FormValueSupplier = new TestFormValueSupplier() - { - FormName = "", - ValueType = typeof(string), - BindResult = true, - BoundValue = "some value" - }, + ModelBindingProviders = new[] { provider }, Navigation = Mock.Of(), Name = "" }; @@ -393,17 +391,16 @@ public void FindCascadingParameters_HandlesSupplyParameterFromFormValues() public void FindCascadingParameters_HandlesSupplyParameterFromFormValues_WithName() { // Arrange + var provider = new TestCascadingFormModelBindingProvider + { + FormName = "some-name", + CurrentValue = "some value", + }; var cascadingModelBinder = new CascadingModelBinder { - FormValueSupplier = new TestFormValueSupplier() - { - FormName = "some-name", - ValueType = typeof(string), - BindResult = true, - BoundValue = "some value" - }, + ModelBindingProviders = new[] { provider }, Navigation = new TestNavigationManager(), - Name = "" + Name = "some-name" }; cascadingModelBinder.UpdateBindingInformation("https://localhost/"); @@ -519,32 +516,25 @@ class CascadingValueTypeBaseClass { } class CascadingValueTypeDerivedClass : CascadingValueTypeBaseClass, ICascadingValueTypeDerivedClassInterface { } interface ICascadingValueTypeDerivedClassInterface { } - private class TestFormValueSupplier : IFormValueSupplier + private class TestCascadingFormModelBindingProvider : CascadingModelBindingProvider { - public string FormName { get; set; } + public required string FormName { get; init; } - public Type ValueType { get; set; } + public required string CurrentValue { get; init; } - public object BoundValue { get; set; } + protected internal override bool AreValuesFixed => true; - public bool BindResult { get; set; } + protected internal override bool CanSupplyValue(ModelBindingContext bindingContext, in CascadingParameterInfo parameterInfo) + => string.Equals(bindingContext.Name, FormName, StringComparison.Ordinal); - public bool CanBind(string formName, Type valueType) - { - return string.Equals(formName, FormName, StringComparison.Ordinal) && - valueType == ValueType; - } + protected internal override object GetCurrentValue(ModelBindingContext bindingContext, in CascadingParameterInfo parameterInfo) + => CurrentValue; - public bool CanConvertSingleValue(Type type) - { - return type == ValueType; - } + protected internal override bool SupportsCascadingParameterAttributeType(Type attributeType) + => attributeType == typeof(SupplyParameterFromFormAttribute); - public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue) - { - boundValue = BoundValue; - return BindResult; - } + protected internal override bool SupportsParameterType(Type parameterType) + => parameterType == typeof(string); } class TestNavigationManager : NavigationManager @@ -557,11 +547,11 @@ public TestNavigationManager() } [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class SupplyParameterFromFormAttribute : Attribute, IFormValueCascadingParameterAttribute +public sealed class SupplyParameterFromFormAttribute : CascadingParameterAttributeBase { /// /// Gets or sets the name for the parameter. The name is used to match /// the form data and decide whether or not the value needs to be bound. /// - public string Name { get; set; } + public override string Name { get; set; } } diff --git a/src/Components/Components/test/CascadingParameterTest.cs b/src/Components/Components/test/CascadingParameterTest.cs index cd6cc5328d43..974b85589fb3 100644 --- a/src/Components/Components/test/CascadingParameterTest.cs +++ b/src/Components/Components/test/CascadingParameterTest.cs @@ -496,15 +496,15 @@ class SecondCascadingParameterConsumerComponent : CascadingParameterCons } [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - class CustomCascadingParameter1Attribute : Attribute, ICascadingParameterAttribute + class CustomCascadingParameter1Attribute : CascadingParameterAttributeBase { - public string Name { get; set; } + public override string Name { get; set; } } [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - class CustomCascadingParameter2Attribute : Attribute, ICascadingParameterAttribute + class CustomCascadingParameter2Attribute : CascadingParameterAttributeBase { - public string Name { get; set; } + public override string Name { get; set; } } class CustomCascadingValueProducer : AutoRenderComponent, ICascadingValueSupplier @@ -513,7 +513,7 @@ class CustomCascadingValueProducer : AutoRenderComponent, ICascading [Parameter] public RenderFragment ChildContent { get; set; } - bool ICascadingValueSupplier.CurrentValueIsFixed => true; + bool ICascadingValueSupplier.IsFixed => true; protected override void BuildRenderTree(RenderTreeBuilder builder) { @@ -537,7 +537,7 @@ object ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo cascadi return Value; } - void ICascadingValueSupplier.Subscribe(ComponentState subscriber) + void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) { throw new NotImplementedException(); } diff --git a/src/Components/Components/test/ParameterViewTest.Assignment.cs b/src/Components/Components/test/ParameterViewTest.Assignment.cs index afb2e7dd0548..738be9e73161 100644 --- a/src/Components/Components/test/ParameterViewTest.Assignment.cs +++ b/src/Components/Components/test/ParameterViewTest.Assignment.cs @@ -692,7 +692,7 @@ public TestCascadingValueProvider(object value) _value = value; } - public bool CurrentValueIsFixed => throw new NotImplementedException(); + public bool IsFixed => throw new NotImplementedException(); public bool CanSupplyValue(in CascadingParameterInfo parameterInfo) { @@ -704,7 +704,7 @@ public object GetCurrentValue(in CascadingParameterInfo parameterInfo) return _value; } - public void Subscribe(ComponentState subscriber) + public void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) { throw new NotImplementedException(); } diff --git a/src/Components/Components/test/ParameterViewTest.cs b/src/Components/Components/test/ParameterViewTest.cs index 60c3a693cd22..9800c0f3395d 100644 --- a/src/Components/Components/test/ParameterViewTest.cs +++ b/src/Components/Components/test/ParameterViewTest.cs @@ -604,7 +604,7 @@ public TestCascadingValue(object value) _value = value; } - public bool CurrentValueIsFixed => false; + public bool IsFixed => false; public bool CanSupplyValue(in CascadingParameterInfo parameterInfo) => throw new NotImplementedException(); @@ -612,7 +612,7 @@ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo) public object GetCurrentValue(in CascadingParameterInfo parameterInfo) => _value; - public void Subscribe(ComponentState subscriber) + public void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) => throw new NotImplementedException(); public void Unsubscribe(ComponentState subscriber) diff --git a/src/Components/Components/test/RouteViewTest.cs b/src/Components/Components/test/RouteViewTest.cs index c791d7243363..f7bdad0d3445 100644 --- a/src/Components/Components/test/RouteViewTest.cs +++ b/src/Components/Components/test/RouteViewTest.cs @@ -22,6 +22,7 @@ public RouteViewTest() _navigationManager = new RouteViewTestNavigationManager(); serviceCollection.AddSingleton(_navigationManager); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); var services = serviceCollection.BuildServiceProvider(); _renderer = new TestRenderer(services); diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 8ef454f5bbf9..c4b6e9c6b17e 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -58,6 +58,8 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection // Form handling services.TryAddScoped(); services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); return new DefaultRazorComponentsBuilder(services); } diff --git a/src/Components/Web/src/Binding/CascadingFormModelBindingProvider.cs b/src/Components/Web/src/Binding/CascadingFormModelBindingProvider.cs new file mode 100644 index 000000000000..d2c28112453e --- /dev/null +++ b/src/Components/Web/src/Binding/CascadingFormModelBindingProvider.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Binding; + +/// +/// Enables component parameters to be supplied from the query string with . +/// +public sealed class CascadingFormModelBindingProvider : CascadingModelBindingProvider +{ + private readonly IFormValueSupplier _formValueSupplier; + + /// + protected internal override bool AreValuesFixed => true; + + /// + /// Constructs a new instance of . + /// + /// The . + public CascadingFormModelBindingProvider(IFormValueSupplier formValueSupplier) + { + _formValueSupplier = formValueSupplier; + } + + /// + protected internal override bool SupportsCascadingParameterAttributeType(Type attributeType) + => attributeType == typeof(SupplyParameterFromFormAttribute); + + /// + protected internal override bool SupportsParameterType(Type type) + => _formValueSupplier.CanConvertSingleValue(type); + + /// + protected internal override bool CanSupplyValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo) + { + var (formName, valueType) = GetFormNameAndValueType(bindingContext, parameterInfo); + return _formValueSupplier.CanBind(formName!, valueType); + } + + /// + protected internal override object? GetCurrentValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo) + { + var (formName, valueType) = GetFormNameAndValueType(bindingContext, parameterInfo); + + if (!_formValueSupplier.TryBind(formName!, valueType, out var boundValue)) + { + // TODO: Report errors + return null; + } + + return boundValue; + } + + private static (string FormName, Type ValueType) GetFormNameAndValueType(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo) + { + var valueType = parameterInfo.PropertyType; + var valueName = parameterInfo.Attribute.Name; + var formName = string.IsNullOrEmpty(valueName) ? + (bindingContext?.Name) : + ModelBindingContext.Combine(bindingContext, valueName); + + return (formName!, valueType); + } +} diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 780363a5e104..cb3b73f4a142 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -2,6 +2,8 @@ *REMOVED*override Microsoft.AspNetCore.Components.Forms.InputFile.OnInitialized() -> void abstract Microsoft.AspNetCore.Components.Forms.FormDataProvider.Entries.get -> System.Collections.Generic.IReadOnlyDictionary! abstract Microsoft.AspNetCore.Components.Forms.FormDataProvider.Name.get -> string? +Microsoft.AspNetCore.Components.Binding.CascadingFormModelBindingProvider +Microsoft.AspNetCore.Components.Binding.CascadingFormModelBindingProvider.CascadingFormModelBindingProvider(Microsoft.AspNetCore.Components.Binding.IFormValueSupplier! formValueSupplier) -> void Microsoft.AspNetCore.Components.Forms.FormDataProvider Microsoft.AspNetCore.Components.Forms.FormDataProvider.FormDataProvider() -> void Microsoft.AspNetCore.Components.Forms.FormDataProvider.IsFormDataAvailable.get -> bool @@ -18,8 +20,6 @@ Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer. Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.StaticHtmlRenderer(System.IServiceProvider! serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Components.RenderTree.WebRenderer.WaitUntilAttachedAsync() -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute -Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.get -> string? -Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.set -> void Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.SupplyParameterFromFormAttribute() -> void Microsoft.AspNetCore.Components.Web.AutoRenderMode Microsoft.AspNetCore.Components.Web.AutoRenderMode.AutoRenderMode() -> void @@ -65,6 +65,8 @@ Microsoft.AspNetCore.Components.Web.WebAssemblyRenderMode.WebAssemblyRenderMode( override Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.Dispatcher.get -> Microsoft.AspNetCore.Components.Dispatcher! override Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.HandleException(System.Exception! exception) -> void override Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.UpdateDisplayAsync(in Microsoft.AspNetCore.Components.RenderTree.RenderBatch renderBatch) -> System.Threading.Tasks.Task! +override Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.get -> string? +override Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.set -> void override Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! override Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! override Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! diff --git a/src/Components/Web/src/SupplyParameterFromFormAttribute.cs b/src/Components/Web/src/SupplyParameterFromFormAttribute.cs index b6e8c00914b5..be92c35bc25a 100644 --- a/src/Components/Web/src/SupplyParameterFromFormAttribute.cs +++ b/src/Components/Web/src/SupplyParameterFromFormAttribute.cs @@ -1,8 +1,6 @@ // 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.Binding; - namespace Microsoft.AspNetCore.Components; /// @@ -10,11 +8,11 @@ namespace Microsoft.AspNetCore.Components; /// the form data for the form with the specified name. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class SupplyParameterFromFormAttribute : Attribute, IFormValueCascadingParameterAttribute +public sealed class SupplyParameterFromFormAttribute : CascadingParameterAttributeBase { /// /// Gets or sets the name for the parameter. The name is used to match /// the form data and decide whether or not the value needs to be bound. /// - public string? Name { get; set; } + public override string? Name { get; set; } } diff --git a/src/Components/Web/test/Forms/EditFormTest.cs b/src/Components/Web/test/Forms/EditFormTest.cs index 59f449e95a21..46f980eeaf7a 100644 --- a/src/Components/Web/test/Forms/EditFormTest.cs +++ b/src/Components/Web/test/Forms/EditFormTest.cs @@ -20,6 +20,8 @@ public EditFormTest() var services = new ServiceCollection(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); _testRenderer = new(services.BuildServiceProvider()); } diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index 3345d113eb4b..d8fed636402d 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -263,5 +263,7 @@ internal void InitializeDefaultServices() }); Services.AddSingleton(); Services.AddSingleton(); + Services.AddSingleton(); + Services.AddSingleton(); } } diff --git a/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs b/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs index e522547c6971..c1d446e79236 100644 --- a/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs +++ b/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs @@ -32,6 +32,8 @@ public static IServiceCollection AddBlazorWebView(this IServiceCollection servic services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); return services; } From 4e562dc4cecdb278aa23c8263fe2f6736a4c9fff Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 13 Jun 2023 14:23:13 -0700 Subject: [PATCH 09/13] Fix test failures --- src/Components/Components/src/PublicAPI.Shipped.txt | 4 ++++ src/Components/Components/src/PublicAPI.Unshipped.txt | 4 ++++ .../RazorComponentsServiceCollectionExtensions.cs | 4 ++-- .../WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs | 1 - .../src/ComponentsWebViewServiceCollectionExtensions.cs | 1 - 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Components/Components/src/PublicAPI.Shipped.txt b/src/Components/Components/src/PublicAPI.Shipped.txt index bada6db3efa1..859bce795398 100644 --- a/src/Components/Components/src/PublicAPI.Shipped.txt +++ b/src/Components/Components/src/PublicAPI.Shipped.txt @@ -31,6 +31,8 @@ Microsoft.AspNetCore.Components.BindElementAttribute.Suffix.get -> string? Microsoft.AspNetCore.Components.BindElementAttribute.ValueAttribute.get -> string! Microsoft.AspNetCore.Components.CascadingParameterAttribute Microsoft.AspNetCore.Components.CascadingParameterAttribute.CascadingParameterAttribute() -> void +Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string? +Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.CascadingTypeParameterAttribute(string! name) -> void Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.Name.get -> string! @@ -404,6 +406,8 @@ Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.set -> void Microsoft.AspNetCore.Components.Routing.Router.Router() -> void Microsoft.AspNetCore.Components.Routing.Router.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute +Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string? +Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.SupplyParameterFromQueryAttribute() -> void override Microsoft.AspNetCore.Components.LayoutComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! override Microsoft.AspNetCore.Components.MarkupString.ToString() -> string! diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 96969f37414f..1db92ed01743 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -78,12 +78,16 @@ Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParamete Microsoft.AspNetCore.Components.StreamRenderingAttribute Microsoft.AspNetCore.Components.StreamRenderingAttribute.Enabled.get -> bool Microsoft.AspNetCore.Components.StreamRenderingAttribute.StreamRenderingAttribute(bool enabled) -> void +*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string? +*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string? override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool +*REMOVED*Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string? +*REMOVED*Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string? override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void virtual Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.OnBindingContextUpdated(Microsoft.AspNetCore.Components.ModelBindingContext? bindingContext) -> void diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index c4b6e9c6b17e..2b2b46965df4 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -58,8 +58,8 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection // Form handling services.TryAddScoped(); services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); + services.AddScoped(); + services.AddScoped(); return new DefaultRazorComponentsBuilder(services); } diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index d8fed636402d..93b2b7e25e50 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -263,7 +263,6 @@ internal void InitializeDefaultServices() }); Services.AddSingleton(); Services.AddSingleton(); - Services.AddSingleton(); Services.AddSingleton(); } } diff --git a/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs b/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs index c1d446e79236..250301335c9d 100644 --- a/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs +++ b/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs @@ -32,7 +32,6 @@ public static IServiceCollection AddBlazorWebView(this IServiceCollection servic services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); - services.TryAddScoped(); services.TryAddScoped(); return services; From 40f9289ff20816d75d0eab5e33c98fe8c83c5787 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 13 Jun 2023 15:08:59 -0700 Subject: [PATCH 10/13] Some cleanup --- .../src/Binding/CascadingModelBinder.cs | 36 ++++++------ .../Binding/CascadingModelBindingProvider.cs | 8 --- .../CascadingQueryModelBindingProvider.cs | 55 ++++++++++++------- .../Components/src/CascadingValue.cs | 2 +- .../Components/src/ICascadingValueSupplier.cs | 2 +- .../Components/src/PublicAPI.Unshipped.txt | 1 - .../src/Rendering/ComponentState.cs | 2 +- .../Components/test/CascadingParameterTest.cs | 2 +- .../test/ParameterViewTest.Assignment.cs | 2 +- .../Components/test/ParameterViewTest.cs | 2 +- 10 files changed, 59 insertions(+), 53 deletions(-) diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index ab767d4d946f..cb16ea41c33e 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -135,11 +135,6 @@ _bindingContext is null || _bindingContext = new ModelBindingContext(name, bindingId, CanBind); } - foreach (var provider in ModelBindingProviders) - { - provider?.OnBindingContextUpdated(_bindingContext); - } - string GenerateBindingContextId(string name) { var bindingId = Navigation.ToBaseRelativePath(Navigation.GetUriWithQueryParameter("handler", name)); @@ -157,13 +152,8 @@ bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterI void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) { - if (!TryGetProvider(in parameterInfo, out var provider)) - { - // This should never happen because this method only gets called after CanSupplyValue returns true. - // But we want to know if this ever does happen somehow. - throw new InvalidOperationException( - $"Cannot subscribe to changes in values that '{nameof(CascadingModelBinder)}' cannot supply."); - } + // We expect there to always be a provider at this point, because CanSupplyValue must have returned true. + var provider = GetProviderOrThrow(parameterInfo); if (!provider.AreValuesFixed) { @@ -171,14 +161,14 @@ void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingPa } } - void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber) + void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) { - foreach (var provider in ModelBindingProviders) + // We expect there to always be a provider at this point, because CanSupplyValue must have returned true. + var provider = GetProviderOrThrow(parameterInfo); + + if (!provider.AreValuesFixed) { - if (!provider.AreValuesFixed) - { - provider.Unsubscribe(subscriber); - } + provider.Unsubscribe(subscriber); } } @@ -187,6 +177,16 @@ void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber) ? provider.GetCurrentValue(_bindingContext, parameterInfo) : null; + 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 provider; + } + private bool TryGetProvider(in CascadingParameterInfo parameterInfo, [NotNullWhen(true)] out CascadingModelBindingProvider? result) { var attributeType = parameterInfo.Attribute.GetType(); diff --git a/src/Components/Components/src/Binding/CascadingModelBindingProvider.cs b/src/Components/Components/src/Binding/CascadingModelBindingProvider.cs index 641b738584f8..4b75e496a3ce 100644 --- a/src/Components/Components/src/Binding/CascadingModelBindingProvider.cs +++ b/src/Components/Components/src/Binding/CascadingModelBindingProvider.cs @@ -45,14 +45,6 @@ public abstract class CascadingModelBindingProvider /// The value to supply to the parameter. protected internal abstract object? GetCurrentValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo); - /// - /// Invoked when the current gets updated. - /// - /// The updated . - protected internal virtual void OnBindingContextUpdated(ModelBindingContext? bindingContext) - { - } - /// /// Subscribes to changes in supplied values, if they can change. /// diff --git a/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs b/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs index 448c43c151d4..250914478b90 100644 --- a/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs +++ b/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs @@ -9,12 +9,13 @@ namespace Microsoft.AspNetCore.Components.Binding; /// /// Enables component parameters to be supplied from the query string with . /// -public sealed class CascadingQueryModelBindingProvider : CascadingModelBindingProvider +public sealed class CascadingQueryModelBindingProvider : CascadingModelBindingProvider, IDisposable { private readonly QueryParameterValueSupplier _queryParameterValueSupplier = new(); private readonly NavigationManager _navigationManager; private HashSet? _subscribers; + private bool _haveQueryParametersChanged = true; /// protected internal override bool AreValuesFixed => false; @@ -25,6 +26,7 @@ public sealed class CascadingQueryModelBindingProvider : CascadingModelBindingPr public CascadingQueryModelBindingProvider(NavigationManager navigationManager) { _navigationManager = navigationManager; + _navigationManager.LocationChanged += OnLocationChanged; } /// @@ -43,26 +45,47 @@ protected internal override bool CanSupplyValue(ModelBindingContext? bindingCont /// protected internal override object? GetCurrentValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo) { + if (_haveQueryParametersChanged) + { + _haveQueryParametersChanged = false; + UpdateQueryParameters(); + } + var queryParameterName = parameterInfo.Attribute.Name ?? parameterInfo.PropertyName; return _queryParameterValueSupplier.GetQueryParameterValue(parameterInfo.PropertyType, queryParameterName); } /// - protected internal override void OnBindingContextUpdated(ModelBindingContext? bindingContext) + protected internal override void Subscribe(ComponentState subscriber) { - var query = GetQueryString(_navigationManager.Uri); + _subscribers ??= new(); + _subscribers.Add(subscriber); + } - _queryParameterValueSupplier.ReadParametersFromQuery(query); + /// + protected internal override void Unsubscribe(ComponentState subscriber) + { + _subscribers?.Remove(subscriber); + } - if (_subscribers is null) - { - return; - } + private void OnLocationChanged(object? sender, LocationChangedEventArgs args) + { + _haveQueryParametersChanged = true; - foreach (var subscriber in _subscribers) + if (_subscribers is not null) { - subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); + foreach (var subscriber in _subscribers) + { + subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); + } } + } + + private void UpdateQueryParameters() + { + var query = GetQueryString(_navigationManager.Uri); + + _queryParameterValueSupplier.ReadParametersFromQuery(query); static ReadOnlyMemory GetQueryString(string url) { @@ -78,16 +101,8 @@ static ReadOnlyMemory GetQueryString(string url) } } - /// - protected internal override void Subscribe(ComponentState subscriber) + void IDisposable.Dispose() { - _subscribers ??= new(); - _subscribers.Add(subscriber); - } - - /// - protected internal override void Unsubscribe(ComponentState subscriber) - { - _subscribers?.Remove(subscriber); + _navigationManager.LocationChanged -= OnLocationChanged; } } diff --git a/src/Components/Components/src/CascadingValue.cs b/src/Components/Components/src/CascadingValue.cs index c8362331f34d..005abcef4a57 100644 --- a/src/Components/Components/src/CascadingValue.cs +++ b/src/Components/Components/src/CascadingValue.cs @@ -158,7 +158,7 @@ void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingPa _subscribers.Add(subscriber); } - void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber) + void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) { _subscribers?.Remove(subscriber); } diff --git a/src/Components/Components/src/ICascadingValueSupplier.cs b/src/Components/Components/src/ICascadingValueSupplier.cs index fef930467d43..34e1620351ca 100644 --- a/src/Components/Components/src/ICascadingValueSupplier.cs +++ b/src/Components/Components/src/ICascadingValueSupplier.cs @@ -18,5 +18,5 @@ internal interface ICascadingValueSupplier void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo); - void Unsubscribe(ComponentState subscriber); + void Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo); } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 1db92ed01743..f37e9e93b125 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -90,7 +90,6 @@ override Microsoft.AspNetCore.Components.EventCallback.Equals(object? ob *REMOVED*Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string? override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void -virtual Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.OnBindingContextUpdated(Microsoft.AspNetCore.Components.ModelBindingContext? bindingContext) -> void virtual Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.Subscribe(Microsoft.AspNetCore.Components.Rendering.ComponentState! subscriber) -> void virtual Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.Unsubscribe(Microsoft.AspNetCore.Components.Rendering.ComponentState! subscriber) -> void virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index 5dcce2af8197..c2b9276485a1 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -212,7 +212,7 @@ private void RemoveCascadingParameterSubscriptions() var supplier = _cascadingParameters[i].ValueSupplier; if (!supplier.IsFixed) { - supplier.Unsubscribe(this); + supplier.Unsubscribe(this, _cascadingParameters[i].ParameterInfo); } } } diff --git a/src/Components/Components/test/CascadingParameterTest.cs b/src/Components/Components/test/CascadingParameterTest.cs index 974b85589fb3..ba97eeb110f0 100644 --- a/src/Components/Components/test/CascadingParameterTest.cs +++ b/src/Components/Components/test/CascadingParameterTest.cs @@ -542,7 +542,7 @@ void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingPa throw new NotImplementedException(); } - void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber) + void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) { throw new NotImplementedException(); } diff --git a/src/Components/Components/test/ParameterViewTest.Assignment.cs b/src/Components/Components/test/ParameterViewTest.Assignment.cs index 738be9e73161..a85b6d9cd03e 100644 --- a/src/Components/Components/test/ParameterViewTest.Assignment.cs +++ b/src/Components/Components/test/ParameterViewTest.Assignment.cs @@ -709,7 +709,7 @@ public void Subscribe(ComponentState subscriber, in CascadingParameterInfo param throw new NotImplementedException(); } - public void Unsubscribe(ComponentState subscriber) + public void Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) { throw new NotImplementedException(); } diff --git a/src/Components/Components/test/ParameterViewTest.cs b/src/Components/Components/test/ParameterViewTest.cs index 9800c0f3395d..e4a358d491cc 100644 --- a/src/Components/Components/test/ParameterViewTest.cs +++ b/src/Components/Components/test/ParameterViewTest.cs @@ -615,7 +615,7 @@ public object GetCurrentValue(in CascadingParameterInfo parameterInfo) public void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) => throw new NotImplementedException(); - public void Unsubscribe(ComponentState subscriber) + public void Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) => throw new NotImplementedException(); } } From 3474f0b85f98389fe69e65fa28cd6e9b0922d91d Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 13 Jun 2023 15:22:44 -0700 Subject: [PATCH 11/13] More cleanup --- .../Authorization/test/AuthorizeRouteViewTest.cs | 1 - src/Components/Components/src/CascadingValue.cs | 8 ++++---- src/Components/Components/src/ICascadingValueSupplier.cs | 3 --- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs index 05609873187d..83c7549b574b 100644 --- a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs +++ b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs @@ -34,7 +34,6 @@ public AuthorizeRouteViewTest() serviceCollection.AddSingleton(); serviceCollection.AddSingleton(_testAuthorizationService); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); var services = serviceCollection.BuildServiceProvider(); _renderer = new TestRenderer(services); diff --git a/src/Components/Components/src/CascadingValue.cs b/src/Components/Components/src/CascadingValue.cs index 005abcef4a57..a040894ac57c 100644 --- a/src/Components/Components/src/CascadingValue.cs +++ b/src/Components/Components/src/CascadingValue.cs @@ -133,11 +133,11 @@ bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterI return false; } - // We only consider explicitly specified names, not the property name. - var parameterSpecifiedName = cascadingParameterAttribute.Name; + // We only consider explicitly requested names, not the property name. + var requestedName = cascadingParameterAttribute.Name; - return (parameterSpecifiedName == null && Name == null) || // Match on type alone - string.Equals(parameterSpecifiedName, Name, StringComparison.OrdinalIgnoreCase); // Also match on name + return (requestedName == null && Name == null) // Match on type alone + || string.Equals(requestedName, Name, StringComparison.OrdinalIgnoreCase); // Also match on name } object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo) diff --git a/src/Components/Components/src/ICascadingValueSupplier.cs b/src/Components/Components/src/ICascadingValueSupplier.cs index 34e1620351ca..c535d9cfda16 100644 --- a/src/Components/Components/src/ICascadingValueSupplier.cs +++ b/src/Components/Components/src/ICascadingValueSupplier.cs @@ -7,9 +7,6 @@ namespace Microsoft.AspNetCore.Components; internal interface ICascadingValueSupplier { - // This interface exists only so that CascadingParameterState has a way - // to work with all CascadingValue types regardless of T. - bool IsFixed { get; } bool CanSupplyValue(in CascadingParameterInfo parameterInfo); From 688aaba70c2c02318baa185ef5874a85dcd4ec9d Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 14 Jun 2023 10:37:50 -0700 Subject: [PATCH 12/13] Improvements + fix tests --- .../CascadingQueryModelBindingProvider.cs | 62 ++++++++++++++----- ...orComponentsServiceCollectionExtensions.cs | 4 +- ...mponentsServiceCollectionExtensionsTest.cs | 6 ++ ...nentsWebViewServiceCollectionExtensions.cs | 2 +- 4 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs b/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs index 250914478b90..e6e9c976016b 100644 --- a/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs +++ b/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs @@ -15,7 +15,8 @@ public sealed class CascadingQueryModelBindingProvider : CascadingModelBindingPr private readonly NavigationManager _navigationManager; private HashSet? _subscribers; - private bool _haveQueryParametersChanged = true; + private bool _isSubscribedToLocationChanges; + private bool _queryParametersMightHaveChanged = true; /// protected internal override bool AreValuesFixed => false; @@ -26,7 +27,6 @@ public sealed class CascadingQueryModelBindingProvider : CascadingModelBindingPr public CascadingQueryModelBindingProvider(NavigationManager navigationManager) { _navigationManager = navigationManager; - _navigationManager.LocationChanged += OnLocationChanged; } /// @@ -45,9 +45,9 @@ protected internal override bool CanSupplyValue(ModelBindingContext? bindingCont /// protected internal override object? GetCurrentValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo) { - if (_haveQueryParametersChanged) + if (_queryParametersMightHaveChanged) { - _haveQueryParametersChanged = false; + _queryParametersMightHaveChanged = false; UpdateQueryParameters(); } @@ -58,6 +58,8 @@ protected internal override bool CanSupplyValue(ModelBindingContext? bindingCont /// protected internal override void Subscribe(ComponentState subscriber) { + SubscribeToLocationChanges(); + _subscribers ??= new(); _subscribers.Add(subscriber); } @@ -65,19 +67,11 @@ protected internal override void Subscribe(ComponentState subscriber) /// protected internal override void Unsubscribe(ComponentState subscriber) { - _subscribers?.Remove(subscriber); - } - - private void OnLocationChanged(object? sender, LocationChangedEventArgs args) - { - _haveQueryParametersChanged = true; + _subscribers!.Remove(subscriber); - if (_subscribers is not null) + if (_subscribers.Count == 0) { - foreach (var subscriber in _subscribers) - { - subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); - } + UnsubscribeFromLocationChanges(); } } @@ -101,8 +95,44 @@ static ReadOnlyMemory GetQueryString(string url) } } - void IDisposable.Dispose() + private void SubscribeToLocationChanges() + { + if (_isSubscribedToLocationChanges) + { + return; + } + + _isSubscribedToLocationChanges = true; + _queryParametersMightHaveChanged = true; + _navigationManager.LocationChanged += OnLocationChanged; + } + + private void UnsubscribeFromLocationChanges() { + if (!_isSubscribedToLocationChanges) + { + return; + } + + _isSubscribedToLocationChanges = false; _navigationManager.LocationChanged -= OnLocationChanged; } + + private void OnLocationChanged(object? sender, LocationChangedEventArgs args) + { + _queryParametersMightHaveChanged = true; + + if (_subscribers is not null) + { + foreach (var subscriber in _subscribers) + { + subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); + } + } + } + + void IDisposable.Dispose() + { + UnsubscribeFromLocationChanges(); + } } diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 2b2b46965df4..2e291faabbbf 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -58,8 +58,8 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection // Form handling services.TryAddScoped(); services.TryAddScoped(); - services.AddScoped(); - services.AddScoped(); + services.TryAddEnumerable(ServiceDescriptor.Scoped()); + services.TryAddEnumerable(ServiceDescriptor.Scoped()); return new DefaultRazorComponentsBuilder(services); } diff --git a/src/Components/Endpoints/test/RazorComponentsServiceCollectionExtensionsTest.cs b/src/Components/Endpoints/test/RazorComponentsServiceCollectionExtensionsTest.cs index 2d8026719787..3341e31d93e0 100644 --- a/src/Components/Endpoints/test/RazorComponentsServiceCollectionExtensionsTest.cs +++ b/src/Components/Endpoints/test/RazorComponentsServiceCollectionExtensionsTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; @@ -83,6 +84,11 @@ private Dictionary MultiRegistrationServiceTypes { return new Dictionary() { + [typeof(CascadingModelBindingProvider)] = new[] + { + typeof(CascadingFormModelBindingProvider), + typeof(CascadingQueryModelBindingProvider), + } }; } } diff --git a/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs b/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs index 250301335c9d..497a23cf0d2f 100644 --- a/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs +++ b/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs @@ -32,7 +32,7 @@ public static IServiceCollection AddBlazorWebView(this IServiceCollection servic services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); - services.TryAddScoped(); + services.TryAddEnumerable(ServiceDescriptor.Scoped()); return services; } From 32bc52d73d5b6b53a338f5936ed71316dbe56750 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 15 Jun 2023 20:16:32 -0700 Subject: [PATCH 13/13] PR feedback --- .../Components/src/Binding/CascadingModelBinder.cs | 12 +++++++++++- .../RouterTest/NestedQueryParameters.razor | 4 ++-- .../RouterTest/WithQueryParameters.razor | 12 ++++++------ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index cb16ea41c33e..0b1b0e2a5d4e 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -143,7 +143,17 @@ string GenerateBindingContextId(string name) } bool CanBind(Type type) - => ModelBindingProviders.Any(provider => provider.SupportsParameterType(type)); + { + foreach (var provider in ModelBindingProviders) + { + if (provider.SupportsParameterType(type)) + { + return true; + } + } + + return false; + } } bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo) diff --git a/src/Components/test/testassets/BasicTestApp/RouterTest/NestedQueryParameters.razor b/src/Components/test/testassets/BasicTestApp/RouterTest/NestedQueryParameters.razor index 9f14f51fe401..475642b1bbd0 100644 --- a/src/Components/test/testassets/BasicTestApp/RouterTest/NestedQueryParameters.razor +++ b/src/Components/test/testassets/BasicTestApp/RouterTest/NestedQueryParameters.razor @@ -6,7 +6,7 @@ @code { - [Parameter, SupplyParameterFromQuery] public int IntValue { get ; set; } + [SupplyParameterFromQuery] public int IntValue { get ; set; } - [Parameter, SupplyParameterFromQuery(Name = "l")] public long[] LongValues { get ; set; } + [SupplyParameterFromQuery(Name = "l")] public long[] LongValues { get ; set; } } diff --git a/src/Components/test/testassets/BasicTestApp/RouterTest/WithQueryParameters.razor b/src/Components/test/testassets/BasicTestApp/RouterTest/WithQueryParameters.razor index 30cc95a04f10..14631cfe8fd0 100644 --- a/src/Components/test/testassets/BasicTestApp/RouterTest/WithQueryParameters.razor +++ b/src/Components/test/testassets/BasicTestApp/RouterTest/WithQueryParameters.razor @@ -25,15 +25,15 @@ [Parameter] public string OptionalLastName { get ; set; } - [Parameter, SupplyParameterFromQuery] public int IntValue { get ; set; } + [SupplyParameterFromQuery] public int IntValue { get ; set; } - [Parameter, SupplyParameterFromQuery] public DateTime? NullableDateTimeValue { get ; set; } + [SupplyParameterFromQuery] public DateTime? NullableDateTimeValue { get ; set; } - [Parameter, SupplyParameterFromQuery] public DateOnly? NullableDateOnlyValue { get ; set; } + [SupplyParameterFromQuery] public DateOnly? NullableDateOnlyValue { get ; set; } - [Parameter, SupplyParameterFromQuery] public TimeOnly? NullableTimeOnlyValue { get ; set; } + [SupplyParameterFromQuery] public TimeOnly? NullableTimeOnlyValue { get ; set; } - [Parameter, SupplyParameterFromQuery] public string StringValue { get ; set; } + [SupplyParameterFromQuery] public string StringValue { get ; set; } - [Parameter, SupplyParameterFromQuery(Name = "l")] public long[] LongValues { get ; set; } + [SupplyParameterFromQuery(Name = "l")] public long[] LongValues { get ; set; } }