Skip to content

Commit 7132792

Browse files
authored
Modified EditForm to return _fixedEditContext via the EditContext parameter (#24007)
* Modified EditForm to return _fixedEditContext via the EditContext parameter. Also added some tests to cover the new functionality * Swapped to boolean to track provided EditContext * Patched ref assembly * Simplified setting _hasSetEditContextExplicitly * Renamed _fixedEditContext to _editContext * Updated null check in OnParametersSet * Simplified check for EditContext updates based on Model changes
1 parent 1455aae commit 7132792

File tree

3 files changed

+153
-18
lines changed

3 files changed

+153
-18
lines changed

src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public EditForm() { }
3838
[Microsoft.AspNetCore.Components.ParameterAttribute]
3939
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.Forms.EditContext>? ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
4040
[Microsoft.AspNetCore.Components.ParameterAttribute]
41-
public Microsoft.AspNetCore.Components.Forms.EditContext? EditContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
41+
public Microsoft.AspNetCore.Components.Forms.EditContext? EditContext { get { throw null; } set { } }
4242
[Microsoft.AspNetCore.Components.ParameterAttribute]
4343
public object? Model { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
4444
[Microsoft.AspNetCore.Components.ParameterAttribute]

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

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ public class EditForm : ComponentBase
1616
{
1717
private readonly Func<Task> _handleSubmitDelegate; // Cache to avoid per-render allocations
1818

19-
private EditContext? _fixedEditContext;
19+
private EditContext? _editContext;
20+
private bool _hasSetEditContextExplicitly;
2021

2122
/// <summary>
2223
/// Constructs an instance of <see cref="EditForm"/>.
@@ -36,7 +37,16 @@ public EditForm()
3637
/// also supply <see cref="Model"/>, since the model value will be taken
3738
/// from the <see cref="EditContext.Model"/> property.
3839
/// </summary>
39-
[Parameter] public EditContext? EditContext { get; set; }
40+
[Parameter]
41+
public EditContext? EditContext
42+
{
43+
get => _editContext;
44+
set
45+
{
46+
_editContext = value;
47+
_hasSetEditContextExplicitly = value != null;
48+
}
49+
}
4050

4151
/// <summary>
4252
/// Specifies the top-level model object for the form. An edit context will
@@ -73,11 +83,16 @@ public EditForm()
7383
/// <inheritdoc />
7484
protected override void OnParametersSet()
7585
{
76-
if ((EditContext == null) == (Model == null))
86+
if (_hasSetEditContextExplicitly && Model != null)
7787
{
7888
throw new InvalidOperationException($"{nameof(EditForm)} requires a {nameof(Model)} " +
7989
$"parameter, or an {nameof(EditContext)} parameter, but not both.");
8090
}
91+
else if (!_hasSetEditContextExplicitly && Model == null)
92+
{
93+
throw new InvalidOperationException($"{nameof(EditForm)} requires either a {nameof(Model)} " +
94+
$"parameter, or an {nameof(EditContext)} parameter, please provide one of these.");
95+
}
8196

8297
// If you're using OnSubmit, it becomes your responsibility to trigger validation manually
8398
// (e.g., so you can display a "pending" state in the UI). In that case you don't want the
@@ -89,31 +104,31 @@ protected override void OnParametersSet()
89104
$"{nameof(EditForm)}, do not also supply {nameof(OnValidSubmit)} or {nameof(OnInvalidSubmit)}.");
90105
}
91106

92-
// Update _fixedEditContext if we don't have one yet, or if they are supplying a
107+
// Update _editContext if we don't have one yet, or if they are supplying a
93108
// potentially new EditContext, or if they are supplying a different Model
94-
if (_fixedEditContext == null || EditContext != null || Model != _fixedEditContext.Model)
109+
if (Model != null && Model != _editContext?.Model)
95110
{
96-
_fixedEditContext = EditContext ?? new EditContext(Model!);
111+
_editContext = new EditContext(Model!);
97112
}
98113
}
99114

100115
/// <inheritdoc />
101116
protected override void BuildRenderTree(RenderTreeBuilder builder)
102117
{
103-
Debug.Assert(_fixedEditContext != null);
118+
Debug.Assert(_editContext != null);
104119

105-
// If _fixedEditContext changes, tear down and recreate all descendants.
120+
// If _editContext changes, tear down and recreate all descendants.
106121
// This is so we can safely use the IsFixed optimization on CascadingValue,
107-
// optimizing for the common case where _fixedEditContext never changes.
108-
builder.OpenRegion(_fixedEditContext.GetHashCode());
122+
// optimizing for the common case where _editContext never changes.
123+
builder.OpenRegion(_editContext.GetHashCode());
109124

110125
builder.OpenElement(0, "form");
111126
builder.AddMultipleAttributes(1, AdditionalAttributes);
112127
builder.AddAttribute(2, "onsubmit", _handleSubmitDelegate);
113128
builder.OpenComponent<CascadingValue<EditContext>>(3);
114129
builder.AddAttribute(4, "IsFixed", true);
115-
builder.AddAttribute(5, "Value", _fixedEditContext);
116-
builder.AddAttribute(6, "ChildContent", ChildContent?.Invoke(_fixedEditContext));
130+
builder.AddAttribute(5, "Value", _editContext);
131+
builder.AddAttribute(6, "ChildContent", ChildContent?.Invoke(_editContext));
117132
builder.CloseComponent();
118133
builder.CloseElement();
119134

@@ -122,26 +137,26 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
122137

123138
private async Task HandleSubmitAsync()
124139
{
125-
Debug.Assert(_fixedEditContext != null);
140+
Debug.Assert(_editContext != null);
126141

127142
if (OnSubmit.HasDelegate)
128143
{
129144
// When using OnSubmit, the developer takes control of the validation lifecycle
130-
await OnSubmit.InvokeAsync(_fixedEditContext);
145+
await OnSubmit.InvokeAsync(_editContext);
131146
}
132147
else
133148
{
134149
// Otherwise, the system implicitly runs validation on form submission
135-
var isValid = _fixedEditContext.Validate(); // This will likely become ValidateAsync later
150+
var isValid = _editContext.Validate(); // This will likely become ValidateAsync later
136151

137152
if (isValid && OnValidSubmit.HasDelegate)
138153
{
139-
await OnValidSubmit.InvokeAsync(_fixedEditContext);
154+
await OnValidSubmit.InvokeAsync(_editContext);
140155
}
141156

142157
if (!isValid && OnInvalidSubmit.HasDelegate)
143158
{
144-
await OnInvalidSubmit.InvokeAsync(_fixedEditContext);
159+
await OnInvalidSubmit.InvokeAsync(_editContext);
145160
}
146161
}
147162
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Components.Rendering;
8+
using Microsoft.AspNetCore.Components.RenderTree;
9+
using Microsoft.AspNetCore.Components.Test.Helpers;
10+
using Xunit;
11+
12+
namespace Microsoft.AspNetCore.Components.Forms
13+
{
14+
public class EditFormTest
15+
{
16+
17+
[Fact]
18+
public async Task ThrowsIfBothEditContextAndModelAreSupplied()
19+
{
20+
// Arrange
21+
var editForm = new EditForm
22+
{
23+
EditContext = new EditContext(new TestModel()),
24+
Model = new TestModel()
25+
};
26+
var testRenderer = new TestRenderer();
27+
var componentId = testRenderer.AssignRootComponentId(editForm);
28+
29+
// Act/Assert
30+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
31+
() => testRenderer.RenderRootComponentAsync(componentId));
32+
Assert.StartsWith($"{nameof(EditForm)} requires a {nameof(EditForm.Model)} parameter, or an {nameof(EditContext)} parameter, but not both.", ex.Message);
33+
}
34+
35+
[Fact]
36+
public async Task ThrowsIfBothEditContextAndModelAreNull()
37+
{
38+
// Arrange
39+
var editForm = new EditForm();
40+
var testRenderer = new TestRenderer();
41+
var componentId = testRenderer.AssignRootComponentId(editForm);
42+
43+
// Act/Assert
44+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
45+
() => testRenderer.RenderRootComponentAsync(componentId));
46+
Assert.StartsWith($"{nameof(EditForm)} requires either a {nameof(EditForm.Model)} parameter, or an {nameof(EditContext)} parameter, please provide one of these.", ex.Message);
47+
}
48+
49+
[Fact]
50+
public async Task ReturnsEditContextWhenModelParameterUsed()
51+
{
52+
// Arrange
53+
var model = new TestModel();
54+
var rootComponent = new TestEditFormHostComponent
55+
{
56+
Model = model
57+
};
58+
var editFormComponent = await RenderAndGetTestEditFormComponentAsync(rootComponent);
59+
60+
// Act
61+
var returnedEditContext = editFormComponent.EditContext;
62+
63+
// Assert
64+
Assert.NotNull(returnedEditContext);
65+
Assert.Same(model, returnedEditContext.Model);
66+
}
67+
68+
[Fact]
69+
public async Task ReturnsEditContextWhenEditContextParameterUsed()
70+
{
71+
// Arrange
72+
var editContext = new EditContext(new TestModel());
73+
var rootComponent = new TestEditFormHostComponent
74+
{
75+
EditContext = editContext
76+
};
77+
var editFormComponent = await RenderAndGetTestEditFormComponentAsync(rootComponent);
78+
79+
// Act
80+
var returnedEditContext = editFormComponent.EditContext;
81+
82+
// Assert
83+
Assert.Same(editContext, returnedEditContext);
84+
}
85+
86+
private static EditForm FindEditFormComponent(CapturedBatch batch)
87+
=> batch.ReferenceFrames
88+
.Where(f => f.FrameType == RenderTreeFrameType.Component)
89+
.Select(f => f.Component)
90+
.OfType<EditForm>()
91+
.Single();
92+
93+
private static async Task<EditForm> RenderAndGetTestEditFormComponentAsync(TestEditFormHostComponent hostComponent)
94+
{
95+
var testRenderer = new TestRenderer();
96+
var componentId = testRenderer.AssignRootComponentId(hostComponent);
97+
await testRenderer.RenderRootComponentAsync(componentId);
98+
return FindEditFormComponent(testRenderer.Batches.Single());
99+
}
100+
101+
class TestModel
102+
{
103+
public string StringProperty { get; set; }
104+
}
105+
106+
class TestEditFormHostComponent : AutoRenderComponent
107+
{
108+
public EditContext EditContext { get; set; }
109+
public TestModel Model { get; set; }
110+
111+
protected override void BuildRenderTree(RenderTreeBuilder builder)
112+
{
113+
builder.OpenComponent<EditForm>(0);
114+
builder.AddAttribute(1, "Model", Model);
115+
builder.AddAttribute(2, "EditContext", EditContext);
116+
builder.CloseComponent();
117+
}
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)