[Blazor] Render Blazor Webassembly components from MVC (#25203)
* Server pieces * Changes for prerendering * Discover client components * tmp * Cleanup * Cleanups * Undo changes * Remove unwanted changes * Move interop class to its own file * Cleanup unwanted changes * Add test rendering multiple client-side components * Unit tests and E2E tests * Cleanups * Addressed feedback * Rename Client to WebAssembly in RenderMode * Update generated js files * Cleaned up JS and addressed feedback * Client->WebAssembly and other feedback * Unify component discovery code and use webassembly instead of 'client' * Update js files * Fix tests
This commit is contained in:
parent
78a587b02e
commit
402dc41d33
|
|
@ -59,13 +59,13 @@ namespace Microsoft.AspNetCore.Components.Server
|
||||||
{
|
{
|
||||||
private readonly IDataProtector _dataProtector;
|
private readonly IDataProtector _dataProtector;
|
||||||
private readonly ILogger<ServerComponentDeserializer> _logger;
|
private readonly ILogger<ServerComponentDeserializer> _logger;
|
||||||
private readonly ServerComponentTypeCache _rootComponentTypeCache;
|
private readonly RootComponentTypeCache _rootComponentTypeCache;
|
||||||
private readonly ComponentParameterDeserializer _parametersDeserializer;
|
private readonly ComponentParameterDeserializer _parametersDeserializer;
|
||||||
|
|
||||||
public ServerComponentDeserializer(
|
public ServerComponentDeserializer(
|
||||||
IDataProtectionProvider dataProtectionProvider,
|
IDataProtectionProvider dataProtectionProvider,
|
||||||
ILogger<ServerComponentDeserializer> logger,
|
ILogger<ServerComponentDeserializer> logger,
|
||||||
ServerComponentTypeCache rootComponentTypeCache,
|
RootComponentTypeCache rootComponentTypeCache,
|
||||||
ComponentParameterDeserializer parametersDeserializer)
|
ComponentParameterDeserializer parametersDeserializer)
|
||||||
{
|
{
|
||||||
// When we protect the data we use a time-limited data protector with the
|
// When we protect the data we use a time-limited data protector with the
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<StaticFileOptions>, ConfigureStaticFilesOptions>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<StaticFileOptions>, ConfigureStaticFilesOptions>());
|
||||||
services.TryAddSingleton<CircuitFactory>();
|
services.TryAddSingleton<CircuitFactory>();
|
||||||
services.TryAddSingleton<ServerComponentDeserializer>();
|
services.TryAddSingleton<ServerComponentDeserializer>();
|
||||||
services.TryAddSingleton<ServerComponentTypeCache>();
|
services.TryAddSingleton<RootComponentTypeCache>();
|
||||||
services.TryAddSingleton<ComponentParameterDeserializer>();
|
services.TryAddSingleton<ComponentParameterDeserializer>();
|
||||||
services.TryAddSingleton<ComponentParametersTypeCache>();
|
services.TryAddSingleton<ComponentParametersTypeCache>();
|
||||||
services.TryAddSingleton<CircuitIdFactory>();
|
services.TryAddSingleton<CircuitIdFactory>();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||||
<Description>Runtime server features for ASP.NET Core Components.</Description>
|
<Description>Runtime server features for ASP.NET Core Components.</Description>
|
||||||
|
|
@ -54,6 +54,8 @@
|
||||||
<Compile Include="$(ComponentsSharedSourceRoot)src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
|
<Compile Include="$(ComponentsSharedSourceRoot)src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
|
||||||
<Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="Circuits" />
|
<Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="Circuits" />
|
||||||
<Compile Include="$(ComponentsSharedSourceRoot)src\ElementReferenceJsonConverter.cs" />
|
<Compile Include="$(ComponentsSharedSourceRoot)src\ElementReferenceJsonConverter.cs" />
|
||||||
|
<Compile Include="$(ComponentsSharedSourceRoot)src\ComponentParametersTypeCache.cs" />
|
||||||
|
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentTypeCache.cs" />
|
||||||
|
|
||||||
<Compile Include="..\..\Shared\src\BrowserNavigationManagerInterop.cs" />
|
<Compile Include="..\..\Shared\src\BrowserNavigationManagerInterop.cs" />
|
||||||
<Compile Include="..\..\Shared\src\JsonSerializerOptionsProvider.cs" />
|
<Compile Include="..\..\Shared\src\JsonSerializerOptionsProvider.cs" />
|
||||||
|
|
|
||||||
|
|
@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
return new ServerComponentDeserializer(
|
return new ServerComponentDeserializer(
|
||||||
_ephemeralDataProtectionProvider,
|
_ephemeralDataProtectionProvider,
|
||||||
NullLogger<ServerComponentDeserializer>.Instance,
|
NullLogger<ServerComponentDeserializer>.Instance,
|
||||||
new ServerComponentTypeCache(),
|
new RootComponentTypeCache(),
|
||||||
new ComponentParameterDeserializer(NullLogger<ComponentParameterDeserializer>.Instance, new ComponentParametersTypeCache()));
|
new ComponentParameterDeserializer(NullLogger<ComponentParameterDeserializer>.Instance, new ComponentParametersTypeCache()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ using System.Reflection;
|
||||||
namespace Microsoft.AspNetCore.Components
|
namespace Microsoft.AspNetCore.Components
|
||||||
{
|
{
|
||||||
// A cache for root component types
|
// A cache for root component types
|
||||||
internal class ServerComponentTypeCache
|
internal class RootComponentTypeCache
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<Key, Type> _typeToKeyLookUp = new ConcurrentDictionary<Key, Type>();
|
private readonly ConcurrentDictionary<Key, Type> _typeToKeyLookUp = new ConcurrentDictionary<Key, Type>();
|
||||||
|
|
||||||
|
|
@ -39,14 +39,14 @@ namespace Microsoft.AspNetCore.Components
|
||||||
return assembly.GetType(key.Type, throwOnError: false, ignoreCase: false);
|
return assembly.GetType(key.Type, throwOnError: false, ignoreCase: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct Key : IEquatable<Key>
|
private readonly struct Key : IEquatable<Key>
|
||||||
{
|
{
|
||||||
public Key(string assembly, string type) =>
|
public Key(string assembly, string type) =>
|
||||||
(Assembly, Type) = (assembly, type);
|
(Assembly, Type) = (assembly, type);
|
||||||
|
|
||||||
public string Assembly { get; set; }
|
public string Assembly { get; }
|
||||||
|
|
||||||
public string Type { get; set; }
|
public string Type { get; }
|
||||||
|
|
||||||
public override bool Equals(object obj) => Equals((Key)obj);
|
public override bool Equals(object obj) => Equals((Key)obj);
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -7,11 +7,12 @@ import { shouldAutoStart } from './BootCommon';
|
||||||
import { RenderQueue } from './Platform/Circuits/RenderQueue';
|
import { RenderQueue } from './Platform/Circuits/RenderQueue';
|
||||||
import { ConsoleLogger } from './Platform/Logging/Loggers';
|
import { ConsoleLogger } from './Platform/Logging/Loggers';
|
||||||
import { LogLevel, Logger } from './Platform/Logging/Logger';
|
import { LogLevel, Logger } from './Platform/Logging/Logger';
|
||||||
import { discoverComponents, CircuitDescriptor } from './Platform/Circuits/CircuitManager';
|
import { CircuitDescriptor } from './Platform/Circuits/CircuitManager';
|
||||||
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
|
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
|
||||||
import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
|
import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
|
||||||
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
|
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
|
||||||
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
|
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
|
||||||
|
import { discoverComponents, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
|
||||||
|
|
||||||
let renderingFailed = false;
|
let renderingFailed = false;
|
||||||
let started = false;
|
let started = false;
|
||||||
|
|
@ -29,7 +30,7 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
|
||||||
options.reconnectionHandler = options.reconnectionHandler || window['Blazor'].defaultReconnectionHandler;
|
options.reconnectionHandler = options.reconnectionHandler || window['Blazor'].defaultReconnectionHandler;
|
||||||
logger.log(LogLevel.Information, 'Starting up blazor server-side application.');
|
logger.log(LogLevel.Information, 'Starting up blazor server-side application.');
|
||||||
|
|
||||||
const components = discoverComponents(document);
|
const components = discoverComponents(document, 'server') as ServerComponentDescriptor[];
|
||||||
const circuit = new CircuitDescriptor(components);
|
const circuit = new CircuitDescriptor(components);
|
||||||
|
|
||||||
const initialConnection = await initializeConnection(options, logger, circuit);
|
const initialConnection = await initializeConnection(options, logger, circuit);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { DotNet } from '@microsoft/dotnet-js-interop';
|
||||||
import './GlobalExports';
|
import './GlobalExports';
|
||||||
import * as Environment from './Environment';
|
import * as Environment from './Environment';
|
||||||
import { monoPlatform } from './Platform/Mono/MonoPlatform';
|
import { monoPlatform } from './Platform/Mono/MonoPlatform';
|
||||||
import { renderBatch, getRendererer } from './Rendering/Renderer';
|
import { renderBatch, getRendererer, attachRootComponentToElement, attachRootComponentToLogicalElement } from './Rendering/Renderer';
|
||||||
import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
|
import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
|
||||||
import { shouldAutoStart } from './BootCommon';
|
import { shouldAutoStart } from './BootCommon';
|
||||||
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
|
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
|
||||||
|
|
@ -11,6 +11,8 @@ import { WebAssemblyConfigLoader } from './Platform/WebAssemblyConfigLoader';
|
||||||
import { BootConfigResult } from './Platform/BootConfig';
|
import { BootConfigResult } from './Platform/BootConfig';
|
||||||
import { Pointer } from './Platform/Platform';
|
import { Pointer } from './Platform/Platform';
|
||||||
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
|
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
|
||||||
|
import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
|
||||||
|
import { discoverComponents, WebAssemblyComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
|
||||||
|
|
||||||
let started = false;
|
let started = false;
|
||||||
|
|
||||||
|
|
@ -71,8 +73,31 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
|
||||||
const environment = options?.environment;
|
const environment = options?.environment;
|
||||||
|
|
||||||
// Fetch the resources and prepare the Mono runtime
|
// Fetch the resources and prepare the Mono runtime
|
||||||
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
|
||||||
|
// the document.
|
||||||
|
const discoveredComponents = discoverComponents(document, 'webassembly') as WebAssemblyComponentDescriptor[];
|
||||||
|
const componentAttacher = new WebAssemblyComponentAttacher(discoveredComponents);
|
||||||
|
window['Blazor']._internal.registeredComponents = {
|
||||||
|
getRegisteredComponentsCount: () => componentAttacher.getCount(),
|
||||||
|
getId: (index) => componentAttacher.getId(index),
|
||||||
|
getAssembly: (id) => BINDING.js_string_to_mono_string(componentAttacher.getAssembly(id)),
|
||||||
|
getTypeName: (id) => BINDING.js_string_to_mono_string(componentAttacher.getTypeName(id)),
|
||||||
|
getParameterDefinitions: (id) => BINDING.js_string_to_mono_string(componentAttacher.getParameterDefinitions(id) || ''),
|
||||||
|
getParameterValues: (id) => BINDING.js_string_to_mono_string(componentAttacher.getParameterValues(id) || ''),
|
||||||
|
};
|
||||||
|
|
||||||
|
window['Blazor']._internal.attachRootComponentToElement = (selector, componentId, rendererId) => {
|
||||||
|
const element = componentAttacher.resolveRegisteredElement(selector);
|
||||||
|
if (!element) {
|
||||||
|
attachRootComponentToElement(selector, componentId, rendererId);
|
||||||
|
} else {
|
||||||
|
attachRootComponentToLogicalElement(rendererId, element, componentId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bootConfigResult = await bootConfigPromise;
|
||||||
const [resourceLoader] = await Promise.all([
|
const [resourceLoader] = await Promise.all([
|
||||||
WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, options || {}),
|
WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, options || {}),
|
||||||
WebAssemblyConfigLoader.initAsync(bootConfigResult)]);
|
WebAssemblyConfigLoader.initAsync(bootConfigResult)]);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ window['Blazor'] = {
|
||||||
navigateTo,
|
navigateTo,
|
||||||
|
|
||||||
_internal: {
|
_internal: {
|
||||||
attachRootComponentToElement,
|
|
||||||
navigationManager: navigationManagerInternalFunctions,
|
navigationManager: navigationManagerInternalFunctions,
|
||||||
domWrapper: domFunctions,
|
domWrapper: domFunctions,
|
||||||
Virtualize,
|
Virtualize,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { internalFunctions as navigationManagerFunctions } from '../../Services/NavigationManager';
|
import { internalFunctions as navigationManagerFunctions } from '../../Services/NavigationManager';
|
||||||
import { toLogicalRootCommentElement, LogicalElement } from '../../Rendering/LogicalElements';
|
import { toLogicalRootCommentElement, LogicalElement } from '../../Rendering/LogicalElements';
|
||||||
|
import { ServerComponentDescriptor } from '../../Services/ComponentDescriptorDiscovery';
|
||||||
|
|
||||||
export class CircuitDescriptor {
|
export class CircuitDescriptor {
|
||||||
public circuitId?: string;
|
public circuitId?: string;
|
||||||
|
|
||||||
public components: ComponentDescriptor[];
|
public components: ServerComponentDescriptor[];
|
||||||
|
|
||||||
public constructor(components: ComponentDescriptor[]) {
|
public constructor(components: ServerComponentDescriptor[]) {
|
||||||
this.circuitId = undefined;
|
this.circuitId = undefined;
|
||||||
this.components = components;
|
this.components = components;
|
||||||
}
|
}
|
||||||
|
|
@ -54,221 +55,3 @@ export class CircuitDescriptor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComponentMarker {
|
|
||||||
type: string;
|
|
||||||
sequence: number;
|
|
||||||
descriptor: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComponentDescriptor {
|
|
||||||
public type: string;
|
|
||||||
|
|
||||||
public start: Node;
|
|
||||||
|
|
||||||
public end?: Node;
|
|
||||||
|
|
||||||
public sequence: number;
|
|
||||||
|
|
||||||
public descriptor: string;
|
|
||||||
|
|
||||||
public constructor(type: string, start: Node, end: Node | undefined, sequence: number, descriptor: string) {
|
|
||||||
this.type = type;
|
|
||||||
this.start = start;
|
|
||||||
this.end = end;
|
|
||||||
this.sequence = sequence;
|
|
||||||
this.descriptor = descriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toRecord(): ComponentMarker {
|
|
||||||
const result = { type: this.type, sequence: this.sequence, descriptor: this.descriptor };
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function discoverComponents(document: Document): ComponentDescriptor[] {
|
|
||||||
const componentComments = resolveComponentComments(document);
|
|
||||||
const discoveredComponents: ComponentDescriptor[] = [];
|
|
||||||
for (let i = 0; i < componentComments.length; i++) {
|
|
||||||
const componentComment = componentComments[i];
|
|
||||||
const entry = new ComponentDescriptor(
|
|
||||||
componentComment.type,
|
|
||||||
componentComment.start,
|
|
||||||
componentComment.end,
|
|
||||||
componentComment.sequence,
|
|
||||||
componentComment.descriptor,
|
|
||||||
);
|
|
||||||
|
|
||||||
discoveredComponents.push(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
return discoveredComponents.sort((a, b) => a.sequence - b.sequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface ComponentComment {
|
|
||||||
type: 'server';
|
|
||||||
sequence: number;
|
|
||||||
descriptor: string;
|
|
||||||
start: Node;
|
|
||||||
end?: Node;
|
|
||||||
prerenderId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveComponentComments(node: Node): ComponentComment[] {
|
|
||||||
if (!node.hasChildNodes()) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: ComponentComment[] = [];
|
|
||||||
const childNodeIterator = new ComponentCommentIterator(node.childNodes);
|
|
||||||
while (childNodeIterator.next() && childNodeIterator.currentElement) {
|
|
||||||
const componentComment = getComponentComment(childNodeIterator);
|
|
||||||
if (componentComment) {
|
|
||||||
result.push(componentComment);
|
|
||||||
} else {
|
|
||||||
const childResults = resolveComponentComments(childNodeIterator.currentElement);
|
|
||||||
for (let j = 0; j < childResults.length; j++) {
|
|
||||||
const childResult = childResults[j];
|
|
||||||
result.push(childResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blazorCommentRegularExpression = /\W*Blazor:[^{]*(.*)$/;
|
|
||||||
|
|
||||||
function getComponentComment(commentNodeIterator: ComponentCommentIterator): ComponentComment | undefined {
|
|
||||||
const candidateStart = commentNodeIterator.currentElement;
|
|
||||||
|
|
||||||
if (!candidateStart || candidateStart.nodeType !== Node.COMMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (candidateStart.textContent) {
|
|
||||||
const componentStartComment = new RegExp(blazorCommentRegularExpression);
|
|
||||||
const definition = componentStartComment.exec(candidateStart.textContent);
|
|
||||||
const json = definition && definition[1];
|
|
||||||
|
|
||||||
if (json) {
|
|
||||||
try {
|
|
||||||
return createComponentComment(json, candidateStart, commentNodeIterator);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Found malformed component comment at ${candidateStart.textContent}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createComponentComment(json: string, start: Node, iterator: ComponentCommentIterator): ComponentComment {
|
|
||||||
const payload = JSON.parse(json) as ComponentComment;
|
|
||||||
const { type, sequence, descriptor, prerenderId } = payload;
|
|
||||||
if (type !== 'server') {
|
|
||||||
throw new Error(`Invalid component type '${type}'.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!descriptor) {
|
|
||||||
throw new Error('descriptor must be defined when using a descriptor.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sequence === undefined) {
|
|
||||||
throw new Error('sequence must be defined when using a descriptor.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Number.isInteger(sequence)) {
|
|
||||||
throw new Error(`Error parsing the sequence '${sequence}' for component '${json}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prerenderId) {
|
|
||||||
return {
|
|
||||||
type,
|
|
||||||
sequence: sequence,
|
|
||||||
descriptor,
|
|
||||||
start,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const end = getComponentEndComment(prerenderId, iterator);
|
|
||||||
if (!end) {
|
|
||||||
throw new Error(`Could not find an end component comment for '${start}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type,
|
|
||||||
sequence,
|
|
||||||
descriptor,
|
|
||||||
start,
|
|
||||||
prerenderId,
|
|
||||||
end,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getComponentEndComment(prerenderedId: string, iterator: ComponentCommentIterator): ChildNode | undefined {
|
|
||||||
while (iterator.next() && iterator.currentElement) {
|
|
||||||
const node = iterator.currentElement;
|
|
||||||
if (node.nodeType !== Node.COMMENT_NODE) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!node.textContent) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const definition = new RegExp(blazorCommentRegularExpression).exec(node.textContent);
|
|
||||||
const json = definition && definition[1];
|
|
||||||
if (!json) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateEndComponentPayload(json, prerenderedId);
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateEndComponentPayload(json: string, prerenderedId: string): void {
|
|
||||||
const payload = JSON.parse(json) as ComponentComment;
|
|
||||||
if (Object.keys(payload).length !== 1) {
|
|
||||||
throw new Error(`Invalid end of component comment: '${json}'`);
|
|
||||||
}
|
|
||||||
const prerenderedEndId = payload.prerenderId;
|
|
||||||
if (!prerenderedEndId) {
|
|
||||||
throw new Error(`End of component comment must have a value for the prerendered property: '${json}'`);
|
|
||||||
}
|
|
||||||
if (prerenderedEndId !== prerenderedId) {
|
|
||||||
throw new Error(`End of component comment prerendered property must match the start comment prerender id: '${prerenderedId}', '${prerenderedEndId}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ComponentCommentIterator {
|
|
||||||
|
|
||||||
private childNodes: NodeListOf<ChildNode>;
|
|
||||||
|
|
||||||
private currentIndex: number;
|
|
||||||
|
|
||||||
private length: number;
|
|
||||||
|
|
||||||
public currentElement: ChildNode | undefined;
|
|
||||||
|
|
||||||
public constructor(childNodes: NodeListOf<ChildNode>) {
|
|
||||||
this.childNodes = childNodes;
|
|
||||||
this.currentIndex = -1;
|
|
||||||
this.length = childNodes.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public next(): boolean {
|
|
||||||
this.currentIndex++;
|
|
||||||
if (this.currentIndex < this.length) {
|
|
||||||
this.currentElement = this.childNodes[this.currentIndex];
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
this.currentElement = undefined;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { LogicalElement, toLogicalRootCommentElement } from '../Rendering/LogicalElements';
|
||||||
|
import { WebAssemblyComponentDescriptor } from '../Services/ComponentDescriptorDiscovery';
|
||||||
|
|
||||||
|
export class WebAssemblyComponentAttacher {
|
||||||
|
public preregisteredComponents: WebAssemblyComponentDescriptor[];
|
||||||
|
|
||||||
|
private componentsById: { [index: number]: WebAssemblyComponentDescriptor };
|
||||||
|
|
||||||
|
public constructor(components: WebAssemblyComponentDescriptor[]) {
|
||||||
|
this.preregisteredComponents = components;
|
||||||
|
const componentsById = {};
|
||||||
|
for (let index = 0; index < components.length; index++) {
|
||||||
|
const component = components[index];
|
||||||
|
componentsById[component.id] = component;
|
||||||
|
}
|
||||||
|
this.componentsById = componentsById;
|
||||||
|
}
|
||||||
|
|
||||||
|
public resolveRegisteredElement(id: string): LogicalElement | undefined {
|
||||||
|
const parsedId = Number.parseInt(id);
|
||||||
|
if (!Number.isNaN(parsedId)) {
|
||||||
|
return toLogicalRootCommentElement(this.componentsById[parsedId].start as Comment, this.componentsById[parsedId].end as Comment);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getParameterValues(id: number): string | undefined {
|
||||||
|
return this.componentsById[id].parameterValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getParameterDefinitions(id: number): string | undefined {
|
||||||
|
return this.componentsById[id].parameterDefinitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTypeName(id: number): string {
|
||||||
|
return this.componentsById[id].typeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAssembly(id: number): string {
|
||||||
|
return this.componentsById[id].assembly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getId(index: number): number {
|
||||||
|
return this.preregisteredComponents[index].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCount(): number {
|
||||||
|
return this.preregisteredComponents.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,354 @@
|
||||||
|
export function discoverComponents(document: Document, type: 'webassembly' | 'server'): ServerComponentDescriptor[] | WebAssemblyComponentDescriptor[] {
|
||||||
|
switch (type){
|
||||||
|
case 'webassembly':
|
||||||
|
return discoverWebAssemblyComponents(document);
|
||||||
|
case 'server':
|
||||||
|
return discoverServerComponents(document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function discoverServerComponents(document: Document): ServerComponentDescriptor[] {
|
||||||
|
const componentComments = resolveComponentComments(document, 'server') as ServerComponentComment[];
|
||||||
|
const discoveredComponents: ServerComponentDescriptor[] = [];
|
||||||
|
for (let i = 0; i < componentComments.length; i++) {
|
||||||
|
const componentComment = componentComments[i];
|
||||||
|
const entry = new ServerComponentDescriptor(
|
||||||
|
componentComment.type,
|
||||||
|
componentComment.start,
|
||||||
|
componentComment.end,
|
||||||
|
componentComment.sequence,
|
||||||
|
componentComment.descriptor,
|
||||||
|
);
|
||||||
|
|
||||||
|
discoveredComponents.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return discoveredComponents.sort((a, b): number => a.sequence - b.sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
function discoverWebAssemblyComponents(document: Document): WebAssemblyComponentDescriptor[] {
|
||||||
|
const componentComments = resolveComponentComments(document, 'webassembly') as WebAssemblyComponentDescriptor[];
|
||||||
|
const discoveredComponents: WebAssemblyComponentDescriptor[] = [];
|
||||||
|
for (let i = 0; i < componentComments.length; i++) {
|
||||||
|
const componentComment = componentComments[i];
|
||||||
|
const entry = new WebAssemblyComponentDescriptor(
|
||||||
|
componentComment.type,
|
||||||
|
componentComment.start,
|
||||||
|
componentComment.end,
|
||||||
|
componentComment.assembly,
|
||||||
|
componentComment.typeName,
|
||||||
|
componentComment.parameterDefinitions,
|
||||||
|
componentComment.parameterValues,
|
||||||
|
);
|
||||||
|
|
||||||
|
discoveredComponents.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return discoveredComponents.sort((a, b): number => a.id - b.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ComponentComment {
|
||||||
|
type: 'server' | 'webassembly';
|
||||||
|
prerenderId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServerComponentComment {
|
||||||
|
type: 'server';
|
||||||
|
sequence: number;
|
||||||
|
descriptor: string;
|
||||||
|
start: Node;
|
||||||
|
end?: Node;
|
||||||
|
prerenderId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebAssemblyComponentComment {
|
||||||
|
type: 'webassembly';
|
||||||
|
typeName: string;
|
||||||
|
assembly: string;
|
||||||
|
parameterDefinitions?: string;
|
||||||
|
parameterValues?: string;
|
||||||
|
prerenderId?: string;
|
||||||
|
start: Node;
|
||||||
|
end?: Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveComponentComments(node: Node, type: 'webassembly' | 'server'): ComponentComment[] {
|
||||||
|
if (!node.hasChildNodes()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: ComponentComment[] = [];
|
||||||
|
const childNodeIterator = new ComponentCommentIterator(node.childNodes);
|
||||||
|
while (childNodeIterator.next() && childNodeIterator.currentElement) {
|
||||||
|
const componentComment = getComponentComment(childNodeIterator, type);
|
||||||
|
if (componentComment) {
|
||||||
|
result.push(componentComment);
|
||||||
|
} else {
|
||||||
|
const childResults = resolveComponentComments(childNodeIterator.currentElement, type);
|
||||||
|
for (let j = 0; j < childResults.length; j++) {
|
||||||
|
const childResult = childResults[j];
|
||||||
|
result.push(childResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blazorCommentRegularExpression = /\W*Blazor:[^{]*(?<descriptor>.*)$/;
|
||||||
|
|
||||||
|
function getComponentComment(commentNodeIterator: ComponentCommentIterator, type: 'webassembly' | 'server'): ComponentComment | undefined {
|
||||||
|
const candidateStart = commentNodeIterator.currentElement;
|
||||||
|
|
||||||
|
if (!candidateStart || candidateStart.nodeType !== Node.COMMENT_NODE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (candidateStart.textContent) {
|
||||||
|
const componentStartComment = new RegExp(blazorCommentRegularExpression);
|
||||||
|
const definition = componentStartComment.exec(candidateStart.textContent);
|
||||||
|
const json = definition && definition.groups && definition.groups['descriptor'];
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
try {
|
||||||
|
const componentComment = parseCommentPayload(json);
|
||||||
|
switch (type) {
|
||||||
|
case 'webassembly':
|
||||||
|
return createWebAssemblyComponentComment(componentComment as WebAssemblyComponentComment, candidateStart, commentNodeIterator);
|
||||||
|
case 'server':
|
||||||
|
return createServerComponentComment(componentComment as ServerComponentComment, candidateStart, commentNodeIterator);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Found malformed component comment at ${candidateStart.textContent}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCommentPayload(json: string): ComponentComment {
|
||||||
|
const payload = JSON.parse(json) as ComponentComment;
|
||||||
|
const { type } = payload;
|
||||||
|
if (type !== 'server' && type !== 'webassembly') {
|
||||||
|
throw new Error(`Invalid component type '${type}'.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createServerComponentComment(payload: ServerComponentComment, start: Node, iterator: ComponentCommentIterator): ServerComponentComment | undefined {
|
||||||
|
const { type, descriptor, sequence, prerenderId } = payload;
|
||||||
|
if (type !== 'server') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!descriptor) {
|
||||||
|
throw new Error('descriptor must be defined when using a descriptor.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sequence === undefined) {
|
||||||
|
throw new Error('sequence must be defined when using a descriptor.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(sequence)) {
|
||||||
|
throw new Error(`Error parsing the sequence '${sequence}' for component '${JSON.stringify(payload)}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prerenderId) {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
sequence: sequence,
|
||||||
|
descriptor,
|
||||||
|
start,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const end = getComponentEndComment(prerenderId, iterator);
|
||||||
|
if (!end) {
|
||||||
|
throw new Error(`Could not find an end component comment for '${start}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
sequence,
|
||||||
|
descriptor,
|
||||||
|
start,
|
||||||
|
prerenderId,
|
||||||
|
end,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWebAssemblyComponentComment(payload: WebAssemblyComponentComment, start: Node, iterator: ComponentCommentIterator): WebAssemblyComponentComment | undefined {
|
||||||
|
const { type, assembly, typeName, parameterDefinitions, parameterValues, prerenderId } = payload;
|
||||||
|
if (type !== 'webassembly') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!assembly) {
|
||||||
|
throw new Error('assembly must be defined when using a descriptor.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!typeName) {
|
||||||
|
throw new Error('typeName must be defined when using a descriptor.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prerenderId) {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
assembly,
|
||||||
|
typeName,
|
||||||
|
// Parameter definitions and values come Base64 encoded from the server, since they contain random data and can make the
|
||||||
|
// comment invalid. We could unencode them in .NET Code, but that would be slower to do and we can leverage the fact that
|
||||||
|
// JS provides a native function that will be much faster and that we are doing this work while we are fetching
|
||||||
|
// blazor.boot.json
|
||||||
|
parameterDefinitions: parameterDefinitions && atob(parameterDefinitions),
|
||||||
|
parameterValues: parameterValues && atob(parameterValues),
|
||||||
|
start,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const end = getComponentEndComment(prerenderId, iterator);
|
||||||
|
if (!end) {
|
||||||
|
throw new Error(`Could not find an end component comment for '${start}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
assembly,
|
||||||
|
typeName,
|
||||||
|
// Same comment as above.
|
||||||
|
parameterDefinitions: parameterDefinitions && atob(parameterDefinitions),
|
||||||
|
parameterValues: parameterValues && atob(parameterValues),
|
||||||
|
start,
|
||||||
|
prerenderId,
|
||||||
|
end,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getComponentEndComment(prerenderedId: string, iterator: ComponentCommentIterator): ChildNode | undefined {
|
||||||
|
while (iterator.next() && iterator.currentElement) {
|
||||||
|
const node = iterator.currentElement;
|
||||||
|
if (node.nodeType !== Node.COMMENT_NODE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!node.textContent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = new RegExp(blazorCommentRegularExpression).exec(node.textContent);
|
||||||
|
const json = definition && definition[1];
|
||||||
|
if (!json) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateEndComponentPayload(json, prerenderedId);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateEndComponentPayload(json: string, prerenderedId: string): void {
|
||||||
|
const payload = JSON.parse(json) as ComponentComment;
|
||||||
|
if (Object.keys(payload).length !== 1) {
|
||||||
|
throw new Error(`Invalid end of component comment: '${json}'`);
|
||||||
|
}
|
||||||
|
const prerenderedEndId = payload.prerenderId;
|
||||||
|
if (!prerenderedEndId) {
|
||||||
|
throw new Error(`End of component comment must have a value for the prerendered property: '${json}'`);
|
||||||
|
}
|
||||||
|
if (prerenderedEndId !== prerenderedId) {
|
||||||
|
throw new Error(`End of component comment prerendered property must match the start comment prerender id: '${prerenderedId}', '${prerenderedEndId}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComponentCommentIterator {
|
||||||
|
|
||||||
|
private childNodes: NodeListOf<ChildNode>;
|
||||||
|
|
||||||
|
private currentIndex: number;
|
||||||
|
|
||||||
|
private length: number;
|
||||||
|
|
||||||
|
public currentElement: ChildNode | undefined;
|
||||||
|
|
||||||
|
public constructor(childNodes: NodeListOf<ChildNode>) {
|
||||||
|
this.childNodes = childNodes;
|
||||||
|
this.currentIndex = -1;
|
||||||
|
this.length = childNodes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public next(): boolean {
|
||||||
|
this.currentIndex++;
|
||||||
|
if (this.currentIndex < this.length) {
|
||||||
|
this.currentElement = this.childNodes[this.currentIndex];
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this.currentElement = undefined;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServerComponentMarker {
|
||||||
|
type: string;
|
||||||
|
sequence: number;
|
||||||
|
descriptor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ServerComponentDescriptor {
|
||||||
|
public type: string;
|
||||||
|
|
||||||
|
public start: Node;
|
||||||
|
|
||||||
|
public end?: Node;
|
||||||
|
|
||||||
|
public sequence: number;
|
||||||
|
|
||||||
|
public descriptor: string;
|
||||||
|
|
||||||
|
public constructor(type: string, start: Node, end: Node | undefined, sequence: number, descriptor: string) {
|
||||||
|
this.type = type;
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
this.sequence = sequence;
|
||||||
|
this.descriptor = descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toRecord(): ServerComponentMarker {
|
||||||
|
const result = { type: this.type, sequence: this.sequence, descriptor: this.descriptor };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebAssemblyComponentDescriptor {
|
||||||
|
private static globalId = 1;
|
||||||
|
|
||||||
|
public type: 'webassembly';
|
||||||
|
|
||||||
|
public typeName: string;
|
||||||
|
|
||||||
|
public assembly: string;
|
||||||
|
|
||||||
|
public parameterDefinitions?: string;
|
||||||
|
|
||||||
|
public parameterValues?: string;
|
||||||
|
|
||||||
|
public id: number;
|
||||||
|
|
||||||
|
public start: Node;
|
||||||
|
|
||||||
|
public end?: Node;
|
||||||
|
|
||||||
|
public constructor(type: 'webassembly', start: Node, end: Node | undefined, assembly: string, typeName: string, parameterDefinitions?: string, parameterValues?: string) {
|
||||||
|
this.id = WebAssemblyComponentDescriptor.globalId++;
|
||||||
|
this.type = type;
|
||||||
|
this.assembly = assembly;
|
||||||
|
this.typeName = typeName;
|
||||||
|
this.parameterDefinitions = parameterDefinitions;
|
||||||
|
this.parameterValues = parameterValues;
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
|
{
|
||||||
|
internal class RegisteredComponentsInterop
|
||||||
|
{
|
||||||
|
private static readonly string Prefix = "Blazor._internal.registeredComponents.";
|
||||||
|
|
||||||
|
public static readonly string GetRegisteredComponentsCount = Prefix + "getRegisteredComponentsCount";
|
||||||
|
|
||||||
|
public static readonly string GetId = Prefix + "getId";
|
||||||
|
|
||||||
|
public static readonly string GetAssembly = Prefix + "getAssembly";
|
||||||
|
|
||||||
|
public static readonly string GetTypeName = Prefix + "getTypeName";
|
||||||
|
|
||||||
|
public static readonly string GetParameterDefinitions = Prefix + "getParameterDefinitions";
|
||||||
|
|
||||||
|
public static readonly string GetParameterValues = Prefix + "getParameterValues";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
|
|
@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
/// and <paramref name="selector"/>.
|
/// and <paramref name="selector"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
|
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
|
||||||
/// <param name="selector">The DOM element selector.</param>
|
/// <param name="selector">The DOM element selector or component registration id for the component.</param>
|
||||||
public RootComponentMapping(Type componentType, string selector)
|
public RootComponentMapping(Type componentType, string selector)
|
||||||
{
|
{
|
||||||
if (componentType is null)
|
if (componentType is null)
|
||||||
|
|
@ -38,6 +39,19 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
|
|
||||||
ComponentType = componentType;
|
ComponentType = componentType;
|
||||||
Selector = selector;
|
Selector = selector;
|
||||||
|
Parameters = ParameterView.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of <see cref="RootComponentMapping"/> with the provided <paramref name="componentType"/>
|
||||||
|
/// and <paramref name="selector"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
|
||||||
|
/// <param name="selector">The DOM element selector or registration id for the component.</param>
|
||||||
|
/// <param name="parameters">The parameters to pass to the component.</param>
|
||||||
|
public RootComponentMapping(Type componentType, string selector, ParameterView parameters) : this(componentType, selector)
|
||||||
|
{
|
||||||
|
Parameters = parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -49,5 +63,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
/// Gets the DOM element selector.
|
/// Gets the DOM element selector.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Selector { get; }
|
public string Selector { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the parameters to pass to the root component.
|
||||||
|
/// </summary>
|
||||||
|
public ParameterView Parameters { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,17 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
|
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
|
||||||
/// <param name="selector">The DOM element selector.</param>
|
/// <param name="selector">The DOM element selector.</param>
|
||||||
public void Add(Type componentType, string selector)
|
public void Add(Type componentType, string selector)
|
||||||
|
{
|
||||||
|
Add(componentType, selector, ParameterView.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a component mapping to the collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
|
||||||
|
/// <param name="selector">The DOM element selector.</param>
|
||||||
|
/// <param name="parameters">The parameters to the root component.</param>
|
||||||
|
public void Add(Type componentType, string selector, ParameterView parameters)
|
||||||
{
|
{
|
||||||
if (componentType is null)
|
if (componentType is null)
|
||||||
{
|
{
|
||||||
|
|
@ -45,7 +56,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
throw new ArgumentNullException(nameof(selector));
|
throw new ArgumentNullException(nameof(selector));
|
||||||
}
|
}
|
||||||
|
|
||||||
Add(new RootComponentMapping(componentType, selector));
|
Add(new RootComponentMapping(componentType, selector, parameters));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
for (var i = 0; i < rootComponents.Length; i++)
|
for (var i = 0; i < rootComponents.Length; i++)
|
||||||
{
|
{
|
||||||
var rootComponent = rootComponents[i];
|
var rootComponent = rootComponents[i];
|
||||||
await _renderer.AddComponentAsync(rootComponent.ComponentType, rootComponent.Selector);
|
await _renderer.AddComponentAsync(rootComponent.ComponentType, rootComponent.Selector, rootComponent.Parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
await tcs.Task;
|
await tcs.Task;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
public sealed class WebAssemblyHostBuilder
|
public sealed class WebAssemblyHostBuilder
|
||||||
{
|
{
|
||||||
private Func<IServiceProvider> _createServiceProvider;
|
private Func<IServiceProvider> _createServiceProvider;
|
||||||
|
private RootComponentTypeCache _rootComponentCache;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instance of <see cref="WebAssemblyHostBuilder"/> using the most common
|
/// Creates an instance of <see cref="WebAssemblyHostBuilder"/> using the most common
|
||||||
|
|
@ -57,6 +58,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
|
|
||||||
// Retrieve required attributes from JSRuntimeInvoker
|
// Retrieve required attributes from JSRuntimeInvoker
|
||||||
InitializeNavigationManager(jsRuntimeInvoker);
|
InitializeNavigationManager(jsRuntimeInvoker);
|
||||||
|
InitializeRegisteredRootComponents(jsRuntimeInvoker);
|
||||||
InitializeDefaultServices();
|
InitializeDefaultServices();
|
||||||
|
|
||||||
var hostEnvironment = InitializeEnvironment(jsRuntimeInvoker);
|
var hostEnvironment = InitializeEnvironment(jsRuntimeInvoker);
|
||||||
|
|
@ -68,6 +70,38 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void InitializeRegisteredRootComponents(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker)
|
||||||
|
{
|
||||||
|
var componentsCount = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, int>(RegisteredComponentsInterop.GetRegisteredComponentsCount, null, null, null);
|
||||||
|
if (componentsCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var registeredComponents = new WebAssemblyComponentMarker[componentsCount];
|
||||||
|
for (var i = 0; i < componentsCount; i++)
|
||||||
|
{
|
||||||
|
var id = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, int>(RegisteredComponentsInterop.GetId, i, null, null);
|
||||||
|
var assembly = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, string>(RegisteredComponentsInterop.GetAssembly, id, null, null);
|
||||||
|
var typeName = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, string>(RegisteredComponentsInterop.GetTypeName, id, null, null);
|
||||||
|
var serializedParameterDefinitions = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, string>(RegisteredComponentsInterop.GetParameterDefinitions, id, null, null);
|
||||||
|
var serializedParameterValues = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, string>(RegisteredComponentsInterop.GetParameterValues, id, null, null);
|
||||||
|
registeredComponents[i] = new WebAssemblyComponentMarker(WebAssemblyComponentMarker.ClientMarkerType, assembly, typeName, serializedParameterDefinitions, serializedParameterValues, id.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var componentDeserializer = WebAssemblyComponentParameterDeserializer.Instance;
|
||||||
|
foreach (var registeredComponent in registeredComponents)
|
||||||
|
{
|
||||||
|
_rootComponentCache = new RootComponentTypeCache();
|
||||||
|
var componentType = _rootComponentCache.GetRootComponent(registeredComponent.Assembly, registeredComponent.TypeName);
|
||||||
|
var definitions = componentDeserializer.GetParameterDefinitions(registeredComponent.ParameterDefinitions);
|
||||||
|
var values = componentDeserializer.GetParameterValues(registeredComponent.ParameterValues);
|
||||||
|
var parameters = componentDeserializer.DeserializeParameters(definitions, values);
|
||||||
|
|
||||||
|
RootComponents.Add(componentType, registeredComponent.PrerenderId, parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void InitializeNavigationManager(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker)
|
private void InitializeNavigationManager(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker)
|
||||||
{
|
{
|
||||||
var baseUri = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, string>(BrowserNavigationManagerInterop.GetBaseUri, null, null, null);
|
var baseUri = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, string>(BrowserNavigationManagerInterop.GetBaseUri, null, null, null);
|
||||||
|
|
@ -190,7 +224,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
|
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
|
||||||
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
|
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
|
||||||
Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
|
Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
|
||||||
Services.AddLogging(builder => {
|
Services.AddLogging(builder =>
|
||||||
|
{
|
||||||
builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance));
|
builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||||
|
|
@ -15,13 +15,7 @@
|
||||||
<Reference Include="Microsoft.Extensions.Logging" />
|
<Reference Include="Microsoft.Extensions.Logging" />
|
||||||
<Reference Include="Microsoft.JSInterop.WebAssembly" />
|
<Reference Include="Microsoft.JSInterop.WebAssembly" />
|
||||||
|
|
||||||
<ProjectReference
|
<ProjectReference Include="..\..\..\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj" ReferenceOutputAssemblies="false" SkipGetTargetFrameworkProperties="true" UndefineProperties="TargetFramework" Private="false" Condition="'$(BuildNodeJS)' != 'false' and '$(BuildingInsideVisualStudio)' != 'true'" />
|
||||||
Include="..\..\..\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj"
|
|
||||||
ReferenceOutputAssemblies="false"
|
|
||||||
SkipGetTargetFrameworkProperties="true"
|
|
||||||
UndefineProperties="TargetFramework"
|
|
||||||
Private="false"
|
|
||||||
Condition="'$(BuildNodeJS)' != 'false' and '$(BuildingInsideVisualStudio)' != 'true'" />
|
|
||||||
|
|
||||||
<SuppressBaselineReference Include="Microsoft.AspNetCore.Components.WebAssembly.HttpHandler" />
|
<SuppressBaselineReference Include="Microsoft.AspNetCore.Components.WebAssembly.HttpHandler" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
@ -31,6 +25,11 @@
|
||||||
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerializerOptionsProvider.cs" />
|
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerializerOptionsProvider.cs" />
|
||||||
<Compile Include="$(ComponentsSharedSourceRoot)src\WebEventData.cs" />
|
<Compile Include="$(ComponentsSharedSourceRoot)src\WebEventData.cs" />
|
||||||
<Compile Include="$(ComponentsSharedSourceRoot)src\ElementReferenceJsonConverter.cs" />
|
<Compile Include="$(ComponentsSharedSourceRoot)src\ElementReferenceJsonConverter.cs" />
|
||||||
|
<Compile Include="$(ComponentsSharedSourceRoot)src\ComponentParametersTypeCache.cs" />
|
||||||
|
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentTypeCache.cs" />
|
||||||
|
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentSerializationSettings.cs" Link="Prerendering/WebAssemblyComponentSerializationSettings.cs" />
|
||||||
|
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentMarker.cs" Link="Prerendering/WebAssemblyComponentMarker.cs" />
|
||||||
|
<Compile Include="$(SharedSourceRoot)Components\ComponentParameter.cs" Link="Prerendering/ComponentParameter.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components
|
||||||
|
{
|
||||||
|
internal class WebAssemblyComponentParameterDeserializer
|
||||||
|
{
|
||||||
|
private readonly ComponentParametersTypeCache _parametersCache;
|
||||||
|
|
||||||
|
public WebAssemblyComponentParameterDeserializer(
|
||||||
|
ComponentParametersTypeCache parametersCache)
|
||||||
|
{
|
||||||
|
_parametersCache = parametersCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WebAssemblyComponentParameterDeserializer Instance { get; } = new WebAssemblyComponentParameterDeserializer(new ComponentParametersTypeCache());
|
||||||
|
|
||||||
|
public ParameterView DeserializeParameters(IList<ComponentParameter> parametersDefinitions, IList<object> parameterValues)
|
||||||
|
{
|
||||||
|
var parametersDictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (parameterValues.Count != parametersDefinitions.Count)
|
||||||
|
{
|
||||||
|
// Mismatched number of definition/parameter values.
|
||||||
|
throw new InvalidOperationException($"The number of parameter definitions '{parametersDefinitions.Count}' does not match the number parameter values '{parameterValues.Count}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < parametersDefinitions.Count; i++)
|
||||||
|
{
|
||||||
|
var definition = parametersDefinitions[i];
|
||||||
|
if (definition.Name == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("The name is missing in a parameter definition.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.TypeName == null && definition.Assembly == null)
|
||||||
|
{
|
||||||
|
parametersDictionary[definition.Name] = null;
|
||||||
|
}
|
||||||
|
else if (definition.TypeName == null || definition.Assembly == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"The parameter definition for '{definition.Name}' is incomplete: Type='{definition.TypeName}' Assembly='{definition.Assembly}'.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var parameterType = _parametersCache.GetParameterType(definition.Assembly, definition.TypeName);
|
||||||
|
if (parameterType == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"The parameter '{definition.Name} with type '{definition.TypeName}' in assembly '{definition.Assembly}' could not be found.");
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = (JsonElement)parameterValues[i];
|
||||||
|
var parameterValue = JsonSerializer.Deserialize(
|
||||||
|
value.GetRawText(),
|
||||||
|
parameterType,
|
||||||
|
WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
|
||||||
|
parametersDictionary[definition.Name] = parameterValue;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Could not parse the parameter value for parameter '{definition.Name}' of type '{definition.TypeName}' and assembly '{definition.Assembly}'.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParameterView.FromDictionary(parametersDictionary);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComponentParameter[] GetParameterDefinitions(string parametersDefinitions)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<ComponentParameter[]>(parametersDefinitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<object> GetParameterValues(string parameterValues)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<IList<object>>(parameterValues, WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,13 +47,14 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TComponent">The type of the component.</typeparam>
|
/// <typeparam name="TComponent">The type of the component.</typeparam>
|
||||||
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
|
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
|
||||||
|
/// <param name="parameters">The parameters for the component.</param>
|
||||||
/// <returns>A <see cref="Task"/> that represents the asynchronous rendering of the added component.</returns>
|
/// <returns>A <see cref="Task"/> that represents the asynchronous rendering of the added component.</returns>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Callers of this method may choose to ignore the returned <see cref="Task"/> if they do not
|
/// Callers of this method may choose to ignore the returned <see cref="Task"/> if they do not
|
||||||
/// want to await the rendering of the added component.
|
/// want to await the rendering of the added component.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public Task AddComponentAsync<TComponent>(string domElementSelector) where TComponent : IComponent
|
public Task AddComponentAsync<TComponent>(string domElementSelector, ParameterView parameters) where TComponent : IComponent
|
||||||
=> AddComponentAsync(typeof(TComponent), domElementSelector);
|
=> AddComponentAsync(typeof(TComponent), domElementSelector, parameters);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Associates the <see cref="IComponent"/> with the <see cref="WebAssemblyRenderer"/>,
|
/// Associates the <see cref="IComponent"/> with the <see cref="WebAssemblyRenderer"/>,
|
||||||
|
|
@ -61,12 +62,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="componentType">The type of the component.</param>
|
/// <param name="componentType">The type of the component.</param>
|
||||||
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
|
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
|
||||||
|
/// <param name="parameters">The list of root component parameters.</param>
|
||||||
/// <returns>A <see cref="Task"/> that represents the asynchronous rendering of the added component.</returns>
|
/// <returns>A <see cref="Task"/> that represents the asynchronous rendering of the added component.</returns>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Callers of this method may choose to ignore the returned <see cref="Task"/> if they do not
|
/// Callers of this method may choose to ignore the returned <see cref="Task"/> if they do not
|
||||||
/// want to await the rendering of the added component.
|
/// want to await the rendering of the added component.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public Task AddComponentAsync(Type componentType, string domElementSelector)
|
public Task AddComponentAsync(Type componentType, string domElementSelector, ParameterView parameters)
|
||||||
{
|
{
|
||||||
var component = InstantiateComponent(componentType);
|
var component = InstantiateComponent(componentType);
|
||||||
var componentId = AssignRootComponentId(component);
|
var componentId = AssignRootComponentId(component);
|
||||||
|
|
@ -83,7 +85,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
|
||||||
componentId,
|
componentId,
|
||||||
_webAssemblyRendererId);
|
_webAssemblyRendererId);
|
||||||
|
|
||||||
return RenderRootComponentAsync(componentId);
|
return RenderRootComponentAsync(componentId, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
case "Blazor._internal.navigationManager.getUnmarshalledLocationHref":
|
case "Blazor._internal.navigationManager.getUnmarshalledLocationHref":
|
||||||
var testHref = "https://www.example.com/awesome-part-that-will-be-truncated-in-tests/cool";
|
var testHref = "https://www.example.com/awesome-part-that-will-be-truncated-in-tests/cool";
|
||||||
return (TResult)(object)testHref;
|
return (TResult)(object)testHref;
|
||||||
|
case "Blazor._internal.registeredComponents.getRegisteredComponentsCount":
|
||||||
|
return (TResult)(object)0;
|
||||||
default:
|
default:
|
||||||
throw new NotImplementedException($"{nameof(TestWebAssemblyJSRuntimeInvoker)} has no implementation for '{identifier}'.");
|
throw new NotImplementedException($"{nameof(TestWebAssemblyJSRuntimeInvoker)} has no implementation for '{identifier}'.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
||||||
|
<Compile Include="$(RepoRoot)src\Shared\Components\WebAssemblyComponentSerializationSettings.cs" />
|
||||||
|
<Compile Include="$(RepoRoot)src\Shared\Components\WebAssemblyComponentMarker.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||||
|
using Microsoft.AspNetCore.E2ETesting;
|
||||||
|
using OpenQA.Selenium;
|
||||||
|
using TestServer;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.E2ETests.Tests
|
||||||
|
{
|
||||||
|
public class ClientRenderingMultpleComponentsTest : ServerTestBase<BasicTestAppServerSiteFixture<MultipleComponents>>
|
||||||
|
{
|
||||||
|
private const string MarkerPattern = ".*?<!--Blazor:(.*?)-->.*?";
|
||||||
|
|
||||||
|
public ClientRenderingMultpleComponentsTest(
|
||||||
|
BrowserFixture browserFixture,
|
||||||
|
BasicTestAppServerSiteFixture<MultipleComponents> serverFixture,
|
||||||
|
ITestOutputHelper output)
|
||||||
|
: base(browserFixture, serverFixture, output)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime LastLogTimeStamp { get; set; } = DateTime.MinValue;
|
||||||
|
|
||||||
|
public override async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
await base.InitializeAsync();
|
||||||
|
|
||||||
|
// Capture the last log timestamp so that we can filter logs when we
|
||||||
|
// check for duplicate connections.
|
||||||
|
var lastLog = Browser.Manage().Logs.GetLog(LogType.Browser).LastOrDefault();
|
||||||
|
if (lastLog != null)
|
||||||
|
{
|
||||||
|
LastLogTimeStamp = lastLog.Timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanRenderMultipleRootComponents()
|
||||||
|
{
|
||||||
|
Navigate("/Client/multiple-components");
|
||||||
|
|
||||||
|
var greets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
|
||||||
|
|
||||||
|
Assert.Equal(7, greets.Length); // 1 statically rendered + 5 prerendered + 1 server prerendered
|
||||||
|
Assert.DoesNotContain("Hello Red fish", greets);
|
||||||
|
Assert.Single(greets, "Hello John");
|
||||||
|
Assert.Single(greets, "Hello Abraham");
|
||||||
|
Assert.Equal(2, greets.Where(g => g == "Hello Blue fish").Count());
|
||||||
|
Assert.Equal(3, greets.Where(g => string.Equals("Hello", g)).Count()); // 3 server prerendered without parameters
|
||||||
|
var content = Browser.FindElement(By.Id("test-container")).GetAttribute("innerHTML");
|
||||||
|
var markers = ReadMarkers(content);
|
||||||
|
var componentSequence = markers.Select(m => m.Item1.PrerenderId != null).ToArray();
|
||||||
|
Assert.Equal(13, componentSequence.Length);
|
||||||
|
|
||||||
|
// Once the app starts, output changes
|
||||||
|
BeginInteractivity();
|
||||||
|
|
||||||
|
Browser.Exists(By.CssSelector("h3.interactive"));
|
||||||
|
var updatedGreets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
|
||||||
|
Assert.Equal(7, updatedGreets.Where(g => string.Equals("Hello Alfred", g)).Count());
|
||||||
|
Assert.Equal(2, updatedGreets.Where(g => g == "Hello Red fish").Count());
|
||||||
|
Assert.Equal(2, updatedGreets.Where(g => g == "Hello Blue fish").Count());
|
||||||
|
Assert.Single(updatedGreets.Where(g => string.Equals("Hello Albert", g)));
|
||||||
|
Assert.Single(updatedGreets.Where(g => string.Equals("Hello Abraham", g)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private (WebAssemblyComponentMarker, WebAssemblyComponentMarker)[] ReadMarkers(string content)
|
||||||
|
{
|
||||||
|
content = content.Replace("\r\n", "");
|
||||||
|
var matches = Regex.Matches(content, MarkerPattern);
|
||||||
|
var markers = matches.Select(s => JsonSerializer.Deserialize<WebAssemblyComponentMarker>(
|
||||||
|
s.Groups[1].Value,
|
||||||
|
WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
|
||||||
|
|
||||||
|
var prerenderMarkers = markers.Where(m => m.PrerenderId != null).GroupBy(p => p.PrerenderId).Select(g => (g.First(), g.Skip(1).First())).ToArray();
|
||||||
|
var nonPrerenderMarkers = markers.Where(m => m.PrerenderId == null).Select(g => (g, (WebAssemblyComponentMarker)default)).ToArray();
|
||||||
|
|
||||||
|
return prerenderMarkers.Concat(nonPrerenderMarkers).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeginInteractivity()
|
||||||
|
{
|
||||||
|
Browser.FindElement(By.Id("load-boot-script")).Click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,18 @@ namespace TestServer
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.Map("/Client/multiple-components", app =>
|
||||||
|
{
|
||||||
|
app.UseBlazorFrameworkFiles();
|
||||||
|
app.UseStaticFiles();
|
||||||
|
app.UseRouting();
|
||||||
|
app.UseEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
endpoints.MapRazorPages();
|
||||||
|
endpoints.MapFallbackToPage("/Client/MultipleComponents");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.Map("/multiple-components", app =>
|
app.Map("/multiple-components", app =>
|
||||||
{
|
{
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
@page "/multiple-components"
|
||||||
|
@using BasicTestApp.MultipleComponents;
|
||||||
|
|
||||||
|
@{
|
||||||
|
Layout = "./MultipleComponentsLayout.cshtml";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.WebAssemblyPrerendered))
|
||||||
|
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.WebAssembly))
|
||||||
|
<component type="typeof(GreeterComponent)" render-mode="Static" param-name='"John"' />
|
||||||
|
<component type="typeof(GreeterComponent)" render-mode="WebAssembly" />
|
||||||
|
<div id="container">
|
||||||
|
<p>Some content before</p>
|
||||||
|
<component type="typeof(GreeterComponent)" render-mode="WebAssembly" />
|
||||||
|
<p>Some content between</p>
|
||||||
|
<component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" />
|
||||||
|
<p>Some content after</p>
|
||||||
|
<div id="nested-an-extra-level">
|
||||||
|
<p>Some content before</p>
|
||||||
|
<component type="typeof(GreeterComponent)" render-mode="WebAssembly" />
|
||||||
|
<component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" />
|
||||||
|
<p>Some content after</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="container">
|
||||||
|
<component type="typeof(GreeterComponent)" render-mode="WebAssembly" param-name='"Albert"' />
|
||||||
|
<component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" param-name='"Abraham"' />
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
@using BasicTestApp.MultipleComponents;
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Multiple component entry points</title>
|
||||||
|
@* We need to make sure base is set to "/" so that the libraries load correctly *@
|
||||||
|
<base href="~/" />
|
||||||
|
@* This page is used to validate the ability to render multiple root components in a blazor webassembly application.
|
||||||
|
*@
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="test-container">
|
||||||
|
<component type="typeof(GreeterComponent)" render-mode="WebAssembly" param-name='"Red fish"' />
|
||||||
|
<component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" param-name='"Blue fish"' />
|
||||||
|
@RenderBody()
|
||||||
|
<component type="typeof(GreeterComponent)" render-mode="WebAssembly" param-name='"Red fish"' />
|
||||||
|
<component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" param-name='"Blue fish"' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@*
|
||||||
|
So that E2E tests can make assertions about both the prerendered and
|
||||||
|
interactive states, we only load the .js file when told to.
|
||||||
|
*@
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Used unconditionally on Program.cs, so needs to be defined here to avoid the test failing
|
||||||
|
function getCurrentUrl() {
|
||||||
|
return location.href;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button id="load-boot-script" onclick="start()">Load boot script</button>
|
||||||
|
|
||||||
|
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
|
||||||
|
<script>
|
||||||
|
function start() {
|
||||||
|
Blazor.start({
|
||||||
|
logLevel: 1 // LogLevel.Debug
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -66,6 +66,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
case RenderMode.Server:
|
case RenderMode.Server:
|
||||||
case RenderMode.ServerPrerendered:
|
case RenderMode.ServerPrerendered:
|
||||||
case RenderMode.Static:
|
case RenderMode.Static:
|
||||||
|
case RenderMode.WebAssembly:
|
||||||
|
case RenderMode.WebAssemblyPrerendered:
|
||||||
_renderMode = value;
|
_renderMode = value;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
|
{
|
||||||
|
// See the details of the component serialization protocol in WebAssemblyComponentDeserializer.cs on the Components solution.
|
||||||
|
internal class WebAssemblyComponentSerializer
|
||||||
|
{
|
||||||
|
public WebAssemblyComponentMarker SerializeInvocation(Type type, ParameterView parameters, bool prerendered)
|
||||||
|
{
|
||||||
|
var assembly = type.Assembly.GetName().Name;
|
||||||
|
var typeFullName = type.FullName;
|
||||||
|
var (definitions, values) = ComponentParameter.FromParameterView(parameters);
|
||||||
|
|
||||||
|
// We need to serialize and Base64 encode parameters separately since they can contain arbitrary data that might
|
||||||
|
// cause the HTML comment to be invalid (like if you serialize a string that contains two consecutive dashes "--").
|
||||||
|
var serializedDefinitions = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(definitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
|
||||||
|
var serializedValues = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(values, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
|
||||||
|
|
||||||
|
return prerendered ? WebAssemblyComponentMarker.Prerendered(assembly, typeFullName, serializedDefinitions, serializedValues) :
|
||||||
|
WebAssemblyComponentMarker.NonPrerendered(assembly, typeFullName, serializedDefinitions, serializedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal IEnumerable<string> GetPreamble(WebAssemblyComponentMarker record)
|
||||||
|
{
|
||||||
|
var serializedStartRecord = JsonSerializer.Serialize(
|
||||||
|
record,
|
||||||
|
WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
|
||||||
|
if (record.PrerenderId != null)
|
||||||
|
{
|
||||||
|
return PrerenderedStart(serializedStartRecord);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return NonPrerenderedSequence(serializedStartRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IEnumerable<string> PrerenderedStart(string startRecord)
|
||||||
|
{
|
||||||
|
yield return "<!--Blazor:";
|
||||||
|
yield return startRecord;
|
||||||
|
yield return "-->";
|
||||||
|
}
|
||||||
|
|
||||||
|
static IEnumerable<string> NonPrerenderedSequence(string record)
|
||||||
|
{
|
||||||
|
yield return "<!--Blazor:";
|
||||||
|
yield return record;
|
||||||
|
yield return "-->";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal IEnumerable<string> GetEpilogue(WebAssemblyComponentMarker record)
|
||||||
|
{
|
||||||
|
var serializedStartRecord = JsonSerializer.Serialize(
|
||||||
|
record.GetEndRecord(),
|
||||||
|
WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
|
||||||
|
return PrerenderEnd(serializedStartRecord);
|
||||||
|
|
||||||
|
static IEnumerable<string> PrerenderEnd(string endRecord)
|
||||||
|
{
|
||||||
|
yield return "<!--Blazor:";
|
||||||
|
yield return endRecord;
|
||||||
|
yield return "-->";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -177,6 +177,9 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
// Component services for Blazor server-side interop
|
// Component services for Blazor server-side interop
|
||||||
services.TryAddSingleton<ServerComponentSerializer>();
|
services.TryAddSingleton<ServerComponentSerializer>();
|
||||||
|
|
||||||
|
// Component services for Blazor webassembly interop
|
||||||
|
services.TryAddSingleton<WebAssemblyComponentSerializer>();
|
||||||
|
|
||||||
//
|
//
|
||||||
// View Components
|
// View Components
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>
|
<Description>
|
||||||
|
|
@ -31,7 +31,9 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentSerializationSettings.cs" />
|
||||||
<Compile Include="$(SharedSourceRoot)Components\ServerComponentSerializationSettings.cs" />
|
<Compile Include="$(SharedSourceRoot)Components\ServerComponentSerializationSettings.cs" />
|
||||||
|
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentMarker.cs" />
|
||||||
<Compile Include="$(SharedSourceRoot)Components\ServerComponentMarker.cs" />
|
<Compile Include="$(SharedSourceRoot)Components\ServerComponentMarker.cs" />
|
||||||
<Compile Include="$(SharedSourceRoot)Components\ServerComponent.cs" />
|
<Compile Include="$(SharedSourceRoot)Components\ServerComponent.cs" />
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,8 @@ Microsoft.AspNetCore.Mvc.Rendering.RenderMode
|
||||||
Microsoft.AspNetCore.Mvc.Rendering.RenderMode.Server = 2 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
|
Microsoft.AspNetCore.Mvc.Rendering.RenderMode.Server = 2 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
|
||||||
Microsoft.AspNetCore.Mvc.Rendering.RenderMode.ServerPrerendered = 3 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
|
Microsoft.AspNetCore.Mvc.Rendering.RenderMode.ServerPrerendered = 3 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
|
||||||
Microsoft.AspNetCore.Mvc.Rendering.RenderMode.Static = 1 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
|
Microsoft.AspNetCore.Mvc.Rendering.RenderMode.Static = 1 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
|
||||||
|
Microsoft.AspNetCore.Mvc.Rendering.RenderMode.WebAssembly = 4 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
|
||||||
|
Microsoft.AspNetCore.Mvc.Rendering.RenderMode.WebAssemblyPrerendered = 5 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
|
||||||
Microsoft.AspNetCore.Mvc.Rendering.SelectList
|
Microsoft.AspNetCore.Mvc.Rendering.SelectList
|
||||||
Microsoft.AspNetCore.Mvc.Rendering.SelectListGroup
|
Microsoft.AspNetCore.Mvc.Rendering.SelectListGroup
|
||||||
Microsoft.AspNetCore.Mvc.Rendering.SelectListGroup.Disabled.get -> bool
|
Microsoft.AspNetCore.Mvc.Rendering.SelectListGroup.Disabled.get -> bool
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -15,13 +15,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
private static readonly object ComponentSequenceKey = new object();
|
private static readonly object ComponentSequenceKey = new object();
|
||||||
private readonly StaticComponentRenderer _staticComponentRenderer;
|
private readonly StaticComponentRenderer _staticComponentRenderer;
|
||||||
private readonly ServerComponentSerializer _serverComponentSerializer;
|
private readonly ServerComponentSerializer _serverComponentSerializer;
|
||||||
|
private readonly WebAssemblyComponentSerializer _WebAssemblyComponentSerializer;
|
||||||
|
|
||||||
public ComponentRenderer(
|
public ComponentRenderer(
|
||||||
StaticComponentRenderer staticComponentRenderer,
|
StaticComponentRenderer staticComponentRenderer,
|
||||||
ServerComponentSerializer serverComponentSerializer)
|
ServerComponentSerializer serverComponentSerializer,
|
||||||
|
WebAssemblyComponentSerializer WebAssemblyComponentSerializer)
|
||||||
{
|
{
|
||||||
_staticComponentRenderer = staticComponentRenderer;
|
_staticComponentRenderer = staticComponentRenderer;
|
||||||
_serverComponentSerializer = serverComponentSerializer;
|
_serverComponentSerializer = serverComponentSerializer;
|
||||||
|
_WebAssemblyComponentSerializer = WebAssemblyComponentSerializer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IHtmlContent> RenderComponentAsync(
|
public async Task<IHtmlContent> RenderComponentAsync(
|
||||||
|
|
@ -55,6 +58,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
|
RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
|
||||||
RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
|
RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
|
||||||
RenderMode.Static => await StaticComponentAsync(context, componentType, parameterView),
|
RenderMode.Static => await StaticComponentAsync(context, componentType, parameterView),
|
||||||
|
RenderMode.WebAssembly => NonPrerenderedWebAssemblyComponent(context, componentType, parameterView),
|
||||||
|
RenderMode.WebAssemblyPrerendered => await PrerenderedWebAssemblyComponentAsync(context, componentType, parameterView),
|
||||||
_ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(renderMode), nameof(renderMode)),
|
_ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(renderMode), nameof(renderMode)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -99,12 +104,36 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
_serverComponentSerializer.GetEpilogue(currentInvocation));
|
_serverComponentSerializer.GetEpilogue(currentInvocation));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IHtmlContent> PrerenderedWebAssemblyComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
|
||||||
|
{
|
||||||
|
var currentInvocation = _WebAssemblyComponentSerializer.SerializeInvocation(
|
||||||
|
type,
|
||||||
|
parametersCollection,
|
||||||
|
prerendered: true);
|
||||||
|
|
||||||
|
var result = await _staticComponentRenderer.PrerenderComponentAsync(
|
||||||
|
parametersCollection,
|
||||||
|
context,
|
||||||
|
type);
|
||||||
|
|
||||||
|
return new ComponentHtmlContent(
|
||||||
|
_WebAssemblyComponentSerializer.GetPreamble(currentInvocation),
|
||||||
|
result,
|
||||||
|
_WebAssemblyComponentSerializer.GetEpilogue(currentInvocation));
|
||||||
|
}
|
||||||
|
|
||||||
private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
|
private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
|
||||||
{
|
{
|
||||||
var serviceProvider = context.RequestServices;
|
|
||||||
var currentInvocation = _serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
|
var currentInvocation = _serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
|
||||||
|
|
||||||
return new ComponentHtmlContent(_serverComponentSerializer.GetPreamble(currentInvocation));
|
return new ComponentHtmlContent(_serverComponentSerializer.GetPreamble(currentInvocation));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IHtmlContent NonPrerenderedWebAssemblyComponent(HttpContext context, Type type, ParameterView parametersCollection)
|
||||||
|
{
|
||||||
|
var currentInvocation = _WebAssemblyComponentSerializer.SerializeInvocation(type, parametersCollection, prerendered: false);
|
||||||
|
|
||||||
|
return new ComponentHtmlContent(_WebAssemblyComponentSerializer.GetPreamble(currentInvocation));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
Type componentType)
|
Type componentType)
|
||||||
{
|
{
|
||||||
InitializeStandardComponentServices(httpContext);
|
InitializeStandardComponentServices(httpContext);
|
||||||
var loggerFactory = (ILoggerFactory)httpContext.RequestServices.GetService(typeof (ILoggerFactory));
|
var loggerFactory = (ILoggerFactory)httpContext.RequestServices.GetService(typeof(ILoggerFactory));
|
||||||
using (var htmlRenderer = new HtmlRenderer(httpContext.RequestServices, loggerFactory, _encoder.Encode))
|
using (var htmlRenderer = new HtmlRenderer(httpContext.RequestServices, loggerFactory, _encoder.Encode))
|
||||||
{
|
{
|
||||||
ComponentRenderedText result = default;
|
ComponentRenderedText result = default;
|
||||||
|
|
@ -71,15 +71,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
{
|
{
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
|
|
||||||
|
var navigationManager = (IHostEnvironmentNavigationManager)httpContext.RequestServices.GetRequiredService<NavigationManager>();
|
||||||
|
navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request));
|
||||||
|
|
||||||
var authenticationStateProvider = httpContext.RequestServices.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
|
var authenticationStateProvider = httpContext.RequestServices.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
|
||||||
if (authenticationStateProvider != null)
|
if (authenticationStateProvider != null)
|
||||||
{
|
{
|
||||||
var authenticationState = new AuthenticationState(httpContext.User);
|
var authenticationState = new AuthenticationState(httpContext.User);
|
||||||
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
|
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
|
||||||
}
|
}
|
||||||
|
|
||||||
var navigationManager = (IHostEnvironmentNavigationManager)httpContext.RequestServices.GetRequiredService<NavigationManager>();
|
|
||||||
navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Rendering
|
namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
|
|
@ -30,5 +30,16 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
/// </summary>
|
/// </summary>
|
||||||
ServerPrerendered = 3,
|
ServerPrerendered = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders a marker for a Blazor webassembly application. This doesn't include any output from the component.
|
||||||
|
/// When the user-agent starts, it uses this marker to bootstrap a blazor client-side application.
|
||||||
|
/// </summary>
|
||||||
|
WebAssembly = 4,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders the component into static HTML and includes a marker for a Blazor webassembly application.
|
||||||
|
/// When the user-agent starts, it uses this marker to bootstrap a blazor client-side application.
|
||||||
|
/// </summary>
|
||||||
|
WebAssemblyPrerendered = 5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
@ -26,13 +27,239 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
{
|
{
|
||||||
public class ComponentRendererTest
|
public class ComponentRendererTest
|
||||||
{
|
{
|
||||||
private const string PrerenderedServerComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
|
private const string PrerenderedComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
|
||||||
private const string ServerComponentPattern = "^<!--Blazor:(.*?)-->$";
|
private const string ComponentPattern = "^<!--Blazor:(.*?)-->$";
|
||||||
|
|
||||||
private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider();
|
private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider();
|
||||||
|
|
||||||
private readonly ComponentRenderer renderer = GetComponentRenderer();
|
private readonly ComponentRenderer renderer = GetComponentRenderer();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanRender_ParameterlessComponent_ClientMode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var viewContext = GetViewContext();
|
||||||
|
var writer = new StringWriter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.WebAssembly, null);
|
||||||
|
result.WriteTo(writer, HtmlEncoder.Default);
|
||||||
|
var content = writer.ToString();
|
||||||
|
var match = Regex.Match(content, ComponentPattern);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(match.Success);
|
||||||
|
var marker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
Assert.Null(marker.PrerenderId);
|
||||||
|
Assert.Equal("webassembly", marker.Type);
|
||||||
|
Assert.Equal(typeof(TestComponent).Assembly.GetName().Name, marker.Assembly);
|
||||||
|
Assert.Equal(typeof(TestComponent).FullName, marker.TypeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanPrerender_ParameterlessComponent_ClientMode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var viewContext = GetViewContext();
|
||||||
|
var writer = new StringWriter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.WebAssemblyPrerendered, null);
|
||||||
|
result.WriteTo(writer, HtmlEncoder.Default);
|
||||||
|
var content = writer.ToString();
|
||||||
|
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(match.Success);
|
||||||
|
var preamble = match.Groups["preamble"].Value;
|
||||||
|
var preambleMarker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
Assert.NotNull(preambleMarker.PrerenderId);
|
||||||
|
Assert.Equal("webassembly", preambleMarker.Type);
|
||||||
|
Assert.Equal(typeof(TestComponent).Assembly.GetName().Name, preambleMarker.Assembly);
|
||||||
|
Assert.Equal(typeof(TestComponent).FullName, preambleMarker.TypeName);
|
||||||
|
|
||||||
|
var prerenderedContent = match.Groups["content"].Value;
|
||||||
|
Assert.Equal("<h1>Hello world!</h1>", prerenderedContent);
|
||||||
|
|
||||||
|
var epilogue = match.Groups["epilogue"].Value;
|
||||||
|
var epilogueMarker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
|
||||||
|
Assert.Null(epilogueMarker.Assembly);
|
||||||
|
Assert.Null(epilogueMarker.TypeName);
|
||||||
|
Assert.Null(epilogueMarker.Type);
|
||||||
|
Assert.Null(epilogueMarker.ParameterDefinitions);
|
||||||
|
Assert.Null(epilogueMarker.ParameterValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanRender_ComponentWithParameters_ClientMode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var viewContext = GetViewContext();
|
||||||
|
var writer = new StringWriter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent),
|
||||||
|
RenderMode.WebAssembly,
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Name = "Daniel"
|
||||||
|
});
|
||||||
|
result.WriteTo(writer, HtmlEncoder.Default);
|
||||||
|
var content = writer.ToString();
|
||||||
|
var match = Regex.Match(content, ComponentPattern);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(match.Success);
|
||||||
|
var marker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
Assert.Null(marker.PrerenderId);
|
||||||
|
Assert.Equal("webassembly", marker.Type);
|
||||||
|
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, marker.Assembly);
|
||||||
|
Assert.Equal(typeof(GreetingComponent).FullName, marker.TypeName);
|
||||||
|
|
||||||
|
var parameterDefinition = Assert.Single(
|
||||||
|
JsonSerializer.Deserialize<ComponentParameter[]>(Convert.FromBase64String(marker.ParameterDefinitions), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
|
||||||
|
Assert.Equal("Name", parameterDefinition.Name);
|
||||||
|
Assert.Equal("System.String", parameterDefinition.TypeName);
|
||||||
|
Assert.Equal("System.Private.CoreLib", parameterDefinition.Assembly);
|
||||||
|
|
||||||
|
var value = Assert.Single(JsonSerializer.Deserialize<object[]>(Convert.FromBase64String(marker.ParameterValues), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
|
||||||
|
var rawValue = Assert.IsType<JsonElement>(value);
|
||||||
|
Assert.Equal("Daniel", rawValue.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanRender_ComponentWithNullParameters_ClientMode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var viewContext = GetViewContext();
|
||||||
|
var writer = new StringWriter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent),
|
||||||
|
RenderMode.WebAssembly,
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Name = (string)null
|
||||||
|
});
|
||||||
|
result.WriteTo(writer, HtmlEncoder.Default);
|
||||||
|
var content = writer.ToString();
|
||||||
|
var match = Regex.Match(content, ComponentPattern);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(match.Success);
|
||||||
|
var marker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
Assert.Null(marker.PrerenderId);
|
||||||
|
Assert.Equal("webassembly", marker.Type);
|
||||||
|
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, marker.Assembly);
|
||||||
|
Assert.Equal(typeof(GreetingComponent).FullName, marker.TypeName);
|
||||||
|
|
||||||
|
var parameterDefinition = Assert.Single(JsonSerializer.Deserialize<ComponentParameter[]>(Convert.FromBase64String(marker.ParameterDefinitions), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
|
||||||
|
Assert.Equal("Name", parameterDefinition.Name);
|
||||||
|
Assert.Null(parameterDefinition.TypeName);
|
||||||
|
Assert.Null(parameterDefinition.Assembly);
|
||||||
|
|
||||||
|
var value = Assert.Single(JsonSerializer.Deserialize<object[]>(Convert.FromBase64String(marker.ParameterValues), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
|
||||||
|
Assert.Null(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanPrerender_ComponentWithParameters_ClientMode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var viewContext = GetViewContext();
|
||||||
|
var writer = new StringWriter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent),
|
||||||
|
RenderMode.WebAssemblyPrerendered,
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Name = "Daniel"
|
||||||
|
});
|
||||||
|
result.WriteTo(writer, HtmlEncoder.Default);
|
||||||
|
var content = writer.ToString();
|
||||||
|
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(match.Success);
|
||||||
|
var preamble = match.Groups["preamble"].Value;
|
||||||
|
var preambleMarker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
Assert.NotNull(preambleMarker.PrerenderId);
|
||||||
|
Assert.Equal("webassembly", preambleMarker.Type);
|
||||||
|
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, preambleMarker.Assembly);
|
||||||
|
Assert.Equal(typeof(GreetingComponent).FullName, preambleMarker.TypeName);
|
||||||
|
|
||||||
|
var parameterDefinition = Assert.Single(JsonSerializer.Deserialize<ComponentParameter[]>(Convert.FromBase64String(preambleMarker.ParameterDefinitions), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
|
||||||
|
Assert.Equal("Name", parameterDefinition.Name);
|
||||||
|
Assert.Equal("System.String", parameterDefinition.TypeName);
|
||||||
|
Assert.Equal("System.Private.CoreLib", parameterDefinition.Assembly);
|
||||||
|
|
||||||
|
var value = Assert.Single(JsonSerializer.Deserialize<object[]>(Convert.FromBase64String(preambleMarker.ParameterValues), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
|
||||||
|
var rawValue = Assert.IsType<JsonElement>(value);
|
||||||
|
Assert.Equal("Daniel", rawValue.GetString());
|
||||||
|
|
||||||
|
var prerenderedContent = match.Groups["content"].Value;
|
||||||
|
Assert.Equal("<p>Hello Daniel!</p>", prerenderedContent);
|
||||||
|
|
||||||
|
var epilogue = match.Groups["epilogue"].Value;
|
||||||
|
var epilogueMarker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
|
||||||
|
Assert.Null(epilogueMarker.Assembly);
|
||||||
|
Assert.Null(epilogueMarker.TypeName);
|
||||||
|
Assert.Null(epilogueMarker.Type);
|
||||||
|
Assert.Null(epilogueMarker.ParameterDefinitions);
|
||||||
|
Assert.Null(epilogueMarker.ParameterValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanPrerender_ComponentWithNullParameters_ClientMode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var viewContext = GetViewContext();
|
||||||
|
var writer = new StringWriter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent),
|
||||||
|
RenderMode.WebAssemblyPrerendered,
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Name = (string)null
|
||||||
|
});
|
||||||
|
result.WriteTo(writer, HtmlEncoder.Default);
|
||||||
|
var content = writer.ToString();
|
||||||
|
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(match.Success);
|
||||||
|
var preamble = match.Groups["preamble"].Value;
|
||||||
|
var preambleMarker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
Assert.NotNull(preambleMarker.PrerenderId);
|
||||||
|
Assert.Equal("webassembly", preambleMarker.Type);
|
||||||
|
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, preambleMarker.Assembly);
|
||||||
|
Assert.Equal(typeof(GreetingComponent).FullName, preambleMarker.TypeName);
|
||||||
|
|
||||||
|
var parameterDefinition = Assert.Single(JsonSerializer.Deserialize<ComponentParameter[]>(Convert.FromBase64String(preambleMarker.ParameterDefinitions), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
|
||||||
|
Assert.Equal("Name", parameterDefinition.Name);
|
||||||
|
Assert.Null(parameterDefinition.TypeName);
|
||||||
|
Assert.Null(parameterDefinition.Assembly);
|
||||||
|
|
||||||
|
var value = Assert.Single(JsonSerializer.Deserialize<object[]>(Convert.FromBase64String(preambleMarker.ParameterValues), WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
|
||||||
|
Assert.Null(value);
|
||||||
|
|
||||||
|
var prerenderedContent = match.Groups["content"].Value;
|
||||||
|
Assert.Equal("<p>Hello (null)!</p>", prerenderedContent);
|
||||||
|
|
||||||
|
var epilogue = match.Groups["epilogue"].Value;
|
||||||
|
var epilogueMarker = JsonSerializer.Deserialize<WebAssemblyComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
|
||||||
|
Assert.Null(epilogueMarker.Assembly);
|
||||||
|
Assert.Null(epilogueMarker.TypeName);
|
||||||
|
Assert.Null(epilogueMarker.Type);
|
||||||
|
Assert.Null(epilogueMarker.ParameterDefinitions);
|
||||||
|
Assert.Null(epilogueMarker.ParameterValues);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CanRender_ParameterlessComponent()
|
public async Task CanRender_ParameterlessComponent()
|
||||||
{
|
{
|
||||||
|
|
@ -60,7 +287,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
// Act
|
// Act
|
||||||
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
|
||||||
var content = HtmlContentUtilities.HtmlContentToString(result);
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
var match = Regex.Match(content, ServerComponentPattern);
|
var match = Regex.Match(content, ComponentPattern);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(match.Success);
|
Assert.True(match.Success);
|
||||||
|
|
@ -89,7 +316,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
// Act
|
// Act
|
||||||
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
|
||||||
var content = HtmlContentUtilities.HtmlContentToString(result);
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(match.Success);
|
Assert.True(match.Success);
|
||||||
|
|
@ -130,11 +357,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
// Act
|
// Act
|
||||||
var firstResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
|
var firstResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
|
||||||
var firstComponent = HtmlContentUtilities.HtmlContentToString(firstResult);
|
var firstComponent = HtmlContentUtilities.HtmlContentToString(firstResult);
|
||||||
var firstMatch = Regex.Match(firstComponent, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
var firstMatch = Regex.Match(firstComponent, PrerenderedComponentPattern, RegexOptions.Multiline);
|
||||||
|
|
||||||
var secondResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
|
var secondResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
|
||||||
var secondComponent = HtmlContentUtilities.HtmlContentToString(secondResult);
|
var secondComponent = HtmlContentUtilities.HtmlContentToString(secondResult);
|
||||||
var secondMatch = Regex.Match(secondComponent, ServerComponentPattern);
|
var secondMatch = Regex.Match(secondComponent, ComponentPattern);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(firstMatch.Success);
|
Assert.True(firstMatch.Success);
|
||||||
|
|
@ -186,7 +413,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
// Act
|
// Act
|
||||||
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = "Daniel" });
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = "Daniel" });
|
||||||
var content = HtmlContentUtilities.HtmlContentToString(result);
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
var match = Regex.Match(content, ServerComponentPattern);
|
var match = Regex.Match(content, ComponentPattern);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(match.Success);
|
Assert.True(match.Success);
|
||||||
|
|
@ -225,7 +452,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
|
|
||||||
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = (string)null });
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = (string)null });
|
||||||
var content = HtmlContentUtilities.HtmlContentToString(result);
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
var match = Regex.Match(content, ServerComponentPattern);
|
var match = Regex.Match(content, ComponentPattern);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(match.Success);
|
Assert.True(match.Success);
|
||||||
|
|
@ -264,7 +491,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
// Act
|
// Act
|
||||||
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = "Daniel" });
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = "Daniel" });
|
||||||
var content = HtmlContentUtilities.HtmlContentToString(result);
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(match.Success);
|
Assert.True(match.Success);
|
||||||
|
|
@ -315,7 +542,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
// Act
|
// Act
|
||||||
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = (string)null });
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = (string)null });
|
||||||
var content = HtmlContentUtilities.HtmlContentToString(result);
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(match.Success);
|
Assert.True(match.Success);
|
||||||
|
|
@ -554,7 +781,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
</table>";
|
</table>";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await renderer.RenderComponentAsync(viewContext,typeof(AsyncComponent), RenderMode.Static, null);
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(AsyncComponent), RenderMode.Static, null);
|
||||||
var content = HtmlContentUtilities.HtmlContentToString(result);
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -564,7 +791,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
private static ComponentRenderer GetComponentRenderer() =>
|
private static ComponentRenderer GetComponentRenderer() =>
|
||||||
new ComponentRenderer(
|
new ComponentRenderer(
|
||||||
new StaticComponentRenderer(HtmlEncoder.Default),
|
new StaticComponentRenderer(HtmlEncoder.Default),
|
||||||
new ServerComponentSerializer(_dataprotectorProvider));
|
new ServerComponentSerializer(_dataprotectorProvider),
|
||||||
|
new WebAssemblyComponentSerializer());
|
||||||
|
|
||||||
private static ViewContext GetViewContext(HttpContext context = null, Action<IServiceCollection> configureServices = null)
|
private static ViewContext GetViewContext(HttpContext context = null, Action<IServiceCollection> configureServices = null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components
|
||||||
|
{
|
||||||
|
internal struct WebAssemblyComponentMarker
|
||||||
|
{
|
||||||
|
public const string ClientMarkerType = "webassembly";
|
||||||
|
|
||||||
|
public WebAssemblyComponentMarker(string type, string assembly, string typeName, string parameterDefinitions, string parameterValues, string prereenderId) =>
|
||||||
|
(Type, Assembly, TypeName, ParameterDefinitions, ParameterValues, PrerenderId) = (type, assembly, typeName, parameterDefinitions, parameterValues, prereenderId);
|
||||||
|
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
public string Assembly { get; set; }
|
||||||
|
|
||||||
|
public string TypeName { get; set; }
|
||||||
|
|
||||||
|
public string ParameterDefinitions { get; set; }
|
||||||
|
|
||||||
|
public string ParameterValues { get; set; }
|
||||||
|
|
||||||
|
public string PrerenderId { get; set; }
|
||||||
|
|
||||||
|
internal static WebAssemblyComponentMarker NonPrerendered(string assembly, string typeName, string parameterDefinitions, string parameterValues) =>
|
||||||
|
new WebAssemblyComponentMarker(ClientMarkerType, assembly, typeName, parameterDefinitions, parameterValues, null);
|
||||||
|
|
||||||
|
internal static WebAssemblyComponentMarker Prerendered(string assembly, string typeName, string parameterDefinitions, string parameterValues) =>
|
||||||
|
new WebAssemblyComponentMarker(ClientMarkerType, assembly, typeName, parameterDefinitions, parameterValues, Guid.NewGuid().ToString("N"));
|
||||||
|
|
||||||
|
public WebAssemblyComponentMarker GetEndRecord()
|
||||||
|
{
|
||||||
|
if (PrerenderId == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Can't get an end record for non-prerendered components.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WebAssemblyComponentMarker(null, null, null, null, null, PrerenderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components
|
||||||
|
{
|
||||||
|
internal static class WebAssemblyComponentSerializationSettings
|
||||||
|
{
|
||||||
|
public static readonly JsonSerializerOptions JsonSerializationOptions =
|
||||||
|
new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
IgnoreNullValues = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
// <autogenerated />
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v5.0", FrameworkDisplayName = "")]
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
// Runtime Version:4.0.30319.42000
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
[assembly: System.Reflection.AssemblyMetadataAttribute("SourceCommitUrl", "https://github.com/dotnet/aspnetcore/tree/")]
|
||||||
|
[assembly: System.Reflection.AssemblyMetadataAttribute("Serviceable", "True")]
|
||||||
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Microsoft Corporation")]
|
||||||
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
|
[assembly: System.Reflection.AssemblyCopyrightAttribute("© Microsoft Corporation. All rights reserved.")]
|
||||||
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("42.42.42.42424")]
|
||||||
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("5.0.0-dev")]
|
||||||
|
[assembly: System.Reflection.AssemblyProductAttribute("Microsoft ASP.NET Core")]
|
||||||
|
[assembly: System.Reflection.AssemblyTitleAttribute("KitchenSink")]
|
||||||
|
[assembly: System.Reflection.AssemblyVersionAttribute("5.0.0.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyMetadataAttribute("RepositoryUrl", "https://github.com/dotnet/aspnetcore")]
|
||||||
|
[assembly: System.Resources.NeutralResourcesLanguageAttribute("en-US")]
|
||||||
|
|
||||||
|
// Generated by the MSBuild WriteCodeFragment class.
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
5426b0432f23f134c800ff0d43b24a23681aac3d
|
||||||
Loading…
Reference in New Issue