Skip to content

Commit 9ec7bd0

Browse files
SteveSandersonMSwtgodbe
authored andcommitted
Prevent submission of EditForm after disposal
Prevent submission of EditForm after disposal
1 parent 271bf33 commit 9ec7bd0

File tree

6 files changed

+61
-0
lines changed

6 files changed

+61
-0
lines changed

src/Components/Components/src/ComponentBase.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,5 +329,8 @@ Task IHandleAfterRender.OnAfterRenderAsync()
329329
// have to use "async void" and do their own exception handling in
330330
// the case where they want to start an async task.
331331
}
332+
333+
// Exists for 6.0/7.0 patch only. A different solution is used from .NET 8 onwards.
334+
internal bool IsComponentDisposed() => _renderHandle.IsComponentDisposed();
332335
}
333336
}

src/Components/Components/src/RenderHandle.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ public Dispatcher Dispatcher
5252
internal bool IsRendererDisposed => _renderer?.Disposed
5353
?? throw new InvalidOperationException("No renderer has been initialized.");
5454

55+
// Exists for 6.0/7.0 patch only. A different solution is used from .NET 8 onwards.
56+
internal bool IsComponentDisposed() => _renderer?.IsComponentDisposed(_componentId) ?? false;
57+
5558
/// <summary>
5659
/// Notifies the renderer that the component should be rendered.
5760
/// </summary>

src/Components/Components/src/RenderTree/Renderer.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,5 +1114,9 @@ public async ValueTask DisposeAsync()
11141114
}
11151115
}
11161116
}
1117+
1118+
// Exists for 6.0/7.0 patch only. A different solution is used from .NET 8 onwards.
1119+
internal bool IsComponentDisposed(int componentId)
1120+
=> !_componentStateById.ContainsKey(componentId);
11171121
}
11181122
}

src/Components/Web/src/Forms/EditForm.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ private async Task HandleSubmitAsync()
139139
{
140140
Debug.Assert(_editContext != null);
141141

142+
// Exists for 6.0/7.0 patch only. A different solution is used from .NET 8 onwards.
143+
if (IsComponentDisposed())
144+
{
145+
return;
146+
}
147+
142148
if (OnSubmit.HasDelegate)
143149
{
144150
// When using OnSubmit, the developer takes control of the validation lifecycle

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,39 @@ public void InputRangeAttributeOrderDoesNotAffectValue()
929929
Browser.Equal("210", () => rangeWithValueLast.GetDomProperty("value"));
930930
}
931931

932+
[Fact]
933+
public async Task CannotSubmitEditFormSynchronouslyAfterItWasRemoved()
934+
{
935+
var appElement = MountSimpleValidationComponent();
936+
937+
var submitButtonFinder = By.CssSelector("button[type=submit]");
938+
Browser.Exists(submitButtonFinder);
939+
940+
// Remove the form then immediately also submit it, so the server receives both
941+
// the 'remove' and 'submit' commands (in that order) before it updates the UI
942+
appElement.FindElement(By.Id("remove-form")).Click();
943+
944+
try
945+
{
946+
appElement.FindElement(submitButtonFinder).Click();
947+
}
948+
catch (NoSuchElementException)
949+
{
950+
// This should happen on WebAssembly because the form will be removed synchronously
951+
// That means the test has passed
952+
return;
953+
}
954+
955+
// Wait for the removal to complete, which is intentionally delayed to ensure
956+
// this test can submit a second instruction before the first is processed. Then
957+
// wait a bit more to be really sure the second instruction was processed.
958+
Browser.DoesNotExist(submitButtonFinder);
959+
await Task.Delay(1000);
960+
961+
// Verify that the form submit event was not processed
962+
Browser.DoesNotExist(By.Id("last-callback"));
963+
}
964+
932965
private Func<string[]> CreateValidationMessagesAccessor(IWebElement appElement)
933966
{
934967
return () => appElement.FindElements(By.ClassName("validation-message"))

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
@using System.ComponentModel.DataAnnotations
22
@using Microsoft.AspNetCore.Components.Forms
33

4+
@if (!removeForm)
5+
{
46
<EditForm Model="@this" OnValidSubmit="@HandleValidSubmit" OnInvalidSubmit="@HandleInvalidSubmit" autocomplete="off">
57
<DataAnnotationsValidator />
68

@@ -22,16 +24,20 @@
2224
</ul>
2325

2426
</EditForm>
27+
}
2528

2629
@if (lastCallback != null)
2730
{
2831
<span id="last-callback">@lastCallback</span>
2932
}
3033

34+
<p><button id="remove-form" @onclick="RemoveForm">Remove form</button></p>
35+
3136
@code {
3237
protected virtual bool UseExperimentalValidator => false;
3338

3439
string lastCallback;
40+
bool removeForm;
3541

3642
[Required(ErrorMessage = "Please choose a username")]
3743
public string UserName { get; set; }
@@ -49,4 +55,10 @@
4955
{
5056
lastCallback = "OnInvalidSubmit";
5157
}
58+
59+
void RemoveForm()
60+
{
61+
removeForm = true;
62+
Thread.Sleep(1000); // To ensure we can dispatch another event before this completes
63+
}
5264
}

0 commit comments

Comments
 (0)