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:
parent
38bc43bad7
commit
874050f1dd
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\testassets\Ignitor\Ignitor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Ignitor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue