-
Notifications
You must be signed in to change notification settings - Fork 10.3k
[Blazor] Render Blazor Webassembly components from MVC #25203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Blazor] Render Blazor Webassembly components from MVC #25203
Conversation
src/Components/Web.JS/src/Platform/WebAssemblyComponentAttacher.ts
Outdated
Show resolved
Hide resolved
src/Components/WebAssembly/WebAssembly/src/Hosting/RootComponentTypeCache.cs
Outdated
Show resolved
Hide resolved
src/Components/test/E2ETest/Tests/ClientRenderingMultpleComponentsTest.cs
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good
const payload = JSON.parse(json) as ComponentComment; | ||
const { type, sequence, descriptor, prerenderId } = payload; | ||
if (type !== 'server') { | ||
if (type !== 'server' && type !== 'client') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would CircuitManager encounter this? Could you add a comment for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could have a page that renders both a server component and a client component, I guess we don't support that, but it's better to be defensive I think.
src/Components/WebAssembly/WebAssembly/src/Hosting/RootComponentMapping.cs
Outdated
Show resolved
Hide resolved
src/Components/WebAssembly/WebAssembly/src/Hosting/RootComponentTypeCache.cs
Outdated
Show resolved
Hide resolved
src/Components/WebAssembly/WebAssembly/src/Hosting/RootComponentTypeCache.cs
Outdated
Show resolved
Hide resolved
src/Components/WebAssembly/WebAssembly/src/Hosting/RootComponentTypeCache.cs
Outdated
Show resolved
Hide resolved
var registeredComponents = new ClientComponentMarker[componentsCount]; | ||
for (var i = 0; i < componentsCount; i++) | ||
{ | ||
var id = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, int>(RegisteredComponentsInterop.GetId, i, null, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to read these in one call?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK I believe we can't. I know there are functions to read from the heap, but I don't think there are functions to write to the heap. Are there?
I don't think this is incredibly critical since unmarshalled interop should be pretty fast, I'm happy to change this post RC1 if we think it's important.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I don't feel strongly about this, as it's reasonably clear what's going on from the code.
If we wanted to simplify it on the .NET side, we might consider having a single marshalled call that returns the whole set of descriptors. The extra perf impact of that would be marginal because we're doing JSON deserialization for the parameters anyway. Marshalled calls are only slower because of the JSON aspect - in other respects they are basically the same as unmarshalled calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd have to measure it to be certain, but it might actually be faster as a single marshalled call here because we'd only have to go through the process of copying string data from JS to .NET once, instead of 5x for each root component.
However I still don't think it's urgent to optimize this.
src/Components/test/E2ETest/Tests/ClientRenderingMultpleComponentsTest.cs
Show resolved
Hide resolved
src/Components/WebAssembly/WebAssembly/src/Prerendering/ClientComponentParameterDeserializer.cs
Outdated
Show resolved
Hide resolved
src/Components/WebAssembly/WebAssembly/src/Prerendering/ClientComponentParameterDeserializer.cs
Outdated
Show resolved
Hide resolved
🆙 📅 |
throw new Error(`Invalid component type '${type}'.`); | ||
} | ||
|
||
if(type === 'client') | ||
{ | ||
return undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw your comment above about potentially supporting rendering both client and server components in the future. In the meantime, should this actually throw instead of returning?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It’s safer to ignore it I think, the idea here is that you only handle what you know about and leave anything else alone.
src/Components/Web.JS/src/Platform/WebAssemblyComponentAttacher.ts
Outdated
Show resolved
Hide resolved
src/Components/Web.JS/src/Platform/WebAssemblyComponentAttacher.ts
Outdated
Show resolved
Hide resolved
|
||
function validateEndComponentPayload(json: string, prerenderedId: string): void { | ||
const payload = JSON.parse(json) as ComponentComment; | ||
if (Object.keys(payload).length !== 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't expect the end comment to have all the properties of a typical component comment (e.g. type and parameterDefinitions) is it fair to type it like that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This follows the same pattern as the ServerComponentMarker on CircuitManager.ts
If I remember correctly, it was more problematic typing to deal with two different types.
There’s no much value in having an extra type just for this and it complicates signatures and other types.
src/Components/Web.JS/src/Platform/WebAssemblyComponentAttacher.ts
Outdated
Show resolved
Hide resolved
48c5aa4
to
be4ef1a
Compare
const bootConfigResult = await BootConfigResult.initAsync(environment); | ||
const bootConfigPromise = BootConfigResult.initAsync(environment); | ||
|
||
// Leverage the time while we are loading boot.config.json from the network to discover any potentially registered component on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool
src/Components/WebAssembly/WebAssembly/src/Hosting/RootComponentMappingCollection.cs
Show resolved
Hide resolved
src/Components/WebAssembly/WebAssembly/src/Prerendering/ClientComponentParameterDeserializer.cs
Outdated
Show resolved
Hide resolved
src/Mvc/Mvc.ViewFeatures/src/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj
Outdated
Show resolved
Hide resolved
<Compile Include="$(ComponentsSharedSourceRoot)src\ComponentParametersTypeCache.cs" /> | ||
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentTypeCache.cs" /> | ||
<Compile Include="$(SharedSourceRoot)Components\ClientComponentSerializationSettings.cs" Link="Prerendering/ClientComponentSerializationSettings.cs" /> | ||
<Compile Include="$(SharedSourceRoot)Components\ClientComponentMarker.cs" Link="Prerendering/ClientComponentMarker.cs" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are ClientComponentSerializationSettings
and ClientComponentMarker
used for both server-side components and webassembly ones? It seems like probably so since they are now in shared source, but I can't see any corresponding older server-specific versions of the code having been removed in favour of the new shared ones.
Or if these are wasm-specific, would it be possible rename them to WebAssemblyComponentSerializationSettings
and WebAssemblyComponentMarker
? We've been preferring webassembly as the general term rather than "client" due to it being less ambiguous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've renamed them.
I'm keeping the markers separate between SSB and Wasm. While they share some things in common, they represent implementation details so it's better to keep them separate to avoid accidental changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
We discussed offline and agreed on a bit of refactoring to consolidate the TypeScript code. Besides that, approving :)
a7dfd7b
to
a992114
Compare
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
|
SummaryWe want to enable rendering blazor webassembly components from MVC applications, this means rendering a placeholder for a component as well MotivationWe've received feedback from multiple customers indicating that this is something they want to be able to do. In addition to that, this feature enables a few scenarios that are currently hard or impossible to achieve:
Goals
Non-goals
Scenarios
Risks
Interaction with other parts of the framework
Detailed designThe component tag helper will accept two new render modes:
<component Type="typeof(App)" RenderMode="Client" /> <component Type="typeof(App)" RenderMode="ClientPrerendered" param-Age="18" /> A comment with a descriptor will be emitted for each component that gets rendered into the page, similar to what is done for server-side Blazor. Non prerendered components are represented by a single comment containing the descriptor: <!--Blazor:{"type":"client","assembly":"BasicTestApp","typeName":"BasicTestApp.MultipleComponents.GreeterComponent","parameterDefinitions":"W3sibmFtZSI6Im5hbWUiLCJ0eXBlTmFtZSI6IlN5c3RlbS5TdHJpbmciLCJhc3NlbWJseSI6IlN5c3RlbS5Qcml2YXRlLkNvcmVMaWIifV0=","parameterValues":"WyJSZWQgZmlzaCJd"}--> Prerendered components are represented by a pair of comments that surround the prerendered content and that include a matching "prerenderId": <!--Blazor:{"type":"client","assembly":"BasicTestApp","typeName":"BasicTestApp.MultipleComponents.GreeterComponent","parameterDefinitions":"W3sibmFtZSI6Im5hbWUiLCJ0eXBlTmFtZSI6IlN5c3RlbS5TdHJpbmciLCJhc3NlbWJseSI6IlN5c3RlbS5Qcml2YXRlLkNvcmVMaWIifV0=","parameterValues":"WyJCbHVlIGZpc2giXQ==","prerenderId":"f005b7adf03f41febb9ec9195b96e342"}-->
<div class="greet-wrapper"><h3 class="">Greeter component</h3>
<p class="greet">Hello Blue fish</p></div>
<!--Blazor:{"prerenderId":"f005b7adf03f41febb9ec9195b96e342"}--> Each descriptor contains a type, that differentiates it from other descriptor types (server), the assembly name for the root component, the full type name of the root component, and the list of parameter definitions and parameters as JSON serialized and Base64 encoded strings. Base64 encoding is necessary to ensure that the parameters and their definitions are valid HTML comments, given that parameter values can contain user input. For example, a string parameter with the value When the Blazor webassembly application loads, it finds all the component descriptors on the page and gives each one a unique ID. When the .NET code boots up, it queries the JS Boot code to check if there are "registered" components, and if there are, it retrieves each component descriptor through JS interop and adds it to the list of RootComponents in the WebAssemblyHostBuilder. The application has a chance to see the root components that were registered and can perform any tweaks or modifications necessary. As a side DrawbacksConsidered alternativesOpen questions |
Description
We want to enable rendering blazor webassembly components from MVC applications, this means rendering a placeholder for a component as well
as prerendering a component that gets re-rendered client-side when the Blazor webassembly application starts.
Customer Impact
Regression?
No
Risk
Low. We are enabling the same functionality that existed for server-side Blazor application with the same level of support. The code is shared to a large degree with the existing server-side Blazor support and follows the same approach, with a similar set of supported scenarios.
There hasn't been relevant feedback since 3.1 about the support we have in server-side Blazor, so the features we offer cover the majority of scenarios our customers care about.
There has been trickling feedback from customers about the desire to do this, so we decided to do a stretch and include it last minute, since the functionality and scope is well understood and covered by an existing group of tests.