Refactoring to prepare for remote rendering.
- Prepare for building multiple entrypoint variants of the .js library - Use async interop more consistently for rendering and event handling - Add binary serializer for RenderBatch with tests
This commit is contained in:
parent
cafb56569d
commit
5bccac05fc
|
|
@ -24,11 +24,11 @@
|
||||||
<Exec Command="npm install" />
|
<Exec Command="npm install" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="RunWebpack" AfterTargets="ResolveReferences" Inputs="@(WebpackInputs)" Outputs="dist\blazor.js" DependsOnTargets="EnsureNpmRestored">
|
<Target Name="RunWebpack" AfterTargets="ResolveReferences" Inputs="@(WebpackInputs)" Outputs="dist\blazor.webassembly.js" DependsOnTargets="EnsureNpmRestored">
|
||||||
<RemoveDir Directories="dist" />
|
<RemoveDir Directories="dist" />
|
||||||
<Exec Command="npm run build" />
|
<Exec Command="npm run build" />
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="dist\blazor.js" LogicalName="blazor./blazor.js" />
|
<EmbeddedResource Include="dist\blazor.webassembly.js" LogicalName="blazor./blazor.webassembly.js" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Target>
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
import '../../Microsoft.JSInterop/JavaScriptRuntime/src/Microsoft.JSInterop';
|
import '../../Microsoft.JSInterop/JavaScriptRuntime/src/Microsoft.JSInterop';
|
||||||
import { platform } from './Environment';
|
|
||||||
import { getAssemblyNameFromUrl } from './Platform/Url';
|
|
||||||
import './GlobalExports';
|
import './GlobalExports';
|
||||||
|
import * as Environment from './Environment';
|
||||||
|
import { monoPlatform } from './Platform/Mono/MonoPlatform';
|
||||||
|
import { getAssemblyNameFromUrl } from './Platform/Url';
|
||||||
|
import { renderBatch } from './Rendering/Renderer';
|
||||||
|
import { RenderBatch } from './Rendering/RenderBatch/RenderBatch';
|
||||||
|
import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
|
||||||
|
import { Pointer } from './Platform/Platform';
|
||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
|
// Configure environment for execution under Mono WebAssembly with shared-memory rendering
|
||||||
|
const platform = Environment.setPlatform(monoPlatform);
|
||||||
|
window['Blazor']._internal.renderBatch = (browserRendererId: number, batchAddress: Pointer) => {
|
||||||
|
renderBatch(browserRendererId, new SharedMemoryRenderBatch(batchAddress));
|
||||||
|
};
|
||||||
|
|
||||||
// Read startup config from the <script> element that's importing this file
|
// Read startup config from the <script> element that's importing this file
|
||||||
const allScriptElems = document.getElementsByTagName('script');
|
const allScriptElems = document.getElementsByTagName('script');
|
||||||
const thisScriptElem = (document.currentScript || allScriptElems[allScriptElems.length - 1]) as HTMLScriptElement;
|
const thisScriptElem = (document.currentScript || allScriptElems[allScriptElems.length - 1]) as HTMLScriptElement;
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
// Expose an export called 'platform' of the interface type 'Platform',
|
// Expose an export called 'platform' of the interface type 'Platform',
|
||||||
// so that consumers can be agnostic about which implementation they use.
|
// so that consumers can be agnostic about which implementation they use.
|
||||||
// Basic alternative to having an actual DI container.
|
// Basic alternative to having an actual DI container.
|
||||||
import { Platform } from './Platform/Platform';
|
import { Platform } from './Platform/Platform';
|
||||||
import { monoPlatform } from './Platform/Mono/MonoPlatform';
|
|
||||||
export const platform: Platform = monoPlatform;
|
export let platform: Platform;
|
||||||
|
|
||||||
|
export function setPlatform(platformInstance: Platform) {
|
||||||
|
platform = platformInstance;
|
||||||
|
return platform;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,17 @@
|
||||||
import { platform } from './Environment';
|
import { platform } from './Environment';
|
||||||
import { navigateTo, internalFunctions as uriHelperInternalFunctions } from './Services/UriHelper';
|
import { navigateTo, internalFunctions as uriHelperInternalFunctions } from './Services/UriHelper';
|
||||||
import { internalFunctions as httpInternalFunctions } from './Services/Http';
|
import { internalFunctions as httpInternalFunctions } from './Services/Http';
|
||||||
import { attachRootComponentToElement, renderBatch } from './Rendering/Renderer';
|
import { attachRootComponentToElement } from './Rendering/Renderer';
|
||||||
import { Pointer } from './Platform/Platform';
|
import { Pointer } from './Platform/Platform';
|
||||||
import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
// Make the following APIs available in global scope for invocation from JS
|
||||||
// When the library is loaded in a browser via a <script> element, make the
|
window['Blazor'] = {
|
||||||
// following APIs available in global scope for invocation from JS
|
platform,
|
||||||
window['Blazor'] = {
|
navigateTo,
|
||||||
platform,
|
|
||||||
navigateTo,
|
|
||||||
|
|
||||||
_internal: {
|
_internal: {
|
||||||
attachRootComponentToElement,
|
attachRootComponentToElement,
|
||||||
renderBatch: (browserRendererId: number, batchAddress: Pointer) => renderBatch(browserRendererId, new SharedMemoryRenderBatch(batchAddress)),
|
http: httpInternalFunctions,
|
||||||
http: httpInternalFunctions,
|
uriHelper: uriHelperInternalFunctions
|
||||||
uriHelper: uriHelperInternalFunctions
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -320,13 +320,7 @@ function countDescendantFrames(batch: RenderBatch, frame: RenderTreeFrame): numb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function raiseEvent(event: Event, browserRendererId: number, componentId: number, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>) {
|
async function raiseEvent(event: Event, browserRendererId: number, componentId: number, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>) {
|
||||||
if (!raiseEventMethod) {
|
|
||||||
raiseEventMethod = platform.findMethod(
|
|
||||||
'Microsoft.AspNetCore.Blazor.Browser', 'Microsoft.AspNetCore.Blazor.Browser.Rendering', 'BrowserRendererEventDispatcher', 'DispatchEvent'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventDescriptor = {
|
const eventDescriptor = {
|
||||||
browserRendererId,
|
browserRendererId,
|
||||||
componentId,
|
componentId,
|
||||||
|
|
@ -334,8 +328,9 @@ function raiseEvent(event: Event, browserRendererId: number, componentId: number
|
||||||
eventArgsType: eventArgs.type
|
eventArgsType: eventArgs.type
|
||||||
};
|
};
|
||||||
|
|
||||||
platform.callMethod(raiseEventMethod, null, [
|
await DotNet.invokeMethodAsync(
|
||||||
platform.toDotNetString(JSON.stringify(eventDescriptor)),
|
'Microsoft.AspNetCore.Blazor.Browser',
|
||||||
platform.toDotNetString(JSON.stringify(eventArgs.data))
|
'DispatchEvent',
|
||||||
]);
|
eventDescriptor,
|
||||||
|
JSON.stringify(eventArgs.data));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,19 +52,12 @@ function performInternalNavigation(absoluteInternalHref: string) {
|
||||||
handleInternalNavigation();
|
handleInternalNavigation();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInternalNavigation() {
|
async function handleInternalNavigation() {
|
||||||
if (!notifyLocationChangedMethod) {
|
await DotNet.invokeMethodAsync(
|
||||||
notifyLocationChangedMethod = platform.findMethod(
|
'Microsoft.AspNetCore.Blazor.Browser',
|
||||||
'Microsoft.AspNetCore.Blazor.Browser',
|
'NotifyLocationChanged',
|
||||||
'Microsoft.AspNetCore.Blazor.Browser.Services',
|
location.href
|
||||||
'BrowserUriHelper',
|
);
|
||||||
'NotifyLocationChanged'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
platform.callMethod(notifyLocationChangedMethod, null, [
|
|
||||||
platform.toDotNetString(location.href)
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let testAnchor: HTMLAnchorElement;
|
let testAnchor: HTMLAnchorElement;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ module.exports = {
|
||||||
module: {
|
module: {
|
||||||
rules: [{ test: /\.ts?$/, loader: 'ts-loader' }]
|
rules: [{ test: /\.ts?$/, loader: 'ts-loader' }]
|
||||||
},
|
},
|
||||||
entry: { 'blazor': './src/Boot.ts' },
|
entry: {
|
||||||
|
'blazor.webassembly': './src/Boot.WebAssembly.ts',
|
||||||
|
},
|
||||||
output: { path: path.join(__dirname, '/dist'), filename: '[name].js' }
|
output: { path: path.join(__dirname, '/dist'), filename: '[name].js' }
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,26 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
|
||||||
/// Provides mechanisms for dispatching events to components in a <see cref="BrowserRenderer"/>.
|
/// Provides mechanisms for dispatching events to components in a <see cref="BrowserRenderer"/>.
|
||||||
/// This is marked 'internal' because it only gets invoked from JS code.
|
/// This is marked 'internal' because it only gets invoked from JS code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class BrowserRendererEventDispatcher
|
public static class BrowserRendererEventDispatcher
|
||||||
{
|
{
|
||||||
// We receive the information as JSON strings because of current interop limitations:
|
// TODO: Fix this for multi-user scenarios. Currently it doesn't stop people from
|
||||||
// - Can't pass unboxed value types from JS to .NET (yet all the IDs are ints)
|
// triggering events for other people by passing an arbitrary browserRendererId.
|
||||||
// - Can't pass more than 4 args from JS to .NET
|
//
|
||||||
// This can be simplified in the future when the Mono WASM runtime is enhanced.
|
// Preferred fix: Instead of storing the Renderer instances in a static dictionary
|
||||||
public static void DispatchEvent(string eventDescriptorJson, string eventArgsJson)
|
// store them within the context of a Circuit. Then we'll only look up the ones
|
||||||
|
// associated with the caller's circuit. This takes care of ensuring they are
|
||||||
|
// released when the circuit is closed too.
|
||||||
|
//
|
||||||
|
// More generally, we must move away from using statics for any per-user state
|
||||||
|
// now that we have multi-user scenarios.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For framework use only.
|
||||||
|
/// </summary>
|
||||||
|
[JSInvokable(nameof(DispatchEvent))]
|
||||||
|
public static void DispatchEvent(
|
||||||
|
BrowserEventDescriptor eventDescriptor, string eventArgsJson)
|
||||||
{
|
{
|
||||||
var eventDescriptor = Json.Deserialize<BrowserEventDescriptor>(eventDescriptorJson);
|
|
||||||
var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
|
var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
|
||||||
var browserRenderer = BrowserRendererRegistry.Find(eventDescriptor.BrowserRendererId);
|
var browserRenderer = BrowserRendererRegistry.Find(eventDescriptor.BrowserRendererId);
|
||||||
browserRenderer.DispatchBrowserEvent(
|
browserRenderer.DispatchBrowserEvent(
|
||||||
|
|
@ -60,11 +71,29 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class BrowserEventDescriptor
|
/// <summary>
|
||||||
|
/// For framework use only.
|
||||||
|
/// </summary>
|
||||||
|
public class BrowserEventDescriptor
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// For framework use only.
|
||||||
|
/// </summary>
|
||||||
public int BrowserRendererId { get; set; }
|
public int BrowserRendererId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For framework use only.
|
||||||
|
/// </summary>
|
||||||
public int ComponentId { get; set; }
|
public int ComponentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For framework use only.
|
||||||
|
/// </summary>
|
||||||
public int EventHandlerId { get; set; }
|
public int EventHandlerId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For framework use only.
|
||||||
|
/// </summary>
|
||||||
public string EventArgsType { get; set; }
|
public string EventArgsType { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,11 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void NotifyLocationChanged(string newAbsoluteUri)
|
/// <summary>
|
||||||
|
/// For framework use only.
|
||||||
|
/// </summary>
|
||||||
|
[JSInvokable(nameof(NotifyLocationChanged))]
|
||||||
|
public static void NotifyLocationChanged(string newAbsoluteUri)
|
||||||
{
|
{
|
||||||
_cachedAbsoluteUri = newAbsoluteUri;
|
_cachedAbsoluteUri = newAbsoluteUri;
|
||||||
_onLocationChanged?.Invoke(null, newAbsoluteUri);
|
_onLocationChanged?.Invoke(null, newAbsoluteUri);
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ namespace Microsoft.AspNetCore.Blazor.Build
|
||||||
|
|
||||||
var attributesDict = attributes.ToDictionary(x => x.Key, x => x.Value);
|
var attributesDict = attributes.ToDictionary(x => x.Key, x => x.Value);
|
||||||
attributesDict.Remove("type");
|
attributesDict.Remove("type");
|
||||||
attributesDict["src"] = "_framework/blazor.js";
|
attributesDict["src"] = "_framework/blazor.webassembly.js";
|
||||||
attributesDict["main"] = assemblyNameWithExtension;
|
attributesDict["main"] = assemblyNameWithExtension;
|
||||||
attributesDict["entrypoint"] = assemblyEntryPoint;
|
attributesDict["entrypoint"] = assemblyEntryPoint;
|
||||||
attributesDict["references"] = referencesAttribute;
|
attributesDict["references"] = referencesAttribute;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
|
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
|
||||||
<metadata>
|
<metadata>
|
||||||
<id>Microsoft.AspNetCore.Blazor.Build</id>
|
<id>Microsoft.AspNetCore.Blazor.Build</id>
|
||||||
|
|
@ -17,6 +17,6 @@
|
||||||
<file src="$publishdir$\netcoreapp2.1\**\*" target="tools/" />
|
<file src="$publishdir$\netcoreapp2.1\**\*" target="tools/" />
|
||||||
<file src="..\mono\dist\optimized\**\*" target="tools\mono" />
|
<file src="..\mono\dist\optimized\**\*" target="tools\mono" />
|
||||||
<file src="..\mono\tools\binaries\illink\**\*.*" target="tools\illink" />
|
<file src="..\mono\tools\binaries\illink\**\*.*" target="tools\illink" />
|
||||||
<file src="..\Microsoft.AspNetCore.Blazor.Browser.JS\dist\blazor.js" target="tools\blazor\blazor.js" />
|
<file src="..\Microsoft.AspNetCore.Blazor.Browser.JS\dist\blazor.*.js" target="tools\blazor" />
|
||||||
</files>
|
</files>
|
||||||
</package>
|
</package>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<MonoBaseClassLibraryFacadesPath>$(BlazorMonoRuntimeBasePath)dist/optimized/bcl/Facades/</MonoBaseClassLibraryFacadesPath>
|
<MonoBaseClassLibraryFacadesPath>$(BlazorMonoRuntimeBasePath)dist/optimized/bcl/Facades/</MonoBaseClassLibraryFacadesPath>
|
||||||
<MonoAsmjsRuntimePath>$(BlazorMonoRuntimeBasePath)dist/optimized/asmjs/</MonoAsmjsRuntimePath>
|
<MonoAsmjsRuntimePath>$(BlazorMonoRuntimeBasePath)dist/optimized/asmjs/</MonoAsmjsRuntimePath>
|
||||||
<MonoWasmRuntimePath>$(BlazorMonoRuntimeBasePath)dist/optimized/wasm/</MonoWasmRuntimePath>
|
<MonoWasmRuntimePath>$(BlazorMonoRuntimeBasePath)dist/optimized/wasm/</MonoWasmRuntimePath>
|
||||||
<BlazorJsPath>$(MSBuildThisFileDirectory)../Microsoft.AspNetCore.Blazor.Browser.JS/dist/blazor.js</BlazorJsPath>
|
<BlazorJsPath>$(MSBuildThisFileDirectory)../Microsoft.AspNetCore.Blazor.Browser.JS/dist/blazor.*.js</BlazorJsPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Import Project="$(MSBuildThisFileDirectory)targets/All.props" />
|
<Import Project="$(MSBuildThisFileDirectory)targets/All.props" />
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<MonoBaseClassLibraryFacadesPath>$(BlazorMonoRuntimeBasePath)tools/mono/bcl/Facades/</MonoBaseClassLibraryFacadesPath>
|
<MonoBaseClassLibraryFacadesPath>$(BlazorMonoRuntimeBasePath)tools/mono/bcl/Facades/</MonoBaseClassLibraryFacadesPath>
|
||||||
<MonoAsmjsRuntimePath>$(BlazorMonoRuntimeBasePath)tools/mono/asmjs/</MonoAsmjsRuntimePath>
|
<MonoAsmjsRuntimePath>$(BlazorMonoRuntimeBasePath)tools/mono/asmjs/</MonoAsmjsRuntimePath>
|
||||||
<MonoWasmRuntimePath>$(BlazorMonoRuntimeBasePath)tools/mono/wasm/</MonoWasmRuntimePath>
|
<MonoWasmRuntimePath>$(BlazorMonoRuntimeBasePath)tools/mono/wasm/</MonoWasmRuntimePath>
|
||||||
<BlazorJsPath>$(BlazorMonoRuntimeBasePath)tools/blazor/blazor.js</BlazorJsPath>
|
<BlazorJsPath>$(BlazorMonoRuntimeBasePath)tools/blazor/blazor.*.js</BlazorJsPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Label="Blazor build outputs">
|
<PropertyGroup Label="Blazor build outputs">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Blazor.Rendering;
|
||||||
|
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Server.Circuits
|
||||||
|
{
|
||||||
|
// TODO: We should consider *not* having this type of infrastructure in the .Server
|
||||||
|
// project, but instead in some new project called .Remote or similar, since it
|
||||||
|
// would also be used in Electron and possibly WebWorker cases.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a custom binary serializer for <see cref="RenderBatch"/> instances.
|
||||||
|
/// This is designed with both server-side and client-side perf in mind:
|
||||||
|
///
|
||||||
|
/// * Array-like regions always have a fixed size per entry (even if some entry types
|
||||||
|
/// don't require as much space as others) so the recipient can index directly.
|
||||||
|
/// * The indices describing where field data starts, where each string value starts,
|
||||||
|
/// etc., are written *after* that data, so when writing the data we don't have to
|
||||||
|
/// compute the locations up front or seek back to an earlier point in the stream.
|
||||||
|
/// The recipient can only process the data after reading it all into a buffer,
|
||||||
|
/// so it's no disadvantage for the location info to be at the end.
|
||||||
|
/// * We only serialize the data that the JS side will need. For example, we don't
|
||||||
|
/// emit frame sequence numbers, or any representation of nonstring attribute
|
||||||
|
/// values, or component instances, etc.
|
||||||
|
///
|
||||||
|
/// We don't have or need a .NET reader for this format. We only read it from JS code.
|
||||||
|
/// </summary>
|
||||||
|
internal class RenderBatchWriter : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<string> _strings;
|
||||||
|
private readonly BinaryWriter _binaryWriter;
|
||||||
|
|
||||||
|
public RenderBatchWriter(Stream output, bool leaveOpen)
|
||||||
|
{
|
||||||
|
_strings = new List<string>();
|
||||||
|
_binaryWriter = new BinaryWriter(output, Encoding.UTF8, leaveOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(in RenderBatch renderBatch)
|
||||||
|
{
|
||||||
|
var updatedComponentsOffset = Write(renderBatch.UpdatedComponents);
|
||||||
|
var referenceFramesOffset = Write(renderBatch.ReferenceFrames);
|
||||||
|
var disposedComponentIdsOffset = Write(renderBatch.DisposedComponentIDs);
|
||||||
|
var disposedEventHandlerIdsOffset = Write(renderBatch.DisposedEventHandlerIDs);
|
||||||
|
var stringTableOffset = WriteStringTable();
|
||||||
|
|
||||||
|
_binaryWriter.Write(updatedComponentsOffset);
|
||||||
|
_binaryWriter.Write(referenceFramesOffset);
|
||||||
|
_binaryWriter.Write(disposedComponentIdsOffset);
|
||||||
|
_binaryWriter.Write(disposedEventHandlerIdsOffset);
|
||||||
|
_binaryWriter.Write(stringTableOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Write(in ArrayRange<RenderTreeDiff> diffs)
|
||||||
|
{
|
||||||
|
var count = diffs.Count;
|
||||||
|
var diffsIndexes = new int[count];
|
||||||
|
var array = diffs.Array;
|
||||||
|
var baseStream = _binaryWriter.BaseStream;
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
diffsIndexes[i] = (int)baseStream.Position;
|
||||||
|
Write(array[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now write out the table of locations
|
||||||
|
var tableStartPos = (int)baseStream.Position;
|
||||||
|
_binaryWriter.Write(count);
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
_binaryWriter.Write(diffsIndexes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableStartPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Write(in RenderTreeDiff diff)
|
||||||
|
{
|
||||||
|
_binaryWriter.Write(diff.ComponentId);
|
||||||
|
|
||||||
|
var edits = diff.Edits;
|
||||||
|
_binaryWriter.Write(edits.Count);
|
||||||
|
|
||||||
|
var editsArray = edits.Array;
|
||||||
|
var editsEndIndexExcl = edits.Offset + edits.Count;
|
||||||
|
for (var i = edits.Offset; i < editsEndIndexExcl; i++)
|
||||||
|
{
|
||||||
|
Write(editsArray[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Write(in RenderTreeEdit edit)
|
||||||
|
{
|
||||||
|
// We want all RenderTreeEdit outputs to be of the same length, so that
|
||||||
|
// the recipient can index into the array directly without walking it.
|
||||||
|
// So we output some value for all properties, even when not applicable
|
||||||
|
// for this specific RenderTreeEditType.
|
||||||
|
_binaryWriter.Write((int)edit.Type);
|
||||||
|
_binaryWriter.Write(edit.SiblingIndex);
|
||||||
|
_binaryWriter.Write(edit.ReferenceFrameIndex);
|
||||||
|
WriteString(edit.RemovedAttributeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Write(in ArrayRange<RenderTreeFrame> frames)
|
||||||
|
{
|
||||||
|
var startPos = (int)_binaryWriter.BaseStream.Position;
|
||||||
|
|
||||||
|
var array = frames.Array;
|
||||||
|
var count = frames.Count;
|
||||||
|
_binaryWriter.Write(count);
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
Write(array[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return startPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Write(in RenderTreeFrame frame)
|
||||||
|
{
|
||||||
|
_binaryWriter.Write((int)frame.FrameType);
|
||||||
|
|
||||||
|
// We want each frame to take up the same number of bytes, so that the
|
||||||
|
// recipient can index into the array directly instead of having to
|
||||||
|
// walk through it.
|
||||||
|
// Since we can fit every frame type into 3 ints, use that as the
|
||||||
|
// common size. For smaller frames, we add padding to expand it to
|
||||||
|
// 12 bytes (i.e., 3 x 4-byte ints).
|
||||||
|
// The total size then for each frame is 16 bytes (frame type, then
|
||||||
|
// 3 other ints).
|
||||||
|
switch (frame.FrameType)
|
||||||
|
{
|
||||||
|
case RenderTreeFrameType.Attribute:
|
||||||
|
WriteString(frame.AttributeName);
|
||||||
|
WriteString(frame.AttributeValue as string);
|
||||||
|
_binaryWriter.Write(frame.AttributeEventHandlerId);
|
||||||
|
break;
|
||||||
|
case RenderTreeFrameType.Component:
|
||||||
|
_binaryWriter.Write(frame.ComponentSubtreeLength);
|
||||||
|
_binaryWriter.Write(frame.ComponentId);
|
||||||
|
WritePadding(_binaryWriter, 4);
|
||||||
|
break;
|
||||||
|
case RenderTreeFrameType.ComponentReferenceCapture:
|
||||||
|
// The client doesn't need to know about these. But we still have
|
||||||
|
// to include them in the array otherwise the ReferenceFrameIndex
|
||||||
|
// values in the edits data would be wrong.
|
||||||
|
WritePadding(_binaryWriter, 12);
|
||||||
|
break;
|
||||||
|
case RenderTreeFrameType.Element:
|
||||||
|
_binaryWriter.Write(frame.ElementSubtreeLength);
|
||||||
|
WriteString(frame.ElementName);
|
||||||
|
WritePadding(_binaryWriter, 4);
|
||||||
|
break;
|
||||||
|
case RenderTreeFrameType.ElementReferenceCapture:
|
||||||
|
_binaryWriter.Write(frame.ElementReferenceCaptureId);
|
||||||
|
WritePadding(_binaryWriter, 8);
|
||||||
|
break;
|
||||||
|
case RenderTreeFrameType.Region:
|
||||||
|
_binaryWriter.Write(frame.RegionSubtreeLength);
|
||||||
|
WritePadding(_binaryWriter, 8);
|
||||||
|
break;
|
||||||
|
case RenderTreeFrameType.Text:
|
||||||
|
WriteString(frame.TextContent);
|
||||||
|
WritePadding(_binaryWriter, 8);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentException($"Unsupported frame type: {frame.FrameType}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int Write(in ArrayRange<int> numbers)
|
||||||
|
{
|
||||||
|
var startPos = (int)_binaryWriter.BaseStream.Position;
|
||||||
|
_binaryWriter.Write(numbers.Count);
|
||||||
|
|
||||||
|
var array = numbers.Array;
|
||||||
|
var count = numbers.Count;
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
_binaryWriter.Write(array[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return startPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteString(string value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
_binaryWriter.Write(-1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var stringIndex = _strings.Count;
|
||||||
|
_binaryWriter.Write(stringIndex);
|
||||||
|
_strings.Add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int WriteStringTable()
|
||||||
|
{
|
||||||
|
// Capture the locations of each string
|
||||||
|
var stringsCount = _strings.Count;
|
||||||
|
var locations = new int[stringsCount];
|
||||||
|
|
||||||
|
for (var i = 0; i < stringsCount; i++)
|
||||||
|
{
|
||||||
|
var stringValue = _strings[i];
|
||||||
|
locations[i] = (int)_binaryWriter.BaseStream.Position;
|
||||||
|
_binaryWriter.Write(stringValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now write the locations
|
||||||
|
var locationsStartPos = (int)_binaryWriter.BaseStream.Position;
|
||||||
|
for (var i = 0; i < stringsCount; i++)
|
||||||
|
{
|
||||||
|
_binaryWriter.Write(locations[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return locationsStartPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WritePadding(BinaryWriter writer, int numBytes)
|
||||||
|
{
|
||||||
|
while (numBytes >= 4)
|
||||||
|
{
|
||||||
|
writer.Write(0);
|
||||||
|
numBytes -= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (numBytes > 0)
|
||||||
|
{
|
||||||
|
writer.Write((byte)0);
|
||||||
|
numBytes--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_binaryWriter.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,4 +13,8 @@
|
||||||
<PackageReference Include="Mono.Cecil" Version="0.10.0-beta7" />
|
<PackageReference Include="Mono.Cecil" Version="0.10.0-beta7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Microsoft.AspNetCore.Blazor\Microsoft.AspNetCore.Blazor.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Test")]
|
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Test")]
|
||||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Browser.Test")]
|
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Browser.Test")]
|
||||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Build.Test")]
|
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Build.Test")]
|
||||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Performance")]
|
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Performance")]
|
||||||
|
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Server.Test")]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using AngleSharp.Parser.Html;
|
using AngleSharp.Parser.Html;
|
||||||
|
|
@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||||
var linkElems = parsedHtml.Body.QuerySelectorAll("link");
|
var linkElems = parsedHtml.Body.QuerySelectorAll("link");
|
||||||
var scriptElem = scriptElems[0];
|
var scriptElem = scriptElems[0];
|
||||||
Assert.False(scriptElem.HasChildNodes);
|
Assert.False(scriptElem.HasChildNodes);
|
||||||
Assert.Equal("_framework/blazor.js", scriptElem.GetAttribute("src"));
|
Assert.Equal("_framework/blazor.webassembly.js", scriptElem.GetAttribute("src"));
|
||||||
Assert.Equal("MyApp.Entrypoint.dll", scriptElem.GetAttribute("main"));
|
Assert.Equal("MyApp.Entrypoint.dll", scriptElem.GetAttribute("main"));
|
||||||
Assert.Equal("MyNamespace.MyType::MyMethod", scriptElem.GetAttribute("entrypoint"));
|
Assert.Equal("MyNamespace.MyType::MyMethod", scriptElem.GetAttribute("entrypoint"));
|
||||||
Assert.Equal("System.Abc.dll,MyApp.ClassLib.dll", scriptElem.GetAttribute("references"));
|
Assert.Equal("System.Abc.dll,MyApp.ClassLib.dll", scriptElem.GetAttribute("references"));
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Blazor.Server.Circuits;
|
using Microsoft.AspNetCore.Blazor.Server.Circuits;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspnetCore.Blazor.Server
|
namespace Microsoft.AspNetCore.Blazor.Server
|
||||||
{
|
{
|
||||||
public class CircuitSynchronizationContextTest
|
public class CircuitSynchronizationContextTest
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,311 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Blazor.Components;
|
||||||
|
using Microsoft.AspNetCore.Blazor.Rendering;
|
||||||
|
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||||
|
using Microsoft.AspNetCore.Blazor.Server.Circuits;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Server
|
||||||
|
{
|
||||||
|
public class RenderBatchWriterTest
|
||||||
|
{
|
||||||
|
static object NullStringMarker = new object();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanSerializeEmptyRenderBatch()
|
||||||
|
{
|
||||||
|
// Arrange/Act
|
||||||
|
var bytes = Serialize(new RenderBatch());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
AssertBinaryContents(bytes, /* startIndex */ 0,
|
||||||
|
0, // Length of UpdatedComponents
|
||||||
|
0, // Length of ReferenceFrames
|
||||||
|
0, // Length of DisposedComponentIds
|
||||||
|
0, // Length of DisposedEventHandlerIds
|
||||||
|
|
||||||
|
0, // Index of UpdatedComponents
|
||||||
|
4, // Index of ReferenceFrames
|
||||||
|
8, // Index of DisposedComponentIds
|
||||||
|
12, // Index of DisposedEventHandlerIds
|
||||||
|
16 // Index of Strings
|
||||||
|
);
|
||||||
|
Assert.Equal(36, bytes.Length); // No other data
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanIncludeDisposedComponentIds()
|
||||||
|
{
|
||||||
|
// Arrange/Act
|
||||||
|
var bytes = Serialize(new RenderBatch(
|
||||||
|
default,
|
||||||
|
default,
|
||||||
|
new ArrayRange<int>(new[] { 123, int.MaxValue, int.MinValue, 456 }, 3), // Only use first 3 to show that param is respected
|
||||||
|
default));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
AssertBinaryContents(bytes, /* startIndex */ 0,
|
||||||
|
0, // Length of UpdatedComponents
|
||||||
|
0, // Length of ReferenceFrames
|
||||||
|
3, 123, int.MaxValue, int.MinValue, // DisposedComponentIds as length-prefixed array
|
||||||
|
0, // Length of DisposedEventHandlerIds
|
||||||
|
|
||||||
|
0, // Index of UpdatedComponents
|
||||||
|
4, // Index of ReferenceFrames
|
||||||
|
8, // Index of DisposedComponentIds
|
||||||
|
24, // Index of DisposedEventHandlerIds
|
||||||
|
28 // Index of strings
|
||||||
|
);
|
||||||
|
Assert.Equal(48, bytes.Length); // No other data
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanIncludeDisposedEventHandlerIds()
|
||||||
|
{
|
||||||
|
// Arrange/Act
|
||||||
|
var bytes = Serialize(new RenderBatch(
|
||||||
|
new ArrayRange<RenderTreeDiff>(),
|
||||||
|
new ArrayRange<RenderTreeFrame>(),
|
||||||
|
new ArrayRange<int>(),
|
||||||
|
new ArrayRange<int>(new[] { 123, int.MaxValue, int.MinValue, 456 }, 3) // Only use first 3 to show that param is respected
|
||||||
|
));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
AssertBinaryContents(bytes, /* startIndex */ 0,
|
||||||
|
0, // Length of UpdatedComponents
|
||||||
|
0, // Length of ReferenceFrames
|
||||||
|
0, // Length of DisposedComponentIds
|
||||||
|
3, 123, int.MaxValue, int.MinValue, // DisposedEventHandlerIds as length-prefixed array
|
||||||
|
|
||||||
|
0, // Index of UpdatedComponents
|
||||||
|
4, // Index of ReferenceFrames
|
||||||
|
8, // Index of DisposedComponentIds
|
||||||
|
12, // Index of DisposedEventHandlerIds
|
||||||
|
28 // Index of strings
|
||||||
|
);
|
||||||
|
Assert.Equal(48, bytes.Length); // No other data
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanIncludeUpdatedComponentsWithEmptyEdits()
|
||||||
|
{
|
||||||
|
// Arrange/Act
|
||||||
|
var bytes = Serialize(new RenderBatch(
|
||||||
|
new ArrayRange<RenderTreeDiff>(new[]
|
||||||
|
{
|
||||||
|
new RenderTreeDiff(123, default),
|
||||||
|
new RenderTreeDiff(int.MaxValue, default),
|
||||||
|
}, 2),
|
||||||
|
default,
|
||||||
|
default,
|
||||||
|
default));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
AssertBinaryContents(bytes, /* startIndex */ 0,
|
||||||
|
// UpdatedComponents[0]
|
||||||
|
123, // ComponentId
|
||||||
|
0, // Edits length
|
||||||
|
|
||||||
|
// UpdatedComponents[1]
|
||||||
|
int.MaxValue, // ComponentId
|
||||||
|
0, // Edits length
|
||||||
|
|
||||||
|
2, // Length of UpdatedComponents
|
||||||
|
0, // Index of UpdatedComponents[0]
|
||||||
|
8, // Index of UpdatedComponents[1]
|
||||||
|
|
||||||
|
0, // Length of ReferenceFrames
|
||||||
|
0, // Length of DisposedComponentIds
|
||||||
|
0, // Length of DisposedEventHandlerIds
|
||||||
|
|
||||||
|
16, // Index of UpdatedComponents
|
||||||
|
28, // Index of ReferenceFrames
|
||||||
|
32, // Index of DisposedComponentIds
|
||||||
|
36, // Index of DisposedEventHandlerIds
|
||||||
|
40 // Index of strings
|
||||||
|
);
|
||||||
|
Assert.Equal(60, bytes.Length); // No other data
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanIncludeEdits()
|
||||||
|
{
|
||||||
|
// Arrange/Act
|
||||||
|
var edits = new[]
|
||||||
|
{
|
||||||
|
default, // Skipped (because offset=1 below)
|
||||||
|
RenderTreeEdit.PrependFrame(456, 789),
|
||||||
|
RenderTreeEdit.RemoveFrame(101),
|
||||||
|
RenderTreeEdit.SetAttribute(102, 103),
|
||||||
|
RenderTreeEdit.RemoveAttribute(104, "Some removed attribute"),
|
||||||
|
RenderTreeEdit.UpdateText(105, 106),
|
||||||
|
RenderTreeEdit.StepIn(107),
|
||||||
|
RenderTreeEdit.StepOut(),
|
||||||
|
};
|
||||||
|
var bytes = Serialize(new RenderBatch(
|
||||||
|
new ArrayRange<RenderTreeDiff>(new[]
|
||||||
|
{
|
||||||
|
new RenderTreeDiff(123, new ArraySegment<RenderTreeEdit>(
|
||||||
|
edits, 1, edits.Length - 1)) // Skip first to show offset is respected
|
||||||
|
}, 1),
|
||||||
|
default,
|
||||||
|
default,
|
||||||
|
default));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var diffsStartIndex = ReadInt(bytes, bytes.Length - 20);
|
||||||
|
AssertBinaryContents(bytes, diffsStartIndex,
|
||||||
|
1, // Number of diffs
|
||||||
|
0); // Index of diffs[0]
|
||||||
|
|
||||||
|
AssertBinaryContents(bytes, 0,
|
||||||
|
123, // Component ID for diff 0
|
||||||
|
7, // diff[0].Edits.Count
|
||||||
|
RenderTreeEditType.PrependFrame, 456, 789, NullStringMarker,
|
||||||
|
RenderTreeEditType.RemoveFrame, 101, 0, NullStringMarker,
|
||||||
|
RenderTreeEditType.SetAttribute, 102, 103, NullStringMarker,
|
||||||
|
RenderTreeEditType.RemoveAttribute, 104, 0, "Some removed attribute",
|
||||||
|
RenderTreeEditType.UpdateText, 105, 106, NullStringMarker,
|
||||||
|
RenderTreeEditType.StepIn, 107, 0, NullStringMarker,
|
||||||
|
RenderTreeEditType.StepOut, 0, 0, NullStringMarker
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanIncludeReferenceFrames()
|
||||||
|
{
|
||||||
|
// Arrange/Act
|
||||||
|
var bytes = Serialize(new RenderBatch(
|
||||||
|
default,
|
||||||
|
new ArrayRange<RenderTreeFrame>(new[] {
|
||||||
|
RenderTreeFrame.Attribute(123, "Attribute with string value", "String value"),
|
||||||
|
RenderTreeFrame.Attribute(124, "Attribute with nonstring value", 1),
|
||||||
|
RenderTreeFrame.Attribute(125, "Attribute with delegate value", new Action(() => { }))
|
||||||
|
.WithAttributeEventHandlerId(789),
|
||||||
|
RenderTreeFrame.ChildComponent(126, typeof(object))
|
||||||
|
.WithComponentSubtreeLength(5678)
|
||||||
|
.WithComponentInstance(2000, new FakeComponent()),
|
||||||
|
RenderTreeFrame.ComponentReferenceCapture(127, value => { }, 1001),
|
||||||
|
RenderTreeFrame.Element(128, "Some element")
|
||||||
|
.WithElementSubtreeLength(1234),
|
||||||
|
RenderTreeFrame.ElementReferenceCapture(129, value => { })
|
||||||
|
.WithElementReferenceCaptureId(12121),
|
||||||
|
RenderTreeFrame.Region(130)
|
||||||
|
.WithRegionSubtreeLength(1234),
|
||||||
|
RenderTreeFrame.Text(131, "Some text"),
|
||||||
|
}, 9),
|
||||||
|
default,
|
||||||
|
default));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var referenceFramesStartIndex = ReadInt(bytes, bytes.Length - 16);
|
||||||
|
AssertBinaryContents(bytes, referenceFramesStartIndex,
|
||||||
|
9, // Number of frames
|
||||||
|
RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0,
|
||||||
|
RenderTreeFrameType.Attribute, "Attribute with nonstring value", NullStringMarker, 0,
|
||||||
|
RenderTreeFrameType.Attribute, "Attribute with delegate value", NullStringMarker, 789,
|
||||||
|
RenderTreeFrameType.Component, 5678, 2000, 0,
|
||||||
|
RenderTreeFrameType.ComponentReferenceCapture, 0, 0, 0,
|
||||||
|
RenderTreeFrameType.Element, 1234, "Some element", 0,
|
||||||
|
RenderTreeFrameType.ElementReferenceCapture, 12121, 0, 0,
|
||||||
|
RenderTreeFrameType.Region, 1234, 0, 0,
|
||||||
|
RenderTreeFrameType.Text, "Some text", 0, 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Span<byte> Serialize(RenderBatch renderBatch)
|
||||||
|
{
|
||||||
|
using (var ms = new MemoryStream())
|
||||||
|
using (var writer = new RenderBatchWriter(ms, leaveOpen: false))
|
||||||
|
{
|
||||||
|
writer.Write(renderBatch);
|
||||||
|
return new Span<byte>(ms.ToArray(), 0, (int)ms.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AssertBinaryContents(Span<byte> data, int startIndex, params object[] entries)
|
||||||
|
{
|
||||||
|
var bytes = data.ToArray();
|
||||||
|
|
||||||
|
// The string table position is given by the final int
|
||||||
|
var stringTableStartPosition = BitConverter.ToInt32(bytes, bytes.Length - 4);
|
||||||
|
|
||||||
|
using (var ms = new MemoryStream(bytes))
|
||||||
|
using (var reader = new BinaryReader(ms))
|
||||||
|
{
|
||||||
|
ms.Seek(startIndex, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
foreach (var expectedEntryIterationVar in entries)
|
||||||
|
{
|
||||||
|
// Assume enums are represented as ints
|
||||||
|
var expectedEntry = expectedEntryIterationVar.GetType().IsEnum
|
||||||
|
? (int)expectedEntryIterationVar
|
||||||
|
: expectedEntryIterationVar;
|
||||||
|
|
||||||
|
if (expectedEntry is int expectedInt)
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedInt, reader.ReadInt32());
|
||||||
|
}
|
||||||
|
else if (expectedEntry is string || expectedEntry == NullStringMarker)
|
||||||
|
{
|
||||||
|
// For strings, we have to look up the value in the table of strings
|
||||||
|
// that appears at the end of the serialized data
|
||||||
|
var indexIntoStringTable = reader.ReadInt32();
|
||||||
|
var expectedString = expectedEntry as string;
|
||||||
|
if (expectedString == null)
|
||||||
|
{
|
||||||
|
Assert.Equal(-1, indexIntoStringTable);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The string table entries are all length-prefixed UTF8 blobs
|
||||||
|
var tableEntryPos = BitConverter.ToInt32(bytes, stringTableStartPosition + 4 * indexIntoStringTable);
|
||||||
|
var length = (int)ReadUnsignedLEB128(bytes, tableEntryPos, out var numLEB128Bytes);
|
||||||
|
var value = Encoding.UTF8.GetString(bytes, tableEntryPos + numLEB128Bytes, length);
|
||||||
|
Assert.Equal(expectedString, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Unsupported type: {expectedEntry.GetType().FullName}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ReadInt(Span<byte> bytes, int startOffset)
|
||||||
|
=> BitConverter.ToInt32(bytes.Slice(startOffset, 4).ToArray(), 0);
|
||||||
|
|
||||||
|
public static uint ReadUnsignedLEB128(byte[] bytes, int startOffset, out int numBytesRead)
|
||||||
|
{
|
||||||
|
var result = (uint)0;
|
||||||
|
var shift = 0;
|
||||||
|
var currentByte = (byte)128;
|
||||||
|
numBytesRead = 0;
|
||||||
|
|
||||||
|
for (var count = 0; count < 4 && currentByte >= 128; count++)
|
||||||
|
{
|
||||||
|
currentByte = bytes[startOffset + count];
|
||||||
|
result += (uint)(currentByte & 0x7f) << shift;
|
||||||
|
shift += 7;
|
||||||
|
numBytesRead++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeComponent : IComponent
|
||||||
|
{
|
||||||
|
public void Init(RenderHandle renderHandle)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public void SetParameters(ParameterCollection parameters)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ namespace BasicTestApp
|
||||||
JSRuntime.Current.InvokeAsync<object>("testReady");
|
JSRuntime.Current.InvokeAsync<object>("testReady");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JSInvokable(nameof(MountTestComponent))]
|
||||||
public static void MountTestComponent(string componentTypeName)
|
public static void MountTestComponent(string componentTypeName)
|
||||||
{
|
{
|
||||||
var componentType = Type.GetType(componentTypeName);
|
var componentType = Type.GetType(componentTypeName);
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,7 @@
|
||||||
// The Xunit test code calls this when setting up tests for specific components
|
// The Xunit test code calls this when setting up tests for specific components
|
||||||
function mountTestComponent(typeName) {
|
function mountTestComponent(typeName) {
|
||||||
document.getElementById('source-info').innerHTML = '<code><tt>' + typeName.replace(/\./g, '/') + '.cshtml</code></strong>';
|
document.getElementById('source-info').innerHTML = '<code><tt>' + typeName.replace(/\./g, '/') + '.cshtml</code></strong>';
|
||||||
var method = Blazor.platform.findMethod('BasicTestApp', 'BasicTestApp', 'Program', 'MountTestComponent');
|
DotNet.invokeMethodAsync('BasicTestApp', 'MountTestComponent', typeName);
|
||||||
Blazor.platform.callMethod(method, null, [Blazor.platform.toDotNetString(typeName)]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used by ElementRefComponent
|
// Used by ElementRefComponent
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue