diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 07e51aca6bd3..89989d7da345 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -18,3 +18,4 @@ static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponen static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.GetComponentKey() -> object? diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index c1ee3c2f1646..c5bddeb88d2d 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -339,6 +339,36 @@ internal ValueTask DisposeInBatchAsync(RenderBatchBuilder batchBuilder) return DisposeAsync(); } + /// + /// Gets the component key for this component instance. + /// This is used for state persistence and component identification across render modes. + /// + /// The component key, or null if no key is available. + protected internal virtual object? GetComponentKey() + { + if (ParentComponentState is not { } parentComponentState) + { + return null; + } + + // Check if the parentComponentState has a `@key` directive applied to the current component. + var frames = parentComponentState.CurrentRenderTree.GetFrames(); + for (var i = 0; i < frames.Count; i++) + { + ref var currentFrame = ref frames.Array[i]; + if (currentFrame.FrameType != RenderTreeFrameType.Component || + !ReferenceEquals(Component, currentFrame.Component)) + { + // Skip any frame that is not the current component. + continue; + } + + return currentFrame.ComponentKey; + } + + return null; + } + private string GetDebuggerDisplay() { return $"ComponentId = {ComponentId}, Type = {Component.GetType().Name}, Disposed = {_componentWasDisposed}"; diff --git a/src/Components/Components/src/SupplyParameterFromPersistentComponentStateValueProvider.cs b/src/Components/Components/src/SupplyParameterFromPersistentComponentStateValueProvider.cs index 4168bc8dedf0..d157dfbd3bb4 100644 --- a/src/Components/Components/src/SupplyParameterFromPersistentComponentStateValueProvider.cs +++ b/src/Components/Components/src/SupplyParameterFromPersistentComponentStateValueProvider.cs @@ -45,7 +45,9 @@ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo) return state.TryTakeFromJson(storageKey, parameterInfo.PropertyType, out var value) ? value : null; } - [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")] [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "OpenComponent already has the right set of attributes")] [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")] + [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")] + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "OpenComponent already has the right set of attributes")] + [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")] public void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo) { var propertyName = parameterInfo.PropertyName; @@ -221,35 +223,46 @@ private static void GrowBuffer(ref byte[]? pool, ref Span keyBuffer, int? private static object? GetSerializableKey(ComponentState componentState) { - if (componentState.ParentComponentState is not { } parentComponentState) + var componentKey = componentState.GetComponentKey(); + if (componentKey != null && IsSerializableKey(componentKey)) { - return null; + return componentKey; } - // Check if the parentComponentState has a `@key` directive applied to the current component. - var frames = parentComponentState.CurrentRenderTree.GetFrames(); - for (var i = 0; i < frames.Count; i++) + return null; + } + + private static string GetComponentType(ComponentState componentState) => componentState.Component.GetType().FullName!; + + private static string GetParentComponentType(ComponentState componentState) + { + if (componentState.ParentComponentState == null) + { + return ""; + } + if (componentState.ParentComponentState.Component == null) { - ref var currentFrame = ref frames.Array[i]; - if (currentFrame.FrameType != RenderTree.RenderTreeFrameType.Component || - !ReferenceEquals(componentState.Component, currentFrame.Component)) + return ""; + } + + if (componentState.ParentComponentState.ParentComponentState != null) + { + var renderer = componentState.Renderer; + var parentRenderMode = renderer.GetComponentRenderMode(componentState.ParentComponentState.Component); + var grandParentRenderMode = renderer.GetComponentRenderMode(componentState.ParentComponentState.ParentComponentState.Component); + if (parentRenderMode != grandParentRenderMode) { - // Skip any frame that is not the current component. - continue; + // This is the case when EndpointHtmlRenderer introduces an SSRRenderBoundary component. + // We want to return "" because the SSRRenderBoundary component is not a real component + // and won't appear on the component tree in the WebAssemblyRenderer and RemoteRenderer + // interactive scenarios. + return ""; } - - var componentKey = currentFrame.ComponentKey; - return !IsSerializableKey(componentKey) ? null : componentKey; } - return null; + return GetComponentType(componentState.ParentComponentState); } - private static string GetComponentType(ComponentState componentState) => componentState.Component.GetType().FullName!; - - private static string GetParentComponentType(ComponentState componentState) => - componentState.ParentComponentState == null ? "" : GetComponentType(componentState.ParentComponentState); - private static byte[] KeyFactory((string parentComponentType, string componentType, string propertyName) parts) => SHA256.HashData(Encoding.UTF8.GetBytes(string.Join(".", parts.parentComponentType, parts.componentType, parts.propertyName))); diff --git a/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs b/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs index 5ff3e11be8fe..00a7dfdf7968 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointComponentState.cs @@ -15,10 +15,12 @@ namespace Microsoft.AspNetCore.Components.Endpoints; internal sealed class EndpointComponentState : ComponentState { private static readonly ConcurrentDictionary _streamRenderingAttributeByComponentType = new(); - + private readonly EndpointHtmlRenderer _renderer; public EndpointComponentState(Renderer renderer, int componentId, IComponent component, ComponentState? parentComponentState) : base(renderer, componentId, component, parentComponentState) { + _renderer = (EndpointHtmlRenderer)renderer; + var streamRenderingAttribute = _streamRenderingAttributeByComponentType.GetOrAdd(component.GetType(), type => type.GetCustomAttribute()); @@ -35,6 +37,22 @@ public EndpointComponentState(Renderer renderer, int componentId, IComponent com public bool StreamRendering { get; } + protected override object? GetComponentKey() + { + if (ParentComponentState != null && ParentComponentState.Component is SSRRenderModeBoundary boundary) + { + var (sequence, key) = _renderer.GetSequenceAndKey(ParentComponentState); + var marker = boundary.GetComponentMarkerKey(sequence, key); + if (!marker.Equals(default)) + { + return marker.Serialized(); + } + } + + // Fall back to the default implementation + return base.GetComponentKey(); + } + /// /// MetadataUpdateHandler event. This is invoked by the hot reload host via reflection. /// diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index a7bdea9a75d2..16c44c92f641 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Web.HtmlRendering; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; @@ -298,6 +299,42 @@ internal static ServerComponentInvocationSequence GetOrCreateInvocationId(HttpCo return (ServerComponentInvocationSequence)result!; } + internal (int sequence, object? key) GetSequenceAndKey(ComponentState boundaryComponentState) + { + if (boundaryComponentState is null || boundaryComponentState.Component is not SSRRenderModeBoundary boundary) + { + throw new InvalidOperationException( + "The parent component state must be an SSRRenderModeBoundary to get the sequence and key."); + } + + // The boundary is at the root (not supported, but we handle it gracefully) + if (boundaryComponentState.ParentComponentState is null) + { + return (0, null); + } + + // Grab the parent of the boundary component. We need to find the SSRRenderModeBoundary component marker frame + // within it. As when we do `@rendermode="InteractiveServer" @key="some-key" the sequence we are interested in + // is the one on the SSRRenderModeBoundary component marker frame, not the one on the nested component frame. + // Same for the key. + var targetState = boundaryComponentState.ParentComponentState; + var frames = GetCurrentRenderTreeFrames(targetState.ComponentId); + for (var i = 0; i < frames.Count; i++) + { + ref var frame = ref frames.Array[i]; + if (frame.FrameType == RenderTreeFrameType.Component && + frame.Component is SSRRenderModeBoundary candidate && + ReferenceEquals(candidate, boundary)) + { + // This is the component marker frame, so we can use its sequence and key + return (frame.Sequence, frame.ComponentKey); + } + } + + throw new InvalidOperationException( + "The parent component state does not have a valid SSRRenderModeBoundary component marker frame."); + } + // An implementation of IHtmlContent that holds a reference to a component until we're ready to emit it as HTML to the response. // We don't construct the actual HTML until we receive the call to WriteTo. public class PrerenderedComponentHtmlContent : IHtmlAsyncContent diff --git a/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs b/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs index a8bc7bd7e74d..5c931c61b6f5 100644 --- a/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs +++ b/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs @@ -220,4 +220,14 @@ private ComponentMarkerKey GenerateMarkerKey(int sequence, object? componentKey) FormattedComponentKey = formattedComponentKey, }; } + + /// + /// Gets the ComponentMarkerKey for this boundary if it has been computed. + /// This is used for state persistence across render modes. + /// + /// The ComponentMarkerKey if available, null otherwise. + internal ComponentMarkerKey GetComponentMarkerKey(int sequence, object? componentKey) + { + return _markerKey ??= GenerateMarkerKey(sequence, componentKey); + } } diff --git a/src/Components/Server/src/Circuits/RemoteComponentState.cs b/src/Components/Server/src/Circuits/RemoteComponentState.cs new file mode 100644 index 000000000000..9ad13cdd40fe --- /dev/null +++ b/src/Components/Server/src/Circuits/RemoteComponentState.cs @@ -0,0 +1,38 @@ +// 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.Server.Circuits; + +/// +/// Specialized ComponentState for Server/Remote rendering that supports ComponentMarkerKey for state persistence. +/// +internal sealed class RemoteComponentState : ComponentState +{ + private readonly RemoteRenderer _renderer; + + public RemoteComponentState( + RemoteRenderer renderer, + int componentId, + IComponent component, + ComponentState? parentComponentState) + : base(renderer, componentId, component, parentComponentState) + { + _renderer = renderer; + } + + protected override object? GetComponentKey() + { + var markerKey = _renderer.GetMarkerKey(this); + + // If we have a ComponentMarkerKey, return it for state persistence consistency + if (markerKey != default) + { + return markerKey.Serialized(); + } + + // Fall back to the default implementation + return base.GetComponentKey(); + } +} diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 31b29206212b..8e161697abdb 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.SignalR; @@ -313,6 +314,18 @@ protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessed _ => throw new NotSupportedException($"Cannot create a component of type '{componentType}' because its render mode '{renderMode}' is not supported by interactive server-side rendering."), }; + protected override ComponentState CreateComponentState(int componentId, IComponent component, ComponentState? parentComponentState) + { + return new RemoteComponentState(this, componentId, component, parentComponentState); + } + + internal ComponentMarkerKey GetMarkerKey(RemoteComponentState remoteComponentState) + { + return remoteComponentState.ParentComponentState != null ? + default : + _webRootComponentManager!.GetRootComponentKey(remoteComponentState.ComponentId); + } + private void ProcessPendingBatch(string? errorMessageOrNull, UnacknowledgedRenderBatch entry) { var elapsedTime = entry.ValueStopwatch.GetElapsedTime(); diff --git a/src/Components/Shared/src/WebRootComponentManager.cs b/src/Components/Shared/src/WebRootComponentManager.cs index 99057d534148..43de04bd6b81 100644 --- a/src/Components/Shared/src/WebRootComponentManager.cs +++ b/src/Components/Shared/src/WebRootComponentManager.cs @@ -86,12 +86,26 @@ private WebRootComponent GetRequiredWebRootComponent(int ssrComponentId) #if COMPONENTS_SERVER internal IEnumerable<(int id, ComponentMarkerKey key, (Type componentType, ParameterView parameters))> GetRootComponents() { - foreach (var (id, (key, type, parameters)) in _webRootComponents) + foreach (var (id, (_, key, type, parameters)) in _webRootComponents) { yield return (id, key, (type, parameters)); } } + #endif + internal ComponentMarkerKey GetRootComponentKey(int componentId) + { + foreach (var (_, candidate) in _webRootComponents) + { + var(id, key, _, _) = candidate; + if (id == componentId) + { + return key; + } + } + + return default; + } private sealed class WebRootComponent { @@ -135,17 +149,17 @@ private WebRootComponent( _latestParameters = initialParameters; } -#if COMPONENTS_SERVER public void Deconstruct( + out int interactiveComponentId, out ComponentMarkerKey key, out Type componentType, out ParameterView parameters) { + interactiveComponentId = _interactiveComponentId; key = _key; componentType = _componentType; parameters = _latestParameters.Parameters; } -#endif public Task UpdateAsync( Renderer renderer, diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyComponentState.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyComponentState.cs new file mode 100644 index 000000000000..fde903ed2872 --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyComponentState.cs @@ -0,0 +1,38 @@ +// 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.WebAssembly.Rendering; + +/// +/// Specialized ComponentState for WebAssembly rendering that supports ComponentMarkerKey for state persistence. +/// +internal sealed class WebAssemblyComponentState : ComponentState +{ + private readonly WebAssemblyRenderer _renderer; + + public WebAssemblyComponentState( + WebAssemblyRenderer renderer, + int componentId, + IComponent component, + ComponentState? parentComponentState) + : base(renderer, componentId, component, parentComponentState) + { + _renderer = renderer; + } + + protected override object? GetComponentKey() + { + var markerKey = _renderer.GetMarkerKey(this); + + // If we have a ComponentMarkerKey, return it for state persistence consistency + if (markerKey != default) + { + return markerKey.Serialized(); + } + + // Fall back to the default implementation + return base.GetComponentKey(); + } +} diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index d71cd1d63fcb..08bf6a23a278 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices.JavaScript; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web.Infrastructure; @@ -191,6 +192,18 @@ protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessed _ => throw new NotSupportedException($"Cannot create a component of type '{componentType}' because its render mode '{renderMode}' is not supported by WebAssembly rendering."), }; + protected override ComponentState CreateComponentState(int componentId, IComponent component, ComponentState? parentComponentState) + { + return new WebAssemblyComponentState(this, componentId, component, parentComponentState); + } + + internal ComponentMarkerKey GetMarkerKey(WebAssemblyComponentState webAssemblyComponentState) + { + return webAssemblyComponentState.ParentComponentState != null ? + default : + _webRootComponentManager!.GetRootComponentKey(webAssemblyComponentState.ComponentId); + } + private static partial class Log { [LoggerMessage(100, LogLevel.Critical, "Unhandled exception rendering component: {Message}", EventName = "ExceptionRenderingComponent")] diff --git a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs index b99a682a9457..130b518aa210 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs @@ -1400,6 +1400,58 @@ public void CanPersistMultiplePrerenderedStateDeclaratively_Auto_PersistsOnWebAs Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-auto-2")).Text); } + [Fact] + public void CanPersistMultipleRootPrerenderedStateDeclaratively_WebAssembly() + { + Navigate($"{ServerPathBase}/persist-multiple-root-component-state-declaratively?wasm=true"); + + Browser.Equal("restored 1", () => Browser.FindElement(By.Id("wasm-1")).Text); + Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-wasm-1")).Text); + + Browser.Equal("restored 2", () => Browser.FindElement(By.Id("wasm-2")).Text); + Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-wasm-2")).Text); + } + + [Fact] + public void CanPersistMultipleRootPrerenderedStateDeclaratively_Server() + { + Navigate($"{ServerPathBase}/persist-multiple-root-component-state-declaratively?server=true"); + + Browser.Equal("restored 1", () => Browser.FindElement(By.Id("server-1")).Text); + Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-server-1")).Text); + + Browser.Equal("restored 2", () => Browser.FindElement(By.Id("server-2")).Text); + Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-server-2")).Text); + } + + [Fact] + public void CanPersistMultipleRootPrerenderedStateDeclaratively_Auto_PersistsOnServer() + { + Navigate(ServerPathBase); + Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text); + BlockWebAssemblyResourceLoad(); + + Navigate($"{ServerPathBase}/persist-multiple-root-component-state-declaratively?auto=true"); + + Browser.Equal("restored 1", () => Browser.FindElement(By.Id("auto-1")).Text); + Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-auto-1")).Text); + + Browser.Equal("restored 2", () => Browser.FindElement(By.Id("auto-2")).Text); + Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-auto-2")).Text); + } + + [Fact] + public void CanPersistMultipleRootPrerenderedStateDeclaratively_Auto_PersistsOnWebAssembly() + { + Navigate($"{ServerPathBase}/persist-multiple-root-component-state-declaratively?auto=true"); + + Browser.Equal("restored 1", () => Browser.FindElement(By.Id("auto-1")).Text); + Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-auto-1")).Text); + + Browser.Equal("restored 2", () => Browser.FindElement(By.Id("auto-2")).Text); + Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-auto-2")).Text); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor index f08a419ef6f6..f21620148737 100644 --- a/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor @@ -107,6 +107,6 @@ public class TestServiceProvider : IServiceProvider { public object GetService(Type serviceType) - => throw new NotImplementedException(); + => null; } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistMultipleRootServerState.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistMultipleRootServerState.razor new file mode 100644 index 000000000000..5718db6e96f5 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistMultipleRootServerState.razor @@ -0,0 +1,46 @@ +@page "/persist-multiple-root-component-state-declaratively" +@using Microsoft.AspNetCore.Components.Web +@using TestContentPackage + +

Persist multiple State Components declaratively

+ +@if (Server.GetValueOrDefault()) +{ + Server Persist State Component 1 + +
+ Server Persist State Component 2 + +
+} + +@if (WebAssembly.GetValueOrDefault()) +{ + WebAssembly Persist State Component 1 + +
+ WebAssembly Persist State Component 2 + +
+} + +@if (Auto.GetValueOrDefault()) +{ + Auto Persist State Component 1 + +
+ Auto Persist State Component 2 + +
+} + +@code { + [Parameter, SupplyParameterFromQuery(Name = "server")] + public bool? Server { get; set; } + + [Parameter, SupplyParameterFromQuery(Name = "wasm")] + public bool? WebAssembly { get; set; } + + [Parameter, SupplyParameterFromQuery(Name = "auto")] + public bool? Auto { get; set; } +} diff --git a/src/Shared/Components/ComponentMarker.cs b/src/Shared/Components/ComponentMarker.cs index f58394d74d2b..8bf072db25af 100644 --- a/src/Shared/Components/ComponentMarker.cs +++ b/src/Shared/Components/ComponentMarker.cs @@ -113,6 +113,7 @@ internal struct ComponentEndMarker public string? PrerenderId { get; set; } } +[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] internal struct ComponentMarkerKey : IEquatable { public ComponentMarkerKey(string locationHash, string? formattedComponentKey) @@ -146,4 +147,13 @@ public override readonly bool Equals(object? obj) public override readonly int GetHashCode() => HashCode.Combine(LocationHash, FormattedComponentKey); + + private readonly string GetDebuggerDisplay() => $"LocationHash: {LocationHash}, Key: {FormattedComponentKey ?? "null"}"; + + internal readonly string? Serialized() + { + return this == default + ? null + : $"{LocationHash}:{FormattedComponentKey}"; + } }