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
{
internal class ComponentNode : ContainerNode
public class ComponentNode : ContainerNode
{
private readonly int _componentId;

View File

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

View File

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

View File

@ -11,7 +11,7 @@ using Microsoft.AspNetCore.SignalR.Client;
namespace Ignitor
{
internal class ElementNode : ContainerNode
public class ElementNode : ContainerNode
{
private readonly Dictionary<string, object> _attributes;
private readonly Dictionary<string, object> _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,

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
{
internal class MarkupNode : Node
public class MarkupNode : Node
{
public MarkupNode(string markupContent)
{

View File

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

View File

@ -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<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;
}
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();
// <!-- 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();
string circuitId = await GetPrerenderedCircuitId(uri);
var builder = new HubConnectionBuilder();
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)
{
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; }
}
}
}

View File

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