Skip to content

Commit 3c44704

Browse files
Optionally set render mode on a component type (rather than at the call site) (#19)
* Simplify ComponentRenderMode to actually be a numeric value * Implement ComponentRenderModeAttribute * Eliminate the artificial split in the counter component now the page itself can be marked as interactive
1 parent fcb0bdb commit 3c44704

File tree

11 files changed

+99
-68
lines changed

11 files changed

+99
-68
lines changed

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
#nullable enable
22
Microsoft.AspNetCore.Components.ComponentRenderMode
3-
Microsoft.AspNetCore.Components.ComponentRenderMode.ComponentRenderMode(byte numericValue) -> void
3+
Microsoft.AspNetCore.Components.ComponentRenderMode.Unspecified = 0 -> Microsoft.AspNetCore.Components.ComponentRenderMode
4+
Microsoft.AspNetCore.Components.ComponentRenderModeAttribute
5+
Microsoft.AspNetCore.Components.ComponentRenderModeAttribute.ComponentRenderModeAttribute(Microsoft.AspNetCore.Components.ComponentRenderMode mode) -> void
6+
Microsoft.AspNetCore.Components.ComponentRenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.ComponentRenderMode
47
Microsoft.AspNetCore.Components.NavigationManager.HistoryEntryState.get -> string?
58
Microsoft.AspNetCore.Components.NavigationManager.HistoryEntryState.set -> void
69
Microsoft.AspNetCore.Components.NavigationManager.NotifyLocationChangingAsync(string! uri, string? state, bool isNavigationIntercepted) -> System.Threading.Tasks.ValueTask<bool>
710
Microsoft.AspNetCore.Components.NavigationManager.RegisterLocationChangingHandler(System.Func<Microsoft.AspNetCore.Components.Routing.LocationChangingContext!, System.Threading.Tasks.ValueTask>! locationChangingHandler) -> System.IDisposable!
811
Microsoft.AspNetCore.Components.NavigationOptions.HistoryEntryState.get -> string?
912
Microsoft.AspNetCore.Components.NavigationOptions.HistoryEntryState.init -> void
10-
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddAttribute(int sequence, string! name, Microsoft.AspNetCore.Components.ComponentRenderMode! renderMode) -> void
13+
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddAttribute(int sequence, string! name, Microsoft.AspNetCore.Components.ComponentRenderMode renderMode) -> void
1114
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddContent(int sequence, Microsoft.AspNetCore.Components.MarkupString? markupContent) -> void
12-
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentRenderMode.get -> byte
15+
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentRenderMode.get -> Microsoft.AspNetCore.Components.ComponentRenderMode
1316
Microsoft.AspNetCore.Components.RouteData.FormValues.get -> System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, Microsoft.Extensions.Primitives.StringValues>>?
1417
Microsoft.AspNetCore.Components.RouteData.FormValues.init -> void
15-
Microsoft.AspNetCore.Components.RouteView.RenderMode.get -> Microsoft.AspNetCore.Components.ComponentRenderMode!
18+
Microsoft.AspNetCore.Components.RouteView.RenderMode.get -> Microsoft.AspNetCore.Components.ComponentRenderMode
1619
Microsoft.AspNetCore.Components.RouteView.RenderMode.set -> void
1720
Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs.HistoryEntryState.get -> string?
1821
Microsoft.AspNetCore.Components.Routing.LocationChangingContext
@@ -30,7 +33,6 @@ Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute
3033
Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.get -> string?
3134
Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.set -> void
3235
Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.SupplyParameterFromFormAttribute() -> void
33-
readonly Microsoft.AspNetCore.Components.ComponentRenderMode.NumericValue -> byte
3436
static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.CreateInferredEventCallback<T>(object! receiver, Microsoft.AspNetCore.Components.EventCallback<T> callback, T value) -> Microsoft.AspNetCore.Components.EventCallback<T>
3537
static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.InvokeAsynchronousDelegate(System.Action! callback) -> System.Threading.Tasks.Task!
3638
static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.InvokeAsynchronousDelegate(System.Func<System.Threading.Tasks.Task!>! callback) -> System.Threading.Tasks.Task!
@@ -68,7 +70,6 @@ static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.Crea
6870
static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback<string?> setter, string! existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.ChangeEventArgs!>
6971
static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder<T>(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback<T> setter, T existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.ChangeEventArgs!>
7072
static Microsoft.AspNetCore.Components.ParameterView.DangerouslyCaptureUnboundComponentParameters(Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame> frames, int ownerIndex) -> Microsoft.AspNetCore.Components.ParameterView
71-
static readonly Microsoft.AspNetCore.Components.ComponentRenderMode.Unspecified -> Microsoft.AspNetCore.Components.ComponentRenderMode!
7273
virtual Microsoft.AspNetCore.Components.NavigationManager.HandleLocationChangingHandlerException(System.Exception! ex, Microsoft.AspNetCore.Components.Routing.LocationChangingContext! context) -> void
7374
virtual Microsoft.AspNetCore.Components.NavigationManager.SetNavigationLockState(bool value) -> void
7475
*REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri!

src/Components/Components/src/RenderTree/ComponentRenderMode.cs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,11 @@ namespace Microsoft.AspNetCore.Components;
66
/// <summary>
77
/// Indicates how a component should be rendered.
88
/// </summary>
9-
public class ComponentRenderMode
9+
public enum ComponentRenderMode : byte
1010
{
1111
/// <summary>
1212
/// Indicates that no render mode was specified, so the component should continue rendering
1313
/// in the same way as its parent.
1414
/// </summary>
15-
public static readonly ComponentRenderMode Unspecified = new ComponentRenderMode(0);
16-
17-
/// <summary>
18-
/// Represents the render mode as a numeric value.
19-
/// </summary>
20-
public readonly byte NumericValue;
21-
22-
/// <summary>
23-
/// Constructs an instance of <see cref="ComponentRenderMode"/>.
24-
/// </summary>
25-
/// <param name="numericValue">A unique numeric value.</param>
26-
protected ComponentRenderMode(byte numericValue)
27-
=> NumericValue = numericValue;
15+
Unspecified = 0
2816
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components;
5+
6+
/// <summary>
7+
/// Specifies how the component prefers to be rendered. This can be overridden at the point
8+
/// of usage.
9+
/// </summary>
10+
[AttributeUsage(AttributeTargets.Class)]
11+
public class ComponentRenderModeAttribute : Attribute
12+
{
13+
/// <summary>
14+
/// Gets the preferred render mode.
15+
/// </summary>
16+
public ComponentRenderMode Mode { get; }
17+
18+
/// <summary>
19+
/// Constructs an instance of <see cref="ComponentRenderModeAttribute"/>.
20+
/// </summary>
21+
/// <param name="mode">The preferred render mode.</param>
22+
public ComponentRenderModeAttribute(ComponentRenderMode mode)
23+
{
24+
Mode = mode;
25+
}
26+
}

src/Components/Components/src/RenderTree/RenderTreeFrame.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public struct RenderTreeFrame
136136
// RenderTreeFrameType.Component
137137
// --------------------------------------------------------------------------------
138138

139-
[FieldOffset(6)] internal byte ComponentRenderModeField;
139+
[FieldOffset(6)] internal ComponentRenderMode ComponentRenderModeField;
140140
[FieldOffset(8)] internal int ComponentSubtreeLengthField;
141141
[FieldOffset(12)] internal int ComponentIdField;
142142
[FieldOffset(16)]
@@ -147,9 +147,9 @@ public struct RenderTreeFrame
147147

148148
/// <summary>
149149
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Component"/>
150-
/// gets the component render mode's numeric value.
150+
/// gets the component render mode.
151151
/// </summary>
152-
public byte ComponentRenderMode => ComponentRenderModeField;
152+
public ComponentRenderMode ComponentRenderMode => ComponentRenderModeField;
153153

154154
/// <summary>
155155
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Component"/>
@@ -269,7 +269,7 @@ private RenderTreeFrame(int sequence, int elementSubtreeLength, string elementNa
269269
}
270270

271271
// Component constructor
272-
private RenderTreeFrame(int sequence, int componentSubtreeLength, [DynamicallyAccessedMembers(LinkerFlags.Component)] Type componentType, ComponentState componentState, object componentKey, byte componentRenderMode)
272+
private RenderTreeFrame(int sequence, int componentSubtreeLength, [DynamicallyAccessedMembers(LinkerFlags.Component)] Type componentType, ComponentState componentState, object componentKey, ComponentRenderMode componentRenderMode)
273273
: this()
274274
{
275275
SequenceField = sequence;
@@ -384,7 +384,7 @@ internal RenderTreeFrame WithComponent(ComponentState componentState)
384384
=> new RenderTreeFrame(SequenceField, componentSubtreeLength: ComponentSubtreeLengthField, ComponentTypeField, componentState, ComponentKeyField, ComponentRenderModeField);
385385

386386
internal RenderTreeFrame WithRenderMode(ComponentRenderMode componentRenderMode)
387-
=> new RenderTreeFrame(SequenceField, componentSubtreeLength: ComponentSubtreeLengthField, ComponentTypeField, ComponentStateField, ComponentKeyField, componentRenderMode.NumericValue);
387+
=> new RenderTreeFrame(SequenceField, componentSubtreeLength: ComponentSubtreeLengthField, ComponentTypeField, ComponentStateField, ComponentKeyField, componentRenderMode);
388388

389389
internal RenderTreeFrame WithAttributeEventHandlerId(ulong eventHandlerId)
390390
=> new RenderTreeFrame(SequenceField, attributeName: AttributeNameField, AttributeValueField, eventHandlerId, AttributeEventUpdatesAttributeNameField);

src/Components/Components/src/RenderTree/RenderTreeFrameArrayBuilder.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Concurrent;
45
using System.Diagnostics.CodeAnalysis;
6+
using System.Reflection;
57
using static Microsoft.AspNetCore.Internal.LinkerFlags;
68

79
namespace Microsoft.AspNetCore.Components.RenderTree;
@@ -87,9 +89,25 @@ public void AppendComponent(int sequence, [DynamicallyAccessedMembers(Component)
8789
SequenceField = sequence,
8890
FrameTypeField = RenderTreeFrameType.Component,
8991
ComponentTypeField = componentType,
92+
93+
// WARNING: We should NOT do this here in any real implementation. This is very inefficient
94+
// considering what a hot code path we're on. Instead, the default mode should be resolved
95+
// at compile time by the Razor compiler and be passed as an extra parameter to some new
96+
// overload of builder.OpenComponent.
97+
ComponentRenderModeField = GetCachedDefaultRenderMode(componentType),
9098
};
9199
}
92100

101+
// WARNING: As explained above, this code should NOT exist in any real implementation.
102+
private static readonly ConcurrentDictionary<Type, ComponentRenderMode> _cachedDefaultRenderModes = new();
103+
private static ComponentRenderMode GetCachedDefaultRenderMode(Type componentType)
104+
=> _cachedDefaultRenderModes.GetOrAdd(componentType,
105+
type => type.GetCustomAttribute<ComponentRenderModeAttribute>() switch
106+
{
107+
ComponentRenderModeAttribute attribute => attribute.Mode,
108+
_ => ComponentRenderMode.Unspecified,
109+
});
110+
93111
public void AppendElementReferenceCapture(int sequence, Action<ElementReference> elementReferenceCaptureAction)
94112
{
95113
if (_itemsInUse == _items.Length)
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
@page "/counter-interactive"
2+
@attribute [ComponentRenderMode(WebComponentRenderMode.Auto)]
23

34
<PageTitle>Counter</PageTitle>
45

56
<h1>Counter</h1>
67

7-
<CounterUI InitialCount="100" rendermode="@WebComponentRenderMode.Auto" />
8+
<p>Current count: @currentCount</p>
9+
10+
<button @onclick="@IncrementCount" class="btn btn-primary">Increment</button>
11+
12+
@code {
13+
int currentCount;
14+
15+
void IncrementCount()
16+
{
17+
currentCount++;
18+
}
19+
}

src/Components/Samples/BlazorUnitedApp/Shared/CounterUI.razor

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/Components/Web/src/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#nullable enable
2+
const Microsoft.AspNetCore.Components.Web.WebComponentRenderMode.Auto = (Microsoft.AspNetCore.Components.ComponentRenderMode)34 -> Microsoft.AspNetCore.Components.ComponentRenderMode
3+
const Microsoft.AspNetCore.Components.Web.WebComponentRenderMode.Server = (Microsoft.AspNetCore.Components.ComponentRenderMode)32 -> Microsoft.AspNetCore.Components.ComponentRenderMode
4+
const Microsoft.AspNetCore.Components.Web.WebComponentRenderMode.WebAssembly = (Microsoft.AspNetCore.Components.ComponentRenderMode)33 -> Microsoft.AspNetCore.Components.ComponentRenderMode
25
Microsoft.AspNetCore.Components.Forms.InputRadio<TValue>.Element.get -> Microsoft.AspNetCore.Components.ElementReference?
36
Microsoft.AspNetCore.Components.Forms.InputRadio<TValue>.Element.set -> void
47
Microsoft.AspNetCore.Components.Routing.NavigationLock
@@ -14,6 +17,3 @@ Microsoft.AspNetCore.Components.Web.MouseEventArgs.MovementX.set -> void
1417
Microsoft.AspNetCore.Components.Web.MouseEventArgs.MovementY.get -> double
1518
Microsoft.AspNetCore.Components.Web.MouseEventArgs.MovementY.set -> void
1619
Microsoft.AspNetCore.Components.Web.WebComponentRenderMode
17-
static readonly Microsoft.AspNetCore.Components.Web.WebComponentRenderMode.Auto -> Microsoft.AspNetCore.Components.Web.WebComponentRenderMode!
18-
static readonly Microsoft.AspNetCore.Components.Web.WebComponentRenderMode.Server -> Microsoft.AspNetCore.Components.Web.WebComponentRenderMode!
19-
static readonly Microsoft.AspNetCore.Components.Web.WebComponentRenderMode.WebAssembly -> Microsoft.AspNetCore.Components.Web.WebComponentRenderMode!

src/Components/Web/src/WebComponentRenderMode.cs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,35 @@ namespace Microsoft.AspNetCore.Components.Web;
66
/// <summary>
77
/// Indicates how a component should be rendered.
88
/// </summary>
9-
public sealed class WebComponentRenderMode : ComponentRenderMode
9+
public static class WebComponentRenderMode
1010
{
11-
// Make sure that any newly-added modes have numeric values that don't clash with others
12-
// in the inheritance hierarchy of this type.
13-
14-
// TODO: Add ServerPrerendered, WebAssemblyPrerendered
11+
// For layering reasons, we need WebComponentRenderMode to be distinct from ComponentRenderMode,
12+
// since M.A.Components itself doesn't know about concepts like "server" or "webassembly".
13+
//
14+
// For overload resolution reasons (i.e., making AddAttribute("rendermode", WebComponentRenderMode.Server) work),
15+
// we need WebComponentRenderMode values to be ComponentRenderMode values (e.g., of an inherited type). But
16+
// unfortunately C# doesn't do inheritance for enums, nor does it support implicit conversions between them.
17+
//
18+
// For attribute usage reasons (i.e., making [ComponentRenderMode(WebComponentRenderMode.Server)] work), we need
19+
// the values to be compile-time constants.
20+
//
21+
// All these requirements are difficult to reconcile, but one technique that does suffice is having ComponentRenderMode
22+
// be an enum, and having "derived types" actually just be consts that cast other numeric values to that enum type.
23+
// When consumed in application code, it looks equivalent to WebComponentRenderMode being a derived enum type.
1524

1625
/// <summary>
1726
/// Indicates that the component should run interactively on the server.
1827
/// </summary>
19-
public static readonly WebComponentRenderMode Server = new WebComponentRenderMode(32);
28+
public const ComponentRenderMode Server = (ComponentRenderMode)32;
2029

2130
/// <summary>
2231
/// Indicates that the component should run interactively using WebAssembly.
2332
/// </summary>
24-
public static readonly WebComponentRenderMode WebAssembly = new WebComponentRenderMode(33);
33+
public const ComponentRenderMode WebAssembly = (ComponentRenderMode)33;
2534

2635
/// <summary>
2736
/// Indicates that the component should run interactively using WebAssembly if already loaded,
2837
/// otherwise on the server.
2938
/// </summary>
30-
public static readonly WebComponentRenderMode Auto = new WebComponentRenderMode(34);
31-
32-
private WebComponentRenderMode(byte numericValue) : base(numericValue)
33-
{
34-
}
39+
public const ComponentRenderMode Auto = (ComponentRenderMode)34;
3540
}

src/Mvc/Mvc.RazorPages/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Microsoft.AspNetCore.Mvc.RazorPages.RazorComponentResult.RazorComponentResult(Mi
1414
Microsoft.AspNetCore.Mvc.RazorPages.RazorComponentResult.RazorComponentResult(Microsoft.AspNetCore.Components.IComponent! component, System.Collections.Generic.IReadOnlyDictionary<string!, object?>? parameters) -> void
1515
Microsoft.AspNetCore.Mvc.RazorPages.RazorComponentResult.RazorComponentResult(System.Type! componentType) -> void
1616
Microsoft.AspNetCore.Mvc.RazorPages.RazorComponentResult.RazorComponentResult(System.Type! componentType, System.Collections.Generic.IReadOnlyDictionary<string!, object?>? parameters) -> void
17-
Microsoft.AspNetCore.Mvc.RazorPages.RazorComponentResult.RenderMode.get -> Microsoft.AspNetCore.Components.ComponentRenderMode!
17+
Microsoft.AspNetCore.Mvc.RazorPages.RazorComponentResult.RenderMode.get -> Microsoft.AspNetCore.Components.ComponentRenderMode
1818
Microsoft.AspNetCore.Mvc.RazorPages.RazorComponentResult.RenderMode.set -> void
1919
Microsoft.AspNetCore.Mvc.RazorPages.RazorComponentResult.WithParameter(string! name, object? value) -> Microsoft.AspNetCore.Mvc.RazorPages.RazorComponentResult!
2020
Microsoft.AspNetCore.Mvc.RazorPages.RazorComponentResult<TComponent>

0 commit comments

Comments
 (0)