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:
parent
b56108fc94
commit
75c95c56bf
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
namespace Ignitor
|
||||
{
|
||||
internal class ComponentNode : ContainerNode
|
||||
public class ComponentNode : ContainerNode
|
||||
{
|
||||
private readonly int _componentId;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Components.RenderTree;
|
|||
|
||||
namespace Ignitor
|
||||
{
|
||||
internal class ElementHive
|
||||
public class ElementHive
|
||||
{
|
||||
private const string SelectValuePropname = "_blazorSelectValue";
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
namespace Ignitor
|
||||
{
|
||||
internal class MarkupNode : Node
|
||||
public class MarkupNode : Node
|
||||
{
|
||||
public MarkupNode(string markupContent)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
namespace Ignitor
|
||||
{
|
||||
internal abstract class Node
|
||||
public abstract class Node
|
||||
{
|
||||
public virtual ContainerNode Parent { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
namespace Ignitor
|
||||
{
|
||||
internal class TextNode : Node
|
||||
public class TextNode : Node
|
||||
{
|
||||
public TextNode(string text)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue