diff --git a/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs b/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs index 64d2a8d8f259..6b6775af4be3 100644 --- a/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs +++ b/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs @@ -58,39 +58,38 @@ public int Compare(FormKey x, FormKey y) { separatorX = x.Value.Span[currentXPos..].IndexOfAny('.', '['); separatorY = y.Value.Span[currentYPos..].IndexOfAny('.', '['); + int compare; if (separatorX == -1 && separatorY == -1) { - // no more segments, compare the remaining substrings + // Both x and y have no more segments, compare the remaining segments return MemoryExtensions.CompareTo(x.Value.Span[currentXPos..], y.Value.Span[currentYPos..], StringComparison.Ordinal); } else if (separatorX == -1) { - // x has no more segments, but y does, so x is less than y - return -1; + // x has no more segments, y has remaining segments + compare = MemoryExtensions.CompareTo(x.Value.Span[currentXPos..], y.Value.Span[currentYPos..][..separatorY], StringComparison.Ordinal); + if (compare == 0 && !checkPrefix) + { + return -1; + } + return compare; } else if (separatorY == -1) { - if (!checkPrefix) + // y has no more segments, x has remaining segments + compare = MemoryExtensions.CompareTo(x.Value.Span[currentXPos..][..separatorX], y.Value.Span[currentYPos..], StringComparison.Ordinal); + if (compare == 0 && !checkPrefix) { - // We are just sorting, so x is greater than y because it has more segments. return 1; } - - var match = MemoryExtensions.CompareTo( - x.Value.Span[currentXPos..][..separatorX], - y.Value.Span[currentYPos..], StringComparison.Ordinal); - - return match; + return compare; } - // both have segments, compare the segments - var segmentX = x.Value.Span[currentXPos..][..separatorX]; - var segmentY = y.Value.Span[currentYPos..][..separatorY]; - var compareResult = MemoryExtensions.CompareTo(segmentX, segmentY, StringComparison.Ordinal); - if (compareResult != 0) + compare = MemoryExtensions.CompareTo(x.Value.Span[currentXPos..][..separatorX], y.Value.Span[currentYPos..][..separatorY], StringComparison.Ordinal); + if (compare != 0) { - return compareResult; + return compare; } currentXPos += separatorX + 1; diff --git a/src/Components/Endpoints/test/Binding/PrefixResolverTests.cs b/src/Components/Endpoints/test/Binding/PrefixResolverTests.cs index 0b710c020706..479a621e5797 100644 --- a/src/Components/Endpoints/test/Binding/PrefixResolverTests.cs +++ b/src/Components/Endpoints/test/Binding/PrefixResolverTests.cs @@ -79,6 +79,40 @@ public void ContainsPrefix_HasEntries_NoMatch(string prefix) Assert.False(result); } + [Theory] + [InlineData("Model")] + [InlineData("Model.Model")] + [InlineData("Model[0].Model")] + public void ContainsPrefix_HasEntries(string prefix) + { + // Arrange - Simulating the exact scenario from debugging + var keys = new string[] { "__RequestVerificationToken", "_handler", "Model.Name", "Model.Model.Name", "Model[0].Model.Name", "Model[0].Name" }; + var container = new PrefixResolver(GetKeys(keys), keys.Length); + + // Act + var result = container.HasPrefix(prefix.AsMemory()); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("Model")] + [InlineData("Model.Model")] + [InlineData("Model[0].Model")] + public void ContainsPrefix_DoesNotHaveEntries(string prefix) + { + // Arrange - Simulating the exact scenario from debugging + var keys = new string[] { "__RequestVerificationToken", "_handler" }; + var container = new PrefixResolver(GetKeys(keys), keys.Length); + + // Act + var result = container.HasPrefix(prefix.AsMemory()); + + // Assert + Assert.False(result); + } + [Theory] [InlineData("a")] [InlineData("b")] diff --git a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs index d6dc438db468..fa3e1a287915 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs @@ -1422,6 +1422,16 @@ void AssertUiState(string expectedStringValue, bool expectedBoolValue) } } + [Fact] + public void EditFormRecursiveBinding() + { + GoTo("forms/recursive-edit-form"); + Browser.Equal("", () => Browser.Exists(By.Id("result-form")).Text); + Browser.Exists(By.Id("text-input")).SendKeys("John"); + Browser.Exists(By.Id("submit-button")).Click(); + Browser.Equal("John", () => Browser.Exists(By.Id("result-form")).Text); + } + [Fact] public void RadioButtonGetsResetAfterSubmittingEnhancedForm() { diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/RecursiveEditFormBinding.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/RecursiveEditFormBinding.razor new file mode 100644 index 000000000000..29b074e41d04 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/RecursiveEditFormBinding.razor @@ -0,0 +1,42 @@ +@page "/forms/recursive-edit-form" +@using Microsoft.AspNetCore.Components.Forms + +Home + + + + + + +

@ModelName

+ +@code { + string ModelName; + + [SupplyParameterFromForm(FormName = "RequestForm")] + private MyModel? Model { get; set; } + + private EditContext FormContext = null!; + + public class MyModel + { + public string? Name { get; set; } + + public MyModel? Parent { get; set; } + } + + protected override void OnInitialized() + { + if (Model == null) + { + Model = new(); + } + + FormContext = new EditContext(Model); + } + + private void Submit() + { + ModelName = Model?.Name ?? string.Empty; + } +}