From 75c95c56bfc05585d72899a3525374b72c5cd8e5 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 19 Jun 2019 10:45:49 +0200 Subject: [PATCH] Complete and make ignitor more extensible (#11289) * Add support for sending dotnet calls from "JS" * Add support for acknowledging render batches. * Extract logic into a separate BlazorClient class so that it can be reused. * Support non prerendered pages. * Allow reacting to incoming JSInterop calls. * Allow reacting to incoming RenderBatch. --- .../test/testassets/Ignitor/BlazorClient.cs | 148 ++++++++++++++++++ .../test/testassets/Ignitor/ComponentNode.cs | 2 +- .../test/testassets/Ignitor/ContainerNode.cs | 2 +- .../test/testassets/Ignitor/ElementHive.cs | 2 +- .../test/testassets/Ignitor/ElementNode.cs | 6 +- .../test/testassets/Ignitor/Error.cs | 10 ++ .../test/testassets/Ignitor/MarkupNode.cs | 2 +- .../test/testassets/Ignitor/Node.cs | 2 +- .../test/testassets/Ignitor/Program.cs | 54 +++++-- .../test/testassets/Ignitor/TextNode.cs | 2 +- 10 files changed, 205 insertions(+), 25 deletions(-) create mode 100644 src/Components/test/testassets/Ignitor/BlazorClient.cs create mode 100644 src/Components/test/testassets/Ignitor/Error.cs diff --git a/src/Components/test/testassets/Ignitor/BlazorClient.cs b/src/Components/test/testassets/Ignitor/BlazorClient.cs new file mode 100644 index 0000000000..30a8e01061 --- /dev/null +++ b/src/Components/test/testassets/Ignitor/BlazorClient.cs @@ -0,0 +1,148 @@ +// 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.Components.Rendering; +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 +{ + public class BlazorClient + { + public BlazorClient() + { + CancellationTokenSource = new CancellationTokenSource(); + TaskCompletionSource = new TaskCompletionSource(); + + CancellationTokenSource.Token.Register(() => + { + TaskCompletionSource.TrySetCanceled(); + }); + } + + private CancellationTokenSource CancellationTokenSource { get; } + private CancellationToken CancellationToken => CancellationTokenSource.Token; + private TaskCompletionSource TaskCompletionSource { get; } + + public bool ConfirmRenderBatch { get; set; } = true; + + public event Action JSInterop; + public event Action RenderBatchReceived; + + public string CircuitId { get; set; } + + public HubConnection HubConnection { get; set; } + + public Task ClickAsync(string elementId) + { + if (!Hive.TryFindElementById(elementId, out var elementNode)) + { + throw new InvalidOperationException($"Could not find element with id {elementId}."); + } + + return elementNode.ClickAsync(HubConnection); + } + + public ElementHive Hive { get; set; } + + public async Task ConnectAsync(Uri uri, bool prerendered) + { + var builder = new HubConnectionBuilder(); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.WithUrl(new Uri(uri, "_blazor/")); + builder.ConfigureLogging(l => l.AddConsole().SetMinimumLevel(LogLevel.Trace)); + var hive = new ElementHive(); + + HubConnection = builder.Build(); + await HubConnection.StartAsync(CancellationToken); + Console.WriteLine("Connected"); + + HubConnection.On("JS.BeginInvokeJS", OnBeginInvokeJS); + HubConnection.On("JS.RenderBatch", OnRenderBatch); + HubConnection.On("JS.OnError", OnError); + HubConnection.Closed += OnClosedAsync; + + // Now everything is registered so we can start the circuit. + if (prerendered) + { + CircuitId = await GetPrerenderedCircuitIdAsync(uri); + return await HubConnection.InvokeAsync("ConnectCircuit", CircuitId); + } + else + { + CircuitId = await HubConnection.InvokeAsync("StartCircuit", uri, uri); + return CircuitId != null; + } + + void OnBeginInvokeJS(int asyncHandle, string identifier, string argsJson) + { + JSInterop?.Invoke(asyncHandle, identifier, argsJson); + } + + void OnRenderBatch(int browserRendererId, int batchId, byte[] batchData) + { + var batch = RenderBatchReader.Read(batchData); + hive.Update(batch); + + if (ConfirmRenderBatch) + { + HubConnection.InvokeAsync("OnRenderCompleted", batchId, /* error */ null); + } + + RenderBatchReceived?.Invoke(browserRendererId, batchId, batchData); + } + + 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; + } + } + + void InvokeDotNetMethod(object callId, string assemblyName, string methodIdentifier, object dotNetObjectId, string argsJson) + { + HubConnection.InvokeAsync("BeginInvokeDotNetFromJS", callId?.ToString(), assemblyName, methodIdentifier, dotNetObjectId ?? 0, argsJson); + } + + private static async Task GetPrerenderedCircuitIdAsync(Uri uri) + { + var httpClient = new HttpClient(); + var response = await httpClient.GetAsync(uri); + var content = await response.Content.ReadAsStringAsync(); + + // + var match = Regex.Match(content, $"{Regex.Escape("")}"); + var json = JsonDocument.Parse(match.Groups[1].Value); + var circuitId = json.RootElement.GetProperty("circuitId").GetString(); + return circuitId; + } + + public void Cancel() + { + CancellationTokenSource.Cancel(); + CancellationTokenSource.Dispose(); + } + } +} diff --git a/src/Components/test/testassets/Ignitor/ComponentNode.cs b/src/Components/test/testassets/Ignitor/ComponentNode.cs index 920733eeb7..a6829bb307 100644 --- a/src/Components/test/testassets/Ignitor/ComponentNode.cs +++ b/src/Components/test/testassets/Ignitor/ComponentNode.cs @@ -3,7 +3,7 @@ namespace Ignitor { - internal class ComponentNode : ContainerNode + public class ComponentNode : ContainerNode { private readonly int _componentId; diff --git a/src/Components/test/testassets/Ignitor/ContainerNode.cs b/src/Components/test/testassets/Ignitor/ContainerNode.cs index 3de2810039..0a8f8a2580 100644 --- a/src/Components/test/testassets/Ignitor/ContainerNode.cs +++ b/src/Components/test/testassets/Ignitor/ContainerNode.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace Ignitor { - internal abstract class ContainerNode : Node + public abstract class ContainerNode : Node { private readonly List _children; diff --git a/src/Components/test/testassets/Ignitor/ElementHive.cs b/src/Components/test/testassets/Ignitor/ElementHive.cs index 5abfb9f156..6bb59da032 100644 --- a/src/Components/test/testassets/Ignitor/ElementHive.cs +++ b/src/Components/test/testassets/Ignitor/ElementHive.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Components.RenderTree; namespace Ignitor { - internal class ElementHive + public class ElementHive { private const string SelectValuePropname = "_blazorSelectValue"; diff --git a/src/Components/test/testassets/Ignitor/ElementNode.cs b/src/Components/test/testassets/Ignitor/ElementNode.cs index c9175162f7..724b3438e2 100644 --- a/src/Components/test/testassets/Ignitor/ElementNode.cs +++ b/src/Components/test/testassets/Ignitor/ElementNode.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.SignalR.Client; namespace Ignitor { - internal class ElementNode : ContainerNode + public class ElementNode : ContainerNode { private readonly Dictionary _attributes; private readonly Dictionary _properties; @@ -79,9 +79,9 @@ namespace Ignitor { if (!Events.TryGetValue("click", out var clickEventDescriptor)) { - Console.WriteLine("Button does not have a click event. Exiting"); - return; + throw new InvalidOperationException("Element does not have a click event."); } + var mouseEventArgs = new UIMouseEventArgs() { Type = clickEventDescriptor.EventName, diff --git a/src/Components/test/testassets/Ignitor/Error.cs b/src/Components/test/testassets/Ignitor/Error.cs new file mode 100644 index 0000000000..b2b8e07e67 --- /dev/null +++ b/src/Components/test/testassets/Ignitor/Error.cs @@ -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 class Error + { + public string Stack { get; set; } + } +} diff --git a/src/Components/test/testassets/Ignitor/MarkupNode.cs b/src/Components/test/testassets/Ignitor/MarkupNode.cs index 0bd611d0db..f9de5d8564 100644 --- a/src/Components/test/testassets/Ignitor/MarkupNode.cs +++ b/src/Components/test/testassets/Ignitor/MarkupNode.cs @@ -3,7 +3,7 @@ namespace Ignitor { - internal class MarkupNode : Node + public class MarkupNode : Node { public MarkupNode(string markupContent) { diff --git a/src/Components/test/testassets/Ignitor/Node.cs b/src/Components/test/testassets/Ignitor/Node.cs index ab3c72092e..07fef1bd4a 100644 --- a/src/Components/test/testassets/Ignitor/Node.cs +++ b/src/Components/test/testassets/Ignitor/Node.cs @@ -3,7 +3,7 @@ namespace Ignitor { - internal abstract class Node + public abstract class Node { public virtual ContainerNode Parent { get; set; } } diff --git a/src/Components/test/testassets/Ignitor/Program.cs b/src/Components/test/testassets/Ignitor/Program.cs index 2de73d4607..ec3a3ede04 100644 --- a/src/Components/test/testassets/Ignitor/Program.cs +++ b/src/Components/test/testassets/Ignitor/Program.cs @@ -30,13 +30,34 @@ namespace Ignitor var uri = new Uri(args[0]); - var program = new Program(); - Console.CancelKeyPress += (sender, e) => { program.Cancel(); }; + var client = new BlazorClient(); + client.JSInterop += OnJSInterop; + Console.CancelKeyPress += (sender, e) => client.Cancel(); + + var done = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // Click the counter button 1000 times + client.RenderBatchReceived += (int browserRendererId, int batchId, byte[] data) => + { + if (batchId < 1000) + { + client.ClickAsync("thecounter"); + } + else + { + done.TrySetResult(true); + } + }; + + await client.ConnectAsync(uri, prerendered: true); + await done.Task; - await program.ExecuteAsync(uri); return 0; } + private static void OnJSInterop(int callId, string identifier, string argsJson) => + Console.WriteLine("JS Invoke: " + identifier + " (" + argsJson + ")"); + public Program() { CancellationTokenSource = new CancellationTokenSource(); @@ -54,14 +75,7 @@ namespace Ignitor public async Task ExecuteAsync(Uri uri) { - var httpClient = new HttpClient(); - var response = await httpClient.GetAsync(uri); - var content = await response.Content.ReadAsStringAsync(); - - // - var match = Regex.Match(content, $"{Regex.Escape("")}"); - var json = JsonDocument.Parse(match.Groups[1].Value); - var circuitId = json.RootElement.GetProperty("circuitId").GetString(); + string circuitId = await GetPrerenderedCircuitId(uri); var builder = new HubConnectionBuilder(); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); @@ -117,6 +131,19 @@ namespace Ignitor } } + private static async Task GetPrerenderedCircuitId(Uri uri) + { + var httpClient = new HttpClient(); + var response = await httpClient.GetAsync(uri); + var content = await response.Content.ReadAsStringAsync(); + + // + var match = Regex.Match(content, $"{Regex.Escape("")}"); + var json = JsonDocument.Parse(match.Groups[1].Value); + var circuitId = json.RootElement.GetProperty("circuitId").GetString(); + return circuitId; + } + private static async Task ClickAsync(string id, ElementHive hive, HubConnection connection) { if (!hive.TryFindElementById(id, out var elementNode)) @@ -133,10 +160,5 @@ namespace Ignitor CancellationTokenSource.Cancel(); CancellationTokenSource.Dispose(); } - - private class Error - { - public string Stack { get; set; } - } } } diff --git a/src/Components/test/testassets/Ignitor/TextNode.cs b/src/Components/test/testassets/Ignitor/TextNode.cs index 7079cc6001..750de36f1b 100644 --- a/src/Components/test/testassets/Ignitor/TextNode.cs +++ b/src/Components/test/testassets/Ignitor/TextNode.cs @@ -3,7 +3,7 @@ namespace Ignitor { - internal class TextNode : Node + public class TextNode : Node { public TextNode(string text) {