From 71327921ed3afde65358a10517d6a108aaa068ee Mon Sep 17 00:00:00 2001 From: Chris Sainty <chrissainty@users.noreply.github.com> Date: Fri, 17 Jul 2020 03:06:50 +0100 Subject: [PATCH] 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 --- ...ft.AspNetCore.Components.Web.netcoreapp.cs | 2 +- src/Components/Web/src/Forms/EditForm.cs | 49 ++++--- src/Components/Web/test/Forms/EditFormTest.cs | 120 ++++++++++++++++++ 3 files changed, 153 insertions(+), 18 deletions(-) create mode 100644 src/Components/Web/test/Forms/EditFormTest.cs diff --git a/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs b/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs index aa04448bd78..d60e512b5db 100644 --- a/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs +++ b/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs @@ -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] diff --git a/src/Components/Web/src/Forms/EditForm.cs b/src/Components/Web/src/Forms/EditForm.cs index e4467b532a5..f5a13fc9fd0 100644 --- a/src/Components/Web/src/Forms/EditForm.cs +++ b/src/Components/Web/src/Forms/EditForm.cs @@ -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); } } } diff --git a/src/Components/Web/test/Forms/EditFormTest.cs b/src/Components/Web/test/Forms/EditFormTest.cs new file mode 100644 index 00000000000..05a390ddcaa --- /dev/null +++ b/src/Components/Web/test/Forms/EditFormTest.cs @@ -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(); + } + } + } +} -- GitLab