using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Speckle.Connectors.Common.Threading;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Utils;
using Speckle.Newtonsoft.Json;
using Speckle.Sdk.Common;
using Speckle.Sdk.Models.Extensions;
namespace Speckle.Connectors.DUI.Bridge;
///
/// Wraps a binding class, and manages its calls from the Frontend to .NET, and sending events from .NET to the the Frontend.
/// Initially inspired by: https://github.com/johot/WebView2-better-bridge
///
#pragma warning disable CS0618 // Type or member is obsolete
[ClassInterface(ClassInterfaceType.AutoDual)]
#pragma warning restore CS0618 // Type or member is obsolete
[ComVisible(true)]
public sealed class BrowserBridge : IBrowserBridge
{
///
/// The name under which we expect the frontend to hoist this bindings class to the global scope.
/// e.g., `receiveBindings` should be available as `window.receiveBindings`.
///
private readonly ConcurrentDictionary _resultsStore = new();
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
private readonly IThreadContext _threadContext;
private readonly IBrowserScriptExecutor _browserScriptExecutor;
private readonly IJsonSerializer _jsonSerializer;
private IReadOnlyDictionary _bindingMethodCache = new Dictionary();
private IBinding? _binding;
private Type? _bindingType;
private readonly ILogger _logger;
public string FrontendBoundName { get; private set; } = "Unknown";
public IBinding? Binding
{
get => _binding;
private set
{
if (_binding != null || this != value?.Parent)
{
throw new ArgumentException($"Binding: {FrontendBoundName} is already bound or does not match bridge");
}
_binding = value;
}
}
public BrowserBridge(
IThreadContext threadContext,
IJsonSerializer jsonSerializer,
ILogger logger,
IBrowserScriptExecutor browserScriptExecutor,
ITopLevelExceptionHandler topLevelExceptionHandler
)
{
_threadContext = threadContext;
_jsonSerializer = jsonSerializer;
_logger = logger;
_browserScriptExecutor = browserScriptExecutor;
_topLevelExceptionHandler = topLevelExceptionHandler;
}
private async Task OnExceptionEvent(Exception ex) =>
await Send(
BasicConnectorBindingCommands.SET_GLOBAL_NOTIFICATION,
new
{
type = ToastNotificationType.DANGER,
title = "Unhandled Exception Occurred",
description = ex.ToFormattedString(),
autoClose = false
}
)
.ConfigureAwait(false);
public void AssociateWithBinding(IBinding binding)
{
// set via binding property to ensure explosion if already bound
Binding = binding;
FrontendBoundName = binding.Name;
_bindingType = binding.GetType();
// Note: we need to filter out getter and setter methods here because they are not really nicely
// supported across browsers, hence the !method.IsSpecialName.
var bindingMethodCache = new Dictionary();
foreach (var m in _bindingType.GetMethods().Where(method => !method.IsSpecialName))
{
bindingMethodCache[m.Name] = m;
}
_bindingMethodCache = bindingMethodCache;
_logger.LogInformation("Bridge bound to front end name {FrontEndName}", binding.Name);
}
///
/// Used by the Frontend bridge logic to understand which methods are available.
///
///
public string[] GetBindingsMethodNames()
{
var bindingNames = _bindingMethodCache.Keys.ToArray();
Debug.WriteLine($"{FrontendBoundName}: " + JsonConvert.SerializeObject(bindingNames, Formatting.Indented));
return bindingNames;
}
//don't wait for browser runs on purpose
public void RunMethod(string methodName, string requestId, string methodArgs) =>
_threadContext
.RunOnWorkerAsync(async () =>
{
var task = await _topLevelExceptionHandler
.CatchUnhandledAsync(async () =>
{
var result = await ExecuteMethod(methodName, methodArgs).ConfigureAwait(false);
string resultJson = _jsonSerializer.Serialize(result);
NotifyUIMethodCallResultReady(requestId, resultJson);
})
.ConfigureAwait(false);
if (task.Exception is not null)
{
string resultJson = SerializeFormattedException(task.Exception);
NotifyUIMethodCallResultReady(requestId, resultJson);
}
})
.FireAndForget();
///
/// Used by the action block to invoke the actual method called by the UI.
///
///
///
/// The was not initialized with an (see )
/// The was not found or the given were not valid for the method call
/// The invoked method throws an exception
/// The Json
private async Task