Prechádzať zdrojové kódy

[Blazor] Blazor InputRadio is broken when parent component implements IHandleEvent (#53245)

The CurrentValue property in the context was being initialized the first time but was not being updated afterwards even though it changed on the InputRadioGroup. That meant that InputRadio components would not see the value immediately as part of the first render.

This was working on other scenarios because the default implementation in ComponentBase will trigger a re-render of the parent component, which in turn will trigger a re-render of the InputGroup component (since it receives a render fragment) and at that point, the component would update the parameter and the re-render of the individual InputRadio elements would work.

When someone implements IHandleEvent, that doesn't necessarily happen, hence the reason for the bug. The fix is to avoid updating the value in the context as a separate step and instead making the context reflect the value from the input right away.
Javier Calvarro Nelson 2 rokov pred
rodič
commit
6cfc445cba

+ 9 - 0
src/Components/Web/src/Forms/IInputRadioValueProvider.cs

@@ -0,0 +1,9 @@
+// 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 interface IInputRadioValueProvider
+{
+    public object? CurrentValue { get; }
+}

+ 5 - 7
src/Components/Web/src/Forms/InputRadioContext.cs

@@ -8,21 +8,19 @@ namespace Microsoft.AspNetCore.Components.Forms;
 /// </summary>
 internal sealed class InputRadioContext
 {
+    private readonly IInputRadioValueProvider _valueProvider;
+
     public InputRadioContext? ParentContext { get; }
     public EventCallback<ChangeEventArgs> ChangeEventCallback { get; }
+    public object? CurrentValue => _valueProvider.CurrentValue;
 
     // Mutable properties that may change any time an InputRadioGroup is rendered
     public string? GroupName { get; set; }
-    public object? CurrentValue { get; set; }
     public string? FieldClass { get; set; }
 
-    /// <summary>
-    /// Instantiates a new <see cref="InputRadioContext" />.
-    /// </summary>
-    /// <param name="parentContext">The parent context, if any.</param>
-    /// <param name="changeEventCallback">The event callback to be invoked when the selected value is changed.</param>
-    public InputRadioContext(InputRadioContext? parentContext, EventCallback<ChangeEventArgs> changeEventCallback)
+    public InputRadioContext(IInputRadioValueProvider valueProvider, InputRadioContext? parentContext, EventCallback<ChangeEventArgs> changeEventCallback)
     {
+        _valueProvider = valueProvider;
         ParentContext = parentContext;
         ChangeEventCallback = changeEventCallback;
     }

+ 5 - 3
src/Components/Web/src/Forms/InputRadioGroup.cs

@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Components.Forms;
 /// <summary>
 /// Groups child <see cref="InputRadio{TValue}"/> components.
 /// </summary>
-public class InputRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : InputBase<TValue>
+public class InputRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : InputBase<TValue>, IInputRadioValueProvider
 {
     private readonly string _defaultGroupName = Guid.NewGuid().ToString("N");
     private InputRadioContext? _context;
@@ -27,6 +27,8 @@ public class InputRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemb
 
     [CascadingParameter] private InputRadioContext? CascadedContext { get; set; }
 
+    object? IInputRadioValueProvider.CurrentValue => CurrentValue;
+
     /// <inheritdoc />
     protected override void OnParametersSet()
     {
@@ -34,7 +36,7 @@ public class InputRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemb
         if (_context is null)
         {
             var changeEventCallback = EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValueAsString = __value, CurrentValueAsString);
-            _context = new InputRadioContext(CascadedContext, changeEventCallback);
+            _context = new InputRadioContext(this, CascadedContext, changeEventCallback);
         }
         else if (_context.ParentContext != CascadedContext)
         {
@@ -59,7 +61,7 @@ public class InputRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemb
             // Otherwise, just use a GUID to disambiguate this group's radio inputs from any others on the page.
             _context.GroupName = _defaultGroupName;
         }
-        _context.CurrentValue = CurrentValue;
+
         _context.FieldClass = EditContext?.FieldCssClass(FieldIdentifier);
     }
 

+ 24 - 0
src/Components/test/E2ETest/Tests/FormsTest.cs

@@ -844,6 +844,30 @@ public class FormsTest : ServerTestBase<ToggleExecutionModeServerFixture<Program
         Browser.Equal("False", () => tuesday.GetDomProperty("checked"));
     }
 
+    [Theory]
+    [InlineData(0)]
+    [InlineData(2)]
+    public void InputRadioGroupWorksWithParentImplementingIHandleEvent(int n)
+    {
+        Browser.Url = new UriBuilder(Browser.Url) { Query = ($"?n={n}") }.ToString();
+        var appElement = Browser.MountTestComponent<InputRadioParentImplementsIHandleEvent>();
+        var zero = appElement.FindElement(By.Id("inputradiogroup-parent-ihandle-event-0"));
+        var one = appElement.FindElement(By.Id("inputradiogroup-parent-ihandle-event-1"));
+
+        Browser.Equal(n == 0 ? "True" : "False", () => zero.GetDomProperty("checked"));
+        Browser.Equal("False", () => one.GetDomProperty("checked"));
+
+        // Observe the changes after a click
+        one.Click();
+        Browser.Equal("False", () => zero.GetDomProperty("checked"));
+        Browser.Equal("True", () => one.GetDomProperty("checked"));
+
+        // Ensure other options can be selected
+        zero.Click();
+        Browser.Equal("False", () => one.GetDomProperty("checked"));
+        Browser.Equal("True", () => zero.GetDomProperty("checked"));
+    }
+
     [Fact]
     public void InputSelectWorksWithMutatingSetter()
     {

+ 14 - 0
src/Components/test/testassets/BasicTestApp/FormsTest/InputRadioParentImplementsIHandleEvent.razor

@@ -0,0 +1,14 @@
+@using Microsoft.AspNetCore.Components.Forms
+@implements IHandleEvent
+
+<InputRadioGroup @bind-Value="N">
+  <InputRadio id="inputradiogroup-parent-ihandle-event-0" Value="0" />
+  <InputRadio id="inputradiogroup-parent-ihandle-event-1" Value="1" />
+</InputRadioGroup>
+
+@code {
+
+  [SupplyParameterFromQuery(Name = "n")] int? N { get; set; }
+
+  Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg) => callback.InvokeAsync(arg);
+}

+ 1 - 0
src/Components/test/testassets/BasicTestApp/Index.razor

@@ -49,6 +49,7 @@
         <option value="BasicTestApp.FormsTest.InputRangeComponent">Input range</option>
         <option value="BasicTestApp.FormsTest.InputsWithoutEditForm">Inputs without EditForm</option>
         <option value="BasicTestApp.FormsTest.InputsWithMutatingSetters">Inputs with mutating setters</option>
+        <option value="BasicTestApp.FormsTest.InputRadioParentImplementsIHandleEvent">Input Radio Parent Implements IHandleEvent</option>
         <option value="BasicTestApp.NavigateOnSubmit">Navigate to submit</option>
         <option value="BasicTestApp.GlobalizationBindCases">Globalization Bind Cases</option>
         <option value="BasicTestApp.GracefulTermination">Graceful Termination</option>