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
This commit is contained in:
parent
1455aaeff1
commit
71327921ed
|
|
@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.Forms.EditContext>? ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.Forms.EditContext? EditContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||
public Microsoft.AspNetCore.Components.Forms.EditContext? EditContext { get { throw null; } set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public object? Model { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
{
|
||||
private readonly Func<Task> _handleSubmitDelegate; // Cache to avoid per-render allocations
|
||||
|
||||
private EditContext? _fixedEditContext;
|
||||
private EditContext? _editContext;
|
||||
private bool _hasSetEditContextExplicitly;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="EditForm"/>.
|
||||
|
|
@ -36,7 +37,16 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
/// also supply <see cref="Model"/>, since the model value will be taken
|
||||
/// from the <see cref="EditContext.Model"/> property.
|
||||
/// </summary>
|
||||
[Parameter] public EditContext? EditContext { get; set; }
|
||||
[Parameter]
|
||||
public EditContext? EditContext
|
||||
{
|
||||
get => _editContext;
|
||||
set
|
||||
{
|
||||
_editContext = value;
|
||||
_hasSetEditContextExplicitly = value != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the top-level model object for the form. An edit context will
|
||||
|
|
@ -73,11 +83,16 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
/// <inheritdoc />
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if ((EditContext == null) == (Model == null))
|
||||
if (_hasSetEditContextExplicitly && Model != null)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(EditForm)} requires a {nameof(Model)} " +
|
||||
$"parameter, or an {nameof(EditContext)} parameter, but not both.");
|
||||
}
|
||||
else if (!_hasSetEditContextExplicitly && Model == null)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(EditForm)} requires either a {nameof(Model)} " +
|
||||
$"parameter, or an {nameof(EditContext)} parameter, please provide one of these.");
|
||||
}
|
||||
|
||||
// If you're using OnSubmit, it becomes your responsibility to trigger validation manually
|
||||
// (e.g., so you can display a "pending" state in the UI). In that case you don't want the
|
||||
|
|
@ -89,31 +104,31 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
$"{nameof(EditForm)}, do not also supply {nameof(OnValidSubmit)} or {nameof(OnInvalidSubmit)}.");
|
||||
}
|
||||
|
||||
// Update _fixedEditContext if we don't have one yet, or if they are supplying a
|
||||
// Update _editContext if we don't have one yet, or if they are supplying a
|
||||
// potentially new EditContext, or if they are supplying a different Model
|
||||
if (_fixedEditContext == null || EditContext != null || Model != _fixedEditContext.Model)
|
||||
if (Model != null && Model != _editContext?.Model)
|
||||
{
|
||||
_fixedEditContext = EditContext ?? new EditContext(Model!);
|
||||
_editContext = new EditContext(Model!);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
Debug.Assert(_fixedEditContext != null);
|
||||
Debug.Assert(_editContext != null);
|
||||
|
||||
// If _fixedEditContext changes, tear down and recreate all descendants.
|
||||
// If _editContext changes, tear down and recreate all descendants.
|
||||
// This is so we can safely use the IsFixed optimization on CascadingValue,
|
||||
// optimizing for the common case where _fixedEditContext never changes.
|
||||
builder.OpenRegion(_fixedEditContext.GetHashCode());
|
||||
// optimizing for the common case where _editContext never changes.
|
||||
builder.OpenRegion(_editContext.GetHashCode());
|
||||
|
||||
builder.OpenElement(0, "form");
|
||||
builder.AddMultipleAttributes(1, AdditionalAttributes);
|
||||
builder.AddAttribute(2, "onsubmit", _handleSubmitDelegate);
|
||||
builder.OpenComponent<CascadingValue<EditContext>>(3);
|
||||
builder.AddAttribute(4, "IsFixed", true);
|
||||
builder.AddAttribute(5, "Value", _fixedEditContext);
|
||||
builder.AddAttribute(6, "ChildContent", ChildContent?.Invoke(_fixedEditContext));
|
||||
builder.AddAttribute(5, "Value", _editContext);
|
||||
builder.AddAttribute(6, "ChildContent", ChildContent?.Invoke(_editContext));
|
||||
builder.CloseComponent();
|
||||
builder.CloseElement();
|
||||
|
||||
|
|
@ -122,26 +137,26 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
|
||||
private async Task HandleSubmitAsync()
|
||||
{
|
||||
Debug.Assert(_fixedEditContext != null);
|
||||
Debug.Assert(_editContext != null);
|
||||
|
||||
if (OnSubmit.HasDelegate)
|
||||
{
|
||||
// When using OnSubmit, the developer takes control of the validation lifecycle
|
||||
await OnSubmit.InvokeAsync(_fixedEditContext);
|
||||
await OnSubmit.InvokeAsync(_editContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, the system implicitly runs validation on form submission
|
||||
var isValid = _fixedEditContext.Validate(); // This will likely become ValidateAsync later
|
||||
var isValid = _editContext.Validate(); // This will likely become ValidateAsync later
|
||||
|
||||
if (isValid && OnValidSubmit.HasDelegate)
|
||||
{
|
||||
await OnValidSubmit.InvokeAsync(_fixedEditContext);
|
||||
await OnValidSubmit.InvokeAsync(_editContext);
|
||||
}
|
||||
|
||||
if (!isValid && OnInvalidSubmit.HasDelegate)
|
||||
{
|
||||
await OnInvalidSubmit.InvokeAsync(_fixedEditContext);
|
||||
await OnInvalidSubmit.InvokeAsync(_editContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Forms
|
||||
{
|
||||
public class EditFormTest
|
||||
{
|
||||
|
||||
[Fact]
|
||||
public async Task ThrowsIfBothEditContextAndModelAreSupplied()
|
||||
{
|
||||
// Arrange
|
||||
var editForm = new EditForm
|
||||
{
|
||||
EditContext = new EditContext(new TestModel()),
|
||||
Model = new TestModel()
|
||||
};
|
||||
var testRenderer = new TestRenderer();
|
||||
var componentId = testRenderer.AssignRootComponentId(editForm);
|
||||
|
||||
// Act/Assert
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => testRenderer.RenderRootComponentAsync(componentId));
|
||||
Assert.StartsWith($"{nameof(EditForm)} requires a {nameof(EditForm.Model)} parameter, or an {nameof(EditContext)} parameter, but not both.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ThrowsIfBothEditContextAndModelAreNull()
|
||||
{
|
||||
// Arrange
|
||||
var editForm = new EditForm();
|
||||
var testRenderer = new TestRenderer();
|
||||
var componentId = testRenderer.AssignRootComponentId(editForm);
|
||||
|
||||
// Act/Assert
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => testRenderer.RenderRootComponentAsync(componentId));
|
||||
Assert.StartsWith($"{nameof(EditForm)} requires either a {nameof(EditForm.Model)} parameter, or an {nameof(EditContext)} parameter, please provide one of these.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsEditContextWhenModelParameterUsed()
|
||||
{
|
||||
// Arrange
|
||||
var model = new TestModel();
|
||||
var rootComponent = new TestEditFormHostComponent
|
||||
{
|
||||
Model = model
|
||||
};
|
||||
var editFormComponent = await RenderAndGetTestEditFormComponentAsync(rootComponent);
|
||||
|
||||
// Act
|
||||
var returnedEditContext = editFormComponent.EditContext;
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(returnedEditContext);
|
||||
Assert.Same(model, returnedEditContext.Model);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsEditContextWhenEditContextParameterUsed()
|
||||
{
|
||||
// Arrange
|
||||
var editContext = new EditContext(new TestModel());
|
||||
var rootComponent = new TestEditFormHostComponent
|
||||
{
|
||||
EditContext = editContext
|
||||
};
|
||||
var editFormComponent = await RenderAndGetTestEditFormComponentAsync(rootComponent);
|
||||
|
||||
// Act
|
||||
var returnedEditContext = editFormComponent.EditContext;
|
||||
|
||||
// Assert
|
||||
Assert.Same(editContext, returnedEditContext);
|
||||
}
|
||||
|
||||
private static EditForm FindEditFormComponent(CapturedBatch batch)
|
||||
=> batch.ReferenceFrames
|
||||
.Where(f => f.FrameType == RenderTreeFrameType.Component)
|
||||
.Select(f => f.Component)
|
||||
.OfType<EditForm>()
|
||||
.Single();
|
||||
|
||||
private static async Task<EditForm> RenderAndGetTestEditFormComponentAsync(TestEditFormHostComponent hostComponent)
|
||||
{
|
||||
var testRenderer = new TestRenderer();
|
||||
var componentId = testRenderer.AssignRootComponentId(hostComponent);
|
||||
await testRenderer.RenderRootComponentAsync(componentId);
|
||||
return FindEditFormComponent(testRenderer.Batches.Single());
|
||||
}
|
||||
|
||||
class TestModel
|
||||
{
|
||||
public string StringProperty { get; set; }
|
||||
}
|
||||
|
||||
class TestEditFormHostComponent : AutoRenderComponent
|
||||
{
|
||||
public EditContext EditContext { get; set; }
|
||||
public TestModel Model { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent<EditForm>(0);
|
||||
builder.AddAttribute(1, "Model", Model);
|
||||
builder.AddAttribute(2, "EditContext", EditContext);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue