Skip to content

Commit cbfad02

Browse files
committed
Binding support for 'bool' values with InputRadioGroup and InputSelect (#35318)
1 parent 2becbdd commit cbfad02

File tree

6 files changed

+159
-10
lines changed

6 files changed

+159
-10
lines changed

src/Components/Components.slnf

+12-3
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,27 @@
4646
"src\\Components\\test\\E2ETestMigration\\Microsoft.AspNetCore.Components.Migration.E2ETests.csproj",
4747
"src\\Components\\test\\E2ETest\\Microsoft.AspNetCore.Components.E2ETests.csproj",
4848
"src\\Components\\test\\testassets\\BasicTestApp\\BasicTestApp.csproj",
49+
"src\\Components\\test\\testassets\\ComponentsApp.App\\ComponentsApp.App.csproj",
4950
"src\\Components\\test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj",
5051
"src\\Components\\test\\testassets\\GlobalizationWasmApp\\GlobalizationWasmApp.csproj",
52+
"src\\Components\\test\\testassets\\LazyTestContentPackage\\LazyTestContentPackage.csproj",
5153
"src\\Components\\test\\testassets\\TestContentPackage\\TestContentPackage.csproj",
5254
"src\\Components\\test\\testassets\\TestServer\\Components.TestServer.csproj",
5355
"src\\DataProtection\\Abstractions\\src\\Microsoft.AspNetCore.DataProtection.Abstractions.csproj",
5456
"src\\DataProtection\\Cryptography.Internal\\src\\Microsoft.AspNetCore.Cryptography.Internal.csproj",
5557
"src\\DataProtection\\Cryptography.KeyDerivation\\src\\Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj",
5658
"src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj",
59+
"src\\DataProtection\\Extensions\\src\\Microsoft.AspNetCore.DataProtection.Extensions.csproj",
5760
"src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj",
61+
"src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj",
5862
"src\\Features\\JsonPatch\\src\\Microsoft.AspNetCore.JsonPatch.csproj",
63+
"src\\FileProviders\\Embedded\\src\\Microsoft.Extensions.FileProviders.Embedded.csproj",
5964
"src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
6065
"src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
6166
"src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj",
6267
"src\\Html.Abstractions\\src\\Microsoft.AspNetCore.Html.Abstractions.csproj",
6368
"src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj",
6469
"src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj",
65-
"src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj",
6670
"src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
6771
"src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
6872
"src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
@@ -79,6 +83,8 @@
7983
"src\\Identity\\Extensions.Stores\\src\\Microsoft.Extensions.Identity.Stores.csproj",
8084
"src\\Identity\\UI\\src\\Microsoft.AspNetCore.Identity.UI.csproj",
8185
"src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj",
86+
"src\\Localization\\Abstractions\\src\\Microsoft.Extensions.Localization.Abstractions.csproj",
87+
"src\\Localization\\Localization\\src\\Microsoft.Extensions.Localization.csproj",
8288
"src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj",
8389
"src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj",
8490
"src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj",
@@ -96,13 +102,15 @@
96102
"src\\Mvc\\Mvc.Core\\src\\Microsoft.AspNetCore.Mvc.Core.csproj",
97103
"src\\Mvc\\Mvc.Cors\\src\\Microsoft.AspNetCore.Mvc.Cors.csproj",
98104
"src\\Mvc\\Mvc.DataAnnotations\\src\\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj",
105+
"src\\Mvc\\Mvc.Formatters.Json\\src\\Microsoft.AspNetCore.Mvc.Formatters.Json.csproj",
99106
"src\\Mvc\\Mvc.Localization\\src\\Microsoft.AspNetCore.Mvc.Localization.csproj",
100107
"src\\Mvc\\Mvc.NewtonsoftJson\\src\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj",
101108
"src\\Mvc\\Mvc.RazorPages\\src\\Microsoft.AspNetCore.Mvc.RazorPages.csproj",
102109
"src\\Mvc\\Mvc.Razor\\src\\Microsoft.AspNetCore.Mvc.Razor.csproj",
103110
"src\\Mvc\\Mvc.TagHelpers\\src\\Microsoft.AspNetCore.Mvc.TagHelpers.csproj",
104111
"src\\Mvc\\Mvc.ViewFeatures\\src\\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj",
105112
"src\\Mvc\\Mvc\\src\\Microsoft.AspNetCore.Mvc.csproj",
113+
"src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj",
106114
"src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj",
107115
"src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj",
108116
"src\\Security\\Authentication\\Cookies\\src\\Microsoft.AspNetCore.Authentication.Cookies.csproj",
@@ -128,7 +136,8 @@
128136
"src\\SignalR\\common\\SignalR.Common\\src\\Microsoft.AspNetCore.SignalR.Common.csproj",
129137
"src\\SignalR\\server\\Core\\src\\Microsoft.AspNetCore.SignalR.Core.csproj",
130138
"src\\SignalR\\server\\SignalR\\src\\Microsoft.AspNetCore.SignalR.csproj",
131-
"src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj"
139+
"src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj",
140+
"src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
132141
]
133142
}
134-
}
143+
}

src/Components/Web/src/Forms/InputExtensions.cs

+45-7
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,61 @@ internal static class InputExtensions
1616
{
1717
try
1818
{
19-
if (BindConverter.TryConvertTo<TValue>(value, CultureInfo.CurrentCulture, out var parsedValue))
19+
// We special-case bool values because BindConverter reserves bool conversion for conditional attributes.
20+
if (typeof(TValue) == typeof(bool))
21+
{
22+
if (TryConvertToBool(value, out result))
23+
{
24+
validationErrorMessage = null;
25+
return true;
26+
}
27+
}
28+
else if (typeof(TValue) == typeof(bool?))
29+
{
30+
if (TryConvertToNullableBool(value, out result))
31+
{
32+
validationErrorMessage = null;
33+
return true;
34+
}
35+
}
36+
else if (BindConverter.TryConvertTo<TValue>(value, CultureInfo.CurrentCulture, out var parsedValue))
2037
{
2138
result = parsedValue;
2239
validationErrorMessage = null;
2340
return true;
2441
}
25-
else
26-
{
27-
result = default;
28-
validationErrorMessage = $"The {input.DisplayName ?? input.FieldIdentifier.FieldName} field is not valid.";
29-
return false;
30-
}
42+
43+
result = default;
44+
validationErrorMessage = $"The {input.DisplayName ?? input.FieldIdentifier.FieldName} field is not valid.";
45+
return false;
3146
}
3247
catch (InvalidOperationException ex)
3348
{
3449
throw new InvalidOperationException($"{input.GetType()} does not support the type '{typeof(TValue)}'.", ex);
3550
}
3651
}
52+
53+
private static bool TryConvertToBool<TValue>(string? value, out TValue result)
54+
{
55+
if (bool.TryParse(value, out var @bool))
56+
{
57+
result = (TValue)(object)@bool;
58+
return true;
59+
}
60+
61+
result = default!;
62+
return false;
63+
}
64+
65+
private static bool TryConvertToNullableBool<TValue>(string? value, out TValue result)
66+
{
67+
if (string.IsNullOrEmpty(value))
68+
{
69+
result = default!;
70+
return true;
71+
}
72+
73+
return TryConvertToBool(value, out result);
74+
}
3775
}
3876
}

src/Components/Web/src/Forms/InputSelect.cs

+16
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
6464
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
6565
=> this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage);
6666

67+
/// <inheritdoc />
68+
protected override string? FormatValueAsString(TValue? value)
69+
{
70+
// We special-case bool values because BindConverter reserves bool conversion for conditional attributes.
71+
if (typeof(TValue) == typeof(bool))
72+
{
73+
return (bool)(object)value! ? "true" : "false";
74+
}
75+
else if (typeof(TValue) == typeof(bool?))
76+
{
77+
return value is not null && (bool)(object)value ? "true" : "false";
78+
}
79+
80+
return base.FormatValueAsString(value);
81+
}
82+
6783
private void SetCurrentValueAsStringArray(string?[]? value)
6884
{
6985
CurrentValue = BindConverter.TryConvertTo<TValue>(value, CultureInfo.CurrentCulture, out var result)

src/Components/Web/src/PublicAPI.Unshipped.txt

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Microsoft.AspNetCore.Components.Web.PageTitle.ChildContent.set -> void
6060
Microsoft.AspNetCore.Components.Web.PageTitle.PageTitle() -> void
6161
override Microsoft.AspNetCore.Components.Forms.InputDate<TValue>.OnParametersSet() -> void
6262
abstract Microsoft.AspNetCore.Components.RenderTree.WebRenderer.AttachRootComponentToBrowser(int componentId, string! domElementSelector) -> void
63+
override Microsoft.AspNetCore.Components.Forms.InputSelect<TValue>.FormatValueAsString(TValue? value) -> string?
6364
override Microsoft.AspNetCore.Components.RenderTree.WebRenderer.Dispose(bool disposing) -> void
6465
override Microsoft.AspNetCore.Components.Routing.FocusOnNavigate.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task!
6566
override Microsoft.AspNetCore.Components.Routing.FocusOnNavigate.OnParametersSet() -> void

src/Components/test/E2ETest/Tests/FormsTest.cs

+64
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,34 @@ public void InputSelectInteractsWithEditContext()
355355
Browser.Equal(new[] { "The TicketClass field is not valid." }, messagesAccessor);
356356
}
357357

358+
[Fact]
359+
public void InputSelectInteractsWithEditContext_BoolValues()
360+
{
361+
var appElement = MountTypicalValidationComponent();
362+
var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("select-bool-values")).FindElement(By.TagName("select")));
363+
var select = ticketClassInput.WrappedElement;
364+
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
365+
366+
// Invalidates on edit
367+
Browser.Equal("valid", () => select.GetAttribute("class"));
368+
ticketClassInput.SelectByText("true");
369+
Browser.Equal("modified invalid", () => select.GetAttribute("class"));
370+
Browser.Equal(new[] { "77 + 33 = 100 is a false statement, unfortunately." }, messagesAccessor);
371+
372+
// Nullable conversion can fail
373+
ticketClassInput.SelectByText("(select)");
374+
Browser.Equal("modified invalid", () => select.GetAttribute("class"));
375+
Browser.Equal(new[]
376+
{
377+
"77 + 33 = 100 is a false statement, unfortunately.",
378+
"The IsSelectMathStatementTrue field is not valid."
379+
}, messagesAccessor);
380+
381+
// Can become valid
382+
ticketClassInput.SelectByText("false");
383+
Browser.Equal("modified valid", () => select.GetAttribute("class"));
384+
}
385+
358386
[Fact]
359387
public void InputSelectInteractsWithEditContext_MultipleAttribute()
360388
{
@@ -521,6 +549,42 @@ public void InputRadioGroupsWithNamesNestedInteractWithEditContext()
521549
IReadOnlyCollection<IWebElement> FindColorInputs() => group.FindElements(By.Name("color"));
522550
}
523551

552+
[Fact]
553+
public void InputRadioGroupWithBoolValuesInteractsWithEditContext()
554+
{
555+
var appElement = MountTypicalValidationComponent();
556+
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
557+
558+
// Validate selected inputs
559+
Browser.False(() => FindTrueInput().Selected);
560+
Browser.True(() => FindFalseInput().Selected);
561+
562+
// Validates on edit
563+
Browser.Equal("valid", () => FindTrueInput().GetAttribute("class"));
564+
Browser.Equal("valid", () => FindFalseInput().GetAttribute("class"));
565+
566+
FindTrueInput().Click();
567+
568+
Browser.Equal("modified valid", () => FindTrueInput().GetAttribute("class"));
569+
Browser.Equal("modified valid", () => FindFalseInput().GetAttribute("class"));
570+
571+
// Can become invalid
572+
FindFalseInput().Click();
573+
574+
Browser.Equal("modified invalid", () => FindTrueInput().GetAttribute("class"));
575+
Browser.Equal("modified invalid", () => FindFalseInput().GetAttribute("class"));
576+
Browser.Equal(new[] { "7 * 3 = 21 is a true statement." }, messagesAccessor);
577+
578+
IReadOnlyCollection<IWebElement> FindInputs()
579+
=> appElement.FindElement(By.ClassName("radio-group-bool-values")).FindElements(By.TagName("input"));
580+
581+
IWebElement FindTrueInput()
582+
=> FindInputs().First(i => string.Equals("True", i.GetAttribute("value")));
583+
584+
IWebElement FindFalseInput()
585+
=> FindInputs().First(i => string.Equals("False", i.GetAttribute("value")));
586+
}
587+
524588
[Fact]
525589
public void CanWireUpINotifyPropertyChangedToEditContext()
526590
{

src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor

+21
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@
7070
</InputSelect>
7171
<span>@string.Join(", ", person.HostileStrings)</span>
7272
</p>
73+
<p class="select-bool-values">
74+
T/F: 77 + 33 = 100<br>
75+
<InputSelect @bind-Value="person.IsSelectMathStatementTrue">
76+
<option>(select)</option>
77+
<option value="true">true</option>
78+
<option value="false">false</option>
79+
</InputSelect>
80+
</p>
7381
<p class="airline">
7482
<InputRadioGroup @bind-Value="person.Airline">
7583
Airline:
@@ -96,6 +104,13 @@
96104
</InputRadioGroup>
97105
</InputRadioGroup>
98106
</p>
107+
<p class="radio-group-bool-values">
108+
T/F: 7 * 3 = 21<br>
109+
<InputRadioGroup @bind-Value="person.IsRadioMathStatementTrue">
110+
<InputRadio Value="true" />true<br>
111+
<InputRadio Value="false" />false<br>
112+
</InputRadioGroup>
113+
</p>
99114
<p class="socks">
100115
Socks color: <InputText @bind-Value="person.SocksColor" />
101116
</p>
@@ -188,6 +203,12 @@
188203
[Required, EnumDataType(typeof(Country))]
189204
public Country? Country { get; set; } = null;
190205

206+
[Required, Range(typeof(bool), "false", "false", ErrorMessage = "77 + 33 = 100 is a false statement, unfortunately.")]
207+
public bool? IsSelectMathStatementTrue { get; set; } = null;
208+
209+
[Required, Range(typeof(bool), "true", "true", ErrorMessage = "7 * 3 = 21 is a true statement.")]
210+
public bool IsRadioMathStatementTrue { get; set; } = false;
211+
191212
[Required, StringLength(10), CustomValidationClassName(Valid = "valid-socks", Invalid = "invalid-socks")]
192213
public string SocksColor { get; set; }
193214

0 commit comments

Comments
 (0)