[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 ILogger<ServerComponentDeserializer> _logger;
|
||||
private readonly ServerComponentTypeCache _rootComponentTypeCache;
|
||||
private readonly RootComponentTypeCache _rootComponentTypeCache;
|
||||
private readonly ComponentParameterDeserializer _parametersDeserializer;
|
||||
|
||||
public ServerComponentDeserializer(
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
ILogger<ServerComponentDeserializer> logger,
|
||||
ServerComponentTypeCache rootComponentTypeCache,
|
||||
RootComponentTypeCache rootComponentTypeCache,
|
||||
ComponentParameterDeserializer parametersDeserializer)
|
||||
{
|
||||
// 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.TryAddSingleton<CircuitFactory>();
|
||||
services.TryAddSingleton<ServerComponentDeserializer>();
|
||||
services.TryAddSingleton<ServerComponentTypeCache>();
|
||||
services.TryAddSingleton<RootComponentTypeCache>();
|
||||
services.TryAddSingleton<ComponentParameterDeserializer>();
|
||||
services.TryAddSingleton<ComponentParametersTypeCache>();
|
||||
services.TryAddSingleton<CircuitIdFactory>();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
<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\ArrayBuilder.cs" LinkBase="Circuits" />
|
||||
<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\JsonSerializerOptionsProvider.cs" />
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
return new ServerComponentDeserializer(
|
||||
_ephemeralDataProtectionProvider,
|
||||
NullLogger<ServerComponentDeserializer>.Instance,
|
||||
new ServerComponentTypeCache(),
|
||||
new RootComponentTypeCache(),
|
||||
new ComponentParameterDeserializer(NullLogger<ComponentParameterDeserializer>.Instance, new ComponentParametersTypeCache()));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ using System.Reflection;
|
|||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
// A cache for root component types
|
||||
internal class ServerComponentTypeCache
|
||||
internal class RootComponentTypeCache
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
private struct Key : IEquatable<Key>
|
||||
private readonly struct Key : IEquatable<Key>
|
||||
{
|
||||
public Key(string assembly, string 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);
|
||||
|
||||
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 { ConsoleLogger } from './Platform/Logging/Loggers';
|
||||
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 { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
|
||||
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
|
||||
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
|
||||
import { discoverComponents, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
|
||||
|
||||
let renderingFailed = false;
|
||||
let started = false;
|
||||
|
|
@ -29,7 +30,7 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
|
|||
options.reconnectionHandler = options.reconnectionHandler || window['Blazor'].defaultReconnectionHandler;
|
||||
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 initialConnection = await initializeConnection(options, logger, circuit);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { DotNet } from '@microsoft/dotnet-js-interop';
|
|||
import './GlobalExports';
|
||||
import * as Environment from './Environment';
|
||||
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 { shouldAutoStart } from './BootCommon';
|
||||
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
|
||||
|
|
@ -11,6 +11,8 @@ import { WebAssemblyConfigLoader } from './Platform/WebAssemblyConfigLoader';
|
|||
import { BootConfigResult } from './Platform/BootConfig';
|
||||
import { Pointer } from './Platform/Platform';
|
||||
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
|
||||
import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
|
||||
import { discoverComponents, WebAssemblyComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
|
||||
|
||||
let started = false;
|
||||
|
||||
|
|
@ -71,8 +73,31 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
|
|||
const environment = options?.environment;
|
||||
|
||||
// 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([
|
||||
WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, options || {}),
|
||||
WebAssemblyConfigLoader.initAsync(bootConfigResult)]);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ window['Blazor'] = {
|
|||
navigateTo,
|
||||
|
||||
_internal: {
|
||||
attachRootComponentToElement,
|
||||
navigationManager: navigationManagerInternalFunctions,
|
||||
domWrapper: domFunctions,
|
||||
Virtualize,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { internalFunctions as navigationManagerFunctions } from '../../Services/NavigationManager';
|
||||
import { toLogicalRootCommentElement, LogicalElement } from '../../Rendering/LogicalElements';
|
||||
import { ServerComponentDescriptor } from '../../Services/ComponentDescriptorDiscovery';
|
||||
|
||||
export class CircuitDescriptor {
|
||||
public circuitId?: string;
|
||||
|
||||
public components: ComponentDescriptor[];
|
||||
public components: ServerComponentDescriptor[];
|
||||
|
||||
public constructor(components: ComponentDescriptor[]) {
|
||||
public constructor(components: ServerComponentDescriptor[]) {
|
||||
this.circuitId = undefined;
|
||||
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.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||
|
|
@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
/// and <paramref name="selector"/>.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
if (componentType is null)
|
||||
|
|
@ -38,6 +39,19 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
|
||||
ComponentType = componentType;
|
||||
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>
|
||||
|
|
@ -49,5 +63,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
/// Gets the DOM element selector.
|
||||
/// </summary>
|
||||
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="selector">The DOM element selector.</param>
|
||||
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)
|
||||
{
|
||||
|
|
@ -45,7 +56,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
throw new ArgumentNullException(nameof(selector));
|
||||
}
|
||||
|
||||
Add(new RootComponentMapping(componentType, selector));
|
||||
Add(new RootComponentMapping(componentType, selector, parameters));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
for (var i = 0; i < rootComponents.Length; i++)
|
||||
{
|
||||
var rootComponent = rootComponents[i];
|
||||
await _renderer.AddComponentAsync(rootComponent.ComponentType, rootComponent.Selector);
|
||||
await _renderer.AddComponentAsync(rootComponent.ComponentType, rootComponent.Selector, rootComponent.Parameters);
|
||||
}
|
||||
|
||||
await tcs.Task;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
public sealed class WebAssemblyHostBuilder
|
||||
{
|
||||
private Func<IServiceProvider> _createServiceProvider;
|
||||
private RootComponentTypeCache _rootComponentCache;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
InitializeNavigationManager(jsRuntimeInvoker);
|
||||
InitializeRegisteredRootComponents(jsRuntimeInvoker);
|
||||
InitializeDefaultServices();
|
||||
|
||||
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)
|
||||
{
|
||||
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<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
|
||||
Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
|
||||
Services.AddLogging(builder => {
|
||||
Services.AddLogging(builder =>
|
||||
{
|
||||
builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
|
|
@ -15,13 +15,7 @@
|
|||
<Reference Include="Microsoft.Extensions.Logging" />
|
||||
<Reference Include="Microsoft.JSInterop.WebAssembly" />
|
||||
|
||||
<ProjectReference
|
||||
Include="..\..\..\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj"
|
||||
ReferenceOutputAssemblies="false"
|
||||
SkipGetTargetFrameworkProperties="true"
|
||||
UndefineProperties="TargetFramework"
|
||||
Private="false"
|
||||
Condition="'$(BuildNodeJS)' != 'false' and '$(BuildingInsideVisualStudio)' != 'true'" />
|
||||
<ProjectReference 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" />
|
||||
</ItemGroup>
|
||||
|
|
@ -31,6 +25,11 @@
|
|||
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerializerOptionsProvider.cs" />
|
||||
<Compile Include="$(ComponentsSharedSourceRoot)src\WebEventData.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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
/// <typeparam name="TComponent">The type of the component.</typeparam>
|
||||
/// <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>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public Task AddComponentAsync<TComponent>(string domElementSelector) where TComponent : IComponent
|
||||
=> AddComponentAsync(typeof(TComponent), domElementSelector);
|
||||
public Task AddComponentAsync<TComponent>(string domElementSelector, ParameterView parameters) where TComponent : IComponent
|
||||
=> AddComponentAsync(typeof(TComponent), domElementSelector, parameters);
|
||||
|
||||
/// <summary>
|
||||
/// Associates the <see cref="IComponent"/> with the <see cref="WebAssemblyRenderer"/>,
|
||||
|
|
@ -61,12 +62,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
|
|||
/// </summary>
|
||||
/// <param name="componentType">The type of the component.</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>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public Task AddComponentAsync(Type componentType, string domElementSelector)
|
||||
public Task AddComponentAsync(Type componentType, string domElementSelector, ParameterView parameters)
|
||||
{
|
||||
var component = InstantiateComponent(componentType);
|
||||
var componentId = AssignRootComponentId(component);
|
||||
|
|
@ -83,7 +85,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
|
|||
componentId,
|
||||
_webAssemblyRendererId);
|
||||
|
||||
return RenderRootComponentAsync(componentId);
|
||||
return RenderRootComponentAsync(componentId, parameters);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
case "Blazor._internal.navigationManager.getUnmarshalledLocationHref":
|
||||
var testHref = "https://www.example.com/awesome-part-that-will-be-truncated-in-tests/cool";
|
||||
return (TResult)(object)testHref;
|
||||
case "Blazor._internal.registeredComponents.getRegisteredComponentsCount":
|
||||
return (TResult)(object)0;
|
||||
default:
|
||||
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\ServerComponentSerializationSettings.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>
|
||||
|
|
|
|||
|
|
@ -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.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.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.
|
||||
|
||||
using System;
|
||||
|
|
@ -66,6 +66,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
case RenderMode.Server:
|
||||
case RenderMode.ServerPrerendered:
|
||||
case RenderMode.Static:
|
||||
case RenderMode.WebAssembly:
|
||||
case RenderMode.WebAssemblyPrerendered:
|
||||
_renderMode = value;
|
||||
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
|
||||
services.TryAddSingleton<ServerComponentSerializer>();
|
||||
|
||||
// Component services for Blazor webassembly interop
|
||||
services.TryAddSingleton<WebAssemblyComponentSerializer>();
|
||||
|
||||
//
|
||||
// View Components
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
|
|
@ -31,7 +31,9 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentSerializationSettings.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)Components\ServerComponentSerializationSettings.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentMarker.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)Components\ServerComponentMarker.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)Components\ServerComponent.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.ServerPrerendered = 3 -> 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.SelectListGroup
|
||||
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.
|
||||
|
||||
using System;
|
||||
|
|
@ -15,13 +15,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
private static readonly object ComponentSequenceKey = new object();
|
||||
private readonly StaticComponentRenderer _staticComponentRenderer;
|
||||
private readonly ServerComponentSerializer _serverComponentSerializer;
|
||||
private readonly WebAssemblyComponentSerializer _WebAssemblyComponentSerializer;
|
||||
|
||||
public ComponentRenderer(
|
||||
StaticComponentRenderer staticComponentRenderer,
|
||||
ServerComponentSerializer serverComponentSerializer)
|
||||
ServerComponentSerializer serverComponentSerializer,
|
||||
WebAssemblyComponentSerializer WebAssemblyComponentSerializer)
|
||||
{
|
||||
_staticComponentRenderer = staticComponentRenderer;
|
||||
_serverComponentSerializer = serverComponentSerializer;
|
||||
_WebAssemblyComponentSerializer = WebAssemblyComponentSerializer;
|
||||
}
|
||||
|
||||
public async Task<IHtmlContent> RenderComponentAsync(
|
||||
|
|
@ -55,6 +58,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
|
||||
RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(viewContext), 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)),
|
||||
};
|
||||
}
|
||||
|
|
@ -99,12 +104,36 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
_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)
|
||||
{
|
||||
var serviceProvider = context.RequestServices;
|
||||
var currentInvocation = _serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
|
||||
|
||||
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.
|
||||
|
||||
using System;
|
||||
|
|
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
Type componentType)
|
||||
{
|
||||
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))
|
||||
{
|
||||
ComponentRenderedText result = default;
|
||||
|
|
@ -71,15 +71,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
{
|
||||
_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;
|
||||
if (authenticationStateProvider != null)
|
||||
{
|
||||
var authenticationState = new AuthenticationState(httpContext.User);
|
||||
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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||
|
|
@ -30,5 +30,16 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
|||
/// </summary>
|
||||
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.IO;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
|
@ -26,13 +27,239 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
{
|
||||
public class ComponentRendererTest
|
||||
{
|
||||
private const string PrerenderedServerComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
|
||||
private const string ServerComponentPattern = "^<!--Blazor:(.*?)-->$";
|
||||
private const string PrerenderedComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
|
||||
private const string ComponentPattern = "^<!--Blazor:(.*?)-->$";
|
||||
|
||||
private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider();
|
||||
|
||||
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]
|
||||
public async Task CanRender_ParameterlessComponent()
|
||||
{
|
||||
|
|
@ -60,7 +287,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
// Act
|
||||
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
|
||||
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||
var match = Regex.Match(content, ServerComponentPattern);
|
||||
var match = Regex.Match(content, ComponentPattern);
|
||||
|
||||
// Assert
|
||||
Assert.True(match.Success);
|
||||
|
|
@ -89,7 +316,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
// Act
|
||||
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
|
||||
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
||||
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
|
||||
|
||||
// Assert
|
||||
Assert.True(match.Success);
|
||||
|
|
@ -130,11 +357,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
// Act
|
||||
var firstResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
|
||||
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 secondComponent = HtmlContentUtilities.HtmlContentToString(secondResult);
|
||||
var secondMatch = Regex.Match(secondComponent, ServerComponentPattern);
|
||||
var secondMatch = Regex.Match(secondComponent, ComponentPattern);
|
||||
|
||||
// Assert
|
||||
Assert.True(firstMatch.Success);
|
||||
|
|
@ -186,7 +413,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
// Act
|
||||
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = "Daniel" });
|
||||
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||
var match = Regex.Match(content, ServerComponentPattern);
|
||||
var match = Regex.Match(content, ComponentPattern);
|
||||
|
||||
// Assert
|
||||
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 content = HtmlContentUtilities.HtmlContentToString(result);
|
||||
var match = Regex.Match(content, ServerComponentPattern);
|
||||
var match = Regex.Match(content, ComponentPattern);
|
||||
|
||||
// Assert
|
||||
Assert.True(match.Success);
|
||||
|
|
@ -264,7 +491,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
// Act
|
||||
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = "Daniel" });
|
||||
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
||||
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
|
||||
|
||||
// Assert
|
||||
Assert.True(match.Success);
|
||||
|
|
@ -315,7 +542,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
// Act
|
||||
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = (string)null });
|
||||
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
||||
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
|
||||
|
||||
// Assert
|
||||
Assert.True(match.Success);
|
||||
|
|
@ -554,7 +781,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
</table>";
|
||||
|
||||
// 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);
|
||||
|
||||
// Assert
|
||||
|
|
@ -564,7 +791,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
private static ComponentRenderer GetComponentRenderer() =>
|
||||
new ComponentRenderer(
|
||||
new StaticComponentRenderer(HtmlEncoder.Default),
|
||||
new ServerComponentSerializer(_dataprotectorProvider));
|
||||
new ServerComponentSerializer(_dataprotectorProvider),
|
||||
new WebAssemblyComponentSerializer());
|
||||
|
||||
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