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