Headless Blazor client (#11112)

* Add Ignitor
* Finish headless Blazor client.
* Added support for click events.
* Move Ignitor into testassets folder.
- Also added Ignitor to the no deps solution.
* Add Ignitor tests to validate RenderBatchReader stays consistent.
This commit is contained in:
N. Taylor Mullen 2019-06-13 10:05:40 -07:00 committed by Doug Bunting
parent 38bc43bad7
commit 874050f1dd
22 changed files with 1845 additions and 3 deletions

View File

@ -212,6 +212,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Co
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Protocols.Json", "..\SignalR\common\Protocols.Json\src\Microsoft.AspNetCore.SignalR.Protocols.Json.csproj", "{ED210157-461B-45BB-9D86-B81A62792C30}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Client", "..\SignalR\clients\csharp\Client\src\Microsoft.AspNetCore.SignalR.Client.csproj", "{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Client.Core", "..\SignalR\clients\csharp\Client.Core\src\Microsoft.AspNetCore.SignalR.Client.Core.csproj", "{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Connections.Client", "..\SignalR\clients\csharp\Http.Connections.Client\src\Microsoft.AspNetCore.Http.Connections.Client.csproj", "{F88118E1-6F4A-4F89-B047-5FFD2889B9F0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ignitor", "test\testassets\Ignitor\Ignitor.csproj", "{A78CE874-76B7-46FE-8009-1ED5258BA0AA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ignitor.Test", "test\Ignitor.Test\Ignitor.Test.csproj", "{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -1338,6 +1348,66 @@ Global
{ED210157-461B-45BB-9D86-B81A62792C30}.Release|x64.Build.0 = Release|Any CPU
{ED210157-461B-45BB-9D86-B81A62792C30}.Release|x86.ActiveCfg = Release|Any CPU
{ED210157-461B-45BB-9D86-B81A62792C30}.Release|x86.Build.0 = Release|Any CPU
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}.Debug|x64.ActiveCfg = Debug|Any CPU
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}.Debug|x64.Build.0 = Debug|Any CPU
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}.Debug|x86.ActiveCfg = Debug|Any CPU
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}.Debug|x86.Build.0 = Debug|Any CPU
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}.Release|Any CPU.Build.0 = Release|Any CPU
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}.Release|x64.ActiveCfg = Release|Any CPU
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}.Release|x64.Build.0 = Release|Any CPU
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}.Release|x86.ActiveCfg = Release|Any CPU
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}.Release|x86.Build.0 = Release|Any CPU
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB}.Debug|x64.ActiveCfg = Debug|Any CPU
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB}.Debug|x64.Build.0 = Debug|Any CPU
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB}.Debug|x86.ActiveCfg = Debug|Any CPU
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB}.Debug|x86.Build.0 = Debug|Any CPU
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB}.Release|Any CPU.Build.0 = Release|Any CPU
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB}.Release|x64.ActiveCfg = Release|Any CPU
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB}.Release|x64.Build.0 = Release|Any CPU
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB}.Release|x86.ActiveCfg = Release|Any CPU
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB}.Release|x86.Build.0 = Release|Any CPU
{F88118E1-6F4A-4F89-B047-5FFD2889B9F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F88118E1-6F4A-4F89-B047-5FFD2889B9F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F88118E1-6F4A-4F89-B047-5FFD2889B9F0}.Debug|x64.ActiveCfg = Debug|Any CPU
{F88118E1-6F4A-4F89-B047-5FFD2889B9F0}.Debug|x64.Build.0 = Debug|Any CPU
{F88118E1-6F4A-4F89-B047-5FFD2889B9F0}.Debug|x86.ActiveCfg = Debug|Any CPU
{F88118E1-6F4A-4F89-B047-5FFD2889B9F0}.Debug|x86.Build.0 = Debug|Any CPU
{F88118E1-6F4A-4F89-B047-5FFD2889B9F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F88118E1-6F4A-4F89-B047-5FFD2889B9F0}.Release|Any CPU.Build.0 = Release|Any CPU
{F88118E1-6F4A-4F89-B047-5FFD2889B9F0}.Release|x64.ActiveCfg = Release|Any CPU
{F88118E1-6F4A-4F89-B047-5FFD2889B9F0}.Release|x64.Build.0 = Release|Any CPU
{F88118E1-6F4A-4F89-B047-5FFD2889B9F0}.Release|x86.ActiveCfg = Release|Any CPU
{F88118E1-6F4A-4F89-B047-5FFD2889B9F0}.Release|x86.Build.0 = Release|Any CPU
{A78CE874-76B7-46FE-8009-1ED5258BA0AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A78CE874-76B7-46FE-8009-1ED5258BA0AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A78CE874-76B7-46FE-8009-1ED5258BA0AA}.Debug|x64.ActiveCfg = Debug|Any CPU
{A78CE874-76B7-46FE-8009-1ED5258BA0AA}.Debug|x64.Build.0 = Debug|Any CPU
{A78CE874-76B7-46FE-8009-1ED5258BA0AA}.Debug|x86.ActiveCfg = Debug|Any CPU
{A78CE874-76B7-46FE-8009-1ED5258BA0AA}.Debug|x86.Build.0 = Debug|Any CPU
{A78CE874-76B7-46FE-8009-1ED5258BA0AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A78CE874-76B7-46FE-8009-1ED5258BA0AA}.Release|Any CPU.Build.0 = Release|Any CPU
{A78CE874-76B7-46FE-8009-1ED5258BA0AA}.Release|x64.ActiveCfg = Release|Any CPU
{A78CE874-76B7-46FE-8009-1ED5258BA0AA}.Release|x64.Build.0 = Release|Any CPU
{A78CE874-76B7-46FE-8009-1ED5258BA0AA}.Release|x86.ActiveCfg = Release|Any CPU
{A78CE874-76B7-46FE-8009-1ED5258BA0AA}.Release|x86.Build.0 = Release|Any CPU
{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA}.Debug|x64.ActiveCfg = Debug|Any CPU
{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA}.Debug|x64.Build.0 = Debug|Any CPU
{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA}.Debug|x86.ActiveCfg = Debug|Any CPU
{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA}.Debug|x86.Build.0 = Debug|Any CPU
{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA}.Release|Any CPU.Build.0 = Release|Any CPU
{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA}.Release|x64.ActiveCfg = Release|Any CPU
{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA}.Release|x64.Build.0 = Release|Any CPU
{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA}.Release|x86.ActiveCfg = Release|Any CPU
{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1438,6 +1508,11 @@ Global
{9088E4E4-B855-457F-AE9E-D86709A5E1F4} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C} = {2FC10057-7A0A-4E34-8302-879925BC0102}
{ED210157-461B-45BB-9D86-B81A62792C30} = {2FC10057-7A0A-4E34-8302-879925BC0102}
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0} = {2FC10057-7A0A-4E34-8302-879925BC0102}
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB} = {2FC10057-7A0A-4E34-8302-879925BC0102}
{F88118E1-6F4A-4F89-B047-5FFD2889B9F0} = {2FC10057-7A0A-4E34-8302-879925BC0102}
{A78CE874-76B7-46FE-8009-1ED5258BA0AA} = {44E0D4F3-4430-4175-B482-0D1AEE4BB699}
{FC2A1EB0-A116-4689-92B7-239B1DCCF4CA} = {E9E9CF3C-CE9B-4282-B2BB-97EFC3872798}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CC3C47E1-AD1A-4619-9CD3-E08A0148E5CE}

View File

@ -5,3 +5,5 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Browser.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Performance, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Ignitor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Ignitor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -29,7 +29,9 @@
"test\\testassets\\ComponentsApp.App\\ComponentsApp.App.csproj",
"test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj",
"test\\testassets\\TestContentPackage\\TestContentPackage.csproj",
"test\\testassets\\TestServer\\Components.TestServer.csproj"
"test\\testassets\\TestServer\\Components.TestServer.csproj",
"test\\testassets\\Ignitor\\Ignitor.csproj",
"test\\Ignitor.Test\\Ignitor.Test.csproj"
]
}
}

View File

@ -2,5 +2,7 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.DevServer, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Ignitor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Ignitor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\testassets\Ignitor\Ignitor.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,347 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Ignitor
{
public class RenderBatchReaderTest
{
static object NullStringMarker = new object();
// All of these tests are copies from the RenderBatchWriterTest but converted to be round-trippable tests.
[Fact]
public void CanRoundTripEmptyRenderBatch()
{
// Arrange/Act
var bytes = RoundTripSerialize(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 CanRoundTripUpdatedComponentsWithEmptyEdits()
{
// Arrange/Act
var bytes = RoundTripSerialize(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 CanRoundTripEdits()
{
// 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(),
RenderTreeEdit.UpdateMarkup(108, 109),
RenderTreeEdit.RemoveAttribute(110, "Some removed attribute"), // To test deduplication
};
var bytes = RoundTripSerialize(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
9, // 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,
RenderTreeEditType.UpdateMarkup, 108, 109, NullStringMarker,
RenderTreeEditType.RemoveAttribute, 110, 0, "Some removed attribute"
);
// We can deduplicate attribute names
Assert.Equal(new[] { "Some removed attribute" }, ReadStringTable(bytes));
}
[Fact]
public void CanRoundTripReferenceFrames()
{
// Arrange/Act
var renderer = new FakeRenderer();
var bytes = RoundTripSerialize(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)
.WithComponent(new ComponentState(renderer, 2000, new FakeComponent(), null)),
RenderTreeFrame.ComponentReferenceCapture(127, value => { }, 1001),
RenderTreeFrame.Element(128, "Some element")
.WithElementSubtreeLength(1234),
RenderTreeFrame.ElementReferenceCapture(129, value => { })
.WithElementReferenceCaptureId("my unique ID"),
RenderTreeFrame.Region(130)
.WithRegionSubtreeLength(1234),
RenderTreeFrame.Text(131, "Some text"),
RenderTreeFrame.Markup(132, "Some markup"),
RenderTreeFrame.Text(133, "\n\t "),
// Testing deduplication
RenderTreeFrame.Attribute(134, "Attribute with string value", "String value"),
RenderTreeFrame.Element(135, "Some element") // Will be deduplicated
.WithElementSubtreeLength(999),
RenderTreeFrame.Text(136, "Some text"), // Will not be deduplicated
RenderTreeFrame.Markup(137, "Some markup"), // Will not be deduplicated
RenderTreeFrame.Text(138, "\n\t "), // Will be deduplicated
}, 16),
default,
default));
// Assert
var referenceFramesStartIndex = ReadInt(bytes, bytes.Length - 16);
AssertBinaryContents(bytes, referenceFramesStartIndex,
16, // 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, "my unique ID", 0, 0,
RenderTreeFrameType.Region, 1234, 0, 0,
RenderTreeFrameType.Text, "Some text", 0, 0,
RenderTreeFrameType.Markup, "Some markup", 0, 0,
RenderTreeFrameType.Text, "\n\t ", 0, 0,
RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0,
RenderTreeFrameType.Element, 999, "Some element", 0,
RenderTreeFrameType.Text, "Some text", 0, 0,
RenderTreeFrameType.Markup, "Some markup", 0, 0,
RenderTreeFrameType.Text, "\n\t ", 0, 0
);
Assert.Equal(new[]
{
"Attribute with string value",
"String value",
"Attribute with nonstring value",
"Attribute with delegate value",
"Some element",
"my unique ID",
"Some text",
"Some markup",
"\n\t ",
"String value",
"Some text",
"Some markup",
}, ReadStringTable(bytes));
}
private Span<byte> RoundTripSerialize(RenderBatch renderBatch)
{
var bytes = Serialize(renderBatch);
var roundTrippedRenderBatch = RenderBatchReader.Read(bytes);
var roundTrippedBytes = Serialize(roundTrippedRenderBatch);
return roundTrippedBytes;
Span<byte> Serialize(RenderBatch batch)
{
using (var ms = new MemoryStream())
using (var writer = new RenderBatchWriter(ms, leaveOpen: false))
{
writer.Write(batch);
return new Span<byte>(ms.ToArray(), 0, (int)ms.Length);
}
}
}
static string[] ReadStringTable(Span<byte> data)
{
var bytes = data.ToArray();
// The string table position is given by the final int, and continues
// until we get to the final set of top-level indices
var stringTableStartPosition = BitConverter.ToInt32(bytes, bytes.Length - 4);
var stringTableEndPositionExcl = bytes.Length - 20;
var result = new List<string>();
for (var entryPosition = stringTableStartPosition;
entryPosition < stringTableEndPositionExcl;
entryPosition += 4)
{
// The string table entries are all length-prefixed UTF8 blobs
var tableEntryPos = BitConverter.ToInt32(bytes, entryPosition);
var length = (int)ReadUnsignedLEB128(bytes, tableEntryPos, out var numLEB128Bytes);
var value = Encoding.UTF8.GetString(bytes, tableEntryPos + numLEB128Bytes, length);
result.Add(value);
}
return result.ToArray();
}
static void AssertBinaryContents(Span<byte> data, int startIndex, params object[] entries)
{
var bytes = data.ToArray();
var stringTableEntries = ReadStringTable(data);
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
{
Assert.Equal(expectedString, stringTableEntries[indexIntoStringTable]);
}
}
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 Configure(RenderHandle renderHandle)
=> throw new NotImplementedException();
public Task SetParametersAsync(ParameterCollection parameters)
=> throw new NotImplementedException();
}
class FakeRenderer : Renderer
{
public FakeRenderer()
: base(new ServiceCollection().BuildServiceProvider(), new RendererSynchronizationContext())
{
}
protected override void HandleException(Exception exception)
{
throw new NotImplementedException();
}
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
=> throw new NotImplementedException();
}
}
}

View File

@ -4,7 +4,7 @@
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="@IncrementCount">Click me</button>
<button class="btn btn-primary" @onclick="@IncrementCount" id="thecounter">Click me</button>
@code {
int currentCount = 0;

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>

View File

@ -0,0 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Ignitor
{
internal class LogicalContainerNode : ContainerNode
{
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Ignitor
{
internal class ComponentNode : ContainerNode
{
private readonly int _componentId;
public ComponentNode(int componentId)
{
_componentId = componentId;
}
}
}

View File

@ -0,0 +1,84 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Ignitor
{
internal abstract class ContainerNode : Node
{
private readonly List<Node> _children;
protected ContainerNode()
{
_children = new List<Node>();
}
public IReadOnlyList<Node> Children => _children;
public void InsertLogicalChild(Node child, int childIndex)
{
if (child is LogicalContainerNode comment && comment.Children.Count > 0)
{
// There's nothing to stop us implementing support for this scenario, and it's not difficult
// (after inserting 'child' itself, also iterate through its logical children and physically
// put them as following-siblings in the DOM). However there's no scenario that requires it
// presently, so if we did implement it there'd be no good way to have tests for it.
throw new Exception("Not implemented: inserting non-empty logical container");
}
if (child.Parent != null)
{
// Likewise, we could easily support this scenario too (in this 'if' block, just splice
// out 'child' from the logical children array of its previous logical parent by using
// Array.prototype.indexOf to determine its previous sibling index).
// But again, since there's not currently any scenario that would use it, we would not
// have any test coverage for such an implementation.
throw new NotSupportedException("Not implemented: moving existing logical children");
}
if (childIndex < Children.Count)
{
// Insert
_children[childIndex] = child;
}
else
{
// Append
_children.Add(child);
}
child.Parent = this;
}
public ContainerNode CreateAndInsertContainer(int childIndex)
{
var containerElement = new LogicalContainerNode();
InsertLogicalChild(containerElement, childIndex);
return containerElement;
}
public ComponentNode CreateAndInsertComponent(int componentId, int childIndex)
{
var componentElement = new ComponentNode(componentId);
InsertLogicalChild(componentElement, childIndex);
return componentElement;
}
public void RemoveLogicalChild(int childIndex)
{
var childToRemove = Children[childIndex];
_children.RemoveAt(childIndex);
// If it's a logical container, also remove its descendants
if (childToRemove is LogicalContainerNode container)
{
while (container.Children.Count > 0)
{
container.RemoveLogicalChild(0);
}
}
}
}
}

View File

@ -0,0 +1,473 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Ignitor
{
internal class ElementHive
{
private const string SelectValuePropname = "_blazorSelectValue";
public Dictionary<int, ComponentNode> Components { get; } = new Dictionary<int, ComponentNode>();
public string SerializedValue => NodeSerializer.Serialize(this);
public void Update(RenderBatch batch)
{
for (var i = 0; i < batch.UpdatedComponents.Count; i++)
{
var diff = batch.UpdatedComponents.Array[i];
var componentId = diff.ComponentId;
var edits = diff.Edits;
UpdateComponent(batch, componentId, edits);
}
for (var i = 0; i < batch.DisposedComponentIDs.Count; i++)
{
DisposeComponent(batch.DisposedComponentIDs.Array[i]);
}
for (var i = 0; i < batch.DisposedEventHandlerIDs.Count; i++)
{
DisposeEventHandler(batch.DisposedEventHandlerIDs.Array[i]);
}
}
public bool TryFindElementById(string id, out ElementNode element)
{
foreach (var kvp in Components)
{
var component = kvp.Value;
if (TryGetElementFromChildren(component, out element))
{
return true;
}
}
element = null;
return false;
bool TryGetElementFromChildren(Node node, out ElementNode foundNode)
{
if (node is ElementNode elementNode &&
elementNode.Attributes.TryGetValue("id", out var elementId) &&
elementId?.ToString() == id)
{
foundNode = elementNode;
return true;
}
if (node is ContainerNode containerNode)
{
for (var i = 0; i < containerNode.Children.Count; i++)
{
if (TryGetElementFromChildren(containerNode.Children[i], out foundNode))
{
return true;
}
}
}
foundNode = null;
return false;
}
}
private void UpdateComponent(RenderBatch batch, int componentId, ArraySegment<RenderTreeEdit> edits)
{
if (!Components.TryGetValue(componentId, out var component))
{
component = new ComponentNode(componentId);
Components.Add(componentId, component);
}
ApplyEdits(batch, component, 0, edits);
}
private void DisposeComponent(int componentId)
{
}
private void DisposeEventHandler(int eventHandlerId)
{
}
private void ApplyEdits(RenderBatch batch, ContainerNode parent, int childIndex, ArraySegment<RenderTreeEdit> edits)
{
var currentDepth = 0;
var childIndexAtCurrentDepth = childIndex;
var permutations = new List<PermutationListEntry>();
for (var editIndex = edits.Offset; editIndex < edits.Offset + edits.Count; editIndex++)
{
var edit = edits.Array[editIndex];
switch (edit.Type)
{
case RenderTreeEditType.PrependFrame:
{
var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex];
var siblingIndex = edit.SiblingIndex;
InsertFrame(batch, parent, childIndexAtCurrentDepth + siblingIndex, batch.ReferenceFrames.Array, frame, edit.ReferenceFrameIndex);
break;
}
case RenderTreeEditType.RemoveFrame:
{
var siblingIndex = edit.SiblingIndex;
parent.RemoveLogicalChild(childIndexAtCurrentDepth + siblingIndex);
break;
}
case RenderTreeEditType.SetAttribute:
{
var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex];
var siblingIndex = edit.SiblingIndex;
var node = parent.Children[childIndexAtCurrentDepth + siblingIndex];
if (node is ElementNode element)
{
ApplyAttribute(batch, element, frame);
}
else
{
throw new Exception("Cannot set attribute on non-element child");
}
break;
}
case RenderTreeEditType.RemoveAttribute:
{
// Note that we don't have to dispose the info we track about event handlers here, because the
// disposed event handler IDs are delivered separately (in the 'disposedEventHandlerIds' array)
var siblingIndex = edit.SiblingIndex;
var node = parent.Children[childIndexAtCurrentDepth + siblingIndex];
if (node is ElementNode element)
{
var attributeName = edit.RemovedAttributeName;
// First try to remove any special property we use for this attribute
if (!TryApplySpecialProperty(batch, element, attributeName, default))
{
// If that's not applicable, it's a regular DOM attribute so remove that
element.RemoveAttribute(attributeName);
}
}
else
{
throw new Exception("Cannot remove attribute from non-element child");
}
break;
}
case RenderTreeEditType.UpdateText:
{
var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex];
var siblingIndex = edit.SiblingIndex;
var node = parent.Children[childIndexAtCurrentDepth + siblingIndex];
if (node is TextNode textNode)
{
textNode.TextContent = frame.TextContent;
}
else
{
throw new Exception("Cannot set text content on non-text child");
}
break;
}
case RenderTreeEditType.UpdateMarkup:
{
var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex];
var siblingIndex = edit.SiblingIndex;
parent.RemoveLogicalChild(childIndexAtCurrentDepth + siblingIndex);
InsertMarkup(parent, childIndexAtCurrentDepth + siblingIndex, frame);
break;
}
case RenderTreeEditType.StepIn:
{
var siblingIndex = edit.SiblingIndex;
parent = (ContainerNode)parent.Children[childIndexAtCurrentDepth + siblingIndex];
currentDepth++;
childIndexAtCurrentDepth = 0;
break;
}
case RenderTreeEditType.StepOut:
{
parent = parent.Parent;
currentDepth--;
childIndexAtCurrentDepth = currentDepth == 0 ? childIndex : 0; // The childIndex is only ever nonzero at zero depth
break;
}
case RenderTreeEditType.PermutationListEntry:
{
permutations.Add(new PermutationListEntry(childIndexAtCurrentDepth + edit.SiblingIndex, childIndexAtCurrentDepth + edit.MoveToSiblingIndex));
break;
}
case RenderTreeEditType.PermutationListEnd:
{
throw new NotSupportedException();
//permuteLogicalChildren(parent, permutations!);
//permutations.Clear();
//break;
}
default:
{
throw new Exception($"Unknown edit type: '{edit.Type}'");
}
}
}
}
private int InsertFrame(RenderBatch batch, ContainerNode parent, int childIndex, ArraySegment<RenderTreeFrame> frames, RenderTreeFrame frame, int frameIndex)
{
switch (frame.FrameType)
{
case RenderTreeFrameType.Element:
{
InsertElement(batch, parent, childIndex, frames, frame, frameIndex);
return 1;
}
case RenderTreeFrameType.Text:
{
InsertText(parent, childIndex, frame);
return 1;
}
case RenderTreeFrameType.Attribute:
{
throw new Exception("Attribute frames should only be present as leading children of element frames.");
}
case RenderTreeFrameType.Component:
{
InsertComponent(parent, childIndex, frame);
return 1;
}
case RenderTreeFrameType.Region:
{
return InsertFrameRange(batch, parent, childIndex, frames, frameIndex + 1, frameIndex + CountDescendantFrames(frame));
}
case RenderTreeFrameType.ElementReferenceCapture:
{
// No action for reference captures.
break;
}
case RenderTreeFrameType.Markup:
{
InsertMarkup(parent, childIndex, frame);
return 1;
}
}
throw new Exception($"Unknown frame type: {frame.FrameType}");
}
private void InsertText(ContainerNode parent, int childIndex, RenderTreeFrame frame)
{
var textContent = frame.TextContent;
var newTextNode = new TextNode(textContent);
parent.InsertLogicalChild(newTextNode, childIndex);
}
private void InsertComponent(ContainerNode parent, int childIndex, RenderTreeFrame frame)
{
// All we have to do is associate the child component ID with its location. We don't actually
// do any rendering here, because the diff for the child will appear later in the render batch.
var childComponentId = frame.ComponentId;
var containerElement = parent.CreateAndInsertComponent(childComponentId, childIndex);
Components[childComponentId] = containerElement;
}
private int InsertFrameRange(RenderBatch batch, ContainerNode parent, int childIndex, ArraySegment<RenderTreeFrame> frames, int startIndex, int endIndexExcl)
{
var origChildIndex = childIndex;
for (var index = startIndex; index < endIndexExcl; index++)
{
var frame = batch.ReferenceFrames.Array[index];
var numChildrenInserted = InsertFrame(batch, parent, childIndex, frames, frame, index);
childIndex += numChildrenInserted;
// Skip over any descendants, since they are already dealt with recursively
index += CountDescendantFrames(frame);
}
return (childIndex - origChildIndex); // Total number of children inserted
}
private void InsertElement(RenderBatch batch, ContainerNode parent, int childIndex, ArraySegment<RenderTreeFrame> frames, RenderTreeFrame frame, int frameIndex)
{
// Note: we don't handle SVG here
var newElement = new ElementNode(frame.ElementName);
parent.InsertLogicalChild(newElement, childIndex);
// Apply attributes
for (var i = frameIndex + 1; i < frameIndex + frame.ElementSubtreeLength; i++)
{
var descendantFrame = batch.ReferenceFrames.Array[i];
if (descendantFrame.FrameType == RenderTreeFrameType.Attribute)
{
ApplyAttribute(batch, newElement, descendantFrame);
}
else
{
// As soon as we see a non-attribute child, all the subsequent child frames are
// not attributes, so bail out and insert the remnants recursively
InsertFrameRange(batch, newElement, 0, frames, i, frameIndex + frame.ElementSubtreeLength);
break;
}
}
}
private void ApplyAttribute(RenderBatch batch, ElementNode elementNode, RenderTreeFrame attributeFrame)
{
var attributeName = attributeFrame.AttributeName;
var eventHandlerId = attributeFrame.AttributeEventHandlerId;
if (eventHandlerId != 0)
{
var firstTwoChars = attributeName.Substring(0, 2);
var eventName = attributeName.Substring(2);
if (firstTwoChars != "on" || string.IsNullOrEmpty(eventName))
{
throw new InvalidOperationException($"Attribute has nonzero event handler ID, but attribute name '${attributeName}' does not start with 'on'.");
}
var descriptor = new ElementNode.ElementEventDescriptor(eventName, eventHandlerId);
elementNode.SetEvent(eventName, descriptor);
return;
}
// First see if we have special handling for this attribute
if (!TryApplySpecialProperty(batch, elementNode, attributeName, attributeFrame))
{
// If not, treat it as a regular string-valued attribute
elementNode.SetAttribute(
attributeName,
attributeFrame.AttributeValue);
}
}
private bool TryApplySpecialProperty(RenderBatch batch, ElementNode element, string attributeName, RenderTreeFrame attributeFrame)
{
switch (attributeName)
{
case "value":
return TryApplyValueProperty(element, attributeFrame);
case "checked":
return TryApplyCheckedProperty(element, attributeFrame);
default:
return false;
}
}
private bool TryApplyValueProperty(ElementNode element, RenderTreeFrame attributeFrame)
{
// Certain elements have built-in behaviour for their 'value' property
switch (element.TagName)
{
case "INPUT":
case "SELECT":
case "TEXTAREA":
{
var value = attributeFrame.AttributeValue;
element.SetProperty("value", value);
if (element.TagName == "SELECT")
{
// <select> is special, in that anything we write to .value will be lost if there
// isn't yet a matching <option>. To maintain the expected behavior no matter the
// element insertion/update order, preserve the desired value separately so
// we can recover it when inserting any matching <option>.
element.SetProperty(SelectValuePropname, value);
}
return true;
}
case "OPTION":
{
var value = attributeFrame.AttributeValue;
if (value != null)
{
element.SetAttribute("value", value);
}
else
{
element.RemoveAttribute("value");
}
return true;
}
default:
return false;
}
}
private bool TryApplyCheckedProperty(ElementNode element, RenderTreeFrame attributeFrame)
{
// Certain elements have built-in behaviour for their 'checked' property
if (element.TagName == "INPUT")
{
var value = attributeFrame.AttributeValue;
element.SetProperty("checked", value);
return true;
}
return false;
}
private void InsertMarkup(ContainerNode parent, int childIndex, RenderTreeFrame markupFrame)
{
var markupContainer = parent.CreateAndInsertContainer(childIndex);
var markupContent = markupFrame.MarkupContent;
var markupNode = new MarkupNode(markupContent);
markupContainer.InsertLogicalChild(markupNode, childIndex);
}
private int CountDescendantFrames(RenderTreeFrame frame)
{
switch (frame.FrameType)
{
// The following frame types have a subtree length. Other frames may use that memory slot
// to mean something else, so we must not read it. We should consider having nominal subtypes
// of RenderTreeFramePointer that prevent access to non-applicable fields.
case RenderTreeFrameType.Component:
return frame.ComponentSubtreeLength - 1;
case RenderTreeFrameType.Element:
return frame.ElementSubtreeLength - 1;
case RenderTreeFrameType.Region:
return frame.RegionSubtreeLength - 1;
default:
return 0;
}
}
private readonly struct PermutationListEntry
{
public readonly int From;
public readonly int To;
public PermutationListEntry(int from, int to)
{
From = from;
To = to;
}
}
}
}

View File

@ -0,0 +1,106 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Browser;
using Microsoft.AspNetCore.SignalR.Client;
namespace Ignitor
{
internal class ElementNode : ContainerNode
{
private readonly Dictionary<string, object> _attributes;
private readonly Dictionary<string, object> _properties;
private readonly Dictionary<string, ElementEventDescriptor> _events;
public ElementNode(string tagName)
{
TagName = tagName ?? throw new ArgumentNullException(nameof(tagName));
_attributes = new Dictionary<string, object>(StringComparer.Ordinal);
_properties = new Dictionary<string, object>(StringComparer.Ordinal);
_events = new Dictionary<string, ElementEventDescriptor>(StringComparer.Ordinal);
}
public string TagName { get; }
public IReadOnlyDictionary<string, object> Attributes => _attributes;
public IReadOnlyDictionary<string, object> Properties => _properties;
public IReadOnlyDictionary<string, ElementEventDescriptor> Events => _events;
public void SetAttribute(string key, object value)
{
_attributes[key] = value;
}
public void RemoveAttribute(string key)
{
_attributes.Remove(key);
}
public void SetProperty(string key, object value)
{
_properties[key] = value;
}
public void SetEvent(string eventName, ElementEventDescriptor descriptor)
{
if (eventName is null)
{
throw new ArgumentNullException(nameof(eventName));
}
if (descriptor is null)
{
throw new ArgumentNullException(nameof(descriptor));
}
_events[eventName] = descriptor;
}
public class ElementEventDescriptor
{
public ElementEventDescriptor(string eventName, int eventId)
{
EventName = eventName ?? throw new ArgumentNullException(nameof(eventName));
EventId = eventId;
}
public string EventName { get; }
public int EventId { get; }
}
public async Task ClickAsync(HubConnection connection)
{
if (!Events.TryGetValue("click", out var clickEventDescriptor))
{
Console.WriteLine("Button does not have a click event. Exiting");
return;
}
var mouseEventArgs = new UIMouseEventArgs()
{
Type = clickEventDescriptor.EventName,
Detail = 1
};
var browserDescriptor = new RendererRegistryEventDispatcher.BrowserEventDescriptor()
{
BrowserRendererId = 0,
EventHandlerId = clickEventDescriptor.EventId,
EventArgsType = "mouse",
};
var serializedJson = JsonSerializer.ToString(mouseEventArgs, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var argsObject = new object[] { browserDescriptor, serializedJson };
var callId = "0";
var assemblyName = "Microsoft.AspNetCore.Components.Browser";
var methodIdentifier = "DispatchEvent";
var dotNetObjectId = 0;
var clickArgs = JsonSerializer.ToString(argsObject, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
await connection.InvokeAsync("BeginInvokeDotNetFromJS", callId, assemblyName, methodIdentifier, dotNetObjectId, clickArgs);
}
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.SignalR.Client" />
<Reference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" />
<Reference Include="Microsoft.Extensions.Logging.Console" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
<Reference Include="System.Text.Json" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,12 @@
// 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.SignalR.Protocol;
namespace Ignitor
{
public class IgnitorMessagePackHubProtocol : MessagePackHubProtocol, IHubProtocol
{
string IHubProtocol.Name => "blazorpack";
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Ignitor
{
internal class MarkupNode : Node
{
public MarkupNode(string markupContent)
{
MarkupContent = markupContent;
}
public string MarkupContent { get; }
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Ignitor
{
internal abstract class Node
{
public virtual ContainerNode Parent { get; set; }
}
}

View File

@ -0,0 +1,196 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
namespace Ignitor
{
internal static class NodeSerializer
{
public static string Serialize(ElementHive hive)
{
using (var writer = new StringWriter())
{
var serializer = new Serializer(writer);
serializer.SerializeHive(hive);
return writer.ToString();
}
}
private class Serializer
{
private readonly TextWriter _writer;
private int _depth;
private bool _atStartOfLine;
public Serializer(TextWriter writer)
{
_writer = writer ?? throw new ArgumentNullException(nameof(writer));
}
public void SerializeHive(ElementHive hive)
{
foreach (var kvp in hive.Components)
{
SerializeComponent(kvp.Key, kvp.Value);
}
}
private void Serialize(Node node)
{
switch (node)
{
case ElementNode elementNode:
{
SerializeElement(elementNode);
break;
}
case TextNode textNode:
{
SerializeTextNode(textNode);
break;
}
case MarkupNode markupNode:
{
SerializeMarkupNode(markupNode);
break;
}
case ContainerNode containerNode:
{
SerializeChildren(containerNode);
break;
}
default:
{
Write("--- UNKNOWN (");
Write(node.GetType().ToString());
WriteLine(") ---");
break;
}
}
}
private void SerializeMarkupNode(MarkupNode markupNode)
{
Write("M: ");
WriteLine(markupNode.MarkupContent.Replace(Environment.NewLine, "\\r\\n"));
}
private void SerializeTextNode(TextNode textNode)
{
Write("T: ");
WriteLine(textNode.TextContent);
}
private void SerializeElement(ElementNode elementNode)
{
Write("<");
Write(elementNode.TagName);
foreach (var attribute in elementNode.Attributes)
{
Write(" ");
Write(attribute.Key);
if (attribute.Value != null)
{
Write("=\"");
Write(attribute.Value.ToString());
Write("\"");
}
}
if (elementNode.Properties.Count > 0)
{
Write(" Properties: [");
foreach (var properties in elementNode.Properties)
{
Write(" ");
Write(properties.Key);
if (properties.Value != null)
{
Write("=\"");
Write(properties.Value.ToString());
Write("\"");
}
}
Write("]");
}
if (elementNode.Events.Count > 0)
{
Write(" Events: [");
foreach (var evt in elementNode.Events)
{
Write(" ");
Write(evt.Value.EventName);
Write("(");
Write(evt.Value.EventId.ToString());
Write(")");
}
Write("]");
}
WriteLine(">");
_depth++;
SerializeChildren(elementNode);
_depth--;
Write("</");
Write(elementNode.TagName);
WriteLine("/>");
}
private void SerializeChildren(ContainerNode containerNode)
{
for (var i = 0; i < containerNode.Children.Count; i++)
{
Serialize(containerNode.Children[i]);
}
}
private void SerializeComponent(int id, ComponentNode component)
{
Write("[Component ( ");
Write(id.ToString());
WriteLine(" )]");
_depth++;
SerializeChildren(component);
_depth--;
}
private void Write(string content)
{
if (_atStartOfLine)
{
WriteIndent();
}
_writer.Write(content);
_atStartOfLine = false;
}
private void WriteLine(string content)
{
if (_atStartOfLine)
{
WriteIndent();
}
_writer.WriteLine(content);
_atStartOfLine = true;
}
private void WriteIndent()
{
var indent = new string(' ', _depth * 4);
_writer.Write(indent);
}
}
}
}

View File

@ -0,0 +1,142 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
namespace Ignitor
{
internal class Program
{
public static async Task<int> Main(string[] args)
{
if (args.Length == 0)
{
Console.WriteLine("a uri is required");
return 1;
}
Console.WriteLine("Press the ANY key to begin.");
Console.ReadLine();
var uri = new Uri(args[0]);
var program = new Program();
Console.CancelKeyPress += (sender, e) => { program.Cancel(); };
await program.ExecuteAsync(uri);
return 0;
}
public Program()
{
CancellationTokenSource = new CancellationTokenSource();
TaskCompletionSource = new TaskCompletionSource<object>();
CancellationTokenSource.Token.Register(() =>
{
TaskCompletionSource.TrySetCanceled();
});
}
private CancellationTokenSource CancellationTokenSource { get; }
private CancellationToken CancellationToken => CancellationTokenSource.Token;
private TaskCompletionSource<object> TaskCompletionSource { get; }
public async Task ExecuteAsync(Uri uri)
{
var httpClient = new HttpClient();
var response = await httpClient.GetAsync(uri);
var content = await response.Content.ReadAsStringAsync();
// <!-- M.A.C.Component:{"circuitId":"CfDJ8KZCIaqnXmdF...PVd6VVzfnmc1","rendererId":"0","componentId":"0"} -->
var match = Regex.Match(content, $"{Regex.Escape("<!-- M.A.C.Component:")}(.+?){Regex.Escape(" -->")}");
var json = JsonDocument.Parse(match.Groups[1].Value);
var circuitId = json.RootElement.GetProperty("circuitId").GetString();
var builder = new HubConnectionBuilder();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IHubProtocol, IgnitorMessagePackHubProtocol>());
builder.WithUrl(new Uri(uri, "_blazor/"));
builder.ConfigureLogging(l => l.AddConsole().SetMinimumLevel(LogLevel.Trace));
var hive = new ElementHive();
await using var connection = builder.Build();
await connection.StartAsync(CancellationToken);
Console.WriteLine("Connected");
connection.On<int, string, string>("JS.BeginInvokeJS", OnBeginInvokeJS);
connection.On<int, int, byte[]>("JS.RenderBatch", OnRenderBatch);
connection.On<Error>("JS.OnError", OnError);
connection.Closed += OnClosedAsync;
// Now everything is registered so we can start the circuit.
var success = await connection.InvokeAsync<bool>("ConnectCircuit", circuitId);
await TaskCompletionSource.Task;
void OnBeginInvokeJS(int asyncHandle, string identifier, string argsJson)
{
Console.WriteLine("JS Invoke: " + identifier + " (" + argsJson + ")");
}
void OnRenderBatch(int browserRendererId, int batchId, byte[] batchData)
{
var batch = RenderBatchReader.Read(batchData);
hive.Update(batch);
// This will click the Counter component repeatedly resulting in infinite requests.
_ = ClickAsync("thecounter", hive, connection);
}
void OnError(Error error)
{
Console.WriteLine("ERROR: " + error.Stack);
}
Task OnClosedAsync(Exception ex)
{
if (ex == null)
{
TaskCompletionSource.TrySetResult(null);
}
else
{
TaskCompletionSource.TrySetException(ex);
}
return Task.CompletedTask;
}
}
private static async Task ClickAsync(string id, ElementHive hive, HubConnection connection)
{
if (!hive.TryFindElementById(id, out var elementNode))
{
Console.WriteLine("Could not find the counter to perform a click. Exiting.");
return;
}
await elementNode.ClickAsync(connection);
}
public void Cancel()
{
CancellationTokenSource.Cancel();
CancellationTokenSource.Dispose();
}
private class Error
{
public string Stack { get; set; }
}
}
}

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ignitor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -0,0 +1,307 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.Extensions.DependencyInjection;
namespace Ignitor
{
public static class RenderBatchReader
{
private static readonly Renderer Renderer = new FakeRenderer();
public static RenderBatch Read(ReadOnlySpan<byte> data)
{
var sections = Sections.Parse(data);
var strings = ReadStringTable(data, sections.GetStringTableIndexes(data));
var diffs = ReadUpdatedComponents(data, sections.GetUpdatedComponentIndexes(data), strings);
var frames = ReadReferenceFrames(sections.GetReferenceFrameData(data), strings);
var disposedComponentIds = ReadDisposedComponentIds(data);
var disposedEventHandlerIds = ReadDisposedEventHandlerIds(data);
return new RenderBatch(diffs, frames, disposedComponentIds, disposedEventHandlerIds);
}
private static string[] ReadStringTable(ReadOnlySpan<byte> data, ReadOnlySpan<byte> indexes)
{
var result = new string[indexes.Length / 4];
for (var i = 0; i < indexes.Length; i += 4)
{
var index = BitConverter.ToInt32(indexes.Slice(i, 4));
// The string table entries are all length-prefixed UTF8 blobs
var length = (int)ReadUnsignedLEB128(data, index, out var numLEB128Bytes);
var value = Encoding.UTF8.GetString(data.Slice(index + numLEB128Bytes, length));
result[i / 4] = value;
}
return result;
}
private static ArrayRange<RenderTreeDiff> ReadUpdatedComponents(ReadOnlySpan<byte> data, ReadOnlySpan<byte> indexes, string[] strings)
{
var result = new RenderTreeDiff[indexes.Length / 4];
for (var i = 0; i < indexes.Length; i += 4)
{
var index = BitConverter.ToInt32(indexes.Slice(i, 4));
var componentId = BitConverter.ToInt32(data.Slice(index, 4));
var editCount = BitConverter.ToInt32(data.Slice(index + 4, 4));
var editData = data.Slice(index + 8);
var edits = new RenderTreeEdit[editCount];
for (var j = 0; j < editCount; j++)
{
var type = (RenderTreeEditType)BitConverter.ToInt32(editData.Slice(0, 4));
var siblingIndex = BitConverter.ToInt32(editData.Slice(4, 4));
// ReferenceFrameIndex and MoveToSiblingIndex share a slot, so this reads
// whichever one applies to the edit type
var referenceFrameIndex = BitConverter.ToInt32(editData.Slice(8, 4));
var removedAttributeName = ReadString(editData.Slice(12, 4), strings);
editData = editData.Slice(16);
switch (type)
{
case RenderTreeEditType.UpdateText:
edits[j] = RenderTreeEdit.UpdateText(siblingIndex, referenceFrameIndex);
break;
case RenderTreeEditType.UpdateMarkup:
edits[j] = RenderTreeEdit.UpdateMarkup(siblingIndex, referenceFrameIndex);
break;
case RenderTreeEditType.SetAttribute:
edits[j] = RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex);
break;
case RenderTreeEditType.RemoveAttribute:
edits[j] = RenderTreeEdit.RemoveAttribute(siblingIndex, removedAttributeName);
break;
case RenderTreeEditType.PrependFrame:
edits[j] = RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex);
break;
case RenderTreeEditType.RemoveFrame:
edits[j] = RenderTreeEdit.RemoveFrame(siblingIndex);
break;
case RenderTreeEditType.StepIn:
edits[j] = RenderTreeEdit.StepIn(siblingIndex);
break;
case RenderTreeEditType.StepOut:
edits[j] = RenderTreeEdit.StepOut();
break;
case RenderTreeEditType.PermutationListEntry:
edits[j] = RenderTreeEdit.PermutationListEntry(siblingIndex, referenceFrameIndex);
break;
case RenderTreeEditType.PermutationListEnd:
edits[j] = RenderTreeEdit.PermutationListEnd();
break;
default:
throw new InvalidOperationException("Unknown edit type:" + type);
}
}
result[i / 4] = new RenderTreeDiff(componentId, new ArraySegment<RenderTreeEdit>(edits));
}
return new ArrayRange<RenderTreeDiff>(result, result.Length);
}
private static ArrayRange<RenderTreeFrame> ReadReferenceFrames(ReadOnlySpan<byte> data, string[] strings)
{
var result = new RenderTreeFrame[data.Length / 16];
for (var i = 0; i < data.Length; i += 16)
{
var frameData = data.Slice(i, 16);
var type = (RenderTreeFrameType)BitConverter.ToInt32(frameData.Slice(0, 4));
// 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 (type)
{
case RenderTreeFrameType.Attribute:
var attributeName = ReadString(frameData.Slice(4, 4), strings);
var attributeValue = ReadString(frameData.Slice(8, 4), strings);
var attributeEventHandlerId = BitConverter.ToInt32(frameData.Slice(12, 4));
result[i / 16] = RenderTreeFrame.Attribute(0, attributeName, attributeValue).WithAttributeEventHandlerId(attributeEventHandlerId);
break;
case RenderTreeFrameType.Component:
var componentSubtreeLength = BitConverter.ToInt32(frameData.Slice(4, 4));
var componentId = BitConverter.ToInt32(frameData.Slice(8, 4)); // Nowhere to put this without creating a ComponentState
result[i / 16] = RenderTreeFrame.ChildComponent(0, componentType: null)
.WithComponentSubtreeLength(componentSubtreeLength)
.WithComponent(new ComponentState(Renderer, componentId, new FakeComponent(), null));
break;
case RenderTreeFrameType.ComponentReferenceCapture:
// Client doesn't process these, skip.
result[i / 16] = RenderTreeFrame.ComponentReferenceCapture(0, null, 0);
break;
case RenderTreeFrameType.Element:
var elementSubtreeLength = BitConverter.ToInt32(frameData.Slice(4, 4));
var elementName = ReadString(frameData.Slice(8, 4), strings);
result[i / 16] = RenderTreeFrame.Element(0, elementName).WithElementSubtreeLength(elementSubtreeLength);
break;
case RenderTreeFrameType.ElementReferenceCapture:
var referenceCaptureId = ReadString(frameData.Slice(4, 4), strings);
result[i / 16] = RenderTreeFrame.ElementReferenceCapture(0, null)
.WithElementReferenceCaptureId(referenceCaptureId);
break;
case RenderTreeFrameType.Region:
var regionSubtreeLength = BitConverter.ToInt32(frameData.Slice(4, 4));
result[i / 16] = RenderTreeFrame.Region(0).WithRegionSubtreeLength(regionSubtreeLength);
break;
case RenderTreeFrameType.Text:
var text = ReadString(frameData.Slice(4, 4), strings);
result[i / 16] = RenderTreeFrame.Text(0, text);
break;
case RenderTreeFrameType.Markup:
var markup = ReadString(frameData.Slice(4, 4), strings);
result[i / 16] = RenderTreeFrame.Markup(0, markup);
break;
default:
throw new ArgumentException($"Unsupported frame type: {type}");
}
}
return new ArrayRange<RenderTreeFrame>(result, result.Length);
}
private static ArrayRange<int> ReadDisposedComponentIds(ReadOnlySpan<byte> data)
{
return new ArrayRange<int>(Array.Empty<int>(), 0);
}
private static ArrayRange<int> ReadDisposedEventHandlerIds(ReadOnlySpan<byte> data)
{
return new ArrayRange<int>(Array.Empty<int>(), 0);
}
private static string ReadString(ReadOnlySpan<byte> data, string[] strings)
{
var index = BitConverter.ToInt32(data.Slice(0, 4));
return index >= 0 ? strings[index] : null;
}
private static uint ReadUnsignedLEB128(ReadOnlySpan<byte> data, 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 = data[startOffset + count];
result += (uint)(currentByte & 0x7f) << shift;
shift += 7;
numBytesRead++;
}
return result;
}
private readonly struct Sections
{
public static Sections Parse(ReadOnlySpan<byte> data)
{
return new Sections(
BitConverter.ToInt32(data.Slice(data.Length - 20, 4)),
BitConverter.ToInt32(data.Slice(data.Length - 16, 4)),
BitConverter.ToInt32(data.Slice(data.Length - 12, 4)),
BitConverter.ToInt32(data.Slice(data.Length - 8, 4)),
BitConverter.ToInt32(data.Slice(data.Length - 4, 4)));
}
private readonly int _updatedComponents;
private readonly int _referenceFrames;
private readonly int _disposedComponentIds;
private readonly int _disposedEventHandlerIds;
private readonly int _strings;
public Sections(int updatedComponents, int referenceFrames, int disposedComponentIds, int disposedEventHandlerIds, int strings)
{
_updatedComponents = updatedComponents;
_referenceFrames = referenceFrames;
_disposedComponentIds = disposedComponentIds;
_disposedEventHandlerIds = disposedEventHandlerIds;
_strings = strings;
}
public ReadOnlySpan<byte> GetUpdatedComponentIndexes(ReadOnlySpan<byte> data)
{
// This is count-prefixed contiguous array of of integers.
var count = BitConverter.ToInt32(data.Slice(_updatedComponents, 4));
return data.Slice(_updatedComponents + 4, count * 4);
}
public ReadOnlySpan<byte> GetReferenceFrameData(ReadOnlySpan<byte> data)
{
// This is a count-prefixed contiguous array of RenderTreeFrame.
var count = BitConverter.ToInt32(data.Slice(_referenceFrames, 4));
return data.Slice(_referenceFrames + 4, count * 16);
}
public ReadOnlySpan<byte> GetStringTableIndexes(ReadOnlySpan<byte> data)
{
// This is a contiguous array of integers delimited by the end of the data section.
return data.Slice(_strings, data.Length - 20 - _strings);
}
}
public class FakeRenderer : Renderer
{
public FakeRenderer()
: base(new ServiceCollection().BuildServiceProvider(), new RendererSynchronizationContext())
{
}
protected override void HandleException(Exception exception)
{
throw new NotImplementedException();
}
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
=> throw new NotImplementedException();
}
public class FakeComponent : IComponent
{
public void Configure(RenderHandle renderHandle)
=> throw new NotImplementedException();
public Task SetParametersAsync(ParameterCollection parameters)
=> throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Ignitor
{
internal class TextNode : Node
{
public TextNode(string text)
{
TextContent = text;
}
public string TextContent { get; set; }
}
}