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.
This commit is contained in:
Javier Calvarro Nelson 2019-06-19 10:45:49 +02:00 committed by GitHub
parent b56108fc94
commit 75c95c56bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 205 additions and 25 deletions

View File

@ -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<object>();
CancellationTokenSource.Token.Register(() =>
{
TaskCompletionSource.TrySetCanceled();
});
}
private CancellationTokenSource CancellationTokenSource { get; }
private CancellationToken CancellationToken => CancellationTokenSource.Token;
private TaskCompletionSource<object> TaskCompletionSource { get; }
public bool ConfirmRenderBatch { get; set; } = true;
public event Action<int, string, string> JSInterop;
public event Action<int, int, byte[]> 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<bool> ConnectAsync(Uri uri, bool prerendered)
{
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();
HubConnection = builder.Build();
await HubConnection.StartAsync(CancellationToken);
Console.WriteLine("Connected");
HubConnection.On<int, string, string>("JS.BeginInvokeJS", OnBeginInvokeJS);
HubConnection.On<int, int, byte[]>("JS.RenderBatch", OnRenderBatch);
HubConnection.On<Error>("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<bool>("ConnectCircuit", CircuitId);
}
else
{
CircuitId = await HubConnection.InvokeAsync<string>("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<string> GetPrerenderedCircuitIdAsync(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();
return circuitId;
}
public void Cancel()
{
CancellationTokenSource.Cancel();
CancellationTokenSource.Dispose();
}
}
}

View File

@ -3,7 +3,7 @@
namespace Ignitor namespace Ignitor
{ {
internal class ComponentNode : ContainerNode public class ComponentNode : ContainerNode
{ {
private readonly int _componentId; private readonly int _componentId;

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace Ignitor namespace Ignitor
{ {
internal abstract class ContainerNode : Node public abstract class ContainerNode : Node
{ {
private readonly List<Node> _children; private readonly List<Node> _children;

View File

@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Components.RenderTree;
namespace Ignitor namespace Ignitor
{ {
internal class ElementHive public class ElementHive
{ {
private const string SelectValuePropname = "_blazorSelectValue"; private const string SelectValuePropname = "_blazorSelectValue";

View File

@ -11,7 +11,7 @@ using Microsoft.AspNetCore.SignalR.Client;
namespace Ignitor namespace Ignitor
{ {
internal class ElementNode : ContainerNode public class ElementNode : ContainerNode
{ {
private readonly Dictionary<string, object> _attributes; private readonly Dictionary<string, object> _attributes;
private readonly Dictionary<string, object> _properties; private readonly Dictionary<string, object> _properties;
@ -79,9 +79,9 @@ namespace Ignitor
{ {
if (!Events.TryGetValue("click", out var clickEventDescriptor)) if (!Events.TryGetValue("click", out var clickEventDescriptor))
{ {
Console.WriteLine("Button does not have a click event. Exiting"); throw new InvalidOperationException("Element does not have a click event.");
return;
} }
var mouseEventArgs = new UIMouseEventArgs() var mouseEventArgs = new UIMouseEventArgs()
{ {
Type = clickEventDescriptor.EventName, Type = clickEventDescriptor.EventName,

View File

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

View File

@ -3,7 +3,7 @@
namespace Ignitor namespace Ignitor
{ {
internal class MarkupNode : Node public class MarkupNode : Node
{ {
public MarkupNode(string markupContent) public MarkupNode(string markupContent)
{ {

View File

@ -3,7 +3,7 @@
namespace Ignitor namespace Ignitor
{ {
internal abstract class Node public abstract class Node
{ {
public virtual ContainerNode Parent { get; set; } public virtual ContainerNode Parent { get; set; }
} }

View File

@ -30,13 +30,34 @@ namespace Ignitor
var uri = new Uri(args[0]); var uri = new Uri(args[0]);
var program = new Program(); var client = new BlazorClient();
Console.CancelKeyPress += (sender, e) => { program.Cancel(); }; client.JSInterop += OnJSInterop;
Console.CancelKeyPress += (sender, e) => client.Cancel();
var done = new TaskCompletionSource<bool>(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; return 0;
} }
private static void OnJSInterop(int callId, string identifier, string argsJson) =>
Console.WriteLine("JS Invoke: " + identifier + " (" + argsJson + ")");
public Program() public Program()
{ {
CancellationTokenSource = new CancellationTokenSource(); CancellationTokenSource = new CancellationTokenSource();
@ -54,14 +75,7 @@ namespace Ignitor
public async Task ExecuteAsync(Uri uri) public async Task ExecuteAsync(Uri uri)
{ {
var httpClient = new HttpClient(); string circuitId = await GetPrerenderedCircuitId(uri);
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(); var builder = new HubConnectionBuilder();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IHubProtocol, IgnitorMessagePackHubProtocol>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IHubProtocol, IgnitorMessagePackHubProtocol>());
@ -117,6 +131,19 @@ namespace Ignitor
} }
} }
private static async Task<string> GetPrerenderedCircuitId(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();
return circuitId;
}
private static async Task ClickAsync(string id, ElementHive hive, HubConnection connection) private static async Task ClickAsync(string id, ElementHive hive, HubConnection connection)
{ {
if (!hive.TryFindElementById(id, out var elementNode)) if (!hive.TryFindElementById(id, out var elementNode))
@ -133,10 +160,5 @@ namespace Ignitor
CancellationTokenSource.Cancel(); CancellationTokenSource.Cancel();
CancellationTokenSource.Dispose(); CancellationTokenSource.Dispose();
} }
private class Error
{
public string Stack { get; set; }
}
} }
} }

View File

@ -3,7 +3,7 @@
namespace Ignitor namespace Ignitor
{ {
internal class TextNode : Node public class TextNode : Node
{ {
public TextNode(string text) public TextNode(string text)
{ {