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:
Steve Sanderson 2018-07-06 14:10:55 +01:00
parent cafb56569d
commit 5bccac05fc
21 changed files with 667 additions and 68 deletions

View File

@ -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>

View File

@ -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;

View File

@ -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;
}

View File

@ -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
}
};

View File

@ -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));
}

View File

@ -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;

View File

@ -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' }
};

View File

@ -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; }
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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>

View File

@ -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" />

View File

@ -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">

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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")]

View File

@ -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"));

View File

@ -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
{

View File

@ -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();
}
}
}

View File

@ -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);

View File

@ -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