diff --git a/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs b/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs
index 0f31f41fc171..795dbe6ea3ed 100644
--- a/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs
+++ b/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs
@@ -61,7 +61,7 @@ protected InputBase() { }
protected TValue CurrentValue { get { throw null; } set { } }
protected string? CurrentValueAsString { get { throw null; } set { } }
protected Microsoft.AspNetCore.Components.Forms.EditContext EditContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
- protected Microsoft.AspNetCore.Components.Forms.FieldIdentifier FieldIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ protected internal Microsoft.AspNetCore.Components.Forms.FieldIdentifier FieldIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
[System.Diagnostics.CodeAnalysis.MaybeNullAttribute]
[System.Diagnostics.CodeAnalysis.AllowNullAttribute]
@@ -71,7 +71,7 @@ protected InputBase() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public System.Linq.Expressions.Expression>? ValueExpression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
protected virtual void Dispose(bool disposing) { }
- protected virtual string? FormatValueAsString(TValue value) { throw null; }
+ protected virtual string? FormatValueAsString([System.Diagnostics.CodeAnalysis.AllowNullAttribute] TValue value) { throw null; }
public override System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
void System.IDisposable.Dispose() { }
protected abstract bool TryParseValueFromString(string? value, [System.Diagnostics.CodeAnalysis.MaybeNullAttribute] out TValue result, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(false)] out string? validationErrorMessage);
@@ -88,7 +88,7 @@ public InputDate() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public string ParsingErrorMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
- protected override string FormatValueAsString(TValue value) { throw null; }
+ protected override string FormatValueAsString([System.Diagnostics.CodeAnalysis.AllowNullAttribute] TValue value) { throw null; }
protected override bool TryParseValueFromString(string? value, [System.Diagnostics.CodeAnalysis.MaybeNullAttribute] out TValue result, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(false)] out string? validationErrorMessage) { throw null; }
}
public partial class InputNumber : Microsoft.AspNetCore.Components.Forms.InputBase
@@ -97,9 +97,34 @@ public InputNumber() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public string ParsingErrorMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
- protected override string? FormatValueAsString(TValue value) { throw null; }
+ protected override string? FormatValueAsString([System.Diagnostics.CodeAnalysis.AllowNullAttribute] TValue value) { throw null; }
protected override bool TryParseValueFromString(string? value, [System.Diagnostics.CodeAnalysis.MaybeNullAttribute] out TValue result, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(false)] out string? validationErrorMessage) { throw null; }
}
+ public partial class InputRadioGroup : Microsoft.AspNetCore.Components.Forms.InputBase
+ {
+ public InputRadioGroup() { }
+ [Microsoft.AspNetCore.Components.ParameterAttribute]
+ public Microsoft.AspNetCore.Components.RenderFragment? ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ [Microsoft.AspNetCore.Components.ParameterAttribute]
+ public string? Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
+ protected override void OnParametersSet() { }
+ protected override bool TryParseValueFromString(string? value, [System.Diagnostics.CodeAnalysis.MaybeNullAttribute] out TValue result, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(false)] out string? validationErrorMessage) { throw null; }
+ }
+ public partial class InputRadio : Microsoft.AspNetCore.Components.ComponentBase
+ {
+ public InputRadio() { }
+ [Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)]
+ public System.Collections.Generic.IReadOnlyDictionary? AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ [Microsoft.AspNetCore.Components.ParameterAttribute]
+ public string? Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ [Microsoft.AspNetCore.Components.ParameterAttribute]
+ [System.Diagnostics.CodeAnalysis.MaybeNullAttribute]
+ [System.Diagnostics.CodeAnalysis.AllowNullAttribute]
+ public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
+ protected override void OnParametersSet() { }
+ }
public partial class InputSelect : Microsoft.AspNetCore.Components.Forms.InputBase
{
public InputSelect() { }
diff --git a/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs b/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs
index 7f99fb8ad414..e224abdafcd1 100644
--- a/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs
+++ b/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs
@@ -59,7 +59,7 @@ protected InputBase() { }
protected TValue CurrentValue { get { throw null; } set { } }
protected string? CurrentValueAsString { get { throw null; } set { } }
protected Microsoft.AspNetCore.Components.Forms.EditContext EditContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
- protected Microsoft.AspNetCore.Components.Forms.FieldIdentifier FieldIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ protected internal Microsoft.AspNetCore.Components.Forms.FieldIdentifier FieldIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
@@ -96,6 +96,29 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Renderin
protected override string? FormatValueAsString(TValue value) { throw null; }
protected override bool TryParseValueFromString(string? value, out TValue result, out string? validationErrorMessage) { throw null; }
}
+ public partial class InputRadioGroup : Microsoft.AspNetCore.Components.Forms.InputBase
+ {
+ public InputRadioGroup() { }
+ [Microsoft.AspNetCore.Components.ParameterAttribute]
+ public Microsoft.AspNetCore.Components.RenderFragment? ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ [Microsoft.AspNetCore.Components.ParameterAttribute]
+ public string? Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
+ protected override void OnParametersSet() { }
+ protected override bool TryParseValueFromString(string? value, out TValue result, out string? validationErrorMessage) { throw null; }
+ }
+ public partial class InputRadio : Microsoft.AspNetCore.Components.ComponentBase
+ {
+ public InputRadio() { }
+ [Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)]
+ public System.Collections.Generic.IReadOnlyDictionary? AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ [Microsoft.AspNetCore.Components.ParameterAttribute]
+ public string? Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ [Microsoft.AspNetCore.Components.ParameterAttribute]
+ public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
+ protected override void OnParametersSet() { }
+ }
public partial class InputSelect : Microsoft.AspNetCore.Components.Forms.InputBase
{
public InputSelect() { }
diff --git a/src/Components/Web/src/Forms/InputBase.cs b/src/Components/Web/src/Forms/InputBase.cs
index b414d3874c03..5a8fceecf822 100644
--- a/src/Components/Web/src/Forms/InputBase.cs
+++ b/src/Components/Web/src/Forms/InputBase.cs
@@ -58,7 +58,7 @@ public abstract class InputBase : ComponentBase, IDisposable
///
/// Gets the for the bound value.
///
- protected FieldIdentifier FieldIdentifier { get; set; }
+ protected internal FieldIdentifier FieldIdentifier { get; set; }
///
/// Gets or sets the current value of the input.
@@ -142,7 +142,7 @@ protected InputBase()
///
/// The value to format.
/// A string representation of the value.
- protected virtual string? FormatValueAsString(TValue value)
+ protected virtual string? FormatValueAsString([AllowNull] TValue value)
=> value?.ToString();
///
diff --git a/src/Components/Web/src/Forms/InputDate.cs b/src/Components/Web/src/Forms/InputDate.cs
index 45583a849e72..e7132988b900 100644
--- a/src/Components/Web/src/Forms/InputDate.cs
+++ b/src/Components/Web/src/Forms/InputDate.cs
@@ -34,7 +34,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
}
///
- protected override string FormatValueAsString(TValue value)
+ protected override string FormatValueAsString([AllowNull] TValue value)
{
switch (value)
{
diff --git a/src/Components/Web/src/Forms/InputExtensions.cs b/src/Components/Web/src/Forms/InputExtensions.cs
new file mode 100644
index 000000000000..a1ace921410b
--- /dev/null
+++ b/src/Components/Web/src/Forms/InputExtensions.cs
@@ -0,0 +1,35 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+
+namespace Microsoft.AspNetCore.Components.Forms
+{
+ internal static class InputExtensions
+ {
+ public static bool TryParseSelectableValueFromString(this InputBase input, string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
+ {
+ try
+ {
+ if (BindConverter.TryConvertTo(value, CultureInfo.CurrentCulture, out var parsedValue))
+ {
+ result = parsedValue;
+ validationErrorMessage = null;
+ return true;
+ }
+ else
+ {
+ result = default;
+ validationErrorMessage = $"The {input.FieldIdentifier.FieldName} field is not valid.";
+ return false;
+ }
+ }
+ catch (InvalidOperationException ex)
+ {
+ throw new InvalidOperationException($"{input.GetType()} does not support the type '{typeof(TValue)}'.", ex);
+ }
+ }
+ }
+}
diff --git a/src/Components/Web/src/Forms/InputNumber.cs b/src/Components/Web/src/Forms/InputNumber.cs
index 9f3b75175ed0..515fc5ceac8b 100644
--- a/src/Components/Web/src/Forms/InputNumber.cs
+++ b/src/Components/Web/src/Forms/InputNumber.cs
@@ -74,7 +74,7 @@ protected override bool TryParseValueFromString(string? value, [MaybeNull] out T
///
/// The value to format.
/// A string representation of the value.
- protected override string? FormatValueAsString(TValue value)
+ protected override string? FormatValueAsString([AllowNull] TValue value)
{
// Avoiding a cast to IFormattable to avoid boxing.
switch (value)
diff --git a/src/Components/Web/src/Forms/InputRadio.cs b/src/Components/Web/src/Forms/InputRadio.cs
new file mode 100644
index 000000000000..4a4ad46dc3a5
--- /dev/null
+++ b/src/Components/Web/src/Forms/InputRadio.cs
@@ -0,0 +1,82 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace Microsoft.AspNetCore.Components.Forms
+{
+ ///
+ /// An input component used for selecting a value from a group of choices.
+ ///
+ public class InputRadio : ComponentBase
+ {
+ ///
+ /// Gets context for this .
+ ///
+ internal InputRadioContext? Context { get; private set; }
+
+ ///
+ /// Gets or sets a collection of additional attributes that will be applied to the input element.
+ ///
+ [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary? AdditionalAttributes { get; set; }
+
+ ///
+ /// Gets or sets the value of this input.
+ ///
+ [AllowNull]
+ [MaybeNull]
+ [Parameter]
+ public TValue Value { get; set; } = default;
+
+ ///
+ /// Gets or sets the name of the parent input radio group.
+ ///
+ [Parameter] public string? Name { get; set; }
+
+ [CascadingParameter] private InputRadioContext? CascadedContext { get; set; }
+
+ private string GetCssClass(string fieldClass)
+ {
+ if (AdditionalAttributes != null &&
+ AdditionalAttributes.TryGetValue("class", out var @class) &&
+ !string.IsNullOrEmpty(Convert.ToString(@class)))
+ {
+ return $"{@class} {fieldClass}";
+ }
+
+ return fieldClass;
+ }
+
+ ///
+ protected override void OnParametersSet()
+ {
+ Context = string.IsNullOrEmpty(Name) ? CascadedContext : CascadedContext?.FindContextInAncestors(Name);
+
+ if (Context == null)
+ {
+ throw new InvalidOperationException($"{GetType()} must have an ancestor {typeof(InputRadioGroup)} " +
+ $"with a matching 'Name' property, if specified.");
+ }
+ }
+
+ ///
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ Debug.Assert(Context != null);
+
+ builder.OpenElement(0, "input");
+ builder.AddMultipleAttributes(1, AdditionalAttributes);
+ builder.AddAttribute(2, "class", GetCssClass(Context.FieldClass));
+ builder.AddAttribute(3, "type", "radio");
+ builder.AddAttribute(4, "name", Context.GroupName);
+ builder.AddAttribute(5, "value", BindConverter.FormatValue(Value?.ToString()));
+ builder.AddAttribute(6, "checked", Context.CurrentValue?.Equals(Value));
+ builder.AddAttribute(7, "onchange", Context.ChangeEventCallback);
+ builder.CloseElement();
+ }
+ }
+}
diff --git a/src/Components/Web/src/Forms/InputRadioContext.cs b/src/Components/Web/src/Forms/InputRadioContext.cs
new file mode 100644
index 000000000000..45f302871bec
--- /dev/null
+++ b/src/Components/Web/src/Forms/InputRadioContext.cs
@@ -0,0 +1,64 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Components.Forms
+{
+ ///
+ /// Describes context for an component.
+ ///
+ internal class InputRadioContext
+ {
+ private readonly InputRadioContext? _parentContext;
+
+ ///
+ /// Gets the name of the input radio group.
+ ///
+ public string GroupName { get; }
+
+ ///
+ /// Gets the current selected value in the input radio group.
+ ///
+ public object? CurrentValue { get; }
+
+ ///
+ /// Gets a css class indicating the validation state of input radio elements.
+ ///
+ public string FieldClass { get; }
+
+ ///
+ /// Gets the event callback to be invoked when the selected value is changed.
+ ///
+ public EventCallback ChangeEventCallback { get; }
+
+ ///
+ /// Instantiates a new .
+ ///
+ /// The parent .
+ /// The name of the input radio group.
+ /// The current selected value in the input radio group.
+ /// The css class indicating the validation state of input radio elements.
+ /// The event callback to be invoked when the selected value is changed.
+ public InputRadioContext(
+ InputRadioContext? parentContext,
+ string groupName,
+ object? currentValue,
+ string fieldClass,
+ EventCallback changeEventCallback)
+ {
+ _parentContext = parentContext;
+
+ GroupName = groupName;
+ CurrentValue = currentValue;
+ FieldClass = fieldClass;
+ ChangeEventCallback = changeEventCallback;
+ }
+
+ ///
+ /// Finds an in the context's ancestors with the matching .
+ ///
+ /// The group name of the ancestor .
+ /// The , or null if none was found.
+ public InputRadioContext? FindContextInAncestors(string groupName)
+ => string.Equals(GroupName, groupName) ? this : _parentContext?.FindContextInAncestors(groupName);
+ }
+}
diff --git a/src/Components/Web/src/Forms/InputRadioGroup.cs b/src/Components/Web/src/Forms/InputRadioGroup.cs
new file mode 100644
index 000000000000..0e30fd483e09
--- /dev/null
+++ b/src/Components/Web/src/Forms/InputRadioGroup.cs
@@ -0,0 +1,57 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace Microsoft.AspNetCore.Components.Forms
+{
+ ///
+ /// Groups child components.
+ ///
+ public class InputRadioGroup : InputBase
+ {
+ private readonly string _defaultGroupName = Guid.NewGuid().ToString("N");
+ private InputRadioContext? _context;
+
+ ///
+ /// Gets or sets the child content to be rendering inside the .
+ ///
+ [Parameter] public RenderFragment? ChildContent { get; set; }
+
+ ///
+ /// Gets or sets the name of the group.
+ ///
+ [Parameter] public string? Name { get; set; }
+
+ [CascadingParameter] private InputRadioContext? CascadedContext { get; set; }
+
+ ///
+ protected override void OnParametersSet()
+ {
+ var groupName = !string.IsNullOrEmpty(Name) ? Name : _defaultGroupName;
+ var fieldClass = EditContext.FieldCssClass(FieldIdentifier);
+ var changeEventCallback = EventCallback.Factory.CreateBinder(this, __value => CurrentValueAsString = __value, CurrentValueAsString);
+
+ _context = new InputRadioContext(CascadedContext, groupName, CurrentValue, fieldClass, changeEventCallback);
+ }
+
+ ///
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ Debug.Assert(_context != null);
+
+ builder.OpenComponent>(2);
+ builder.AddAttribute(3, "IsFixed", true);
+ builder.AddAttribute(4, "Value", _context);
+ builder.AddAttribute(5, "ChildContent", ChildContent);
+ builder.CloseComponent();
+ }
+
+ ///
+ protected override bool TryParseValueFromString(string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
+ => this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage);
+ }
+}
diff --git a/src/Components/Web/src/Forms/InputSelect.cs b/src/Components/Web/src/Forms/InputSelect.cs
index c5e6e54a943c..b7d5dd702552 100644
--- a/src/Components/Web/src/Forms/InputSelect.cs
+++ b/src/Components/Web/src/Forms/InputSelect.cs
@@ -1,9 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-using System;
using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
using Microsoft.AspNetCore.Components.Rendering;
namespace Microsoft.AspNetCore.Components.Forms
@@ -13,8 +11,6 @@ namespace Microsoft.AspNetCore.Components.Forms
///
public class InputSelect : InputBase
{
- private static readonly Type? _nullableUnderlyingType = Nullable.GetUnderlyingType(typeof(TValue));
-
///
/// Gets or sets the child content to be rendering inside the select element.
///
@@ -34,31 +30,6 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
///
protected override bool TryParseValueFromString(string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
- {
- if (typeof(TValue) == typeof(string))
- {
- result = (TValue)(object?)value;
- validationErrorMessage = null;
- return true;
- }
- else if (typeof(TValue).IsEnum || (_nullableUnderlyingType != null && _nullableUnderlyingType.IsEnum))
- {
- var success = BindConverter.TryConvertTo(value, CultureInfo.CurrentCulture, out var parsedValue);
- if (success)
- {
- result = parsedValue;
- validationErrorMessage = null;
- return true;
- }
- else
- {
- result = default;
- validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
- return false;
- }
- }
-
- throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(TValue)}'.");
- }
+ => this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage);
}
}
diff --git a/src/Components/Web/src/Properties/AssemblyInfo.cs b/src/Components/Web/src/Properties/AssemblyInfo.cs
index 2741560028c3..891ea5326c15 100644
--- a/src/Components/Web/src/Properties/AssemblyInfo.cs
+++ b/src/Components/Web/src/Properties/AssemblyInfo.cs
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Web.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/Components/Web/test/Forms/InputRadioTest.cs b/src/Components/Web/test/Forms/InputRadioTest.cs
new file mode 100644
index 000000000000..1447a16316f3
--- /dev/null
+++ b/src/Components/Web/test/Forms/InputRadioTest.cs
@@ -0,0 +1,138 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.Rendering;
+using Microsoft.AspNetCore.Components.RenderTree;
+using Microsoft.AspNetCore.Components.Test.Helpers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Components.Forms
+{
+ public class InputRadioTest
+ {
+ [Fact]
+ public async Task ThrowsOnFirstRenderIfInputRadioHasNoGroup()
+ {
+ var model = new TestModel();
+ var rootComponent = new TestInputRadioHostComponent
+ {
+ EditContext = new EditContext(model),
+ InnerContent = RadioButtonsWithoutGroup(null)
+ };
+
+ var ex = await Assert.ThrowsAsync(() => RenderAndGetTestInputComponentAsync(rootComponent));
+ Assert.Contains($"must have an ancestor", ex.Message);
+ }
+
+ [Fact]
+ public async Task GroupGeneratesNameGuidWhenInvalidNameSupplied()
+ {
+ var model = new TestModel();
+ var rootComponent = new TestInputRadioHostComponent
+ {
+ EditContext = new EditContext(model),
+ InnerContent = RadioButtonsWithGroup(null, () => model.TestEnum)
+ };
+
+ var inputRadioComponents = await RenderAndGetTestInputComponentAsync(rootComponent);
+
+ Assert.All(inputRadioComponents, inputRadio => Assert.True(Guid.TryParseExact(inputRadio.GroupName, "N", out _)));
+ }
+
+ [Fact]
+ public async Task RadioInputContextExistsWhenValidNameSupplied()
+ {
+ var groupName = "group";
+ var model = new TestModel();
+ var rootComponent = new TestInputRadioHostComponent
+ {
+ EditContext = new EditContext(model),
+ InnerContent = RadioButtonsWithGroup(groupName, () => model.TestEnum)
+ };
+
+ var inputRadioComponents = await RenderAndGetTestInputComponentAsync(rootComponent);
+
+ Assert.All(inputRadioComponents, inputRadio => Assert.Equal(groupName, inputRadio.GroupName));
+ }
+
+ private static RenderFragment RadioButtonsWithoutGroup(string name) => (builder) =>
+ {
+ foreach (var selectedValue in (TestEnum[])Enum.GetValues(typeof(TestEnum)))
+ {
+ builder.OpenComponent(0);
+ builder.AddAttribute(1, "Name", name);
+ builder.AddAttribute(2, "Value", selectedValue);
+ builder.CloseComponent();
+ }
+ };
+
+ private static RenderFragment RadioButtonsWithGroup(string name, Expression> valueExpression) => (builder) =>
+ {
+ builder.OpenComponent>(0);
+ builder.AddAttribute(1, "Name", name);
+ builder.AddAttribute(2, "ValueExpression", valueExpression);
+ builder.AddAttribute(2, "ChildContent", new RenderFragment((childBuilder) =>
+ {
+ foreach (var value in (TestEnum[])Enum.GetValues(typeof(TestEnum)))
+ {
+ childBuilder.OpenComponent(0);
+ childBuilder.AddAttribute(1, "Value", value);
+ childBuilder.CloseComponent();
+ }
+ }));
+
+ builder.CloseComponent();
+ };
+
+ private static IEnumerable FindInputRadioComponents(CapturedBatch batch)
+ => batch.ReferenceFrames
+ .Where(f => f.FrameType == RenderTreeFrameType.Component)
+ .Select(f => f.Component)
+ .OfType();
+
+ private static async Task> RenderAndGetTestInputComponentAsync(TestInputRadioHostComponent rootComponent)
+ {
+ var testRenderer = new TestRenderer();
+ var componentId = testRenderer.AssignRootComponentId(rootComponent);
+ await testRenderer.RenderRootComponentAsync(componentId);
+ return FindInputRadioComponents(testRenderer.Batches.Single());
+ }
+
+ private enum TestEnum
+ {
+ One,
+ Two,
+ Three
+ }
+
+ private class TestModel
+ {
+ public TestEnum TestEnum { get; set; }
+ }
+
+ private class TestInputRadio : InputRadio
+ {
+ public string GroupName => Context.GroupName;
+ }
+
+ private class TestInputRadioHostComponent : AutoRenderComponent
+ {
+ public EditContext EditContext { get; set; }
+
+ public RenderFragment InnerContent { get; set; }
+
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenComponent>(0);
+ builder.AddAttribute(1, "Value", EditContext);
+ builder.AddAttribute(2, "ChildContent", InnerContent);
+ builder.CloseComponent();
+ }
+ }
+ }
+}
diff --git a/src/Components/Web/test/Forms/InputSelectTest.cs b/src/Components/Web/test/Forms/InputSelectTest.cs
index 7c49fe03be56..65c3351e5eea 100644
--- a/src/Components/Web/test/Forms/InputSelectTest.cs
+++ b/src/Components/Web/test/Forms/InputSelectTest.cs
@@ -90,6 +90,88 @@ public async Task ParsesCurrentValueWhenUsingNullableEnumWithEmptyValue()
Assert.Null(inputSelectComponent.CurrentValue);
}
+ // See: https://github.com/dotnet/aspnetcore/issues/9939
+ [Fact]
+ public async Task ParsesCurrentValueWhenUsingNotNullableGuid()
+ {
+ // Arrange
+ var model = new TestModel();
+ var rootComponent = new TestInputSelectHostComponent
+ {
+ EditContext = new EditContext(model),
+ ValueExpression = () => model.NotNullableGuid
+ };
+ var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
+
+ // Act
+ var guid = Guid.NewGuid();
+ inputSelectComponent.CurrentValueAsString = guid.ToString();
+
+ // Assert
+ Assert.Equal(guid, inputSelectComponent.CurrentValue);
+ }
+
+ // See: https://github.com/dotnet/aspnetcore/issues/9939
+ [Fact]
+ public async Task ParsesCurrentValueWhenUsingNullableGuid()
+ {
+ // Arrange
+ var model = new TestModel();
+ var rootComponent = new TestInputSelectHostComponent
+ {
+ EditContext = new EditContext(model),
+ ValueExpression = () => model.NullableGuid
+ };
+ var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
+
+ // Act
+ var guid = Guid.NewGuid();
+ inputSelectComponent.CurrentValueAsString = guid.ToString();
+
+ // Assert
+ Assert.Equal(guid, inputSelectComponent.CurrentValue);
+ }
+
+ // See: https://github.com/dotnet/aspnetcore/pull/19562
+ [Fact]
+ public async Task ParsesCurrentValueWhenUsingNotNullableInt()
+ {
+ // Arrange
+ var model = new TestModel();
+ var rootComponent = new TestInputSelectHostComponent
+ {
+ EditContext = new EditContext(model),
+ ValueExpression = () => model.NotNullableInt
+ };
+ var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
+
+ // Act
+ inputSelectComponent.CurrentValueAsString = "42";
+
+ // Assert
+ Assert.Equal(42, inputSelectComponent.CurrentValue);
+ }
+
+ // See: https://github.com/dotnet/aspnetcore/pull/19562
+ [Fact]
+ public async Task ParsesCurrentValueWhenUsingNullableInt()
+ {
+ // Arrange
+ var model = new TestModel();
+ var rootComponent = new TestInputSelectHostComponent
+ {
+ EditContext = new EditContext(model),
+ ValueExpression = () => model.NullableInt
+ };
+ var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
+
+ // Act
+ inputSelectComponent.CurrentValueAsString = "42";
+
+ // Assert
+ Assert.Equal(42, inputSelectComponent.CurrentValue);
+ }
+
private static TestInputSelect FindInputSelectComponent(CapturedBatch batch)
=> batch.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Component)
@@ -117,6 +199,14 @@ class TestModel
public TestEnum NotNullableEnum { get; set; }
public TestEnum? NullableEnum { get; set; }
+
+ public Guid NotNullableGuid { get; set; }
+
+ public Guid? NullableGuid { get; set; }
+
+ public int NotNullableInt { get; set; }
+
+ public int? NullableInt { get; set; }
}
class TestInputSelect : InputSelect
diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs
index 90d298502d78..15495dc7da87 100644
--- a/src/Components/test/E2ETest/Tests/FormsTest.cs
+++ b/src/Components/test/E2ETest/Tests/FormsTest.cs
@@ -301,6 +301,68 @@ public void InputCheckboxInteractsWithEditContext()
Browser.Equal(new[] { "Must accept terms", "Must not be evil" }, messagesAccessor);
}
+ [Fact]
+ public void InputRadioGroupWithoutNameInteractsWithEditContext()
+ {
+ var appElement = MountTypicalValidationComponent();
+ var airlineInputs = appElement.FindElement(By.ClassName("airline")).FindElements(By.TagName("input"));
+ var unknownAirlineInput = airlineInputs.First(i => i.GetAttribute("value").Equals("Unknown"));
+ var bestAirlineInput = airlineInputs.First(i => i.GetAttribute("value").Equals("BestAirline"));
+ var messagesAccessor = CreateValidationMessagesAccessor(appElement);
+
+ // Validate unselected inputs
+ Assert.All(airlineInputs.Where(i => i != unknownAirlineInput), i => Browser.False(() => i.Selected));
+
+ // Validate selected inputs
+ Browser.True(() => unknownAirlineInput.Selected);
+
+ // InputRadio emits additional attributes
+ Browser.True(() => unknownAirlineInput.GetAttribute("extra").Equals("additional"));
+
+ // Validates on edit
+ Assert.All(airlineInputs, i => Browser.Equal("valid", () => i.GetAttribute("class")));
+ bestAirlineInput.Click();
+ Assert.All(airlineInputs, i => Browser.Equal("modified valid", () => i.GetAttribute("class")));
+
+ // Can become invalid
+ unknownAirlineInput.Click();
+ Assert.All(airlineInputs, i => Browser.Equal("modified invalid", () => i.GetAttribute("class")));
+ Browser.Equal(new[] { "Pick a valid airline." }, messagesAccessor);
+ }
+
+ [Fact]
+ public void InputRadioGroupsWithNamesNestedInteractWithEditContext()
+ {
+ var appElement = MountTypicalValidationComponent();
+ var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]"));
+ var group = appElement.FindElement(By.ClassName("nested-radio-group"));
+ var countryInputs = group.FindElements(By.Name("country"));
+ var colorInputs = group.FindElements(By.Name("color"));
+
+ // Validate group counts
+ Assert.Equal(3, countryInputs.Count);
+ Assert.Equal(4, colorInputs.Count);
+
+ // Validate unselected inputs
+ Assert.All(countryInputs, i => Browser.False(() => i.Selected));
+ Assert.All(colorInputs, i => Browser.False(() => i.Selected));
+
+ // Invalidates on submit
+ Assert.All(countryInputs, i => Browser.Equal("valid", () => i.GetAttribute("class")));
+ Assert.All(colorInputs, i => Browser.Equal("valid", () => i.GetAttribute("class")));
+ submitButton.Click();
+ Assert.All(countryInputs, i => Browser.Equal("invalid", () => i.GetAttribute("class")));
+ Assert.All(colorInputs, i => Browser.Equal("invalid", () => i.GetAttribute("class")));
+
+ // Validates on edit
+ countryInputs.First().Click();
+ Assert.All(countryInputs, i => Browser.Equal("modified valid", () => i.GetAttribute("class")));
+ Assert.All(colorInputs, i => Browser.Equal("invalid", () => i.GetAttribute("class")));
+
+ colorInputs.First().Click();
+ Assert.All(colorInputs, i => Browser.Equal("modified valid", () => i.GetAttribute("class")));
+ }
+
[Fact]
public void CanWireUpINotifyPropertyChangedToEditContext()
{
diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
index 69c2f585017c..4cf0a9d80f42 100644
--- a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
+++ b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
@@ -40,6 +40,32 @@
@person.TicketClass
+
+
+ Airline:
+
+ @foreach (var airline in (Airline[])Enum.GetValues(typeof(Airline)))
+ {
+
+ @airline.ToString();
+
+ }
+
+
+
+ Pick one color and one country:
+
+
+ red
+ japan
+ green
+ yemen
+ blue
+ latvia
+ orange
+
+
+
Accepts terms:
@@ -109,11 +135,27 @@
[Required, EnumDataType(typeof(TicketClass))]
public TicketClass TicketClass { get; set; }
+ [Required]
+ [Range(typeof(Airline), nameof(Airline.BestAirline), nameof(Airline.NoNameAirline), ErrorMessage = "Pick a valid airline.")]
+ public Airline Airline { get; set; } = Airline.Unknown;
+
+ [Required, EnumDataType(typeof(Color))]
+ public Color? FavoriteColor { get; set; } = null;
+
+ [Required, EnumDataType(typeof(Country))]
+ public Country? Country { get; set; } = null;
+
public string Username { get; set; }
}
enum TicketClass { Economy, Premium, First }
+ enum Airline { BestAirline, CoolAirline, NoNameAirline, Unknown }
+
+ enum Color { Red, Green, Blue, Orange }
+
+ enum Country { Japan, Yemen, Latvia }
+
List submissionLog = new List(); // So we can assert about the callbacks
void HandleValidSubmit()