diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4bf49bf..cd227008 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
+### Changed
+
+- **Interop:** the components reach their client code through the `_content/IgniteUI.Blazor/interop.js` module (JS isolation) instead of functions on `window`. The internal `window.igSendMessage`, `igCheckReady`, `igWaitForLoaded`, `igRequestLoad`, `igSetResourceString`, `igConvertReturnValue` and `igUnmarshalledDataSource*` functions, the unused `igSendMessages` and the `raisingEvent` flag no longer exist. They were never a supported API. On .NET 8 WebAssembly, unmarshalled data now travels on the same path as on .NET 9+. [#255](https://github.com/IgniteUI/igniteui-blazor/issues/255)
## 1.0.0 - 2026-10-12
ð¢ The first stable release: from here on, a breaking change to the public API comes with a major version.
diff --git a/src/componentsBase/IIgniteUIBlazorRuntime.cs b/src/componentsBase/IIgniteUIBlazorRuntime.cs
index f6d57cab..d7ba38f4 100644
--- a/src/componentsBase/IIgniteUIBlazorRuntime.cs
+++ b/src/componentsBase/IIgniteUIBlazorRuntime.cs
@@ -16,6 +16,9 @@ internal interface IIgniteUIBlazorRuntime
bool IsLoadRequested(string moduleName);
void MarkIsLoadRequested(string moduleName);
bool IsRuntimeValid(bool reevaluate = false);
+
+ /// The client interop module at , shared by the components of the runtime.
+ InteropModule GetInteropModule(string path);
}
internal static class IgniteUIBlazorRuntimeExtensions
diff --git a/src/componentsBase/IgbComponentBase.cs b/src/componentsBase/IgbComponentBase.cs
index a41c3f90..43022058 100644
--- a/src/componentsBase/IgbComponentBase.cs
+++ b/src/componentsBase/IgbComponentBase.cs
@@ -58,13 +58,18 @@ protected IIgniteUIBlazor IgBlazor
// }
_runtime.WebCallback.Register(this);
- _dataSourceManager = new DataSourceManager(this, new RuntimeHelper(JsRuntime, _runtime));
+ _dataSourceManager = new DataSourceManager(this, new RuntimeHelper(JsRuntime, _runtime, () => Interop));
EnsureModulesLoaded();
}
}
internal IIgniteUIBlazorRuntime Runtime => _runtime ?? throw new InvalidOperationException("IgBlazor accessed before dependency injection completed.");
+ private InteropModule? _interop;
+
+ /// The client interop module this component talks to.
+ internal InteropModule Interop => _interop ??= Runtime.GetInteropModule(InteropModule.LitePath);
+
/// Requests the client modules this component needs; override to register more through .
protected virtual void EnsureModulesLoaded()
{
@@ -728,11 +733,11 @@ public async Task EnsureReady()
//Console.WriteLine("ensuring ready: " + this.GetType().Name);
while (!this._ready)
{
- bool ready = await jsRuntime.InvokeAsync("igCheckReady", new object[] { _containerId });
+ bool ready = await Interop.InvokeAsync("checkReady", new object[] { _containerId });
//Console.WriteLine(ready + " -> " + this.GetType().Name);
if (ready)
{
- await jsRuntime.InvokeVoidAsync("igWaitForLoaded");
+ await Interop.InvokeVoidAsync("waitForLoaded");
OnReady();
break;
}
@@ -1629,14 +1634,13 @@ private void ProcessMessageSync(RendererMessage m)
if (m.NativeElements != null)
{
- return await JsRuntime.InvokeAsync("igSendMessage",
+ return await Interop.InvokeAsync("sendMessage",
new object[] { this._containerId, json,
GetObjectRef(), m.NativeElements });
}
else
{
- //json = "window.sendMessage(`" + this._id + "`, `" + json + "`)";
- return await JsRuntime.InvokeAsync("igSendMessage",
+ return await Interop.InvokeAsync("sendMessage",
new object[] { this._containerId, json,
GetObjectRef() });
}
@@ -1669,18 +1673,10 @@ private void SendJson(string json, ElementReference[]? nativeElements)
return;
}
- if (nativeElements != null)
- {
- JsRuntime.InvokeAsync("igSendMessage",
- new object[] { this._containerId, json,
- GetObjectRef(), nativeElements });
- }
- else
- {
- JsRuntime.InvokeAsync("igSendMessage",
- new object[] { this._containerId, json,
- GetObjectRef() });
- }
+ var args = nativeElements != null
+ ? new object[] { this._containerId, json, GetObjectRef(), nativeElements }
+ : new object[] { this._containerId, json, GetObjectRef() };
+ Interop.Post(module => _ = module.InvokeAsync("sendMessage", args));
}
internal void AttachChild(BaseJsonSerializable child)
@@ -1745,16 +1741,18 @@ private object SendJsonSync(string json, ElementReference[]? nativeElements)
{
throw new InvalidOperationException("JsInProcessRuntime is not available.");
}
+ var module = Interop.LoadedInProcess
+ ?? throw new InvalidOperationException("The Ignite UI client interop module is not loaded yet; await EnsureReady() or use the async method.");
if (nativeElements != null)
{
- return JsInProcessRuntime.Invoke("igSendMessage",
+ return module.Invoke("sendMessage",
new object[] {
this._containerId, json,
GetObjectRef(), nativeElements });
}
else
{
- return JsInProcessRuntime.Invoke("igSendMessage",
+ return module.Invoke("sendMessage",
new object[] {
this._containerId, json,
GetObjectRef() });
@@ -3502,7 +3500,7 @@ private async Task TrySendCleanupAsync()
{
return null;
}
- return await JsRuntime.InvokeAsync("igSetResourceString", new object[] { "set", grouping, id, value });
+ return await Interop.InvokeAsync("setResourceString", new object[] { "set", grouping, id, value });
}
internal async Task SetResourceStringAsync(string grouping, string json)
@@ -3511,7 +3509,7 @@ private async Task TrySendCleanupAsync()
{
return null;
}
- return await JsRuntime.InvokeAsync("igSetResourceString", new object[] { "register", grouping, "", json });
+ return await Interop.InvokeAsync("setResourceString", new object[] { "register", grouping, "", json });
}
private void SetPropertyValue(object item, System.Reflection.PropertyInfo property, JsonElement jsonElement)
@@ -3767,7 +3765,7 @@ public interface IIgniteUIBlazor
IJSRuntime JsRuntime { get; }
}
- internal class IgniteUIBlazor : IIgniteUIBlazor, IIgniteUIBlazorRuntime
+ internal class IgniteUIBlazor : IIgniteUIBlazor, IIgniteUIBlazorRuntime, IAsyncDisposable, IDisposable
{
private bool _isRuntimeValid = false;
private bool _isRuntimeChecked = false;
@@ -3823,7 +3821,33 @@ public void RequestLoad(string moduleName)
return;
}
_loadedCache.AddOrUpdate(moduleName, true, (name, oldValue) => true);
- JsRuntime.InvokeAsync("igRequestLoad", moduleName);
+ GetInteropModule(InteropModule.LitePath).Post(module => _ = module.InvokeVoidAsync("requestLoad", moduleName));
+ }
+
+ private readonly ConcurrentDictionary _interopModules = new ConcurrentDictionary();
+
+ public InteropModule GetInteropModule(string path)
+ {
+ return _interopModules.GetOrAdd(path, p => new InteropModule(JsRuntime, p));
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ foreach (var module in _interopModules.Values)
+ {
+ await module.DisposeAsync().ConfigureAwait(false);
+ }
+ _interopModules.Clear();
+ }
+
+ // For containers disposed synchronously: releasing a module reference is a JS call, so it is started and not awaited.
+ public void Dispose()
+ {
+ foreach (var module in _interopModules.Values)
+ {
+ _ = module.DisposeAsync();
+ }
+ _interopModules.Clear();
}
public bool IsLoadRequested(string moduleName)
{
@@ -3981,7 +4005,6 @@ internal class ModuleLoader
public static void Load(IIgniteUIBlazor runtime, string moduleName)
{
runtime.AsRuntime().RequestLoad(moduleName);
- //runtime.JsRuntime.InvokeAsync("igRequestLoad", moduleName);
}
internal static void MarkIsLoadRequested(IIgniteUIBlazor runtime, string moduleName)
diff --git a/src/componentsBase/InteropModule.cs b/src/componentsBase/InteropModule.cs
new file mode 100644
index 00000000..7d251e2c
--- /dev/null
+++ b/src/componentsBase/InteropModule.cs
@@ -0,0 +1,163 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.JSInterop;
+
+namespace IgniteUI.Blazor.Controls
+{
+ ///
+ /// The client interop module of one Ignite UI package, imported through JS isolation (import() )
+ /// instead of calling functions on window . One instance per module path and runtime (circuit or app).
+ ///
+ ///
+ /// Calls made while the module is still being imported are queued and dispatched in call order once it
+ /// arrives, so fire-and-forget messages keep their relative order, as they did with the window functions.
+ ///
+ internal sealed class InteropModule : IAsyncDisposable
+ {
+ /// The interop module of this package.
+ internal const string LitePath = "./_content/IgniteUI.Blazor/interop.js";
+
+ private readonly IJSRuntime _jsRuntime;
+ private readonly object _lock = new object();
+ private readonly List> _pending = new List>();
+ private Task? _loading;
+ private volatile IJSObjectReference? _module;
+ private bool _disposed;
+
+ internal InteropModule(IJSRuntime jsRuntime, string path)
+ {
+ _jsRuntime = jsRuntime;
+ Path = path;
+ }
+
+ internal string Path { get; }
+
+ /// The module, once imported; while it is still loading.
+ internal IJSObjectReference? Loaded => _module;
+
+ /// The module for synchronous calls, where .NET runs in the browser and the module is imported.
+ internal IJSInProcessObjectReference? LoadedInProcess => _module as IJSInProcessObjectReference;
+
+ /// Imports the module, once.
+ internal Task GetAsync()
+ {
+ lock (_lock)
+ {
+ return _loading ??= LoadAsync();
+ }
+ }
+
+ /// Runs with the module: now if it is imported, otherwise after the calls queued before it.
+ internal void Post(Action call)
+ {
+ IJSObjectReference? module;
+ lock (_lock)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+ module = _module;
+ if (module == null)
+ {
+ _pending.Add(call);
+ _loading ??= LoadAsync();
+ return;
+ }
+ }
+ call(module);
+ }
+
+ internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, params object?[]? args)
+ {
+ var module = _module ?? await GetAsync().ConfigureAwait(false);
+ return await module.InvokeAsync(identifier, args).ConfigureAwait(false);
+ }
+
+ internal async ValueTask InvokeVoidAsync(string identifier, params object?[]? args)
+ {
+ var module = _module ?? await GetAsync().ConfigureAwait(false);
+ await module.InvokeVoidAsync(identifier, args).ConfigureAwait(false);
+ }
+
+ private async Task LoadAsync()
+ {
+ IJSObjectReference module;
+ try
+ {
+ module = await _jsRuntime.InvokeAsync("import", Path).ConfigureAwait(false);
+ }
+ catch
+ {
+ lock (_lock)
+ {
+ // Allow a later call to retry (e.g. the circuit was not connected yet); queued calls are dropped,
+ // like calls made on a runtime that could not reach the client.
+ _loading = null;
+ _pending.Clear();
+ }
+ throw;
+ }
+
+ // Drain in order; publish the module only when nothing is left queued, so a call arriving meanwhile
+ // cannot overtake the queued ones.
+ while (true)
+ {
+ Action[] batch;
+ lock (_lock)
+ {
+ if (_pending.Count == 0)
+ {
+ _module = module;
+ break;
+ }
+ batch = _pending.ToArray();
+ _pending.Clear();
+ }
+ foreach (var call in batch)
+ {
+ try
+ {
+ call(module);
+ }
+ catch (Exception)
+ {
+ // Fire-and-forget calls: one failing must not keep the rest from being dispatched.
+ }
+ }
+ }
+ return module;
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ IJSObjectReference? module;
+ lock (_lock)
+ {
+ _disposed = true;
+ _pending.Clear();
+ module = _module;
+ _module = null;
+ }
+ if (module == null)
+ {
+ return;
+ }
+ try
+ {
+ await module.DisposeAsync().ConfigureAwait(false);
+ }
+ catch (JSDisconnectedException)
+ {
+ }
+ catch (JSException)
+ {
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ }
+ }
+}
diff --git a/src/componentsBase/RuntimeHelper.cs b/src/componentsBase/RuntimeHelper.cs
index 74834fec..7ddcb507 100644
--- a/src/componentsBase/RuntimeHelper.cs
+++ b/src/componentsBase/RuntimeHelper.cs
@@ -1,137 +1,68 @@
-#if NET8_0
-using System.Diagnostics.CodeAnalysis;
-using System.Linq.Expressions;
-#endif
using System.Runtime.CompilerServices;
using Microsoft.JSInterop;
namespace IgniteUI.Blazor.Controls
{
- // The InvokeUnmarshalled fast path exists only on net8 (API removed in net9+), so the probe and its
- // trim/AOT surface compile only there; net9+ always uses the raw-pointer InvokeVoid path.
+ // Sends unmarshalled data (WebAssembly) to the component's client interop module: a synchronous in-process call
+ // that passes the address of the local holding the columns array, which the client reads off the heap for the
+ // duration of the call. The same path on every target framework (the net8-only InvokeUnmarshalled fast path
+ // needed a window global).
internal class RuntimeHelper
{
-#if NET8_0
- private Func? _callSendUnmarshalledColumnMessage;
- private Func? _callSendUnmarshalledColumnDataIntentMessage;
-#endif
- private IJSInProcessRuntime? _inprocRuntime;
private readonly IIgniteUIBlazorRuntime? _igBlazor;
+ private readonly Func? _interop;
-#if NET8_0
- [DynamicDependency(
- DynamicallyAccessedMemberTypes.PublicMethods,
- "Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime",
- "Microsoft.AspNetCore.Components.WebAssembly")]
- [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Probes the net8-only InvokeUnmarshalled methods, preserved via the DynamicDependency above; absence falls back to the raw-pointer InvokeVoid path.")]
- [UnconditionalSuppressMessage("Trimming", "IL2060", Justification = "The generic arguments are statically referenced framework/library types, and the InvokeUnmarshalled generic parameters carry no DynamicallyAccessedMembers requirements.")]
- [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The DynamicDependency above marks the runtime's RequiresUnreferencedCode members (Invoke, GetValue, SetValue, ...); the probe filters by name and never invokes them.")]
- [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "net8 Blazor WASM-only probe (IJSInProcessRuntime + InvokeUnmarshalled); no NativeAOT target exists for net8 wasm, and under Mono AOT the interpreter executes this.")]
-#endif
- public RuntimeHelper(IJSRuntime? runtime, IIgniteUIBlazorRuntime igBlazor)
+ public RuntimeHelper(IJSRuntime? runtime, IIgniteUIBlazorRuntime igBlazor, Func? interop)
{
_igBlazor = igBlazor;
- var inprocRuntime = runtime as IJSInProcessRuntime;
- _inprocRuntime = inprocRuntime;
- if (inprocRuntime != null)
- {
- IsInproc = true;
- }
-#if NET8_0
- if (IsInproc && inprocRuntime != null)
- {
- var unmarshalled = inprocRuntime.GetType().GetMethods().Where(m => m.Name == "InvokeUnmarshalled").ToList();
-
- if (unmarshalled.Count > 0)
- {
- var target = unmarshalled.Where(m => m.GetGenericArguments() != null &&
- m.GetGenericArguments().Length == 4).FirstOrDefault();
- if (target != null)
- {
- var meth = target.MakeGenericMethod(new Type[] {
- typeof(string),
- typeof(int),
- typeof(UnmarshalledColumn[]),
- typeof(string)
- });
-
- var jsRuntimeParam = Expression.Parameter(typeof(IJSInProcessRuntime), "jsRuntime");
- var methodNameParam = Expression.Parameter(typeof(string), "methodName");
- var refNameParam = Expression.Parameter(typeof(string), "refName");
- var indexParam = Expression.Parameter(typeof(int), "index");
- var columnsParam = Expression.Parameter(typeof(UnmarshalledColumn[]), "columns");
-
- var wsRuntime = Expression.Convert(jsRuntimeParam, inprocRuntime.GetType());
- var call = Expression.Call(wsRuntime, meth, methodNameParam, refNameParam,
- indexParam, columnsParam);
-
- _callSendUnmarshalledColumnMessage =
- Expression.Lambda>(
- call, jsRuntimeParam, methodNameParam, refNameParam, indexParam, columnsParam).Compile();
- }
-
- target = unmarshalled.Where(m => m.GetGenericArguments() != null &&
- m.GetGenericArguments().Length == 3).FirstOrDefault();
- if (target != null)
- {
- var meth = target.MakeGenericMethod(new Type[] {
- typeof(string),
- typeof(string),
- typeof(string)
- });
-
- var jsRuntimeParam = Expression.Parameter(typeof(IJSInProcessRuntime), "jsRuntime");
- var methodNameParam = Expression.Parameter(typeof(string), "methodName");
- var refNameParam = Expression.Parameter(typeof(string), "refName");
- var dataIntentParam = Expression.Parameter(typeof(string), "index");
-
- var wsRuntime = Expression.Convert(jsRuntimeParam, inprocRuntime.GetType());
- var call = Expression.Call(wsRuntime, meth, methodNameParam, refNameParam,
- dataIntentParam);
+ _interop = interop;
+ IsInproc = runtime is IJSInProcessRuntime;
+ }
- _callSendUnmarshalledColumnDataIntentMessage =
- Expression.Lambda>(
- call, jsRuntimeParam, methodNameParam, refNameParam, dataIntentParam).Compile();
- }
- }
- }
-#endif
+ public void SendUnmarshalledColumnMessage(string methodName, string refName, int index, UnmarshalledColumn[]? columns)
+ {
+ _interop?.Invoke().Post(module => SendUnmarshalledColumnMessageNow(module, methodName, refName, index, columns));
}
- public unsafe string? SendUnmarshalledColumnMessage(string methodName, string refName, int index, UnmarshalledColumn[]? columns)
+ public void SendUnmarshalledColumnDataIntentsMessage(string methodName, string refName, string dataIntents)
{
-#if NET8_0
- if (_callSendUnmarshalledColumnMessage != null && _inprocRuntime != null)
- {
- return _callSendUnmarshalledColumnMessage(_inprocRuntime, methodName, refName, index, columns);
- }
-#endif
- var intptr = Unsafe.AsPointer(ref columns);
- if (_inprocRuntime != null)
+ _interop?.Invoke().Post(module =>
{
- _inprocRuntime.InvokeVoid(methodName, new object[] { refName, index, (int)intptr });
- }
-
- return null;
+ if (module is IUnmarshalledColumnSink sink)
+ {
+ sink.SendDataIntents(methodName, refName, dataIntents);
+ }
+ else
+ {
+ (module as IJSInProcessObjectReference)?.InvokeVoid(methodName, refName, dataIntents);
+ }
+ });
}
- public string? SendUnmarshalledColumnDataIntentsMessage(string methodName, string refName, string dataIntents)
+ private static unsafe void SendUnmarshalledColumnMessageNow(IJSObjectReference module, string methodName, string refName, int index, UnmarshalledColumn[]? columns)
{
-#if NET8_0
- if (_callSendUnmarshalledColumnDataIntentMessage != null && _inprocRuntime != null)
+ if (module is IUnmarshalledColumnSink sink)
{
- return _callSendUnmarshalledColumnDataIntentMessage(_inprocRuntime, methodName, refName, dataIntents);
+ sink.SendColumns(methodName, refName, index, columns);
}
-#endif
- if (_inprocRuntime != null)
+ else if (module is IJSInProcessObjectReference inproc)
{
- _inprocRuntime.InvokeVoid(methodName, new object[] { refName, dataIntents });
+ var intptr = Unsafe.AsPointer(ref columns);
+ inproc.InvokeVoid(methodName, refName, index, (int)intptr);
}
-
- return null;
}
public bool IsInproc { get; private set; }
public bool IsForcedJsonDataMarshalling { get { return _igBlazor?.Settings?.ForceJsonDataMarshalling ?? false; } }
}
+
+ ///
+ /// A module that takes the column arrays themselves instead of their address, which only a WebAssembly heap
+ /// can resolve. Implemented by test doubles of the interop module.
+ ///
+ internal interface IUnmarshalledColumnSink
+ {
+ void SendColumns(string methodName, string refName, int index, UnmarshalledColumn[]? columns);
+ void SendDataIntents(string methodName, string refName, string dataIntents);
+ }
}
diff --git a/src/componentsBase/UnmarshalledDataSource.cs b/src/componentsBase/UnmarshalledDataSource.cs
index f4c70312..d46929db 100644
--- a/src/componentsBase/UnmarshalledDataSource.cs
+++ b/src/componentsBase/UnmarshalledDataSource.cs
@@ -1794,7 +1794,7 @@ public void SendClear(string containerId, string refName)
{
return;
}
- _helper.SendUnmarshalledColumnMessage("igUnmarshalledDataSourceClear", containerId + ":" + refName, -1, GetColumns(refName));
+ _helper.SendUnmarshalledColumnMessage("unmarshalledDataSourceClear", containerId + ":" + refName, -1, GetColumns(refName));
}
public void SendRemove(string containerId, string refName, int index)
@@ -1803,7 +1803,7 @@ public void SendRemove(string containerId, string refName, int index)
{
return;
}
- _helper.SendUnmarshalledColumnMessage("igUnmarshalledDataSourceRemove", containerId + ":" + refName, index, GetColumns(refName));
+ _helper.SendUnmarshalledColumnMessage("unmarshalledDataSourceRemove", containerId + ":" + refName, index, GetColumns(refName));
}
public void SendInsert(string containerId, string refName, int index)
@@ -1812,7 +1812,7 @@ public void SendInsert(string containerId, string refName, int index)
{
return;
}
- _helper.SendUnmarshalledColumnMessage("igUnmarshalledDataSourceInsert", containerId + ":" + refName, index, GetColumns(refName));
+ _helper.SendUnmarshalledColumnMessage("unmarshalledDataSourceInsert", containerId + ":" + refName, index, GetColumns(refName));
}
public void SendUpdate(string containerId, string refName, int index, bool syncDataOnly)
@@ -1821,7 +1821,7 @@ public void SendUpdate(string containerId, string refName, int index, bool syncD
{
return;
}
- _helper.SendUnmarshalledColumnMessage("igUnmarshalledDataSourceUpdate", containerId + ":" + refName + ":" + (syncDataOnly ? "true" : "false"), index, GetColumns(refName));
+ _helper.SendUnmarshalledColumnMessage("unmarshalledDataSourceUpdate", containerId + ":" + refName + ":" + (syncDataOnly ? "true" : "false"), index, GetColumns(refName));
}
public void SendCreate(string containerId, string refName, string? dataIntents)
@@ -1833,9 +1833,9 @@ public void SendCreate(string containerId, string refName, string? dataIntents)
if (dataIntents != null)
{
//Console.WriteLine("sending create data intents");
- _helper.SendUnmarshalledColumnDataIntentsMessage("igUnmarshalledDataSourceCreateDataIntents", containerId + ":" + refName, dataIntents);
+ _helper.SendUnmarshalledColumnDataIntentsMessage("unmarshalledDataSourceCreateDataIntents", containerId + ":" + refName, dataIntents);
}
- _helper.SendUnmarshalledColumnMessage("igUnmarshalledDataSourceCreate", containerId + ":" + refName, -1, GetColumns(refName));
+ _helper.SendUnmarshalledColumnMessage("unmarshalledDataSourceCreate", containerId + ":" + refName, -1, GetColumns(refName));
}
private void OnRemoveId(Guid oldId)
diff --git a/src/src/index.ts b/src/src/index.ts
index c9a19d9e..e8e63560 100644
--- a/src/src/index.ts
+++ b/src/src/index.ts
@@ -35,7 +35,7 @@ _scriptRegistryEvents.addEventListener('registered', (e) => {
if (refs.size === 0) containersPendingScriptRefs.delete(containerId);
}
for (const [containerId, json] of ready) {
- (window as any).igSendMessage(containerId, json, null /* webCallback not used */, [] /* nativeElements not used */);
+ sendMessage(containerId, json, null /* webCallback not used */, [] /* nativeElements not used */);
}
});
function getContainer(id: string): HTMLElement {
@@ -113,7 +113,7 @@ async function callDotNet(webCallback, methodName: string, ...args: any[]): Prom
}
}
-(window as any).igWaitForLoaded = async function waitForLoaded() {
+export const waitForLoaded = async function waitForLoaded() {
if (Loader.instance.isLoading) {
await Loader.instance.loadingPromise;
} else {
@@ -121,16 +121,11 @@ async function callDotNet(webCallback, methodName: string, ...args: any[]): Prom
}
};
-(window as any).igRequestLoad = function requestLoad(module: string) {
+export const requestLoad = function requestLoad(module: string) {
Loader.instance.request(module, cr);
};
-(window as any).igSetResourceString = function setResourceString(
- type: string,
- grouping: string,
- id: string,
- value: string,
-) {
+export const setResourceString = function setResourceString(type: string, grouping: string, id: string, value: string) {
switch (type) {
case 'set':
Loader.instance.setResourceString(grouping, id, value);
@@ -283,8 +278,6 @@ let toReturn = function (retVal: any): any {
return JSON.parse(Loader.stringify(retVal, getMainTarget()));
};
-(window as any).igConvertReturnValue = convertReturnValue;
-
function toSimpleArgs(args: CustomEvent) {
let orig = args;
@@ -490,31 +483,25 @@ function raiseEventImpl(propertyName: string, sender: any, args: any, containerN
Loader.clearMarshalIdByValueOnceByEvent(propertyName);
- try {
- (window as any).raisingEvent = true;
-
- callDotNet(webCallback, 'OnRaiseEvent', name, propertyName, JSON.stringify({ sender: sender, args: outerArgs }));
- if ((window as any).webViewCallback) {
- (window as any).webViewCallback.onRaiseEvent(
- name,
- propertyName,
- JSON.stringify({ sender: sender, args: outerArgs }),
- );
- }
- if (
- (window as any).webkit &&
- (window as any).webkit.messageHandlers &&
- (window as any).webkit.messageHandlers.raiseEvent
- ) {
- (window as any).webkit.messageHandlers.raiseEvent.postMessage({
- name: name,
- propertyName: propertyName,
- sender: sender,
- args: outerArgs,
- });
- }
- } finally {
- (window as any).raisingEvent = false;
+ callDotNet(webCallback, 'OnRaiseEvent', name, propertyName, JSON.stringify({ sender: sender, args: outerArgs }));
+ if ((window as any).webViewCallback) {
+ (window as any).webViewCallback.onRaiseEvent(
+ name,
+ propertyName,
+ JSON.stringify({ sender: sender, args: outerArgs }),
+ );
+ }
+ if (
+ (window as any).webkit &&
+ (window as any).webkit.messageHandlers &&
+ (window as any).webkit.messageHandlers.raiseEvent
+ ) {
+ (window as any).webkit.messageHandlers.raiseEvent.postMessage({
+ name: name,
+ propertyName: propertyName,
+ sender: sender,
+ args: outerArgs,
+ });
}
}
@@ -533,19 +520,12 @@ var raiseEvent = function (propertyName: string, sender: any, args: any, contain
}
};
-(window as any).igCheckReady = function (containerId: string) {
+export const checkReady = function checkReady(containerId: string) {
// var cont = document.getElementById(containerId);
var cont = getContainerByIgIdAttribute(containerId);
return cont !== null && cont !== undefined;
};
-(window as any).igSendMessages = function (json: string) {
- let m = JSON.parse(json);
- for (let i = 0; i < m.length; i++) {
- (window as any).sendMessage(JSON.stringify(m[i]));
- }
-};
-
var propMaps = new WeakMap>();
function getPropMap(target: any) {
if (!target) {
@@ -695,7 +675,12 @@ function updateAngularElement(element: any) {
}
}
-(window as any).igSendMessage = function (containerId: string, json: string, webCallback: any, nativeElements: any[]) {
+export const sendMessage = function sendMessage(
+ containerId: string,
+ json: string,
+ webCallback: any,
+ nativeElements: any[],
+) {
updateContainer(containerId);
let m = JSON.parse(json);
@@ -1473,7 +1458,6 @@ function updateAngularElement(element: any) {
};
declare var Blazor: any;
-declare var BINDING: any;
declare var getValue: any;
declare var Module: any;
@@ -2217,44 +2201,59 @@ function getValueNinePlus(ptr, type) {
}
}
-let isDotnetNinePlus = false;
-
function getArrayDataPtr(value: any): any {
return value + 12;
}
-(window as any).igUnmarshalledDataSourceCreate = function (refName: string, index: number, columns: any) {
- if ((window as any).getValue) {
- getValueActual = (window as any).getValue;
- }
- if ((globalThis as any).getValue) {
- getValueActual = (globalThis as any).getValue;
- }
- if ((self as any).getValue) {
- getValueActual = (self as any).getValue;
- }
- if (!getValueActual && (window as any).Module !== undefined) {
- getValueActual = Module.getValue;
- }
+// Picks the heap readers of the running .NET WebAssembly runtime (getValue on .NET 8, the Blazor.runtime
+// heap views on .NET 9+).
+function ensureHeapAccess() {
if (!getValueActual) {
- getValueActual = getValueNinePlus;
- isDotnetNinePlus = true;
+ if ((window as any).getValue) {
+ getValueActual = (window as any).getValue;
+ }
+ if ((globalThis as any).getValue) {
+ getValueActual = (globalThis as any).getValue;
+ }
+ if ((self as any).getValue) {
+ getValueActual = (self as any).getValue;
+ }
+ if (!getValueActual && (window as any).Module !== undefined) {
+ getValueActual = Module.getValue;
+ }
+ if (!getValueActual) {
+ getValueActual = getValueNinePlus;
+ }
}
- if (Blazor.platform.getArrayLength) {
- getArrayLengthActual = Blazor.platform.getArrayLength;
- }
if (!getArrayLengthActual) {
- getArrayLengthActual = (arr: any) => {
- return getValueActual(getArrayDataPtr(arr), 'i32');
- };
+ if (Blazor.platform.getArrayLength) {
+ getArrayLengthActual = Blazor.platform.getArrayLength;
+ } else {
+ getArrayLengthActual = (arr: any) => {
+ return getValueActual(getArrayDataPtr(arr), 'i32');
+ };
+ }
}
+}
- if (isDotnetNinePlus) columns = Blazor.runtime.getHeapU32(columns);
-
- if (!isDotnetNinePlus) {
- refName = BINDING.conv_string(refName);
+// .NET passes the address of the local that holds the columns array (a synchronous in-process call, so the
+// address is valid for its duration); read the array's address out of it.
+function derefColumns(ptr: any): any {
+ ensureHeapAccess();
+ if (Blazor.runtime && Blazor.runtime.getHeapU32) {
+ return Blazor.runtime.getHeapU32(ptr);
}
+ return getValueActual(ptr, 'i32');
+}
+
+export const unmarshalledDataSourceCreate = function unmarshalledDataSourceCreate(
+ refName: string,
+ index: number,
+ columns: any,
+) {
+ columns = derefColumns(columns);
+
var ind = refName.indexOf(':');
var containerId = refName.substr(0, ind);
updateContainer(containerId);
@@ -2309,11 +2308,10 @@ function getArrayDataPtr(value: any): any {
refValues.set(refName, data);
};
-(window as any).igUnmarshalledDataSourceCreateDataIntents = function (refName: string, intents: string) {
- if (!isDotnetNinePlus) {
- refName = BINDING.conv_string(refName);
- intents = BINDING.conv_string(intents);
- }
+export const unmarshalledDataSourceCreateDataIntents = function unmarshalledDataSourceCreateDataIntents(
+ refName: string,
+ intents: string,
+) {
var ind = refName.indexOf(':');
var containerId = refName.substr(0, ind);
updateContainer(containerId);
@@ -2327,16 +2325,17 @@ function getArrayDataPtr(value: any): any {
refDataIntents.set(refName, dataIntents);
};
-(window as any).igUnmarshalledDataSourceInsert = function (refName: string, index: number, columns: any) {
- if (!isDotnetNinePlus) {
- refName = BINDING.conv_string(refName);
- }
+export const unmarshalledDataSourceInsert = function unmarshalledDataSourceInsert(
+ refName: string,
+ index: number,
+ columns: any,
+) {
var ind = refName.indexOf(':');
var containerId = refName.substr(0, ind);
updateContainer(containerId);
var refName = refName.substr(ind + 1);
- if (isDotnetNinePlus) columns = Blazor.runtime.getHeapU32(columns);
+ columns = derefColumns(columns);
var colItems = getUnmarshalledColumnItems(columns, index);
var item = createOrUpdateUnmarshalledItem(refName, null, colItems);
@@ -2363,10 +2362,11 @@ function getArrayDataPtr(value: any): any {
}
}
};
-(window as any).igUnmarshalledDataSourceUpdate = function (refName: string, index: number, columns: any) {
- if (!isDotnetNinePlus) {
- refName = BINDING.conv_string(refName);
- }
+export const unmarshalledDataSourceUpdate = function unmarshalledDataSourceUpdate(
+ refName: string,
+ index: number,
+ columns: any,
+) {
var ind = refName.indexOf(':');
var containerId = refName.substr(0, ind);
updateContainer(containerId);
@@ -2376,7 +2376,7 @@ function getArrayDataPtr(value: any): any {
var syncDataOnly = refName.substr(ind + 1) == 'true';
refName = n;
- if (isDotnetNinePlus) columns = Blazor.runtime.getHeapU32(columns);
+ columns = derefColumns(columns);
var colItems = getUnmarshalledColumnItems(columns, index);
@@ -2403,12 +2403,12 @@ function getArrayDataPtr(value: any): any {
}
}
};
-(window as any).igUnmarshalledDataSourceRemove = function (refName: string, index: number, columns: any) {
- if (!isDotnetNinePlus) {
- refName = BINDING.conv_string(refName);
- }
-
- if (isDotnetNinePlus) columns = Blazor.runtime.getHeapU32(columns);
+export const unmarshalledDataSourceRemove = function unmarshalledDataSourceRemove(
+ refName: string,
+ index: number,
+ columns: any,
+) {
+ columns = derefColumns(columns);
var ind = refName.indexOf(':');
var containerId = refName.substr(0, ind);
@@ -2437,16 +2437,17 @@ function getArrayDataPtr(value: any): any {
}
}
};
-(window as any).igUnmarshalledDataSourceClear = function (refName: string, index: number, columns: any) {
- if (!isDotnetNinePlus) {
- refName = BINDING.conv_string(refName);
- }
+export const unmarshalledDataSourceClear = function unmarshalledDataSourceClear(
+ refName: string,
+ index: number,
+ columns: any,
+) {
var ind = refName.indexOf(':');
var containerId = refName.substr(0, ind);
updateContainer(containerId);
var refName = refName.substr(ind + 1);
- if (isDotnetNinePlus) columns = Blazor.runtime.getHeapU32(columns);
+ columns = derefColumns(columns);
var cols = getUnmarshalledColumns(columns);
diff --git a/src/src/interop.ts b/src/src/interop.ts
new file mode 100644
index 00000000..fe9868a1
--- /dev/null
+++ b/src/src/interop.ts
@@ -0,0 +1,23 @@
+/**
+ * The client side of the .NET interop, served at `_content/IgniteUI.Blazor/interop.js`.
+ *
+ * The components import this module through `IJSRuntime` (JS isolation) instead of calling functions on `window`,
+ * so the library defines no globals and a second Ignite UI package can bring its own interop module alongside.
+ * The functions live in the interop bundle (index.ts); this entry only gives them a fixed, importable file name, so
+ * importing it reuses the instance the JS initializer already loaded.
+ *
+ * Internal to the Ignite UI packages; not an application API.
+ */
+export {
+ checkReady,
+ requestLoad,
+ sendMessage,
+ setResourceString,
+ unmarshalledDataSourceClear,
+ unmarshalledDataSourceCreate,
+ unmarshalledDataSourceCreateDataIntents,
+ unmarshalledDataSourceInsert,
+ unmarshalledDataSourceRemove,
+ unmarshalledDataSourceUpdate,
+ waitForLoaded,
+} from './index';
diff --git a/tests/IgniteUI.Blazor.Tests/BaseRendererControlDisposalTests.cs b/tests/IgniteUI.Blazor.Tests/BaseRendererControlDisposalTests.cs
index 67885a41..dd163738 100644
--- a/tests/IgniteUI.Blazor.Tests/BaseRendererControlDisposalTests.cs
+++ b/tests/IgniteUI.Blazor.Tests/BaseRendererControlDisposalTests.cs
@@ -2,6 +2,7 @@
using System.Text.Json;
using Bunit;
using IgniteUI.Blazor.Controls;
+using IgniteUI.Blazor.Tests.Interop;
using Microsoft.JSInterop;
namespace IgniteUI.Blazor.Tests;
@@ -31,6 +32,9 @@ public sealed class DisposalCollection
[Collection(DisposalCollection.Name)]
public class IgbComponentBaseDisposalTests : BlazorComponentTestBase
{
+ /// The client interop module the components send their messages to.
+ private BunitJSInterop Module => ((RendererMessageInteropHarness)Interop).Module;
+
[Fact]
public async Task DisposeAsync_WhenInteropThrowsJSException_DoesNotThrow()
{
@@ -39,8 +43,8 @@ public async Task DisposeAsync_WhenInteropThrowsJSException_DoesNotThrow()
// Make the cleanup interop call fail the way a mis-behaving JS side would.
// The most recently registered matching handler wins in bUnit, so this
- // overrides the default "return undefined" answer for igSendMessage.
- JSInterop.Setup("igSendMessage", _ => true)
+ // overrides the default "return undefined" answer for sendMessage.
+ Module.Setup("sendMessage", _ => true)
.SetException(new JSException("simulated JS failure during cleanup"));
var ex = await Record.ExceptionAsync(async () =>
@@ -55,7 +59,7 @@ public async Task DisposeAsync_WhenCircuitDisconnected_DoesNotThrow()
var cut = Render();
var instance = cut.Instance;
- JSInterop.Setup("igSendMessage", _ => true)
+ Module.Setup("sendMessage", _ => true)
.SetException(new JSDisconnectedException("circuit gone"));
var ex = await Record.ExceptionAsync(async () =>
@@ -70,7 +74,7 @@ public async Task DisposeAsync_WhenInteropCanceled_DoesNotThrow()
var cut = Render();
var instance = cut.Instance;
- JSInterop.Setup("igSendMessage", _ => true)
+ Module.Setup("sendMessage", _ => true)
.SetException(new TaskCanceledException("host shutting down"));
var ex = await Record.ExceptionAsync(async () =>
@@ -85,7 +89,7 @@ public async Task DisposeAsync_WhenObjectAlreadyDisposed_DoesNotThrow()
var cut = Render();
var instance = cut.Instance;
- JSInterop.Setup("igSendMessage", _ => true)
+ Module.Setup("sendMessage", _ => true)
.SetException(new ObjectDisposedException("JSRuntime"));
var ex = await Record.ExceptionAsync(async () =>
@@ -102,7 +106,7 @@ public async Task DisposeAsync_WhenUnexpectedExceptionFromInterop_IsSwallowed()
var cut = Render();
var instance = cut.Instance;
- JSInterop.Setup("igSendMessage", _ => true)
+ Module.Setup("sendMessage", _ => true)
.SetException(new InvalidOperationException("unexpected interop failure"));
var ex = await Record.ExceptionAsync(async () =>
@@ -116,7 +120,7 @@ public async Task DisposeAsync_SendsCleanupMessage()
{
Interop.PrimeReady();
var cut = Render();
- var invocationCount = JSInterop.Invocations.Count;
+ var invocationCount = Module.Invocations.Count;
await cut.Instance.DisposeAsync();
@@ -129,7 +133,7 @@ public async Task DisposeAsync_CalledTwice_IsIdempotent()
Interop.PrimeReady();
var cut = Render();
var instance = (IAsyncDisposable)cut.Instance;
- var invocationCount = JSInterop.Invocations.Count;
+ var invocationCount = Module.Invocations.Count;
await instance.DisposeAsync();
@@ -141,8 +145,8 @@ public async Task DisposeAsync_CalledTwice_IsIdempotent()
private void AssertCleanupSentOnce(int invocationCount)
{
var cleanup = Assert.Single(
- JSInterop.Invocations.Skip(invocationCount),
- invocation => invocation.Identifier == "igSendMessage");
+ Module.Invocations.Skip(invocationCount),
+ invocation => invocation.Identifier == "sendMessage");
using var message = JsonDocument.Parse((string)cleanup.Arguments[1]!);
Assert.Equal("cleanup", message.RootElement.GetProperty("type").GetString());
}
@@ -152,8 +156,8 @@ private sealed record Row(string Name);
private IReadOnlyList MessageTypesFor(string containerId)
{
var types = new List();
- foreach (var invocation in JSInterop.Invocations.Where(v =>
- v.Identifier == "igSendMessage" && v.Arguments.Count > 1 && v.Arguments[0] as string == containerId))
+ foreach (var invocation in Module.Invocations.Where(v =>
+ v.Identifier == "sendMessage" && v.Arguments.Count > 1 && v.Arguments[0] as string == containerId))
{
using var message = JsonDocument.Parse((string)invocation.Arguments[1]!);
types.Add(message.RootElement.GetProperty("type").GetString());
@@ -218,8 +222,8 @@ public async Task DisposeAsync_SendsNothingAfterCleanup()
stop.Cancel();
await producer;
- var sent = JSInterop.Invocations
- .Where(v => v.Identifier == "igSendMessage" && v.Arguments.Count > 1 && v.Arguments[0] as string == id)
+ var sent = Module.Invocations
+ .Where(v => v.Identifier == "sendMessage" && v.Arguments.Count > 1 && v.Arguments[0] as string == id)
.Select(v => (string)v.Arguments[1]!)
.ToList();
var cleanup = sent.FindIndex(json => json.Contains("\"type\": \"cleanup\""));
diff --git a/tests/IgniteUI.Blazor.Tests/Interop/RendererMessageInteropHarness.cs b/tests/IgniteUI.Blazor.Tests/Interop/RendererMessageInteropHarness.cs
index b5907c67..60f1d4a6 100644
--- a/tests/IgniteUI.Blazor.Tests/Interop/RendererMessageInteropHarness.cs
+++ b/tests/IgniteUI.Blazor.Tests/Interop/RendererMessageInteropHarness.cs
@@ -9,16 +9,20 @@ namespace IgniteUI.Blazor.Tests.Interop;
///
/// adapter for the current interop implementation:
-/// global JS functions (igSendMessage , igCheckReady , igWaitForLoaded )
+/// exports of the client interop module (sendMessage , checkReady , waitForLoaded )
/// carrying RendererMessage JSON envelopes, with JSâ.NET traffic entering
/// through the JSInvokable surface.
/// All knowledge of that wire format is intentionally concentrated here.
///
public sealed class RendererMessageInteropHarness : InteropHarness
{
- private const string SendMessage = "igSendMessage";
+ private const string SendMessage = "sendMessage";
- private readonly BunitJSInterop _js;
+ // The component traffic goes to the interop module (JS isolation), recorded on its own interop.
+ private readonly BunitJSInterop _module;
+
+ /// The recorded interop of the client module, for tests that assert on the wire itself.
+ internal BunitJSInterop Module => _module;
private readonly IgniteUIBlazor _service;
// Read by invocation matchers on background flush threads while tests add
// stubs on the test thread â must be thread-safe.
@@ -36,15 +40,14 @@ public RendererMessageInteropHarness(BunitJSInterop js, Func dispatc
///
/// With false, drives the in-process
- /// (unmarshalled) data channel instead: the service's runtime carries an
- /// InvokeUnmarshalled method that RuntimeHelper discovers by reflection, so
- /// DataSourceManager picks UnmarshalledDataSource and the column messages are
- /// recorded in instead of crossing to JS.
+ /// (unmarshalled) data channel instead: DataSourceManager picks UnmarshalledDataSource and
+ /// the column messages reach a recording double of the interop module, recorded in
+ /// instead of crossing to JS.
///
public RendererMessageInteropHarness(BunitJSInterop js, Func dispatcher, bool forceJsonDataMarshalling)
: base(dispatcher)
{
- _js = js;
+ _module = js.SetupModule(InteropModule.LitePath);
var runtime = forceJsonDataMarshalling
? js.JSRuntime
: new UnmarshalledRecordingRuntime(js.JSRuntime, RecordUnmarshalledMessage);
@@ -54,7 +57,7 @@ public RendererMessageInteropHarness(BunitJSInterop js, Func dispatc
// an unanswered invokeMethod would otherwise await its return forever.
// Method-specific stubs are excluded here so their handlers always win,
// regardless of bUnit's handler-resolution order.
- _js.Setup(SendMessage, inv => !IsStubbedInvokeMethod(inv))
+ _module.Setup(SendMessage, inv => !IsStubbedInvokeMethod(inv))
.SetResult(ToResultPayload(InteropReturn.Undefined));
}
@@ -93,9 +96,9 @@ private void RecordUnmarshalledMessage(string methodName, string refName, int in
}
///
- /// In-process runtime whose InvokeUnmarshalled methods RuntimeHelper discovers by
- /// name-based reflection â the seam replacing the API modern runtimes removed.
- /// Everything else delegates to bUnit's runtime.
+ /// In-process runtime that hands the components a recording double of the interop module, which takes
+ /// the column arrays directly ( ) â their raw heap address, what the
+ /// real module receives, only resolves on a WebAssembly heap. Everything else delegates to bUnit.
///
private sealed class UnmarshalledRecordingRuntime : Microsoft.JSInterop.IJSInProcessRuntime
{
@@ -108,29 +111,55 @@ public UnmarshalledRecordingRuntime(Microsoft.JSInterop.IJSRuntime inner, Action
_record = record;
}
- public TResult InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1, T2 arg2)
+ public TResult Invoke(string identifier, params object?[]? args)
+ => _inner.Invoke(identifier, args);
+
+ public ValueTask InvokeAsync(string identifier, object?[]? args)
+ => Wrap(identifier, _inner.InvokeAsync(identifier, args));
+
+ public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object?[]? args)
+ => Wrap(identifier, _inner.InvokeAsync(identifier, cancellationToken, args));
+
+ private async ValueTask Wrap(string identifier, ValueTask result)
{
- _record(identifier, (string)(object)arg0!, (int)(object)arg1!, (UnmarshalledColumn[]?)(object?)arg2);
- return default!;
+ var value = await result;
+ if (identifier == "import" && value is Microsoft.JSInterop.IJSInProcessObjectReference module)
+ {
+ return (TValue)(object)new RecordingModule(module, _record);
+ }
+ return value;
}
+ }
+
+ private sealed class RecordingModule : Microsoft.JSInterop.IJSInProcessObjectReference, IUnmarshalledColumnSink
+ {
+ private readonly Microsoft.JSInterop.IJSInProcessObjectReference _inner;
+ private readonly Action _record;
- public TResult InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1)
+ public RecordingModule(Microsoft.JSInterop.IJSInProcessObjectReference inner, Action record)
{
- // Data-intents variant; recorded with no columns.
- _record(identifier, (string)(object)arg0!, -1, null);
- return default!;
+ _inner = inner;
+ _record = record;
}
- public TResult Invoke(string identifier, params object?[]? args)
- => _inner.Invoke(identifier, args);
+ public void SendColumns(string methodName, string refName, int index, UnmarshalledColumn[]? columns) =>
+ _record(methodName, refName, index, columns);
- public ValueTask InvokeAsync(string identifier, object?[]? args)
- => _inner.InvokeAsync(identifier, args);
+ // Data-intents variant; recorded with no columns.
+ public void SendDataIntents(string methodName, string refName, string dataIntents) =>
+ _record(methodName, refName, -1, null);
- public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object?[]? args)
- => _inner.InvokeAsync(identifier, cancellationToken, args);
- }
+ public TValue Invoke(string identifier, params object?[]? args) => _inner.Invoke(identifier, args);
+
+ public ValueTask InvokeAsync(string identifier, object?[]? args) => _inner.InvokeAsync(identifier, args);
+ public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object?[]? args) =>
+ _inner.InvokeAsync(identifier, cancellationToken, args);
+
+ public void Dispose() => _inner.Dispose();
+
+ public ValueTask DisposeAsync() => _inner.DisposeAsync();
+ }
public override IIgniteUIBlazor Service => _service;
public override void ConfigureServices(IServiceCollection services) =>
@@ -147,8 +176,8 @@ public override void PrimeReady()
return;
}
_primed = true;
- _js.Setup("igCheckReady", _ => true).SetResult(true);
- _js.SetupVoid("igWaitForLoaded", _ => true).SetVoidResult();
+ _module.Setup("checkReady", _ => true).SetResult(true);
+ _module.SetupVoid("waitForLoaded", _ => true).SetVoidResult();
}
// The JS-to-.NET entries below run on the dispatcher, where Blazor delivers the real ones.
@@ -216,7 +245,7 @@ public override void SetupMethodResult(string methodName, InteropReturn result)
_stubbedMethods.TryAdd(methodName, true);
if (!_methodHandlers.TryGetValue(methodName, out var handler))
{
- handler = _js.Setup(SendMessage, inv => MethodNameOf(inv) == methodName);
+ handler = _module.Setup(SendMessage, inv => MethodNameOf(inv) == methodName);
_methodHandlers[methodName] = handler;
}
handler.SetResult(ToResultPayload(result));
@@ -225,7 +254,7 @@ public override void SetupMethodResult(string methodName, InteropReturn result)
public override Action WithholdMethodReply(string methodName)
{
_stubbedMethods.TryAdd(methodName, true);
- var handler = _js.Setup(SendMessage, inv => MethodNameOf(inv) == methodName);
+ var handler = _module.Setup(SendMessage, inv => MethodNameOf(inv) == methodName);
_methodHandlers[methodName] = handler;
return result => handler.SetResult(ToResultPayload(result));
}
@@ -445,7 +474,7 @@ public override void ClearObserved() =>
_observedFrom = SnapshotInvocations().Count(i => i.Identifier == SendMessage);
///
- /// Enumerates every recorded igSendMessage as (containerId, parsed message, element handles).
+ /// Enumerates every recorded sendMessage as (containerId, parsed message, element handles).
/// Element handles are not part of the JSON envelope on this stack â they ride as a
/// trailing marshalled argument of the call, alongside the component's object reference.
///
@@ -499,7 +528,7 @@ private IReadOnlyList SnapshotInvocations()
{
try
{
- return [.. _js.Invocations];
+ return [.. _module.Invocations];
}
catch (InvalidOperationException) when (attempt < 200)
{
@@ -545,7 +574,7 @@ private bool IsStubbedInvokeMethod(JSRuntimeInvocation invocation)
}
///
- /// The value igSendMessage resolves with: a JSON *string* element containing the
+ /// The value sendMessage resolves with: a JSON *string* element containing the
/// return envelope ({"retType": ..., "value": ...} ), matching what the JS
/// side produces for method invocations.
///
diff --git a/tests/IgniteUI.Blazor.Tests/UnmarshalledDataChannelTests.cs b/tests/IgniteUI.Blazor.Tests/UnmarshalledDataChannelTests.cs
index 10c6f4c2..81bfd045 100644
--- a/tests/IgniteUI.Blazor.Tests/UnmarshalledDataChannelTests.cs
+++ b/tests/IgniteUI.Blazor.Tests/UnmarshalledDataChannelTests.cs
@@ -109,26 +109,26 @@ public void ObservableCollectionMutations_EmitChannelMessages()
RenderScenario(scenario);
items.Add(ComboDataScenarios.AddedProduct());
- var insert = WaitFor("igUnmarshalledDataSourceInsert");
+ var insert = WaitFor("unmarshalledDataSourceInsert");
Assert.Equal(3, insert.Index);
Assert.Equal(4, Column(insert, "Name").ActualCount);
Assert.Equal("Added", Column(insert, "Name").StringValues[3]);
items.RemoveAt(0);
- var remove = WaitFor("igUnmarshalledDataSourceRemove");
+ var remove = WaitFor("unmarshalledDataSourceRemove");
Assert.Equal(0, remove.Index);
Assert.Equal(3, Column(remove, "Name").ActualCount);
Assert.Equal("Chang", Column(remove, "Name").StringValues[0]);
// A replace crosses as remove + insert at the same index, not as an update message.
items[0] = ComboDataScenarios.ReplacementProduct();
- var replaceInsert = WaitFor("igUnmarshalledDataSourceInsert",
+ var replaceInsert = WaitFor("unmarshalledDataSourceInsert",
m => m.Index == 0 && Column(m, "Name").StringValues[0] == "Replaced");
Assert.Equal(3, Column(replaceInsert, "Name").ActualCount);
// Clear resets the source's columns; the message itself carries none.
items.Clear();
- var clear = WaitFor("igUnmarshalledDataSourceClear");
+ var clear = WaitFor("unmarshalledDataSourceClear");
Assert.Empty(clear.Columns!);
}
@@ -143,7 +143,7 @@ private UnmarshalledColumnMessage RenderScenario(ComboDataScenarios.Scenario sce
.Add(c => c.ValueKey, scenario.ValueKey)
.Add(c => c.DisplayKey, scenario.DisplayKey));
_interop.MakeReady();
- return WaitFor("igUnmarshalledDataSourceCreate");
+ return WaitFor("unmarshalledDataSourceCreate");
}
private UnmarshalledColumnMessage WaitFor(string methodName, Func? match = null) =>
diff --git a/tests/js/static-web-assets.test.mjs b/tests/js/static-web-assets.test.mjs
index 0cbb88dc..09e7d7ab 100644
--- a/tests/js/static-web-assets.test.mjs
+++ b/tests/js/static-web-assets.test.mjs
@@ -23,7 +23,7 @@ test('no emitted file inspects its own URL', () => {
}
});
-test('initializer is a flat import list covering the app entry and api.js', () => {
+test('initializer is a flat import list covering the app entry, api.js and interop.js', () => {
const lines = read('IgniteUI.Blazor.Lite.lib.module.js').trim().split('\n');
const imported = lines.map((l) => {
const m = /^import '\.\/(.+)';$/.exec(l);
@@ -36,6 +36,7 @@ test('initializer is a flat import list covering the app entry and api.js', () =
'app entry missing',
);
assert.ok(imported.includes('api.js'), 'api.js missing');
+ assert.ok(imported.includes('interop.js'), 'interop.js missing');
});
test('api.js exports everything api.d.ts declares', () => {
@@ -71,6 +72,34 @@ test('no Ignite UI window state', () => {
}
});
+test('interop.js exports the client interop the components import', () => {
+ // The .NET side imports this module by path and calls these names (BaseRendererControl, RuntimeHelper, IgniteUIBlazor).
+ assert.deepEqual(exportsOf(read('interop.js')), [
+ 'checkReady',
+ 'requestLoad',
+ 'sendMessage',
+ 'setResourceString',
+ 'unmarshalledDataSourceClear',
+ 'unmarshalledDataSourceCreate',
+ 'unmarshalledDataSourceCreateDataIntents',
+ 'unmarshalledDataSourceInsert',
+ 'unmarshalledDataSourceRemove',
+ 'unmarshalledDataSourceUpdate',
+ 'waitForLoaded',
+ ]);
+});
+
+test('no interop functions on window', () => {
+ // The interop is reached through interop.js (JS isolation); a second Ignite UI package brings its own module.
+ const interopGlobal =
+ /\big(?:SendMessages?|CheckReady|WaitForLoaded|RequestLoad|SetResourceString|ConvertReturnValue|UnmarshalledDataSource\w*)\b|\braisingEvent\b/;
+ assert.match('window.igSendMessage = f', interopGlobal);
+ assert.doesNotMatch('window.igRegisterScript = f', interopGlobal);
+ for (const f of jsFiles) {
+ assert.doesNotMatch(read(f), interopGlobal, f);
+ }
+});
+
test('legacy script globals and compatibility bundle are not emitted', () => {
assert.equal(existsSync(join(wwwroot, 'app.bundle.js')), false);
const emitted = jsFiles.map(read).join('\n');
diff --git a/vite.config.mts b/vite.config.mts
index f401a7a0..5553ec21 100644
--- a/vite.config.mts
+++ b/vite.config.mts
@@ -6,12 +6,14 @@ const outDir = 'src/wwwroot';
const initializer = 'IgniteUI.Blazor.Lite.lib.module.js';
const licenseManifest = 'THIRD-PARTY-LICENSES.md';
/** Public modules keep fixed names so apps can import them and/or import maps can override them. */
-const fixedNames: Record = { api: 'api.js', 'lit-html': 'lit-html.js' };
-const entries = { app: 'src/src/index.ts', api: 'src/src/api.ts' };
+const fixedNames: Record = { api: 'api.js', interop: 'interop.js', 'lit-html': 'lit-html.js' };
+const entries = { app: 'src/src/index.ts', api: 'src/src/api.ts', interop: 'src/src/interop.ts' };
/**
* Emits the Blazor JS initializer as a flat list of static imports.
* Blazor awaits the import, so no startup hooks required for that.
+ * interop.js included so the module the components import is already evaluated: their import() of it resolves to
+ * this instance without another fetch, and nothing about startup depends on window.
*/
function emitInitializer(): Plugin {
return {
@@ -30,6 +32,10 @@ function emitInitializer(): Plugin {
};
visit(app.fileName);
visit(fixedNames.api);
+ if (bundle[fixedNames.interop]?.type !== 'chunk') {
+ throw this.error(`no ${fixedNames.interop} entry chunk â cannot emit ${initializer}`);
+ }
+ visit(fixedNames.interop);
this.emitFile({
type: 'asset',
fileName: initializer,