Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Components/Components/src/CascadingParameterState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
return candidateSupplier;
}

candidate = candidate.ParentComponentState;
candidate = candidate.LogicalParentComponentState;
} while (candidate != null);

// No match
Expand Down
1 change: 1 addition & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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<string!, object!>! routeValues) -> void
*REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary<string!, object!>!
Expand Down
5 changes: 4 additions & 1 deletion src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1039,7 +1042,7 @@ private void HandleExceptionViaErrorBoundary(Exception error, ComponentState? er
return; // Handled successfully
}

candidate = candidate.ParentComponentState;
candidate = candidate.LogicalParentComponentState;
}

// It's unhandled, so treat as fatal
Expand Down
21 changes: 21 additions & 0 deletions src/Components/Components/src/Rendering/ComponentState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Sections;

namespace Microsoft.AspNetCore.Components.Rendering;

Expand Down Expand Up @@ -34,6 +35,9 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
ComponentId = componentId;
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();
Expand All @@ -46,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;
}

/// <summary>
/// Gets the component ID.
/// </summary>
Expand All @@ -61,6 +77,11 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
/// </summary>
public ComponentState? ParentComponentState { get; }

/// <summary>
/// Gets the <see cref="ComponentState"/> of the logical parent component, or null if this is a root component.
/// </summary>
public ComponentState? LogicalParentComponentState { get; }

internal RenderTreeBuilder CurrentRenderTree { get; set; }

internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, out Exception? renderFragmentException)
Expand Down

This file was deleted.

This file was deleted.

6 changes: 2 additions & 4 deletions src/Components/Components/src/Sections/SectionContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Components.Sections;
/// <summary>
/// Provides content to <see cref="SectionOutlet"/> components with matching <see cref="SectionId"/>s.
/// </summary>
public sealed class SectionContent : ISectionContentProvider, IComponent, IDisposable
public sealed class SectionContent : IComponent, IDisposable
{
private object? _registeredIdentifier;
private bool? _registeredIsDefaultContent;
Expand Down Expand Up @@ -35,8 +35,6 @@ public sealed class SectionContent : ISectionContentProvider, IComponent, IDispo
/// </summary>
[Parameter] public RenderFragment? ChildContent { get; set; }

RenderFragment? ISectionContentProvider.Content => ChildContent;

void IComponent.Attach(RenderHandle renderHandle)
{
_registry = renderHandle.Dispatcher.SectionRegistry;
Expand Down Expand Up @@ -79,7 +77,7 @@ Task IComponent.SetParametersAsync(ParameterView parameters)
_registeredIsDefaultContent = IsDefaultContent;
}

_registry.NotifyContentChanged(identifier, this);
_registry.NotifyContentProviderChanged(identifier, this);

return Task.CompletedTask;
}
Expand Down
55 changes: 48 additions & 7 deletions src/Components/Components/src/Sections/SectionOutlet.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// 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;

/// <summary>
/// Renders content provided by <see cref="SectionContent"/> components with matching <see cref="SectionId"/>s.
/// </summary>
[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 = _ => { };

private object? _subscribedIdentifier;
private RenderHandle _renderHandle;
private SectionRegistry _registry = default!;
private RenderFragment? _content;
private SectionContent? _currentContentProvider;

/// <summary>
/// Gets or sets the <see cref="string"/> ID that determines which <see cref="SectionContent"/> instances will provide
Expand All @@ -28,6 +28,8 @@ public sealed class SectionOutlet : ISectionContentSubscriber, IComponent, IDisp
/// </summary>
[Parameter] public object? SectionId { get; set; }

internal IComponent? CurrentLogicalParent => _currentContentProvider;

void IComponent.Attach(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
Expand Down Expand Up @@ -73,9 +75,9 @@ Task IComponent.SetParametersAsync(ParameterView parameters)
return Task.CompletedTask;
}

void ISectionContentSubscriber.ContentChanged(RenderFragment? content)
internal void ContentUpdated(SectionContent? provider)
{
_content = content;
_currentContentProvider = provider;
RenderContent();
}

Expand All @@ -89,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<SectionOutletContentRenderer>(0);
builder.SetKey(fragment);
builder.AddComponentParameter(1, SectionOutletContentRenderer.ContentParameterName, fragment);
builder.CloseComponent();
}

/// <inheritdoc/>
Expand All @@ -100,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.
internal sealed 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<RenderFragment>(ContentParameterName)!;
_renderHandle.Render(fragment);
return Task.CompletedTask;
}
}
}
32 changes: 16 additions & 16 deletions src/Components/Components/src/Sections/SectionRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ namespace Microsoft.AspNetCore.Components.Sections;

internal sealed class SectionRegistry
{
private readonly Dictionary<object, ISectionContentSubscriber> _subscribersByIdentifier = new();
private readonly Dictionary<object, List<ISectionContentProvider>> _providersByIdentifier = new();
private readonly Dictionary<object, SectionOutlet> _subscribersByIdentifier = new();
private readonly Dictionary<object, List<SectionContent>> _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))
{
Expand All @@ -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))
{
Expand All @@ -46,21 +46,21 @@ 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);
NotifyContentChangedForSubscriber(identifier, contentProvider);
}
}

public void Subscribe(object identifier, ISectionContentSubscriber subscriber)
public void Subscribe(object identifier, SectionOutlet subscriber)
{
if (_subscribersByIdentifier.ContainsKey(identifier))
{
throw new InvalidOperationException($"There is already a subscriber to the content with the given section ID '{identifier}'.");
}

// Notify the new subscriber with any existing content.
var content = GetCurrentProviderContentOrDefault(identifier);
subscriber.ContentChanged(content);
var provider = GetCurrentProviderContentOrDefault(identifier);
subscriber.ContentUpdated(provider);

_subscribersByIdentifier.Add(identifier, subscriber);
}
Expand All @@ -73,7 +73,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))
{
Expand All @@ -84,25 +84,25 @@ public void NotifyContentChanged(object identifier, ISectionContentProvider prov
// most recently added provider changes.
if (providers.Count != 0 && providers[^1] == provider)
{
NotifyContentChangedForSubscriber(identifier, provider.Content);
NotifyContentChangedForSubscriber(identifier, provider);
}
}

private static RenderFragment? GetCurrentProviderContentOrDefault(List<ISectionContentProvider> providers)
private static SectionContent? GetCurrentProviderContentOrDefault(List<SectionContent> 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 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public EndpointComponentState(Renderer renderer, int componentId, IComponent com
}
else
{
var parentEndpointComponentState = (EndpointComponentState?)parentComponentState;
var parentEndpointComponentState = (EndpointComponentState?)LogicalParentComponentState;
StreamRendering = parentEndpointComponentState?.StreamRendering ?? false;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>>
{
public SectionsWithStreamingRenderingTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> 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 and StreamingRendering attribute 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<int, Action<IWebElement>>(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);
}
}
Loading