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" />
|
||||
</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" />
|
||||
<Exec Command="npm run build" />
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="dist\blazor.js" LogicalName="blazor./blazor.js" />
|
||||
<EmbeddedResource Include="dist\blazor.webassembly.js" LogicalName="blazor./blazor.webassembly.js" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
import '../../Microsoft.JSInterop/JavaScriptRuntime/src/Microsoft.JSInterop';
|
||||
import { platform } from './Environment';
|
||||
import { getAssemblyNameFromUrl } from './Platform/Url';
|
||||
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() {
|
||||
// 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
|
||||
const allScriptElems = document.getElementsByTagName('script');
|
||||
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.
|
||||
// Basic alternative to having an actual DI container.
|
||||
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 { navigateTo, internalFunctions as uriHelperInternalFunctions } from './Services/UriHelper';
|
||||
import { internalFunctions as httpInternalFunctions } from './Services/Http';
|
||||
import { attachRootComponentToElement, renderBatch } from './Rendering/Renderer';
|
||||
import { attachRootComponentToElement } from './Rendering/Renderer';
|
||||
import { Pointer } from './Platform/Platform';
|
||||
import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// When the library is loaded in a browser via a <script> element, make the
|
||||
// following APIs available in global scope for invocation from JS
|
||||
window['Blazor'] = {
|
||||
platform,
|
||||
navigateTo,
|
||||
// Make the following APIs available in global scope for invocation from JS
|
||||
window['Blazor'] = {
|
||||
platform,
|
||||
navigateTo,
|
||||
|
||||
_internal: {
|
||||
attachRootComponentToElement,
|
||||
renderBatch: (browserRendererId: number, batchAddress: Pointer) => renderBatch(browserRendererId, new SharedMemoryRenderBatch(batchAddress)),
|
||||
http: httpInternalFunctions,
|
||||
uriHelper: uriHelperInternalFunctions
|
||||
}
|
||||
};
|
||||
}
|
||||
_internal: {
|
||||
attachRootComponentToElement,
|
||||
http: httpInternalFunctions,
|
||||
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>) {
|
||||
if (!raiseEventMethod) {
|
||||
raiseEventMethod = platform.findMethod(
|
||||
'Microsoft.AspNetCore.Blazor.Browser', 'Microsoft.AspNetCore.Blazor.Browser.Rendering', 'BrowserRendererEventDispatcher', 'DispatchEvent'
|
||||
);
|
||||
}
|
||||
|
||||
async function raiseEvent(event: Event, browserRendererId: number, componentId: number, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>) {
|
||||
const eventDescriptor = {
|
||||
browserRendererId,
|
||||
componentId,
|
||||
|
|
@ -334,8 +328,9 @@ function raiseEvent(event: Event, browserRendererId: number, componentId: number
|
|||
eventArgsType: eventArgs.type
|
||||
};
|
||||
|
||||
platform.callMethod(raiseEventMethod, null, [
|
||||
platform.toDotNetString(JSON.stringify(eventDescriptor)),
|
||||
platform.toDotNetString(JSON.stringify(eventArgs.data))
|
||||
]);
|
||||
await DotNet.invokeMethodAsync(
|
||||
'Microsoft.AspNetCore.Blazor.Browser',
|
||||
'DispatchEvent',
|
||||
eventDescriptor,
|
||||
JSON.stringify(eventArgs.data));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,19 +52,12 @@ function performInternalNavigation(absoluteInternalHref: string) {
|
|||
handleInternalNavigation();
|
||||
}
|
||||
|
||||
function handleInternalNavigation() {
|
||||
if (!notifyLocationChangedMethod) {
|
||||
notifyLocationChangedMethod = platform.findMethod(
|
||||
'Microsoft.AspNetCore.Blazor.Browser',
|
||||
'Microsoft.AspNetCore.Blazor.Browser.Services',
|
||||
'BrowserUriHelper',
|
||||
'NotifyLocationChanged'
|
||||
);
|
||||
}
|
||||
|
||||
platform.callMethod(notifyLocationChangedMethod, null, [
|
||||
platform.toDotNetString(location.href)
|
||||
]);
|
||||
async function handleInternalNavigation() {
|
||||
await DotNet.invokeMethodAsync(
|
||||
'Microsoft.AspNetCore.Blazor.Browser',
|
||||
'NotifyLocationChanged',
|
||||
location.href
|
||||
);
|
||||
}
|
||||
|
||||
let testAnchor: HTMLAnchorElement;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ module.exports = {
|
|||
module: {
|
||||
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' }
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,15 +10,26 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
|
|||
/// Provides mechanisms for dispatching events to components in a <see cref="BrowserRenderer"/>.
|
||||
/// This is marked 'internal' because it only gets invoked from JS code.
|
||||
/// </summary>
|
||||
internal static class BrowserRendererEventDispatcher
|
||||
public static class BrowserRendererEventDispatcher
|
||||
{
|
||||
// We receive the information as JSON strings because of current interop limitations:
|
||||
// - Can't pass unboxed value types from JS to .NET (yet all the IDs are ints)
|
||||
// - 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.
|
||||
public static void DispatchEvent(string eventDescriptorJson, string eventArgsJson)
|
||||
// TODO: Fix this for multi-user scenarios. Currently it doesn't stop people from
|
||||
// triggering events for other people by passing an arbitrary browserRendererId.
|
||||
//
|
||||
// Preferred fix: Instead of storing the Renderer instances in a static dictionary
|
||||
// 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 browserRenderer = BrowserRendererRegistry.Find(eventDescriptor.BrowserRendererId);
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// For framework use only.
|
||||
/// </summary>
|
||||
public int ComponentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For framework use only.
|
||||
/// </summary>
|
||||
public int EventHandlerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For framework use only.
|
||||
/// </summary>
|
||||
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;
|
||||
_onLocationChanged?.Invoke(null, newAbsoluteUri);
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ namespace Microsoft.AspNetCore.Blazor.Build
|
|||
|
||||
var attributesDict = attributes.ToDictionary(x => x.Key, x => x.Value);
|
||||
attributesDict.Remove("type");
|
||||
attributesDict["src"] = "_framework/blazor.js";
|
||||
attributesDict["src"] = "_framework/blazor.webassembly.js";
|
||||
attributesDict["main"] = assemblyNameWithExtension;
|
||||
attributesDict["entrypoint"] = assemblyEntryPoint;
|
||||
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">
|
||||
<metadata>
|
||||
<id>Microsoft.AspNetCore.Blazor.Build</id>
|
||||
|
|
@ -17,6 +17,6 @@
|
|||
<file src="$publishdir$\netcoreapp2.1\**\*" target="tools/" />
|
||||
<file src="..\mono\dist\optimized\**\*" target="tools\mono" />
|
||||
<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>
|
||||
</package>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<MonoBaseClassLibraryFacadesPath>$(BlazorMonoRuntimeBasePath)dist/optimized/bcl/Facades/</MonoBaseClassLibraryFacadesPath>
|
||||
<MonoAsmjsRuntimePath>$(BlazorMonoRuntimeBasePath)dist/optimized/asmjs/</MonoAsmjsRuntimePath>
|
||||
<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>
|
||||
|
||||
<Import Project="$(MSBuildThisFileDirectory)targets/All.props" />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<MonoBaseClassLibraryFacadesPath>$(BlazorMonoRuntimeBasePath)tools/mono/bcl/Facades/</MonoBaseClassLibraryFacadesPath>
|
||||
<MonoAsmjsRuntimePath>$(BlazorMonoRuntimeBasePath)tools/mono/asmjs/</MonoAsmjsRuntimePath>
|
||||
<MonoWasmRuntimePath>$(BlazorMonoRuntimeBasePath)tools/mono/wasm/</MonoWasmRuntimePath>
|
||||
<BlazorJsPath>$(BlazorMonoRuntimeBasePath)tools/blazor/blazor.js</BlazorJsPath>
|
||||
<BlazorJsPath>$(BlazorMonoRuntimeBasePath)tools/blazor/blazor.*.js</BlazorJsPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Blazor\Microsoft.AspNetCore.Blazor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Test")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Browser.Test")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Build.Test")]
|
||||
[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.
|
||||
|
||||
using AngleSharp.Parser.Html;
|
||||
|
|
@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
var linkElems = parsedHtml.Body.QuerySelectorAll("link");
|
||||
var scriptElem = scriptElems[0];
|
||||
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("MyNamespace.MyType::MyMethod", scriptElem.GetAttribute("entrypoint"));
|
||||
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 Xunit;
|
||||
|
||||
namespace Microsoft.AspnetCore.Blazor.Server
|
||||
namespace Microsoft.AspNetCore.Blazor.Server
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
[JSInvokable(nameof(MountTestComponent))]
|
||||
public static void MountTestComponent(string componentTypeName)
|
||||
{
|
||||
var componentType = Type.GetType(componentTypeName);
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@
|
|||
// The Xunit test code calls this when setting up tests for specific components
|
||||
function mountTestComponent(typeName) {
|
||||
document.getElementById('source-info').innerHTML = '<code><tt>' + typeName.replace(/\./g, '/') + '.cshtml</code></strong>';
|
||||
var method = Blazor.platform.findMethod('BasicTestApp', 'BasicTestApp', 'Program', 'MountTestComponent');
|
||||
Blazor.platform.callMethod(method, null, [Blazor.platform.toDotNetString(typeName)]);
|
||||
DotNet.invokeMethodAsync('BasicTestApp', 'MountTestComponent', typeName);
|
||||
}
|
||||
|
||||
// Used by ElementRefComponent
|
||||
|
|
|
|||
Loading…
Reference in New Issue