From 874050f1ddfa80f5fbba7ac87eaaa442de7e0f71 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Thu, 13 Jun 2019 10:05:40 -0700 Subject: [PATCH] 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. --- src/Components/Components.sln | 75 +++ .../Components/src/Properties/AssemblyInfo.cs | 2 + src/Components/ComponentsNoDeps.slnf | 4 +- .../Server/src/Properties/AssemblyInfo.cs | 2 + .../test/Ignitor.Test/Ignitor.Test.csproj | 11 + .../Ignitor.Test/RenderBatchReaderTest.cs | 347 +++++++++++++ .../ComponentsApp.App/Pages/Counter.razor | 2 +- .../ComponentsApp.Server.csproj | 2 +- .../test/testassets/Ignitor/CommentNode.cs | 9 + .../test/testassets/Ignitor/ComponentNode.cs | 15 + .../test/testassets/Ignitor/ContainerNode.cs | 84 ++++ .../test/testassets/Ignitor/ElementHive.cs | 473 ++++++++++++++++++ .../test/testassets/Ignitor/ElementNode.cs | 106 ++++ .../test/testassets/Ignitor/Ignitor.csproj | 16 + .../Ignitor/IgnitorMessagePackHubProtocol.cs | 12 + .../test/testassets/Ignitor/MarkupNode.cs | 15 + .../test/testassets/Ignitor/Node.cs | 10 + .../test/testassets/Ignitor/NodeSerializer.cs | 196 ++++++++ .../test/testassets/Ignitor/Program.cs | 142 ++++++ .../Ignitor/Properties/AssemblyInfo.cs | 3 + .../testassets/Ignitor/RenderBatchReader.cs | 307 ++++++++++++ .../test/testassets/Ignitor/TextNode.cs | 15 + 22 files changed, 1845 insertions(+), 3 deletions(-) create mode 100644 src/Components/test/Ignitor.Test/Ignitor.Test.csproj create mode 100644 src/Components/test/Ignitor.Test/RenderBatchReaderTest.cs create mode 100644 src/Components/test/testassets/Ignitor/CommentNode.cs create mode 100644 src/Components/test/testassets/Ignitor/ComponentNode.cs create mode 100644 src/Components/test/testassets/Ignitor/ContainerNode.cs create mode 100644 src/Components/test/testassets/Ignitor/ElementHive.cs create mode 100644 src/Components/test/testassets/Ignitor/ElementNode.cs create mode 100644 src/Components/test/testassets/Ignitor/Ignitor.csproj create mode 100644 src/Components/test/testassets/Ignitor/IgnitorMessagePackHubProtocol.cs create mode 100644 src/Components/test/testassets/Ignitor/MarkupNode.cs create mode 100644 src/Components/test/testassets/Ignitor/Node.cs create mode 100644 src/Components/test/testassets/Ignitor/NodeSerializer.cs create mode 100644 src/Components/test/testassets/Ignitor/Program.cs create mode 100644 src/Components/test/testassets/Ignitor/Properties/AssemblyInfo.cs create mode 100644 src/Components/test/testassets/Ignitor/RenderBatchReader.cs create mode 100644 src/Components/test/testassets/Ignitor/TextNode.cs diff --git a/src/Components/Components.sln b/src/Components/Components.sln index f0245a5de9..6c6003b0a6 100644 --- a/src/Components/Components.sln +++ b/src/Components/Components.sln @@ -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} diff --git a/src/Components/Components/src/Properties/AssemblyInfo.cs b/src/Components/Components/src/Properties/AssemblyInfo.cs index 68939a888a..47201f1bda 100644 --- a/src/Components/Components/src/Properties/AssemblyInfo.cs +++ b/src/Components/Components/src/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Components/ComponentsNoDeps.slnf b/src/Components/ComponentsNoDeps.slnf index ed8d3e9e48..6098dc29e8 100644 --- a/src/Components/ComponentsNoDeps.slnf +++ b/src/Components/ComponentsNoDeps.slnf @@ -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" ] } } \ No newline at end of file diff --git a/src/Components/Server/src/Properties/AssemblyInfo.cs b/src/Components/Server/src/Properties/AssemblyInfo.cs index e509761c2c..c9bfce34b1 100644 --- a/src/Components/Server/src/Properties/AssemblyInfo.cs +++ b/src/Components/Server/src/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Components/test/Ignitor.Test/Ignitor.Test.csproj b/src/Components/test/Ignitor.Test/Ignitor.Test.csproj new file mode 100644 index 0000000000..ad35b17b42 --- /dev/null +++ b/src/Components/test/Ignitor.Test/Ignitor.Test.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.0 + + + + + + + diff --git a/src/Components/test/Ignitor.Test/RenderBatchReaderTest.cs b/src/Components/test/Ignitor.Test/RenderBatchReaderTest.cs new file mode 100644 index 0000000000..710814705b --- /dev/null +++ b/src/Components/test/Ignitor.Test/RenderBatchReaderTest.cs @@ -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(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(new[] + { + new RenderTreeDiff(123, new ArraySegment( + 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(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 RoundTripSerialize(RenderBatch renderBatch) + { + var bytes = Serialize(renderBatch); + var roundTrippedRenderBatch = RenderBatchReader.Read(bytes); + var roundTrippedBytes = Serialize(roundTrippedRenderBatch); + + return roundTrippedBytes; + + Span Serialize(RenderBatch batch) + { + using (var ms = new MemoryStream()) + using (var writer = new RenderBatchWriter(ms, leaveOpen: false)) + { + writer.Write(batch); + return new Span(ms.ToArray(), 0, (int)ms.Length); + } + } + } + + static string[] ReadStringTable(Span 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(); + 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 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 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(); + } + } +} diff --git a/src/Components/test/testassets/ComponentsApp.App/Pages/Counter.razor b/src/Components/test/testassets/ComponentsApp.App/Pages/Counter.razor index ea87f6be2d..06a30e57f0 100644 --- a/src/Components/test/testassets/ComponentsApp.App/Pages/Counter.razor +++ b/src/Components/test/testassets/ComponentsApp.App/Pages/Counter.razor @@ -4,7 +4,7 @@

Current count: @currentCount

- + @code { int currentCount = 0; diff --git a/src/Components/test/testassets/ComponentsApp.Server/ComponentsApp.Server.csproj b/src/Components/test/testassets/ComponentsApp.Server/ComponentsApp.Server.csproj index d5ac161574..7f9d81bfb8 100644 --- a/src/Components/test/testassets/ComponentsApp.Server/ComponentsApp.Server.csproj +++ b/src/Components/test/testassets/ComponentsApp.Server/ComponentsApp.Server.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 diff --git a/src/Components/test/testassets/Ignitor/CommentNode.cs b/src/Components/test/testassets/Ignitor/CommentNode.cs new file mode 100644 index 0000000000..9f273e5d61 --- /dev/null +++ b/src/Components/test/testassets/Ignitor/CommentNode.cs @@ -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 + { + } +} diff --git a/src/Components/test/testassets/Ignitor/ComponentNode.cs b/src/Components/test/testassets/Ignitor/ComponentNode.cs new file mode 100644 index 0000000000..920733eeb7 --- /dev/null +++ b/src/Components/test/testassets/Ignitor/ComponentNode.cs @@ -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; + } + } +} diff --git a/src/Components/test/testassets/Ignitor/ContainerNode.cs b/src/Components/test/testassets/Ignitor/ContainerNode.cs new file mode 100644 index 0000000000..3de2810039 --- /dev/null +++ b/src/Components/test/testassets/Ignitor/ContainerNode.cs @@ -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 _children; + + protected ContainerNode() + { + _children = new List(); + } + + public IReadOnlyList 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); + } + } + } + } +} diff --git a/src/Components/test/testassets/Ignitor/ElementHive.cs b/src/Components/test/testassets/Ignitor/ElementHive.cs new file mode 100644 index 0000000000..5abfb9f156 --- /dev/null +++ b/src/Components/test/testassets/Ignitor/ElementHive.cs @@ -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 Components { get; } = new Dictionary(); + + 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 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 edits) + { + var currentDepth = 0; + var childIndexAtCurrentDepth = childIndex; + var permutations = new List(); + + 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 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 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 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") + { + //