Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions src/componentsBase/IIgniteUIBlazorRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ internal interface IIgniteUIBlazorRuntime
bool IsLoadRequested(string moduleName);
void MarkIsLoadRequested(string moduleName);
bool IsRuntimeValid(bool reevaluate = false);

/// <summary>The client interop module at <paramref name="path"/>, shared by the components of the runtime.</summary>
InteropModule GetInteropModule(string path);
}

internal static class IgniteUIBlazorRuntimeExtensions
Expand Down
73 changes: 48 additions & 25 deletions src/componentsBase/IgbComponentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>The client interop module this component talks to.</summary>
internal InteropModule Interop => _interop ??= Runtime.GetInteropModule(InteropModule.LitePath);

/// <summary>Requests the client modules this component needs; override to register more through <see cref="IgBlazor"/>.</summary>
protected virtual void EnsureModulesLoaded()
{
Expand Down Expand Up @@ -728,11 +733,11 @@ public async Task EnsureReady()
//Console.WriteLine("ensuring ready: " + this.GetType().Name);
while (!this._ready)
{
bool ready = await jsRuntime.InvokeAsync<bool>("igCheckReady", new object[] { _containerId });
bool ready = await Interop.InvokeAsync<bool>("checkReady", new object[] { _containerId });
//Console.WriteLine(ready + " -> " + this.GetType().Name);
if (ready)
{
await jsRuntime.InvokeVoidAsync("igWaitForLoaded");
await Interop.InvokeVoidAsync("waitForLoaded");
OnReady();
break;
}
Expand Down Expand Up @@ -1629,14 +1634,13 @@ private void ProcessMessageSync(RendererMessage m)

if (m.NativeElements != null)
{
return await JsRuntime.InvokeAsync<object>("igSendMessage",
return await Interop.InvokeAsync<object>("sendMessage",
new object[] { this._containerId, json,
GetObjectRef(), m.NativeElements });
}
else
{
//json = "window.sendMessage(`" + this._id + "`, `" + json + "`)";
return await JsRuntime.InvokeAsync<object>("igSendMessage",
return await Interop.InvokeAsync<object>("sendMessage",
new object[] { this._containerId, json,
GetObjectRef() });
}
Expand Down Expand Up @@ -1669,18 +1673,10 @@ private void SendJson(string json, ElementReference[]? nativeElements)
return;
}

if (nativeElements != null)
{
JsRuntime.InvokeAsync<object>("igSendMessage",
new object[] { this._containerId, json,
GetObjectRef(), nativeElements });
}
else
{
JsRuntime.InvokeAsync<object>("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<object>("sendMessage", args));
}

internal void AttachChild(BaseJsonSerializable child)
Expand Down Expand Up @@ -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<object>("igSendMessage",
return module.Invoke<object>("sendMessage",
new object[] {
this._containerId, json,
GetObjectRef(), nativeElements });
}
else
{
return JsInProcessRuntime.Invoke<object>("igSendMessage",
return module.Invoke<object>("sendMessage",
new object[] {
this._containerId, json,
GetObjectRef() });
Expand Down Expand Up @@ -3502,7 +3500,7 @@ private async Task TrySendCleanupAsync()
{
return null;
}
return await JsRuntime.InvokeAsync<object>("igSetResourceString", new object[] { "set", grouping, id, value });
return await Interop.InvokeAsync<object>("setResourceString", new object[] { "set", grouping, id, value });
}

internal async Task<object?> SetResourceStringAsync(string grouping, string json)
Expand All @@ -3511,7 +3509,7 @@ private async Task TrySendCleanupAsync()
{
return null;
}
return await JsRuntime.InvokeAsync<object>("igSetResourceString", new object[] { "register", grouping, "", json });
return await Interop.InvokeAsync<object>("setResourceString", new object[] { "register", grouping, "", json });
}

private void SetPropertyValue(object item, System.Reflection.PropertyInfo property, JsonElement jsonElement)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -3823,7 +3821,33 @@ public void RequestLoad(string moduleName)
return;
}
_loadedCache.AddOrUpdate(moduleName, true, (name, oldValue) => true);
JsRuntime.InvokeAsync<object>("igRequestLoad", moduleName);
GetInteropModule(InteropModule.LitePath).Post(module => _ = module.InvokeVoidAsync("requestLoad", moduleName));
}

private readonly ConcurrentDictionary<string, InteropModule> _interopModules = new ConcurrentDictionary<string, InteropModule>();

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)
{
Expand Down Expand Up @@ -3981,7 +4005,6 @@ internal class ModuleLoader
public static void Load(IIgniteUIBlazor runtime, string moduleName)
{
runtime.AsRuntime().RequestLoad(moduleName);
//runtime.JsRuntime.InvokeAsync<object>("igRequestLoad", moduleName);
}

internal static void MarkIsLoadRequested(IIgniteUIBlazor runtime, string moduleName)
Expand Down
163 changes: 163 additions & 0 deletions src/componentsBase/InteropModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.JSInterop;

namespace IgniteUI.Blazor.Controls
{
/// <summary>
/// The client interop module of one Ignite UI package, imported through JS isolation (<c>import()</c>)
/// instead of calling functions on <c>window</c>. One instance per module path and runtime (circuit or app).
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
internal sealed class InteropModule : IAsyncDisposable
{
/// <summary>The interop module of this package.</summary>
internal const string LitePath = "./_content/IgniteUI.Blazor/interop.js";

private readonly IJSRuntime _jsRuntime;
private readonly object _lock = new object();
private readonly List<Action<IJSObjectReference>> _pending = new List<Action<IJSObjectReference>>();
private Task<IJSObjectReference>? _loading;
private volatile IJSObjectReference? _module;
private bool _disposed;

internal InteropModule(IJSRuntime jsRuntime, string path)
{
_jsRuntime = jsRuntime;
Path = path;
}

internal string Path { get; }

/// <summary>The module, once imported; <see langword="null"/> while it is still loading.</summary>
internal IJSObjectReference? Loaded => _module;

/// <summary>The module for synchronous calls, where .NET runs in the browser and the module is imported.</summary>
internal IJSInProcessObjectReference? LoadedInProcess => _module as IJSInProcessObjectReference;

/// <summary>Imports the module, once.</summary>
internal Task<IJSObjectReference> GetAsync()
{
lock (_lock)
{
return _loading ??= LoadAsync();
}
}

/// <summary>Runs <paramref name="call"/> with the module: now if it is imported, otherwise after the calls queued before it.</summary>
internal void Post(Action<IJSObjectReference> call)
{
IJSObjectReference? module;
lock (_lock)
{
if (_disposed)
{
return;
}
module = _module;
if (module == null)
{
_pending.Add(call);
_loading ??= LoadAsync();
return;
}
}
call(module);
}

internal async ValueTask<TValue> 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<TValue>(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<IJSObjectReference> LoadAsync()
{
IJSObjectReference module;
try
{
module = await _jsRuntime.InvokeAsync<IJSObjectReference>("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<IJSObjectReference>[] 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.
}
Comment on lines +122 to +125
}
}
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)
{
}
Comment on lines +149 to +151
catch (JSException)
{
}
Comment on lines +152 to +154
catch (OperationCanceledException)
{
}
Comment on lines +155 to +157
catch (ObjectDisposedException)
{
}
Comment on lines +158 to +160
}
}
}
Loading
Loading