From 0fd2da0d1bda17b9003ac72e1d011a2db8c177e7 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Tue, 30 May 2023 17:53:37 +0200 Subject: [PATCH 01/15] Added LogicalParentComponentState to fix how CascadingParameters, ErrorBoundary and StreamingRendering interact with Sections --- .../Components/src/CascadingParameterState.cs | 2 +- .../Components/src/PublicAPI.Unshipped.txt | 2 + src/Components/Components/src/RenderHandle.cs | 10 +++++ .../Components/src/RenderTree/Renderer.cs | 12 +++++- .../src/Rendering/ComponentState.cs | 14 +++++++ .../src/Sections/ISectionContentProvider.cs | 9 ---- .../src/Sections/ISectionContentSubscriber.cs | 9 ---- .../Components/src/Sections/SectionContent.cs | 6 +-- .../Components/src/Sections/SectionOutlet.cs | 10 +++-- .../src/Sections/SectionRegistry.cs | 41 ++++++++++++------- .../src/Rendering/EndpointComponentState.cs | 25 ++++++++--- 11 files changed, 93 insertions(+), 47 deletions(-) delete mode 100644 src/Components/Components/src/Sections/ISectionContentProvider.cs delete mode 100644 src/Components/Components/src/Sections/ISectionContentSubscriber.cs diff --git a/src/Components/Components/src/CascadingParameterState.cs b/src/Components/Components/src/CascadingParameterState.cs index 52caad659d0b..ae153719fe1c 100644 --- a/src/Components/Components/src/CascadingParameterState.cs +++ b/src/Components/Components/src/CascadingParameterState.cs @@ -69,7 +69,7 @@ public static IReadOnlyList FindCascadingParameters(Com return candidateSupplier; } - candidate = candidate.ParentComponentState; + candidate = candidate.LogicalParentComponentState; } while (candidate != null); // No match diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index a03797860d22..af46616377a5 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -19,6 +19,7 @@ Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collectio Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task! *REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri! Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri! +Microsoft.AspNetCore.Components.Rendering.ComponentState.LogicalParentComponentState.get -> Microsoft.AspNetCore.Components.Rendering.ComponentState? Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.SetEventHandlerName(string! eventHandlerName) -> void *REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, System.Collections.Generic.IReadOnlyDictionary! routeValues) -> void *REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary! @@ -61,6 +62,7 @@ override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bo override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask +virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.LogicalParentComponentStateChanged(Microsoft.AspNetCore.Components.Rendering.ComponentState? logicalParentComponent) -> void virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.CreateComponentState(int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> Microsoft.AspNetCore.Components.Rendering.ComponentState! virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool quiesce) -> System.Threading.Tasks.Task! diff --git a/src/Components/Components/src/RenderHandle.cs b/src/Components/Components/src/RenderHandle.cs index fdd61f32ef3a..e5d9b29a8881 100644 --- a/src/Components/Components/src/RenderHandle.cs +++ b/src/Components/Components/src/RenderHandle.cs @@ -77,6 +77,16 @@ public Task DispatchExceptionAsync(Exception exception) return Dispatcher.InvokeAsync(() => renderer!.HandleComponentException(exception, componentId)); } + internal void LogicalParentComponentChanged(IComponent? logicalParentComponent) + { + if (_renderer == null) + { + ThrowNotInitialized(); + } + + _renderer.LogicalParentComponentChanged(_componentId, logicalParentComponent); + } + [DoesNotReturn] private static void ThrowNotInitialized() { diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 187ffb6d8f79..4b30acf28fdd 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -352,6 +352,16 @@ private ComponentState AttachAndInitComponent(IComponent component, int parentCo protected virtual ComponentState CreateComponentState(int componentId, IComponent component, ComponentState? parentComponentState) => new ComponentState(this, componentId, component, parentComponentState); + internal void LogicalParentComponentChanged(int componentId, IComponent? logicalParentComponent) + { + var component = _componentStateById[componentId]; + + var logicalParentComponentState = logicalParentComponent != null ? + _componentStateByComponent[logicalParentComponent] : component.ParentComponentState; + + component.LogicalParentComponentStateChanged(logicalParentComponentState); + } + /// /// Updates the visible UI. /// @@ -1039,7 +1049,7 @@ private void HandleExceptionViaErrorBoundary(Exception error, ComponentState? er return; // Handled successfully } - candidate = candidate.ParentComponentState; + candidate = candidate.LogicalParentComponentState; } // It's unhandled, so treat as fatal diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index f7de9f106215..44abd29509e2 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -33,6 +33,7 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, { ComponentId = componentId; ParentComponentState = parentComponentState; + LogicalParentComponentState = parentComponentState; Component = component ?? throw new ArgumentNullException(nameof(component)); _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); _cascadingParameters = CascadingParameterState.FindCascadingParameters(this); @@ -61,8 +62,21 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, /// public ComponentState? ParentComponentState { get; } + /// + /// Gets the of the logical parent component, or null if this is a root component. + /// + public ComponentState? LogicalParentComponentState { get; private set; } + internal RenderTreeBuilder CurrentRenderTree { get; set; } + /// + /// Changes the of the logical parent component. + /// + protected internal virtual void LogicalParentComponentStateChanged(ComponentState? logicalParentComponent) + { + LogicalParentComponentState = logicalParentComponent; + } + internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, out Exception? renderFragmentException) { renderFragmentException = null; diff --git a/src/Components/Components/src/Sections/ISectionContentProvider.cs b/src/Components/Components/src/Sections/ISectionContentProvider.cs deleted file mode 100644 index 093e4f8e06b3..000000000000 --- a/src/Components/Components/src/Sections/ISectionContentProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Components.Sections; - -internal interface ISectionContentProvider -{ - RenderFragment? Content { get; } -} diff --git a/src/Components/Components/src/Sections/ISectionContentSubscriber.cs b/src/Components/Components/src/Sections/ISectionContentSubscriber.cs deleted file mode 100644 index eb87f5557465..000000000000 --- a/src/Components/Components/src/Sections/ISectionContentSubscriber.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Components.Sections; - -internal interface ISectionContentSubscriber -{ - void ContentChanged(RenderFragment? content); -} diff --git a/src/Components/Components/src/Sections/SectionContent.cs b/src/Components/Components/src/Sections/SectionContent.cs index 6929434b4292..c2d402ff6d97 100644 --- a/src/Components/Components/src/Sections/SectionContent.cs +++ b/src/Components/Components/src/Sections/SectionContent.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Components.Sections; /// /// Provides content to components with matching s. /// -public sealed class SectionContent : ISectionContentProvider, IComponent, IDisposable +public sealed class SectionContent : IComponent, IDisposable { private object? _registeredIdentifier; private bool? _registeredIsDefaultContent; @@ -35,8 +35,6 @@ public sealed class SectionContent : ISectionContentProvider, IComponent, IDispo /// [Parameter] public RenderFragment? ChildContent { get; set; } - RenderFragment? ISectionContentProvider.Content => ChildContent; - void IComponent.Attach(RenderHandle renderHandle) { _registry = renderHandle.Dispatcher.SectionRegistry; @@ -79,7 +77,7 @@ Task IComponent.SetParametersAsync(ParameterView parameters) _registeredIsDefaultContent = IsDefaultContent; } - _registry.NotifyContentChanged(identifier, this); + _registry.NotifyContentProviderChanged(identifier, this); return Task.CompletedTask; } diff --git a/src/Components/Components/src/Sections/SectionOutlet.cs b/src/Components/Components/src/Sections/SectionOutlet.cs index f8595f8f1d35..2be40ebba105 100644 --- a/src/Components/Components/src/Sections/SectionOutlet.cs +++ b/src/Components/Components/src/Sections/SectionOutlet.cs @@ -6,8 +6,7 @@ namespace Microsoft.AspNetCore.Components.Sections; /// /// Renders content provided by components with matching s. /// -[StreamRendering(true)] // Because the content may be provided by a streaming component -public sealed class SectionOutlet : ISectionContentSubscriber, IComponent, IDisposable +public sealed class SectionOutlet : IComponent, IDisposable { private static readonly RenderFragment _emptyRenderFragment = _ => { }; @@ -73,7 +72,12 @@ Task IComponent.SetParametersAsync(ParameterView parameters) return Task.CompletedTask; } - void ISectionContentSubscriber.ContentChanged(RenderFragment? content) + internal void LogicalParentComponentChanged(IComponent? logicalParentComponent) + { + _renderHandle.LogicalParentComponentChanged(logicalParentComponent); + } + + internal void ContentChanged(RenderFragment? content) { _content = content; RenderContent(); diff --git a/src/Components/Components/src/Sections/SectionRegistry.cs b/src/Components/Components/src/Sections/SectionRegistry.cs index 3e5a0908c5d1..0f0d2cd1b4b6 100644 --- a/src/Components/Components/src/Sections/SectionRegistry.cs +++ b/src/Components/Components/src/Sections/SectionRegistry.cs @@ -5,10 +5,10 @@ namespace Microsoft.AspNetCore.Components.Sections; internal sealed class SectionRegistry { - private readonly Dictionary _subscribersByIdentifier = new(); - private readonly Dictionary> _providersByIdentifier = new(); + private readonly Dictionary _subscribersByIdentifier = new(); + private readonly Dictionary> _providersByIdentifier = new(); - public void AddProvider(object identifier, ISectionContentProvider provider, bool isDefaultProvider) + public void AddProvider(object identifier, SectionContent provider, bool isDefaultProvider) { if (!_providersByIdentifier.TryGetValue(identifier, out var providers)) { @@ -26,7 +26,7 @@ public void AddProvider(object identifier, ISectionContentProvider provider, boo } } - public void RemoveProvider(object identifier, ISectionContentProvider provider) + public void RemoveProvider(object identifier, SectionContent provider) { if (!_providersByIdentifier.TryGetValue(identifier, out var providers)) { @@ -46,12 +46,14 @@ public void RemoveProvider(object identifier, ISectionContentProvider provider) { // We just removed the most recently added provider, meaning we need to change // the current content to that of second most recently added provider. - var content = GetCurrentProviderContentOrDefault(providers); - NotifyContentChangedForSubscriber(identifier, content); + var contentProvider = GetCurrentProviderContentOrDefault(providers); + + NotifyLogicalParentComponentForSubscriber(identifier, contentProvider); + NotifyContentChangedForSubscriber(identifier, contentProvider?.ChildContent); } } - public void Subscribe(object identifier, ISectionContentSubscriber subscriber) + public void Subscribe(object identifier, SectionOutlet subscriber) { if (_subscribersByIdentifier.ContainsKey(identifier)) { @@ -59,8 +61,10 @@ public void Subscribe(object identifier, ISectionContentSubscriber subscriber) } // Notify the new subscriber with any existing content. - var content = GetCurrentProviderContentOrDefault(identifier); - subscriber.ContentChanged(content); + var provider = GetCurrentProviderContentOrDefault(identifier); + + subscriber.LogicalParentComponentChanged(provider); + subscriber.ContentChanged(provider?.ChildContent); _subscribersByIdentifier.Add(identifier, subscriber); } @@ -73,7 +77,7 @@ public void Unsubscribe(object identifier) } } - public void NotifyContentChanged(object identifier, ISectionContentProvider provider) + public void NotifyContentProviderChanged(object identifier, SectionContent provider) { if (!_providersByIdentifier.TryGetValue(identifier, out var providers)) { @@ -84,20 +88,29 @@ public void NotifyContentChanged(object identifier, ISectionContentProvider prov // most recently added provider changes. if (providers.Count != 0 && providers[^1] == provider) { - NotifyContentChangedForSubscriber(identifier, provider.Content); + NotifyLogicalParentComponentForSubscriber(identifier, provider); + NotifyContentChangedForSubscriber(identifier, provider.ChildContent); } } - private static RenderFragment? GetCurrentProviderContentOrDefault(List providers) + private static SectionContent? GetCurrentProviderContentOrDefault(List providers) => providers.Count != 0 - ? providers[^1].Content + ? providers[^1] : null; - private RenderFragment? GetCurrentProviderContentOrDefault(object identifier) + private SectionContent? GetCurrentProviderContentOrDefault(object identifier) => _providersByIdentifier.TryGetValue(identifier, out var existingList) ? GetCurrentProviderContentOrDefault(existingList) : null; + private void NotifyLogicalParentComponentForSubscriber(object identifier, IComponent? logicalParentComponent) + { + if (_subscribersByIdentifier.TryGetValue(identifier, out var subscriber)) + { + subscriber.LogicalParentComponentChanged(logicalParentComponent); + } + } + private void NotifyContentChangedForSubscriber(object identifier, RenderFragment? content) { if (_subscribersByIdentifier.TryGetValue(identifier, out var subscriber)) diff --git a/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs b/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs index 784ec09bcb1d..93ca0b274975 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; +using System.ComponentModel; using System.Reflection; using System.Reflection.Metadata; using Microsoft.AspNetCore.Components.Endpoints; @@ -19,8 +20,22 @@ internal sealed class EndpointComponentState : ComponentState public EndpointComponentState(Renderer renderer, int componentId, IComponent component, ComponentState? parentComponentState) : base(renderer, componentId, component, parentComponentState) { - var streamRenderingAttribute = _streamRenderingAttributeByComponentType.GetOrAdd(component.GetType(), - type => type.GetCustomAttribute()); + SetStreamingRendering(); + } + + public bool StreamRendering { get; private set; } + + protected override void LogicalParentComponentStateChanged(ComponentState? logicalParentComponent) + { + base.LogicalParentComponentStateChanged(logicalParentComponent); + + SetStreamingRendering(); + } + + private void SetStreamingRendering() + { + var streamRenderingAttribute = _streamRenderingAttributeByComponentType.GetOrAdd(Component.GetType(), + type => type.GetCustomAttribute()); if (streamRenderingAttribute is not null) { @@ -28,13 +43,11 @@ public EndpointComponentState(Renderer renderer, int componentId, IComponent com } else { - var parentEndpointComponentState = (EndpointComponentState?)parentComponentState; - StreamRendering = parentEndpointComponentState?.StreamRendering ?? false; + var logicalParentEndpointComponentState = (EndpointComponentState?)LogicalParentComponentState; + StreamRendering = logicalParentEndpointComponentState?.StreamRendering ?? false; } } - public bool StreamRendering { get; } - /// /// MetadataUpdateHandler event. This is invoked by the hot reload host via reflection. /// From de79f5cdf5ea7d7916464bfa91371bebcc6f466d Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Tue, 30 May 2023 17:54:01 +0200 Subject: [PATCH 02/15] fix spelling in SectionsTest --- src/Components/test/E2ETest/Tests/SectionsTest.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Components/test/E2ETest/Tests/SectionsTest.cs b/src/Components/test/E2ETest/Tests/SectionsTest.cs index 14766324bd13..7dcfc25e2d2d 100644 --- a/src/Components/test/E2ETest/Tests/SectionsTest.cs +++ b/src/Components/test/E2ETest/Tests/SectionsTest.cs @@ -31,7 +31,7 @@ protected override void InitializeAsyncCore() } [Fact] - public void RenderTwoSectionOutletsWithSameSectionId_TrowsException() + public void RenderTwoSectionOutletsWithSameSectionId_ThrowsException() { _appElement.FindElement(By.Id("section-outlet-same-id")).Click(); @@ -43,7 +43,7 @@ public void RenderTwoSectionOutletsWithSameSectionId_TrowsException() } [Fact] - public void RenderTwoSectionOutletsWithSameSectionName_TrowsException() + public void RenderTwoSectionOutletsWithSameSectionName_ThrowsException() { _appElement.FindElement(By.Id("section-outlet-same-name")).Click(); @@ -55,7 +55,7 @@ public void RenderTwoSectionOutletsWithSameSectionName_TrowsException() } [Fact] - public void RenderTwoSectionOutletsWithEqualSectionNameToSectionId_TrowsException() + public void RenderTwoSectionOutletsWithEqualSectionNameToSectionId_ThrowsException() { _appElement.FindElement(By.Id("section-outlet-equal-name-id")).Click(); @@ -67,7 +67,7 @@ public void RenderTwoSectionOutletsWithEqualSectionNameToSectionId_TrowsExceptio } [Fact] - public void RenderSectionOutletWithSectionNameAndSectionId_TrowsException() + public void RenderSectionOutletWithSectionNameAndSectionId_ThrowsException() { _appElement.FindElement(By.Id("section-outlet-with-name-id")).Click(); @@ -79,7 +79,7 @@ public void RenderSectionOutletWithSectionNameAndSectionId_TrowsException() } [Fact] - public void RenderSectionOutletWithoutSectionNameAndSectionId_TrowsException() + public void RenderSectionOutletWithoutSectionNameAndSectionId_ThrowsException() { _appElement.FindElement(By.Id("section-outlet-without-name-id")).Click(); @@ -156,7 +156,7 @@ public void SectionContentWithSectionNameGetsDisposed_OldSectionOutletNoLongerRe } [Fact] - public void SectionOutletWithSectionNameGetsDisposed_ContentDissapears() + public void SectionOutletWithSectionNameGetsDisposed_ContentDisappears() { // Render Counter and change its id so the content is rendered in second SectionOutlet _appElement.FindElement(By.Id("counter-render-section-content")).Click(); From c77e39fad21f06a5002151e78c30f5ca706318f1 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Tue, 30 May 2023 17:54:51 +0200 Subject: [PATCH 03/15] e2e tests for Sections with Cascading Parameters --- .../SectionsWithCascadingParametersTest.cs | 46 +++++++++++++++++++ .../SectionsWithCascadingParameters.razor | 29 ++++++++++++ .../TextComponentWithCascadingParameter.razor | 9 ++++ 3 files changed, 84 insertions(+) create mode 100644 src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs create mode 100644 src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithCascadingParameters.razor create mode 100644 src/Components/test/testassets/BasicTestApp/SectionsTest/TextComponentWithCascadingParameter.razor diff --git a/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs b/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs new file mode 100644 index 000000000000..0bcd2bbc4501 --- /dev/null +++ b/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BasicTestApp; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.E2ETesting; +using Xunit.Abstractions; +using Microsoft.AspNetCore.Components.E2ETest; +using OpenQA.Selenium; +using Microsoft.AspNetCore.Components.Sections; + +namespace Microsoft.AspNetCore.Components.E2ETests.Tests; + +public class SectionsWithCascadingParametersTest : ServerTestBase> +{ + private IWebElement _appElement; + + public SectionsWithCascadingParametersTest + (BrowserFixture browserFixture, + ToggleExecutionModeServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + protected override void InitializeAsyncCore() + { + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); + _appElement = Browser.MountTestComponent(); + } + + [Fact] + public void SectionOutletRendersContentProvidedBySectionContentWithCascadingParameter() + { + Browser.Equal("First Section with additional text for first section", () => Browser.Exists(By.TagName("p")).Text); + } + + [Fact] + public void SectionContentChanged_SectionOutletRendersContentWithCorrectCascadingParameter() + { + _appElement.FindElement(By.Id("render-second-section-content")).Click(); + + Browser.Equal("Second Section with additional text for second section", () => Browser.Exists(By.TagName("p")).Text); + } +} diff --git a/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithCascadingParameters.razor b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithCascadingParameters.razor new file mode 100644 index 000000000000..f47060c7b7ae --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithCascadingParameters.razor @@ -0,0 +1,29 @@ +@using Microsoft.AspNetCore.Components.Sections + + + + + + + + + + + +@if (SecondSectionContentIsRendered) { + + + + + +} + + +@code { + private bool SecondSectionContentIsRendered = false; + + private string AdditionalTextForFirstSection = "with additional text for first section"; + + private string AdditionalTextForSecondSection = "with additional text for second section"; + +} diff --git a/src/Components/test/testassets/BasicTestApp/SectionsTest/TextComponentWithCascadingParameter.razor b/src/Components/test/testassets/BasicTestApp/SectionsTest/TextComponentWithCascadingParameter.razor new file mode 100644 index 000000000000..0000841c1322 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/SectionsTest/TextComponentWithCascadingParameter.razor @@ -0,0 +1,9 @@ +

@Text @AdditionalText

+ +@code { + [Parameter] + public string Text { get; set; } + + [CascadingParameter] + public string AdditionalText { get; set; } +} From e35f31f9b48dc62588d0171f5ffb707fc9f2d375 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Tue, 30 May 2023 17:55:39 +0200 Subject: [PATCH 04/15] e2e tests for Sections with ErrorBoundary --- .../Tests/SectionsWithErrorBoundaryTest.cs | 54 +++++++++++++++++++ .../test/testassets/BasicTestApp/Index.razor | 2 + .../ComponentThatThrowsException.razor | 2 + .../SectionsWithErrorBoundary.razor | 31 +++++++++++ 4 files changed, 89 insertions(+) create mode 100644 src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs create mode 100644 src/Components/test/testassets/BasicTestApp/SectionsTest/ComponentThatThrowsException.razor create mode 100644 src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor diff --git a/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs b/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs new file mode 100644 index 000000000000..8f0c7b2d1b95 --- /dev/null +++ b/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BasicTestApp; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.E2ETesting; +using Xunit.Abstractions; +using Microsoft.AspNetCore.Components.E2ETest; +using OpenQA.Selenium; +using Microsoft.AspNetCore.Components.Sections; + +namespace Microsoft.AspNetCore.Components.E2ETests.Tests; + +public class SectionsWithErrorBoundaryTest : ServerTestBase> +{ + private IWebElement _appElement; + + public SectionsWithErrorBoundaryTest + (BrowserFixture browserFixture, + ToggleExecutionModeServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + protected override void InitializeAsyncCore() + { + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); + _appElement = Browser.MountTestComponent(); + } + + [Fact] + public void ErrorBoundaryForSectionContentHandlesExceptionInSectionOutlet() + { + Browser.Equal("First Section", () => Browser.Exists(By.TagName("h1")).Text); + + _appElement.FindElement(By.Id("error-button")).Click(); + + Browser.Exists(By.ClassName("blazor-error-boundary")); + } + + [Fact] + public void SectionContentChanged_ErrorBoundaryHandlesExceptionInSectionOutlet() + { + _appElement.FindElement(By.Id("change-section-content")).Click(); + + Browser.Equal("Second Section", () => Browser.Exists(By.TagName("h1")).Text); + + _appElement.FindElement(By.Id("error-button")).Click(); + + Browser.Equal("Sorry!", () => Browser.Exists(By.TagName("p")).Text); + } +} diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index ef0d905db26f..97d0bf35d865 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -107,6 +107,8 @@ + + @System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription diff --git a/src/Components/test/testassets/BasicTestApp/SectionsTest/ComponentThatThrowsException.razor b/src/Components/test/testassets/BasicTestApp/SectionsTest/ComponentThatThrowsException.razor new file mode 100644 index 000000000000..154cb2cf4848 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/SectionsTest/ComponentThatThrowsException.razor @@ -0,0 +1,2 @@ + + diff --git a/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor new file mode 100644 index 000000000000..93d190b78904 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor @@ -0,0 +1,31 @@ +@using Microsoft.AspNetCore.Components.Sections + + + + + +@if (!ChangeSectionContent) { + + +

First Section

+ +
+
+} +else { + + + +

Second Section

+ +
+
+ +

Sorry!

+
+
+} + +@code { + private bool ChangeSectionContent = false; +} From c694a57b291c5a2bd718a1653b2d4b0be087e935 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Tue, 30 May 2023 18:35:12 +0200 Subject: [PATCH 05/15] remove using --- src/Components/Endpoints/src/Rendering/EndpointComponentState.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs b/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs index 93ca0b274975..a2fd568c86c1 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; -using System.ComponentModel; using System.Reflection; using System.Reflection.Metadata; using Microsoft.AspNetCore.Components.Endpoints; From f0c8ebcc963028c0506763a6bd31c750a51265c8 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Wed, 31 May 2023 20:35:08 +0200 Subject: [PATCH 06/15] added e2e test for sections with streaming rendering --- .../SectionsWithStreamingRenderingTest.cs | 68 +++++++++++++++++++ .../SectionsWithStreamingRendering.razor | 17 +++++ 2 files changed, 85 insertions(+) create mode 100644 src/Components/test/E2ETest/ServerRenderingTests/SectionsWithStreamingRenderingTest.cs create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/SectionsWithStreamingRendering.razor diff --git a/src/Components/test/E2ETest/ServerRenderingTests/SectionsWithStreamingRenderingTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/SectionsWithStreamingRenderingTest.cs new file mode 100644 index 000000000000..2ac19ac39ee0 --- /dev/null +++ b/src/Components/test/E2ETest/ServerRenderingTests/SectionsWithStreamingRenderingTest.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests; + +public class SectionsWithStreamingRenderingTest : ServerTestBase>> +{ + public SectionsWithStreamingRenderingTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + public override Task InitializeAsync() + => InitializeAsync(BrowserFixture.StreamingContext); + + [Fact] + public void StreamingRenderingForSectionOutletContentIsDeterminedByMatchingSectionContent() + { + Navigate($"{ServerPathBase}/sections-with-streaming"); + + Browser.Equal("Sections with Streaming", () => Browser.Exists(By.TagName("h1")).Text); + + // Second SectionContent overrides the content for SectionOutlet + + Browser.DoesNotExist(By.Id("first-section-content")); + + CanPerformStreamingRendering(); + } + + private void CanPerformStreamingRendering() + { + // Initial "waiting" state + var getStatusText = () => Browser.Exists(By.Id("status")); + var getDisplayedItems = () => Browser.FindElements(By.TagName("li")); + Assert.Equal("Waiting for more...", getStatusText().Text); + Assert.Empty(getDisplayedItems()); + + // Can add items + for (var i = 1; i <= 3; i++) + { + // Each time we click, there's another streaming render batch and the UI is updated + Browser.FindElement(By.Id("add-item-link")).Click(); + Browser.Collection(getDisplayedItems, Enumerable.Range(1, i).Select>(index => + { + return actualItem => Assert.Equal($"Item {index}", actualItem.Text); + }).ToArray()); + Assert.Equal("Waiting for more...", getStatusText().Text); + + // These are insta-removed so they don't pollute anything + Browser.DoesNotExist(By.TagName("blazor-ssr")); + } + + // Can finish the response + Browser.FindElement(By.Id("end-response-link")).Click(); + Browser.Equal("Finished", () => getStatusText().Text); + } +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/SectionsWithStreamingRendering.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/SectionsWithStreamingRendering.razor new file mode 100644 index 000000000000..d67c3754a929 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/SectionsWithStreamingRendering.razor @@ -0,0 +1,17 @@ +@page "/sections-with-streaming" +@using Microsoft.AspNetCore.Components.Sections + + +

Sections with Streaming

+ + + + +

First Section Content

+
+ + + + + + From ee18eb7a23b6996a2bf481d1820d1b617f6d4f61 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Wed, 31 May 2023 20:35:27 +0200 Subject: [PATCH 07/15] changed tests --- .../SectionsWithCascadingParametersTest.cs | 12 +++--------- .../Tests/SectionsWithErrorBoundaryTest.cs | 18 +++++++----------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs b/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs index 0bcd2bbc4501..7438560d7fc5 100644 --- a/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs +++ b/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs @@ -14,8 +14,6 @@ namespace Microsoft.AspNetCore.Components.E2ETests.Tests; public class SectionsWithCascadingParametersTest : ServerTestBase> { - private IWebElement _appElement; - public SectionsWithCascadingParametersTest (BrowserFixture browserFixture, ToggleExecutionModeServerFixture serverFixture, @@ -27,19 +25,15 @@ public SectionsWithCascadingParametersTest protected override void InitializeAsyncCore() { Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); - _appElement = Browser.MountTestComponent(); + Browser.MountTestComponent(); } [Fact] - public void SectionOutletRendersContentProvidedBySectionContentWithCascadingParameter() + public void CascadingParameterForSectionOutletContentIsDeterminedByMatchingSectionContent() { Browser.Equal("First Section with additional text for first section", () => Browser.Exists(By.TagName("p")).Text); - } - [Fact] - public void SectionContentChanged_SectionOutletRendersContentWithCorrectCascadingParameter() - { - _appElement.FindElement(By.Id("render-second-section-content")).Click(); + Browser.FindElement(By.Id("render-second-section-content")).Click(); Browser.Equal("Second Section with additional text for second section", () => Browser.Exists(By.TagName("p")).Text); } diff --git a/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs b/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs index 8f0c7b2d1b95..1303b110ada5 100644 --- a/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs +++ b/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs @@ -14,8 +14,6 @@ namespace Microsoft.AspNetCore.Components.E2ETests.Tests; public class SectionsWithErrorBoundaryTest : ServerTestBase> { - private IWebElement _appElement; - public SectionsWithErrorBoundaryTest (BrowserFixture browserFixture, ToggleExecutionModeServerFixture serverFixture, @@ -27,27 +25,25 @@ public SectionsWithErrorBoundaryTest protected override void InitializeAsyncCore() { Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); - _appElement = Browser.MountTestComponent(); + Browser.MountTestComponent(); } [Fact] - public void ErrorBoundaryForSectionContentHandlesExceptionInSectionOutlet() + public void ErrorBoundaryForSectionOutletContentIsDeterminedByMatchingSectionContent() { Browser.Equal("First Section", () => Browser.Exists(By.TagName("h1")).Text); - _appElement.FindElement(By.Id("error-button")).Click(); + Browser.FindElement(By.Id("error-button")).Click(); Browser.Exists(By.ClassName("blazor-error-boundary")); - } - [Fact] - public void SectionContentChanged_ErrorBoundaryHandlesExceptionInSectionOutlet() - { - _appElement.FindElement(By.Id("change-section-content")).Click(); + // Switch to the second SectionContent + + Browser.FindElement(By.Id("change-section-content")).Click(); Browser.Equal("Second Section", () => Browser.Exists(By.TagName("h1")).Text); - _appElement.FindElement(By.Id("error-button")).Click(); + Browser.FindElement(By.Id("error-button")).Click(); Browser.Equal("Sorry!", () => Browser.Exists(By.TagName("p")).Text); } From 622c8358bbe290c44857df3253479df6d044fcd9 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Thu, 1 Jun 2023 06:53:55 +0200 Subject: [PATCH 08/15] fix test for sections with streaming rendering --- .../SectionsWithStreamingRenderingTest.cs | 2 +- .../RazorComponentEndpointsStartup.cs | 2 +- .../Components/DisableStreaming.razor | 5 ++ .../Components/EnableStreaming.razor | 5 ++ ...otEnabledStreamingRenderingComponent.razor | 70 ++++++++++++++++++ .../SectionsWithStreamingRendering.razor | 22 +++--- .../Pages/StreamingRendering.razor | 71 +------------------ 7 files changed, 97 insertions(+), 80 deletions(-) create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Components/DisableStreaming.razor create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Components/EnableStreaming.razor create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Components/NotEnabledStreamingRenderingComponent.razor diff --git a/src/Components/test/E2ETest/ServerRenderingTests/SectionsWithStreamingRenderingTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/SectionsWithStreamingRenderingTest.cs index 2ac19ac39ee0..914655379abf 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/SectionsWithStreamingRenderingTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/SectionsWithStreamingRenderingTest.cs @@ -31,7 +31,7 @@ public void StreamingRenderingForSectionOutletContentIsDeterminedByMatchingSecti Browser.Equal("Sections with Streaming", () => Browser.Exists(By.TagName("h1")).Text); - // Second SectionContent overrides the content for SectionOutlet + // Second SectionContent overrides the content and StreamingRendering attribute for SectionOutlet Browser.DoesNotExist(By.Id("first-section-content")); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 87aa55f5d1f3..7ad7ee14dd2f 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -46,7 +46,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorComponents(); - StreamingRendering.MapEndpoints(endpoints); + NotEnabledStreamingRenderingComponent.MapEndpoints(endpoints); StreamingRenderingForm.MapEndpoints(endpoints); }); }); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/DisableStreaming.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/DisableStreaming.razor new file mode 100644 index 000000000000..b0f772fb506c --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/DisableStreaming.razor @@ -0,0 +1,5 @@ +@attribute [StreamRendering(false)] +@ChildContent +@code { + [Parameter] public RenderFragment ChildContent { get; set; } +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/EnableStreaming.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/EnableStreaming.razor new file mode 100644 index 000000000000..01697350947a --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/EnableStreaming.razor @@ -0,0 +1,5 @@ +@attribute [StreamRendering(true)] +@ChildContent +@code { + [Parameter] public RenderFragment ChildContent { get; set; } +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/NotEnabledStreamingRenderingComponent.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/NotEnabledStreamingRenderingComponent.razor new file mode 100644 index 000000000000..f4f30c154f10 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/NotEnabledStreamingRenderingComponent.razor @@ -0,0 +1,70 @@ +@using System.IO.Pipelines; +@using System.Threading.Channels; + +

+ Every time you make a request to add item, we'll + add an item to this streaming response. +

+

+ Complete the response by visiting end response. +

+ +
    + @foreach (var item in items) + { +
  • @item
  • + } +
+ +

+ @if (finished) + { + Finished + } + else + { + Waiting for more... + } +

+ +@code { + // Caution: Don't use statics like this in real apps. This is only for an E2E test. If you did this + // in production, different users/requests could interfere with each other. + static Channel StreamingDataChannel; + static int StreamingDataChannelCount; + + const string AddItemUrl = "streaming/add-item"; + const string EndResponseUrl = "streaming/end-response"; + + bool finished; + List items = new(); + + protected override async Task OnInitializedAsync() + { + StreamingDataChannel = Channel.CreateUnbounded(); + StreamingDataChannelCount = 0; + + await foreach (var item in StreamingDataChannel.Reader.ReadAllAsync()) + { + items.Add(item); + StateHasChanged(); + } + + finished = true; + } + + public static void MapEndpoints(IEndpointRouteBuilder endpoints) + { + endpoints.MapGet(AddItemUrl, () => + { + StreamingDataChannel.Writer.TryWrite($"Item {++StreamingDataChannelCount}"); + return "Added item"; + }); + + endpoints.MapGet(EndResponseUrl, () => + { + StreamingDataChannel.Writer.Complete(); + return "Response ended"; + }); + } +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/SectionsWithStreamingRendering.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/SectionsWithStreamingRendering.razor index d67c3754a929..5711e9bc577d 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/SectionsWithStreamingRendering.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/SectionsWithStreamingRendering.razor @@ -1,17 +1,21 @@ @page "/sections-with-streaming" @using Microsoft.AspNetCore.Components.Sections -

Sections with Streaming

- - - -

First Section Content

-
+ + + - - - + + +

First Section Content

+
+
+ + + + + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering.razor index a15d7ec3793a..8baaa0bd6acc 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering.razor @@ -1,74 +1,7 @@ @page "/streaming" -@using System.IO.Pipelines; -@using System.Threading.Channels; + @attribute [StreamRendering(true)]

Streaming Rendering

-

- Every time you make a request to add item, we'll - add an item to this streaming response. -

-

- Complete the response by visiting end response. -

- -
    - @foreach (var item in items) - { -
  • @item
  • - } -
- -

- @if (finished) - { - Finished - } - else - { - Waiting for more... - } -

- -@code { - // Caution: Don't use statics like this in real apps. This is only for an E2E test. If you did this - // in production, different users/requests could interfere with each other. - static Channel StreamingDataChannel; - static int StreamingDataChannelCount; - - const string AddItemUrl = "streaming/add-item"; - const string EndResponseUrl = "streaming/end-response"; - - bool finished; - List items = new(); - - protected override async Task OnInitializedAsync() - { - StreamingDataChannel = Channel.CreateUnbounded(); - StreamingDataChannelCount = 0; - - await foreach (var item in StreamingDataChannel.Reader.ReadAllAsync()) - { - items.Add(item); - StateHasChanged(); - } - - finished = true; - } - - public static void MapEndpoints(IEndpointRouteBuilder endpoints) - { - endpoints.MapGet(AddItemUrl, () => - { - StreamingDataChannel.Writer.TryWrite($"Item {++StreamingDataChannelCount}"); - return "Added item"; - }); - - endpoints.MapGet(EndResponseUrl, () => - { - StreamingDataChannel.Writer.Complete(); - return "Response ended"; - }); - } -} + From 18a4759c545e1b84544838cb4586f06f434daef5 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Mon, 12 Jun 2023 13:04:00 +0200 Subject: [PATCH 09/15] added more e2e tests for sections with cascading parameter --- .../SectionsWithCascadingParametersTest.cs | 61 ++++++++++++++++++- .../SectionsWithCascadingParameters.razor | 49 +++++++++++---- 2 files changed, 97 insertions(+), 13 deletions(-) diff --git a/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs b/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs index 7438560d7fc5..601d6a38fdee 100644 --- a/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs +++ b/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs @@ -29,12 +29,71 @@ protected override void InitializeAsyncCore() } [Fact] - public void CascadingParameterForSectionOutletContentIsDeterminedByMatchingSectionContent() + public void RenderSectionContent_CascadingParameterForSectionOutletIsDeterminedByMatchingSectionContent() { + // Doesn't matter if SectionOutlet is rendered before or after SectionContent + Browser.FindElement(By.Id("render-section-outlet")).Click(); + Browser.FindElement(By.Id("render-second-section-content")).Click(); + + Browser.Equal("Second Section with additional text for second section", () => Browser.Exists(By.TagName("p")).Text); + } + + [Fact] + public void RenderTwoSectionContentsWithSameId_CascadingParameterForSectionOutletIsDeterminedByLastRenderedSectionContent() + { + Browser.FindElement(By.Id("render-second-section-content")).Click(); + Browser.FindElement(By.Id("render-first-section-content")).Click(); + Browser.FindElement(By.Id("render-section-outlet")).Click(); + Browser.Equal("First Section with additional text for first section", () => Browser.Exists(By.TagName("p")).Text); + } + [Fact] + public void SecondSectionContentIdChanged_CascadingParameterForSectionOutletIsDeterminedByFirstSectionContent() + { + Browser.FindElement(By.Id("render-section-outlet")).Click(); + Browser.FindElement(By.Id("render-first-section-content")).Click(); Browser.FindElement(By.Id("render-second-section-content")).Click(); + Browser.FindElement(By.Id("change-second-section-content-id")).Click(); + + Browser.Equal("First Section with additional text for first section", () => Browser.Exists(By.TagName("p")).Text); + } + + [Fact] + public void SecondSectionContentDisposed_CascadingParameterForSectionOutletIsDeterminedByFirstSectionContent() + { + Browser.FindElement(By.Id("render-first-section-content")).Click(); + Browser.FindElement(By.Id("render-second-section-content")).Click(); + Browser.FindElement(By.Id("render-section-outlet")).Click(); + + Browser.FindElement(By.Id("dispose-second-section-content")).Click(); + + Browser.Equal("First Section with additional text for first section", () => Browser.Exists(By.TagName("p")).Text); + } + + [Fact] + public void FirstSectionContentDisposedThenRenderSecondSectionContent_CascadingParameterForSectionOutletIsDeterminedBySecondSectionContent() + { + Browser.FindElement(By.Id("render-section-outlet")).Click(); + Browser.FindElement(By.Id("render-first-section-content")).Click(); + + Browser.FindElement(By.Id("dispose-first-section-content")).Click(); + Browser.FindElement(By.Id("render-second-section-content")).Click(); + + Browser.Equal("Second Section with additional text for second section", () => Browser.Exists(By.TagName("p")).Text); + } + + [Fact] + public void SectionOutletIdChanged_CascadingParameterForSectionOutletIsDeterminedByMatchingSectionContent() + { + Browser.FindElement(By.Id("render-section-outlet")).Click(); + Browser.FindElement(By.Id("render-first-section-content")).Click(); + Browser.FindElement(By.Id("change-second-section-content-id")).Click(); + Browser.FindElement(By.Id("render-second-section-content")).Click(); + + Browser.FindElement(By.Id("change-section-outlet-id")).Click(); + Browser.Equal("Second Section with additional text for second section", () => Browser.Exists(By.TagName("p")).Text); } } diff --git a/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithCascadingParameters.razor b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithCascadingParameters.razor index f47060c7b7ae..90b738896b67 100644 --- a/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithCascadingParameters.razor +++ b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithCascadingParameters.razor @@ -1,29 +1,54 @@ @using Microsoft.AspNetCore.Components.Sections - - + +
+ +
+ +
+ +
+
+ +
+ + +@if (SectionOutletIsRendered) +{ + +} - - - - - +@if (FirstSectionContentIsRendered) +{ + + + + + +} -@if (SecondSectionContentIsRendered) { +@if (SecondSectionContentIsRendered) +{ - + } - @code { + private string AdditionalTextForFirstSection = "with additional text for first section"; + private string AdditionalTextForSecondSection = "with additional text for second section"; + + private bool FirstSectionContentIsRendered = false; private bool SecondSectionContentIsRendered = false; + private bool SectionOutletIsRendered = false; - private string AdditionalTextForFirstSection = "with additional text for first section"; + private static string id = "csc-section-test"; + private static object anotherId = new(); - private string AdditionalTextForSecondSection = "with additional text for second section"; + private object SectionOutletId = id; + private object SecondSectionContentId = id; } From 5017e8243518636848de50a360b9a35a76c6b9e6 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Mon, 12 Jun 2023 13:04:20 +0200 Subject: [PATCH 10/15] added more e2e tests for sections with error boundary --- .../Tests/SectionsWithErrorBoundaryTest.cs | 67 +++++++++++++++++-- .../SectionsWithErrorBoundary.razor | 41 ++++++++++-- 2 files changed, 96 insertions(+), 12 deletions(-) diff --git a/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs b/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs index 1303b110ada5..3bd735cac6f7 100644 --- a/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs +++ b/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs @@ -29,20 +29,77 @@ protected override void InitializeAsyncCore() } [Fact] - public void ErrorBoundaryForSectionOutletContentIsDeterminedByMatchingSectionContent() + public void RenderSectionContent_ErrorBoundaryForSectionOutletContentIsDeterminedByMatchingSectionContent() { - Browser.Equal("First Section", () => Browser.Exists(By.TagName("h1")).Text); + // Doesn't matter if SectionOutlet is rendered before or after SectionContent + Browser.FindElement(By.Id("render-section-outlet")).Click(); + Browser.FindElement(By.Id("render-second-section-content")).Click(); + + Browser.FindElement(By.Id("error-button")).Click(); + + Browser.Equal("Sorry!", () => Browser.Exists(By.TagName("p")).Text); + } + + [Fact] + public void RenderTwoSectionContentsWithSameId_ErrorBoundaryForSectionOutletIsDeterminedByLastRenderedSectionContent() + { + // show that after second sc error thrown then first is now rendered and is functional + Browser.FindElement(By.Id("render-second-section-content")).Click(); + Browser.FindElement(By.Id("render-first-section-content")).Click(); + Browser.FindElement(By.Id("render-section-outlet")).Click(); Browser.FindElement(By.Id("error-button")).Click(); Browser.Exists(By.ClassName("blazor-error-boundary")); + } - // Switch to the second SectionContent + [Fact] + public void SecondSectionContentIdChanged_ErrorBoundaryForSectionOutletIsDeterminedByFirstSectionContent() + { + Browser.FindElement(By.Id("render-second-section-content")).Click(); + Browser.FindElement(By.Id("render-first-section-content")).Click(); + Browser.FindElement(By.Id("render-section-outlet")).Click(); + + Browser.FindElement(By.Id("error-button")).Click(); - Browser.FindElement(By.Id("change-section-content")).Click(); + Browser.Exists(By.ClassName("blazor-error-boundary")); + } - Browser.Equal("Second Section", () => Browser.Exists(By.TagName("h1")).Text); + [Fact] + public void SecondSectionContentDisposed_ErrorBoundaryForSectionOutletIsDeterminedByFirstSectionContent() + { + Browser.FindElement(By.Id("render-first-section-content")).Click(); + Browser.FindElement(By.Id("render-second-section-content")).Click(); + Browser.FindElement(By.Id("render-section-outlet")).Click(); + + Browser.FindElement(By.Id("dispose-second-section-content")).Click(); + Browser.FindElement(By.Id("error-button")).Click(); + + Browser.Exists(By.ClassName("blazor-error-boundary")); + } + + [Fact] + public void FirstSectionContentDisposedThenRenderSecondSectionContent_ErrorBoundaryForSectionOutletIsDeterminedBySecondSectionContent() + { + Browser.FindElement(By.Id("render-section-outlet")).Click(); + Browser.FindElement(By.Id("render-first-section-content")).Click(); + + Browser.FindElement(By.Id("dispose-first-section-content")).Click(); + Browser.FindElement(By.Id("render-second-section-content")).Click(); + Browser.FindElement(By.Id("error-button")).Click(); + + Browser.Equal("Sorry!", () => Browser.Exists(By.TagName("p")).Text); + } + + [Fact] + public void SectionOutletIdChanged_ErrorBoundaryForSectionOutletIsDeterminedByMatchingSectionContent() + { + Browser.FindElement(By.Id("render-section-outlet")).Click(); + Browser.FindElement(By.Id("render-first-section-content")).Click(); + Browser.FindElement(By.Id("change-second-section-content-id")).Click(); + Browser.FindElement(By.Id("render-second-section-content")).Click(); + Browser.FindElement(By.Id("change-section-outlet-id")).Click(); Browser.FindElement(By.Id("error-button")).Click(); Browser.Equal("Sorry!", () => Browser.Exists(By.TagName("p")).Text); diff --git a/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor index 93d190b78904..6edb00c7c309 100644 --- a/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor +++ b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor @@ -1,21 +1,40 @@ @using Microsoft.AspNetCore.Components.Sections - + +
+ +
+ +
+ +
+ +
+ +
+ - -@if (!ChangeSectionContent) { +@if (SectionOutletIsRendered) +{ + +} + +@if (FirstSectionContentIsRendered) +{ - +

First Section

} -else { + +@if (SecondSectionContentIsRendered) +{ - +

Second Section

@@ -27,5 +46,13 @@ else { } @code { - private bool ChangeSectionContent = false; + private bool FirstSectionContentIsRendered = false; + private bool SecondSectionContentIsRendered = false; + private bool SectionOutletIsRendered = false; + + private static string id = "errb-section-test"; + private static object anotherId = new(); + + private object SectionOutletId = id; + private object SecondSectionContentId = id; } From f3a808829bf20d53acc868eb9185eeaf07bbbfb7 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Mon, 12 Jun 2023 13:45:52 +0200 Subject: [PATCH 11/15] added test case: cascading value changes --- .../Tests/SectionsWithCascadingParametersTest.cs | 11 +++++++++++ .../SectionsWithCascadingParameters.razor | 2 ++ 2 files changed, 13 insertions(+) diff --git a/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs b/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs index 601d6a38fdee..a0a010122c16 100644 --- a/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs +++ b/src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs @@ -38,6 +38,17 @@ public void RenderSectionContent_CascadingParameterForSectionOutletIsDeterminedB Browser.Equal("Second Section with additional text for second section", () => Browser.Exists(By.TagName("p")).Text); } + [Fact] + public void ChangeCascadingValueForSectionContent_CascadingValueForSectionOutletIsDeterminedByMatchingSectionContent() + { + Browser.FindElement(By.Id("render-first-section-content")).Click(); + Browser.FindElement(By.Id("render-section-outlet")).Click(); + + Browser.FindElement(By.Id("change-cascading-value")).Click(); + + Browser.Equal("First Section with additional text for second section", () => Browser.Exists(By.TagName("p")).Text); + } + [Fact] public void RenderTwoSectionContentsWithSameId_CascadingParameterForSectionOutletIsDeterminedByLastRenderedSectionContent() { diff --git a/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithCascadingParameters.razor b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithCascadingParameters.razor index 90b738896b67..dd2e1e8ca53d 100644 --- a/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithCascadingParameters.razor +++ b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithCascadingParameters.razor @@ -13,6 +13,8 @@
+
+ @if (SectionOutletIsRendered) { From a1f65963d07fbae5ac6dc1132facc1403a0a2bc5 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Mon, 12 Jun 2023 13:46:12 +0200 Subject: [PATCH 12/15] added test error content changes --- .../E2ETest/Tests/SectionsWithErrorBoundaryTest.cs | 14 +++++++++++++- .../SectionsTest/SectionsWithErrorBoundary.razor | 14 ++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs b/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs index 3bd735cac6f7..54302fc5eb27 100644 --- a/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs +++ b/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs @@ -33,11 +33,23 @@ public void RenderSectionContent_ErrorBoundaryForSectionOutletContentIsDetermine { // Doesn't matter if SectionOutlet is rendered before or after SectionContent Browser.FindElement(By.Id("render-section-outlet")).Click(); + Browser.FindElement(By.Id("render-first-section-content")).Click(); + + Browser.FindElement(By.Id("error-button")).Click(); + + Browser.Exists(By.ClassName("blazor-error-boundary")); + } + + [Fact] + public void ChangeErrorContent_ErrorContentForSectionOutletContentIsDeterminedByMatchingSectionContent() + { Browser.FindElement(By.Id("render-second-section-content")).Click(); + Browser.FindElement(By.Id("render-section-outlet")).Click(); Browser.FindElement(By.Id("error-button")).Click(); + Browser.FindElement(By.Id("change-error-content")).Click(); - Browser.Equal("Sorry!", () => Browser.Exists(By.TagName("p")).Text); + Browser.Equal("Error content changed.", () => Browser.Exists(By.TagName("p")).Text); } [Fact] diff --git a/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor index 6edb00c7c309..c7d34717ee54 100644 --- a/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor +++ b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor @@ -13,7 +13,8 @@
- +
+ @if (SectionOutletIsRendered) { @@ -40,7 +41,14 @@
-

Sorry!

+ @if(!ErrorContentChanged) + { +

Sorry!

+ } + else + { +

Error content changed.

+ }
} @@ -55,4 +63,6 @@ private object SectionOutletId = id; private object SecondSectionContentId = id; + + private bool ErrorContentChanged = false; } From 6ae0f5d87b417664bfb3ba2eb13a2d0b285466f1 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Mon, 12 Jun 2023 13:50:07 +0200 Subject: [PATCH 13/15] Revert "added test error content changes" This reverts commit a1f65963d07fbae5ac6dc1132facc1403a0a2bc5. --- .../E2ETest/Tests/SectionsWithErrorBoundaryTest.cs | 14 +------------- .../SectionsTest/SectionsWithErrorBoundary.razor | 14 ++------------ 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs b/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs index 54302fc5eb27..3bd735cac6f7 100644 --- a/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs +++ b/src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs @@ -33,23 +33,11 @@ public void RenderSectionContent_ErrorBoundaryForSectionOutletContentIsDetermine { // Doesn't matter if SectionOutlet is rendered before or after SectionContent Browser.FindElement(By.Id("render-section-outlet")).Click(); - Browser.FindElement(By.Id("render-first-section-content")).Click(); - - Browser.FindElement(By.Id("error-button")).Click(); - - Browser.Exists(By.ClassName("blazor-error-boundary")); - } - - [Fact] - public void ChangeErrorContent_ErrorContentForSectionOutletContentIsDeterminedByMatchingSectionContent() - { Browser.FindElement(By.Id("render-second-section-content")).Click(); - Browser.FindElement(By.Id("render-section-outlet")).Click(); Browser.FindElement(By.Id("error-button")).Click(); - Browser.FindElement(By.Id("change-error-content")).Click(); - Browser.Equal("Error content changed.", () => Browser.Exists(By.TagName("p")).Text); + Browser.Equal("Sorry!", () => Browser.Exists(By.TagName("p")).Text); } [Fact] diff --git a/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor index c7d34717ee54..6edb00c7c309 100644 --- a/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor +++ b/src/Components/test/testassets/BasicTestApp/SectionsTest/SectionsWithErrorBoundary.razor @@ -13,8 +13,7 @@
-
- + @if (SectionOutletIsRendered) { @@ -41,14 +40,7 @@
- @if(!ErrorContentChanged) - { -

Sorry!

- } - else - { -

Error content changed.

- } +

Sorry!

} @@ -63,6 +55,4 @@ private object SectionOutletId = id; private object SecondSectionContentId = id; - - private bool ErrorContentChanged = false; } From 976dd4be683460517348bff398a8cbbf37f8502c Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 13 Jun 2023 19:46:58 +0100 Subject: [PATCH 14/15] Alternate approach that sets the LogicalParentComponentState during ComponentState construction so it never has to change --- .../Components/src/PublicAPI.Unshipped.txt | 1 - src/Components/Components/src/RenderHandle.cs | 10 ---- .../Components/src/RenderTree/Renderer.cs | 13 +---- .../src/Rendering/ComponentState.cs | 27 +++++---- .../Components/src/Sections/SectionOutlet.cs | 57 +++++++++++++++---- .../src/Sections/SectionRegistry.cs | 23 ++------ .../src/Rendering/EndpointComponentState.cs | 24 ++------ 7 files changed, 78 insertions(+), 77 deletions(-) diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 21a092ef07a3..b0c2ed0eb7da 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -66,7 +66,6 @@ override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bo override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask -virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.LogicalParentComponentStateChanged(Microsoft.AspNetCore.Components.Rendering.ComponentState? logicalParentComponent) -> void virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.CreateComponentState(int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> Microsoft.AspNetCore.Components.Rendering.ComponentState! virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool quiesce) -> System.Threading.Tasks.Task! diff --git a/src/Components/Components/src/RenderHandle.cs b/src/Components/Components/src/RenderHandle.cs index e5d9b29a8881..fdd61f32ef3a 100644 --- a/src/Components/Components/src/RenderHandle.cs +++ b/src/Components/Components/src/RenderHandle.cs @@ -77,16 +77,6 @@ public Task DispatchExceptionAsync(Exception exception) return Dispatcher.InvokeAsync(() => renderer!.HandleComponentException(exception, componentId)); } - internal void LogicalParentComponentChanged(IComponent? logicalParentComponent) - { - if (_renderer == null) - { - ThrowNotInitialized(); - } - - _renderer.LogicalParentComponentChanged(_componentId, logicalParentComponent); - } - [DoesNotReturn] private static void ThrowNotInitialized() { diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 4b30acf28fdd..2ec4e2fba269 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -126,6 +126,9 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid protected ComponentState GetComponentState(int componentId) => GetRequiredComponentState(componentId); + internal ComponentState GetComponentState(IComponent component) + => _componentStateByComponent.GetValueOrDefault(component); + private async void RenderRootComponentsOnHotReload() { // Before re-rendering the root component, also clear any well-known caches in the framework @@ -352,16 +355,6 @@ private ComponentState AttachAndInitComponent(IComponent component, int parentCo protected virtual ComponentState CreateComponentState(int componentId, IComponent component, ComponentState? parentComponentState) => new ComponentState(this, componentId, component, parentComponentState); - internal void LogicalParentComponentChanged(int componentId, IComponent? logicalParentComponent) - { - var component = _componentStateById[componentId]; - - var logicalParentComponentState = logicalParentComponent != null ? - _componentStateByComponent[logicalParentComponent] : component.ParentComponentState; - - component.LogicalParentComponentStateChanged(logicalParentComponentState); - } - /// /// Updates the visible UI. /// diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index 44abd29509e2..7f8ee6af8e9c 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Sections; namespace Microsoft.AspNetCore.Components.Rendering; @@ -33,7 +34,9 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, { ComponentId = componentId; ParentComponentState = parentComponentState; - LogicalParentComponentState = parentComponentState; + LogicalParentComponentState = parentComponentState?.Component is SectionOutlet sectionOutlet + ? (GetSectionOutletLogicalParent(renderer, sectionOutlet) ?? parentComponentState) + : parentComponentState; Component = component ?? throw new ArgumentNullException(nameof(component)); _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); _cascadingParameters = CascadingParameterState.FindCascadingParameters(this); @@ -47,6 +50,18 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, } } + private static ComponentState? GetSectionOutletLogicalParent(Renderer renderer, SectionOutlet sectionOutlet) + { + // This will return null if the SectionOutlet is not currently rendering any content + if (sectionOutlet.CurrentLogicalParent is { } logicalParent + && renderer.GetComponentState(logicalParent) is { } logicalParentComponentState) + { + return logicalParentComponentState; + } + + return null; + } + /// /// Gets the component ID. /// @@ -65,18 +80,10 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, /// /// Gets the of the logical parent component, or null if this is a root component. /// - public ComponentState? LogicalParentComponentState { get; private set; } + public ComponentState? LogicalParentComponentState { get; } internal RenderTreeBuilder CurrentRenderTree { get; set; } - /// - /// Changes the of the logical parent component. - /// - protected internal virtual void LogicalParentComponentStateChanged(ComponentState? logicalParentComponent) - { - LogicalParentComponentState = logicalParentComponent; - } - internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, out Exception? renderFragmentException) { renderFragmentException = null; diff --git a/src/Components/Components/src/Sections/SectionOutlet.cs b/src/Components/Components/src/Sections/SectionOutlet.cs index 2be40ebba105..5d7a96b39d1d 100644 --- a/src/Components/Components/src/Sections/SectionOutlet.cs +++ b/src/Components/Components/src/Sections/SectionOutlet.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Components.Rendering; + namespace Microsoft.AspNetCore.Components.Sections; /// @@ -9,11 +11,10 @@ namespace Microsoft.AspNetCore.Components.Sections; public sealed class SectionOutlet : IComponent, IDisposable { private static readonly RenderFragment _emptyRenderFragment = _ => { }; - private object? _subscribedIdentifier; private RenderHandle _renderHandle; private SectionRegistry _registry = default!; - private RenderFragment? _content; + private SectionContent? _currentContentProvider; /// /// Gets or sets the ID that determines which instances will provide @@ -27,6 +28,8 @@ public sealed class SectionOutlet : IComponent, IDisposable /// [Parameter] public object? SectionId { get; set; } + internal IComponent? CurrentLogicalParent => _currentContentProvider; + void IComponent.Attach(RenderHandle renderHandle) { _renderHandle = renderHandle; @@ -72,14 +75,9 @@ Task IComponent.SetParametersAsync(ParameterView parameters) return Task.CompletedTask; } - internal void LogicalParentComponentChanged(IComponent? logicalParentComponent) - { - _renderHandle.LogicalParentComponentChanged(logicalParentComponent); - } - - internal void ContentChanged(RenderFragment? content) + internal void ContentUpdated(SectionContent? provider) { - _content = content; + _currentContentProvider = provider; RenderContent(); } @@ -93,7 +91,17 @@ private void RenderContent() return; } - _renderHandle.Render(_content ?? _emptyRenderFragment); + _renderHandle.Render(BuildRenderTree); + } + + private void BuildRenderTree(RenderTreeBuilder builder) + { + var fragment = _currentContentProvider?.ChildContent ?? _emptyRenderFragment; + + builder.OpenComponent(0); + builder.SetKey(fragment); + builder.AddComponentParameter(1, SectionOutletContentRenderer.ContentParameterName, fragment); + builder.CloseComponent(); } /// @@ -104,4 +112,33 @@ public void Dispose() _registry.Unsubscribe(_subscribedIdentifier); } } + + // This component simply renders the RenderFragment it is given + // The reason for rendering SectionOutlet output via this component is so that + // [1] We can use @key to guarantee that we only preserve descendant component + // instances when they come from the same SectionContent, not unrelated ones + // [2] We know that whenever the SectionContent is changed to another one, there + // will be a new ComponentState established to represent this intermediate + // component, and it will already have the correct LogicalParentComponentState + // so anything computed from this (e.g., whether or not streaming rendering is + // enabled) will be freshly re-evaluated, without that information having to + // change in place on an existing ComponentState. + private class SectionOutletContentRenderer : IComponent + { + public const string ContentParameterName = "content"; + + private RenderHandle _renderHandle; + + public void Attach(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + public Task SetParametersAsync(ParameterView parameters) + { + var fragment = parameters.GetValueOrDefault(ContentParameterName)!; + _renderHandle.Render(fragment); + return Task.CompletedTask; + } + } } diff --git a/src/Components/Components/src/Sections/SectionRegistry.cs b/src/Components/Components/src/Sections/SectionRegistry.cs index 0f0d2cd1b4b6..d722034cf993 100644 --- a/src/Components/Components/src/Sections/SectionRegistry.cs +++ b/src/Components/Components/src/Sections/SectionRegistry.cs @@ -47,9 +47,7 @@ public void RemoveProvider(object identifier, SectionContent provider) // We just removed the most recently added provider, meaning we need to change // the current content to that of second most recently added provider. var contentProvider = GetCurrentProviderContentOrDefault(providers); - - NotifyLogicalParentComponentForSubscriber(identifier, contentProvider); - NotifyContentChangedForSubscriber(identifier, contentProvider?.ChildContent); + NotifyContentChangedForSubscriber(identifier, contentProvider); } } @@ -62,9 +60,7 @@ public void Subscribe(object identifier, SectionOutlet subscriber) // Notify the new subscriber with any existing content. var provider = GetCurrentProviderContentOrDefault(identifier); - - subscriber.LogicalParentComponentChanged(provider); - subscriber.ContentChanged(provider?.ChildContent); + subscriber.ContentUpdated(provider); _subscribersByIdentifier.Add(identifier, subscriber); } @@ -88,8 +84,7 @@ public void NotifyContentProviderChanged(object identifier, SectionContent provi // most recently added provider changes. if (providers.Count != 0 && providers[^1] == provider) { - NotifyLogicalParentComponentForSubscriber(identifier, provider); - NotifyContentChangedForSubscriber(identifier, provider.ChildContent); + NotifyContentChangedForSubscriber(identifier, provider); } } @@ -103,19 +98,11 @@ public void NotifyContentProviderChanged(object identifier, SectionContent provi ? GetCurrentProviderContentOrDefault(existingList) : null; - private void NotifyLogicalParentComponentForSubscriber(object identifier, IComponent? logicalParentComponent) - { - if (_subscribersByIdentifier.TryGetValue(identifier, out var subscriber)) - { - subscriber.LogicalParentComponentChanged(logicalParentComponent); - } - } - - private void NotifyContentChangedForSubscriber(object identifier, RenderFragment? content) + private void NotifyContentChangedForSubscriber(object identifier, SectionContent? provider) { if (_subscribersByIdentifier.TryGetValue(identifier, out var subscriber)) { - subscriber.ContentChanged(content); + subscriber.ContentUpdated(provider); } } } diff --git a/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs b/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs index a2fd568c86c1..5ff3e11be8fe 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs @@ -19,22 +19,8 @@ internal sealed class EndpointComponentState : ComponentState public EndpointComponentState(Renderer renderer, int componentId, IComponent component, ComponentState? parentComponentState) : base(renderer, componentId, component, parentComponentState) { - SetStreamingRendering(); - } - - public bool StreamRendering { get; private set; } - - protected override void LogicalParentComponentStateChanged(ComponentState? logicalParentComponent) - { - base.LogicalParentComponentStateChanged(logicalParentComponent); - - SetStreamingRendering(); - } - - private void SetStreamingRendering() - { - var streamRenderingAttribute = _streamRenderingAttributeByComponentType.GetOrAdd(Component.GetType(), - type => type.GetCustomAttribute()); + var streamRenderingAttribute = _streamRenderingAttributeByComponentType.GetOrAdd(component.GetType(), + type => type.GetCustomAttribute()); if (streamRenderingAttribute is not null) { @@ -42,11 +28,13 @@ private void SetStreamingRendering() } else { - var logicalParentEndpointComponentState = (EndpointComponentState?)LogicalParentComponentState; - StreamRendering = logicalParentEndpointComponentState?.StreamRendering ?? false; + var parentEndpointComponentState = (EndpointComponentState?)LogicalParentComponentState; + StreamRendering = parentEndpointComponentState?.StreamRendering ?? false; } } + public bool StreamRendering { get; } + /// /// MetadataUpdateHandler event. This is invoked by the hot reload host via reflection. /// From fb5b10251c0026807d28ab04d3fd18de7d44f7ab Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 14 Jun 2023 11:06:42 +0100 Subject: [PATCH 15/15] Microscopically more efficient implementation --- src/Components/Components/src/Rendering/ComponentState.cs | 6 +++--- src/Components/Components/src/Sections/SectionOutlet.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index 7f8ee6af8e9c..46a7320cd85c 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -34,10 +34,10 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, { ComponentId = componentId; ParentComponentState = parentComponentState; - LogicalParentComponentState = parentComponentState?.Component is SectionOutlet sectionOutlet - ? (GetSectionOutletLogicalParent(renderer, sectionOutlet) ?? parentComponentState) - : parentComponentState; Component = component ?? throw new ArgumentNullException(nameof(component)); + LogicalParentComponentState = component is SectionOutlet.SectionOutletContentRenderer + ? (GetSectionOutletLogicalParent(renderer, (SectionOutlet)parentComponentState!.Component) ?? parentComponentState) + : parentComponentState; _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); _cascadingParameters = CascadingParameterState.FindCascadingParameters(this); CurrentRenderTree = new RenderTreeBuilder(); diff --git a/src/Components/Components/src/Sections/SectionOutlet.cs b/src/Components/Components/src/Sections/SectionOutlet.cs index 5d7a96b39d1d..89b014d907d7 100644 --- a/src/Components/Components/src/Sections/SectionOutlet.cs +++ b/src/Components/Components/src/Sections/SectionOutlet.cs @@ -123,7 +123,7 @@ public void Dispose() // so anything computed from this (e.g., whether or not streaming rendering is // enabled) will be freshly re-evaluated, without that information having to // change in place on an existing ComponentState. - private class SectionOutletContentRenderer : IComponent + internal sealed class SectionOutletContentRenderer : IComponent { public const string ContentParameterName = "content";