Skip to content

Commit b056b7a

Browse files
SteveSandersonMSwtgodbe
authored andcommitted
Prevent submission of EditForm after disposal
Prevent submission of EditForm after disposal
1 parent 9e447a7 commit b056b7a

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
@@ -326,4 +326,7 @@ Task IHandleAfterRender.OnAfterRenderAsync()
326326
// have to use "async void" and do their own exception handling in
327327
// the case where they want to start an async task.
328328
}
329+
330+
// Exists for 6.0/7.0 patch only. A different solution is used from .NET 8 onwards.
331+
internal bool IsComponentDisposed() => _renderHandle.IsComponentDisposed();
329332
}

src/Components/Components/src/RenderHandle.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,7 @@ private static void ThrowNotInitialized()
7070
{
7171
throw new InvalidOperationException("The render handle is not yet assigned.");
7272
}
73+
74+
// Exists for 6.0/7.0 patch only. A different solution is used from .NET 8 onwards.
75+
internal bool IsComponentDisposed() => _renderer?.IsComponentDisposed(_componentId) ?? false;
7376
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,4 +1119,8 @@ public async ValueTask DisposeAsync()
11191119
}
11201120
}
11211121
}
1122+
1123+
// Exists for 6.0/7.0 patch only. A different solution is used from .NET 8 onwards.
1124+
internal bool IsComponentDisposed(int componentId)
1125+
=> !_componentStateById.ContainsKey(componentId);
11221126
}

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

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

139+
// Exists for 6.0/7.0 patch only. A different solution is used from .NET 8 onwards.
140+
if (IsComponentDisposed())
141+
{
142+
return;
143+
}
144+
139145
if (OnSubmit.HasDelegate)
140146
{
141147
// 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
@@ -809,6 +809,39 @@ public void CanHaveModelLevelValidationErrors()
809809
Browser.Collection(logEntries, x => Assert.Equal("OnValidSubmit", x));
810810
}
811811

812+
[Fact]
813+
public async Task CannotSubmitEditFormSynchronouslyAfterItWasRemoved()
814+
{
815+
var appElement = MountSimpleValidationComponent();
816+
817+
var submitButtonFinder = By.CssSelector("button[type=submit]");
818+
Browser.Exists(submitButtonFinder);
819+
820+
// Remove the form then immediately also submit it, so the server receives both
821+
// the 'remove' and 'submit' commands (in that order) before it updates the UI
822+
appElement.FindElement(By.Id("remove-form")).Click();
823+
824+
try
825+
{
826+
appElement.FindElement(submitButtonFinder).Click();
827+
}
828+
catch (NoSuchElementException)
829+
{
830+
// This should happen on WebAssembly because the form will be removed synchronously
831+
// That means the test has passed
832+
return;
833+
}
834+
835+
// Wait for the removal to complete, which is intentionally delayed to ensure
836+
// this test can submit a second instruction before the first is processed. Then
837+
// wait a bit more to be really sure the second instruction was processed.
838+
Browser.DoesNotExist(submitButtonFinder);
839+
await Task.Delay(1000);
840+
841+
// Verify that the form submit event was not processed
842+
Browser.DoesNotExist(By.Id("last-callback"));
843+
}
844+
812845
private Func<string[]> CreateValidationMessagesAccessor(IWebElement appElement, string messageSelector = ".validation-message")
813846
{
814847
return () => appElement.FindElements(By.CssSelector(messageSelector))

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)