Skip to content

Commit

Permalink
Blazor Web project interactive options improvements. Rename render mo…
Browse files Browse the repository at this point in the history
…des. (#50684)

Fixes #50433 (Add root level interactivity option)
Fixes #50646 (Remove workaround for Counter component)
Fixes #50636 (Clarify the names of the interactive render modes)

In terms of the code we now emit, there should be nothing controversial here. The template just has to do quite a bit of if/else in many places to account for all these options and how rendermodes are used and not used based on them.

The PR is big because the renames have really wide impact, but almost all the "files changes" are just due to renames. The only real code changes are in the project templates.

# Testing impact

Adding this option, the BlazorWeb template now has **so many** possible combinations of options, including:

 - Whether or not to enable Server interactivity
 - Whether or not to enable WebAssembly interactivity
 - Whether or not to be interactive from the root
 - Whether or not to include sample content
 - Whether or not to use ProgramMain

So that's around 32 combinations of output - without even accounting for auth options! We don't currently have E2E verification of any of them, as those tests are skipped due to unreliability. We're going to have to lean hard on CTI validations for this, and make sure all the important combinations are covered - cc @mkArtakMSFT.

# Options design update

Having a list of 6 separate checkboxes in VS is pretty unpleasant and hard to understand:

<img src="https://github.com/dotnet/aspnetcore/assets/1101362/93713e83-0793-4140-82e1-95ca63580e3d" width="500" />

So, in this PR I'm proposing (and have implemented, but we can still change it), a change to use dropdowns for the interactivity type and location options. This reduces the number of inputs by one, and means they can be more self-describing:

<img src="https://github.com/dotnet/aspnetcore/assets/1101362/649c93fd-d464-499c-b1f2-36436ebf4e3c" width="500" />

 * The "interactivity type" choices are:
   * **None**
   * **Server** (default)
   * **WebAssembly**
   * **Auto (Server and WebAssembly)**.
 * The "interactivity location" choices are:
   * **Per page/component** (default)
   * **Global**

Note that "interactivity location" is disabled if interactivity type == "None", but [only CLI honors that right now](dotnet/templating#5648) (VS should add support later, and until then, location will have no effect if there's no interactivity).

I think this is much easier to understand, since you no longer have to infer that enabling both Server and WebAssembly means you're going to get Auto. It's also much better in the CLI, since it was completely ridiculous before that `--use-server` defaulted to true but `--use-wasm` defaulted to false, so to get WebAssembly you needed to set `--use-server false --use wasm`. Now you would say `--interactivity webassembly` (and not `wasm` - that was weird too).

![image](https://github.com/dotnet/aspnetcore/assets/1101362/0b4751ad-f91b-4bac-8edf-9e31aa761fbf)
  • Loading branch information
SteveSandersonMS committed Sep 15, 2023
1 parent 20e0a78 commit 8ef1934
Show file tree
Hide file tree
Showing 76 changed files with 725 additions and 345 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ internal class RazorComponentDataSourceOptions
.Create(
equals: (x, y) => (x,y) switch
{
(ServerRenderMode, ServerRenderMode) => true,
(WebAssemblyRenderMode, WebAssemblyRenderMode) => true,
(InteractiveServerRenderMode, InteractiveServerRenderMode) => true,
(InteractiveWebAssemblyRenderMode, InteractiveWebAssemblyRenderMode) => true,
_ => false,
},
getHashCode: obj => obj switch
{
ServerRenderMode => 1,
WebAssemblyRenderMode => 2,
InteractiveServerRenderMode => 1,
InteractiveWebAssemblyRenderMode => 2,
_ => throw new InvalidOperationException($"Unknown render mode: {obj}"),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ private void UpdateEndpoints()
if (!found)
{
throw new InvalidOperationException($"Unable to find a provider for the render mode: {renderMode.GetType().FullName}. This generally " +
"means that a call to 'AddWebAssemblyComponents' or 'AddServerComponents' is missing. " +
"For example, change builder.Services.AddRazorComponents() to builder.Services.AddRazorComponents().AddServerComponents().");
"means that a call to 'AddInteractiveWebAssemblyComponents' or 'AddInteractiveServerComponents' is missing. " +
"For example, change builder.Services.AddRazorComponents() to builder.Services.AddRazorComponents().AddInteractiveServerComponents().");
}
}

Expand Down
18 changes: 9 additions & 9 deletions src/Components/Endpoints/src/Discovery/ComponentInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,20 @@ private string GetDebuggerDisplay()

private string GetRenderMode()
{
if (RenderMode is ServerRenderMode { Prerender: var server })
if (RenderMode is InteractiveServerRenderMode { Prerender: var server })
{
var size = (nameof(ServerRenderMode).Length - "RenderModeComparer".Length);
return $"RenderModeComparer = {nameof(ServerRenderMode)[0..size]}, Prerendered = {server}";
var size = (nameof(InteractiveServerRenderMode).Length - "RenderModeComparer".Length);
return $"RenderModeComparer = {nameof(InteractiveServerRenderMode)[0..size]}, Prerendered = {server}";
}
if (RenderMode is WebAssemblyRenderMode { Prerender: var wasm })
if (RenderMode is InteractiveWebAssemblyRenderMode { Prerender: var wasm })
{
var size = (nameof(WebAssemblyRenderMode).Length - "RenderModeComparer".Length);
return $"RenderModeComparer = {nameof(WebAssemblyRenderMode)[0..size]}, Prerendered = {wasm}";
var size = (nameof(InteractiveWebAssemblyRenderMode).Length - "RenderModeComparer".Length);
return $"RenderModeComparer = {nameof(InteractiveWebAssemblyRenderMode)[0..size]}, Prerendered = {wasm}";
}
if (RenderMode is AutoRenderMode { Prerender: var auto })
if (RenderMode is InteractiveAutoRenderMode { Prerender: var auto })
{
var size = (nameof(AutoRenderMode).Length - "RenderModeComparer".Length);
return $"RenderModeComparer = {nameof(AutoRenderMode)[0..size]}, Prerendered = {auto}";
var size = (nameof(InteractiveAutoRenderMode).Length - "RenderModeComparer".Length);
return $"RenderModeComparer = {nameof(InteractiveAutoRenderMode)[0..size]}, Prerendered = {auto}";
}

return "RenderModeComparer = Unknown, Prerendered = Unknown";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ public ISet<IComponentRenderMode> GetDeclaredRenderModesByDiscoveredComponents()
var component = Components[i];
switch (component.RenderMode)
{
case ServerRenderMode:
set.Add(RenderMode.Server);
case InteractiveServerRenderMode:
set.Add(RenderMode.InteractiveServer);
break;
case WebAssemblyRenderMode:
set.Add(RenderMode.WebAssembly);
case InteractiveWebAssemblyRenderMode:
set.Add(RenderMode.InteractiveWebAssembly);
break;
case AutoRenderMode:
set.Add(RenderMode.Server);
set.Add(RenderMode.WebAssembly);
case InteractiveAutoRenderMode:
set.Add(RenderMode.InteractiveServer);
set.Add(RenderMode.InteractiveWebAssembly);
break;
default:
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ internal static void UpdateSaveStateRenderMode(HttpContext httpContext, ICompone
{
var currentInvocation = mode switch
{
ServerRenderMode => InvokedRenderModes.Mode.Server,
WebAssemblyRenderMode => InvokedRenderModes.Mode.WebAssembly,
AutoRenderMode => throw new NotImplementedException("TODO: To be able to support AutoRenderMode, we have to serialize persisted state in both WebAssembly and Server formats, or unify the two formats."),
InteractiveServerRenderMode => InvokedRenderModes.Mode.Server,
InteractiveWebAssemblyRenderMode => InvokedRenderModes.Mode.WebAssembly,
InteractiveAutoRenderMode => throw new NotImplementedException("TODO: To be able to support InteractiveAutoRenderMode, we have to serialize persisted state in both WebAssembly and Server formats, or unify the two formats."),
_ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(mode), nameof(mode)),
};

Expand All @@ -88,9 +88,9 @@ internal static void UpdateSaveStateRenderMode(HttpContext httpContext, ICompone

private static bool ModeEnablesPrerendering(IComponentRenderMode? mode) => mode switch
{
ServerRenderMode { Prerender: true } => true,
WebAssemblyRenderMode { Prerender: true } => true,
AutoRenderMode { Prerender: true } => true,
InteractiveServerRenderMode { Prerender: true } => true,
InteractiveWebAssemblyRenderMode { Prerender: true } => true,
InteractiveAutoRenderMode { Prerender: true } => true,
_ => false
};

Expand Down
24 changes: 12 additions & 12 deletions src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ public SSRRenderModeBoundary(
_renderMode = renderMode;
_prerender = renderMode switch
{
ServerRenderMode mode => mode.Prerender,
WebAssemblyRenderMode mode => mode.Prerender,
AutoRenderMode mode => mode.Prerender,
InteractiveServerRenderMode mode => mode.Prerender,
InteractiveWebAssemblyRenderMode mode => mode.Prerender,
InteractiveAutoRenderMode mode => mode.Prerender,
_ => throw new ArgumentException($"Server-side rendering does not support the render mode '{renderMode}'.", nameof(renderMode))
};
}
Expand All @@ -65,14 +65,14 @@ private static void AssertRenderModeIsConfigured(HttpContext httpContext, Type c
var configuredModes = configuredRenderModesMetadata.ConfiguredRenderModes;

// We have to allow for specified rendermodes being subclases of the known types
if (renderMode is ServerRenderMode || renderMode is AutoRenderMode)
if (renderMode is InteractiveServerRenderMode || renderMode is InteractiveAutoRenderMode)
{
AssertRenderModeIsConfigured<ServerRenderMode>(componentType, renderMode, configuredModes, "AddServerRenderMode");
AssertRenderModeIsConfigured<InteractiveServerRenderMode>(componentType, renderMode, configuredModes, "AddInteractiveServerRenderMode");
}

if (renderMode is WebAssemblyRenderMode || renderMode is AutoRenderMode)
if (renderMode is InteractiveWebAssemblyRenderMode || renderMode is InteractiveAutoRenderMode)
{
AssertRenderModeIsConfigured<WebAssemblyRenderMode>(componentType, renderMode, configuredModes, "AddWebAssemblyRenderMode");
AssertRenderModeIsConfigured<InteractiveWebAssemblyRenderMode>(componentType, renderMode, configuredModes, "AddInteractiveWebAssemblyRenderMode");
}
}

Expand Down Expand Up @@ -165,13 +165,13 @@ public ComponentMarker ToMarker(HttpContext httpContext, int sequence, object? k

var marker = _renderMode switch
{
ServerRenderMode server => ComponentMarker.Create(ComponentMarker.ServerMarkerType, server.Prerender, _markerKey),
WebAssemblyRenderMode webAssembly => ComponentMarker.Create(ComponentMarker.WebAssemblyMarkerType, webAssembly.Prerender, _markerKey),
AutoRenderMode auto => ComponentMarker.Create(ComponentMarker.AutoMarkerType, auto.Prerender, _markerKey),
InteractiveServerRenderMode server => ComponentMarker.Create(ComponentMarker.ServerMarkerType, server.Prerender, _markerKey),
InteractiveWebAssemblyRenderMode webAssembly => ComponentMarker.Create(ComponentMarker.WebAssemblyMarkerType, webAssembly.Prerender, _markerKey),
InteractiveAutoRenderMode auto => ComponentMarker.Create(ComponentMarker.AutoMarkerType, auto.Prerender, _markerKey),
_ => throw new UnreachableException($"Unknown render mode {_renderMode.GetType().FullName}"),
};

if (_renderMode is ServerRenderMode or AutoRenderMode)
if (_renderMode is InteractiveServerRenderMode or InteractiveAutoRenderMode)
{
// Lazy because we don't actually want to require a whole chain of services including Data Protection
// to be required unless you actually use Server render mode.
Expand All @@ -181,7 +181,7 @@ public ComponentMarker ToMarker(HttpContext httpContext, int sequence, object? k
serverComponentSerializer.SerializeInvocation(ref marker, invocationId, _componentType, parameters);
}

if (_renderMode is WebAssemblyRenderMode or AutoRenderMode)
if (_renderMode is InteractiveWebAssemblyRenderMode or InteractiveAutoRenderMode)
{
WebAssemblyComponentSerializer.SerializeInvocation(ref marker, _componentType, parameters);
}
Expand Down
32 changes: 16 additions & 16 deletions src/Components/Endpoints/test/EndpointHtmlRendererTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task CanRender_ParameterlessComponent_ClientMode()
var writer = new StringWriter();

// Act
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new WebAssemblyRenderMode(prerender: false), ParameterView.Empty);
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new InteractiveWebAssemblyRenderMode(prerender: false), ParameterView.Empty);
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
var content = writer.ToString();
var match = Regex.Match(content, ComponentPattern);
Expand All @@ -76,7 +76,7 @@ public async Task CanPrerender_ParameterlessComponent_ClientMode()
var writer = new StringWriter();

// Act
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.WebAssembly, ParameterView.Empty);
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.InteractiveWebAssembly, ParameterView.Empty);
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
Expand Down Expand Up @@ -115,7 +115,7 @@ public async Task CanRender_ComponentWithParameters_ClientMode()

// Act
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent),
new WebAssemblyRenderMode(prerender: false),
new InteractiveWebAssemblyRenderMode(prerender: false),
ParameterView.FromDictionary(new Dictionary<string, object>
{
{ "Name", "Daniel" }
Expand Down Expand Up @@ -152,7 +152,7 @@ public async Task CanRender_ComponentWithNullParameters_ClientMode()

// Act
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent),
new WebAssemblyRenderMode(prerender: false),
new InteractiveWebAssemblyRenderMode(prerender: false),
ParameterView.FromDictionary(new Dictionary<string, object>
{
{ "Name", null }
Expand Down Expand Up @@ -187,7 +187,7 @@ public async Task CanPrerender_ComponentWithParameters_ClientMode()

// Act
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent),
RenderMode.WebAssembly,
RenderMode.InteractiveWebAssembly,
ParameterView.FromDictionary(new Dictionary<string, object>
{
{ "Name", "Daniel" }
Expand Down Expand Up @@ -236,7 +236,7 @@ public async Task CanPrerender_ComponentWithNullParameters_ClientMode()

// Act
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent),
RenderMode.WebAssembly,
RenderMode.InteractiveWebAssembly,
ParameterView.FromDictionary(new Dictionary<string, object>
{
{ "Name", null }
Expand Down Expand Up @@ -300,7 +300,7 @@ public async Task CanRender_ParameterlessComponent_ServerMode()
.ToTimeLimitedDataProtector();

// Act
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new ServerRenderMode(false), ParameterView.Empty);
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new InteractiveServerRenderMode(false), ParameterView.Empty);
var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result));
var match = Regex.Match(content, ComponentPattern);

Expand Down Expand Up @@ -332,7 +332,7 @@ public async Task CanPrerender_ParameterlessComponent_ServerMode()
.ToTimeLimitedDataProtector();

// Act
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.Server, ParameterView.Empty);
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.InteractiveServer, ParameterView.Empty);
var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result));
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);

Expand Down Expand Up @@ -376,8 +376,8 @@ public async Task Prerender_ServerAndClientComponentUpdatesInvokedPrerenderModes

// Act
var parameters = ParameterView.FromDictionary(new Dictionary<string, object> { { "Name", "SomeName" } });
var server = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.Server, parameters);
var client = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.WebAssembly, parameters);
var server = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.InteractiveServer, parameters);
var client = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.InteractiveWebAssembly, parameters);

// Assert
var (_, mode) = Assert.Single(httpContext.Items, (kvp) => kvp.Value is InvokedRenderModes);
Expand All @@ -393,11 +393,11 @@ public async Task CanRenderMultipleServerComponents()
.ToTimeLimitedDataProtector();

// Act
var firstResult = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new ServerRenderMode(true), ParameterView.Empty);
var firstResult = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new InteractiveServerRenderMode(true), ParameterView.Empty);
var firstComponent = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(firstResult));
var firstMatch = Regex.Match(firstComponent, PrerenderedComponentPattern, RegexOptions.Multiline);

var secondResult = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new ServerRenderMode(false), ParameterView.Empty);
var secondResult = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new InteractiveServerRenderMode(false), ParameterView.Empty);
var secondComponent = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(secondResult));
var secondMatch = Regex.Match(secondComponent, ComponentPattern);

Expand Down Expand Up @@ -451,7 +451,7 @@ public async Task CanRender_ComponentWithParameters_ServerMode()

// Act
var parameters = ParameterView.FromDictionary(new Dictionary<string, object> { { "Name", "SomeName" } });
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), new ServerRenderMode(false), parameters);
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), new InteractiveServerRenderMode(false), parameters);
var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result));
var match = Regex.Match(content, ComponentPattern);

Expand Down Expand Up @@ -490,7 +490,7 @@ public async Task CanRender_ComponentWithNullParameters_ServerMode()

// Act
var parameters = ParameterView.FromDictionary(new Dictionary<string, object> { { "Name", null } });
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), new ServerRenderMode(false), parameters);
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), new InteractiveServerRenderMode(false), parameters);
var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result));
var match = Regex.Match(content, ComponentPattern);

Expand Down Expand Up @@ -529,7 +529,7 @@ public async Task CanPrerender_ComponentWithParameters_ServerPrerenderedMode()

// Act
var parameters = ParameterView.FromDictionary(new Dictionary<string, object> { { "Name", "SomeName" } });
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.Server, parameters);
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.InteractiveServer, parameters);
var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result));
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);

Expand Down Expand Up @@ -580,7 +580,7 @@ public async Task CanPrerender_ComponentWithNullParameters_ServerPrerenderedMode

// Act
var parameters = ParameterView.FromDictionary(new Dictionary<string, object> { { "Name", null } });
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.Server, parameters);
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.InteractiveServer, parameters);
var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result));
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);

Expand Down
4 changes: 2 additions & 2 deletions src/Components/Endpoints/test/HotReloadServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,15 +187,15 @@ private static RazorComponentEndpointDataSource<TComponent> CreateDataSource<TCo
}
else
{
result.Options.ConfiguredRenderModes.Add(new ServerRenderMode());
result.Options.ConfiguredRenderModes.Add(new InteractiveServerRenderMode());
}

return result;
}

private class StaticComponent : ComponentBase { }

[RenderModeServer]
[RenderModeInteractiveServer]
private class ServerComponent : ComponentBase { }

private class MockEndpointProvider : RenderModeEndpointProvider
Expand Down
Loading

0 comments on commit 8ef1934

Please sign in to comment.