Skip to content

refactor: move the client interop off window into interop.js - #522

Draft
dkamburov wants to merge 14 commits into
masterfrom
dkamburov/full-package-extensibility
Draft

dkamburov wants to merge 14 commits into
masterfrom
dkamburov/full-package-extensibility

Conversation

@dkamburov

@dkamburov dkamburov commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Description

Moves the client interop off window into an ES module (interop.js), using Blazor JS isolation.

The components used to call igSendMessage, igCheckReady, igWaitForLoaded, igRequestLoad, igSetResourceString and igUnmarshalledDataSource* as functions on window. They now import _content/IgniteUI.Blazor/interop.js through IJSRuntime instead.

Client (TypeScript)

  • index.ts exports the interop functions instead of assigning them to window.
  • A new fixed-name entry, interop.ts, is emitted as interop.js and re-exports them. Importing it reuses the instance the initializer already loaded.
  • The JS initializer (IgniteUI.Blazor.Lite.lib.module.js) now preloads interop.js next to the app entry and api.js. The first component's import() then resolves without another fetch (Audit and update the IgniteUI.Blazor bootstrap script for current hook support #256).
  • Removed igSendMessages (dead code that called an undefined window.sendMessage), igConvertReturnValue and the raisingEvent flag.
  • The *Script late-registration replay in index.ts now calls sendMessage directly instead of window.igSendMessage.

.NET

  • New InteropModule imports the module once per runtime and path. Calls made while it is still importing are queued and dispatched in call order, so fire-and-forget messages keep their order. Synchronous calls (WASM) use the imported IJSInProcessObjectReference once EnsureReady has completed. The runtime service disposes its modules.
  • Unmarshalled data (WASM) now goes to the module on every TFM, as a synchronous in-process call with the address of the local holding the columns. The net8-only InvokeUnmarshalled fast path needed a window global and is dropped. The client reads the address with whichever heap reader the runtime has: getValue on net8, Blazor.runtime on net9+.

Tests

  • The bUnit harness records the module interop. The unmarshalled channel records through an IUnmarshalledColumnSink module double.
  • static-web-assets tests pin the exports of interop.js, assert the initializer covers it, and assert that no interop function is put on window.
  • Fixed static-web-assets.test.mjs, which had a missing }); and could not be parsed.

Motivation / Context

Library globals on window collide when two Ignite UI packages are on one page, because each one overwrites the other's. Isolating the interop in a module removes that.

Type of Change (check all that apply):

  • Bug fix
  • New functionality
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactoring (no functional changes)
  • Documentation
  • Demos
  • CI/CD
  • Tests
  • Changelog

Component(s) / Area(s) Affected:

How Has This Been Tested?

  • Unit tests
  • Manual testing
  • Automated e2e tests

Test Configuration:

  • .NET version:
  • Hosting model:
  • Browser(s):
  • OS:

Screenshots / Recordings

Checklist:

  • All relevant tags have been applied to this PR
  • This PR includes unit tests covering all the new code
  • This PR includes API docs for newly added methods/properties
  • This PR includes general feature table updates in the root README.MD
  • This PR includes CHANGELOG.MD updates for newly added functionality
  • This PR contains breaking changes
  • This PR includes behavioral changes and the feature specification has been updated with them
  • Accessibility (ARIA, keyboard navigation, focus management) has been verified

Closes #255

dkamburov and others added 11 commits September 30, 2026 19:10
The licensed/Trial IgniteUI.Blazor package is moving onto this package for its
small components and builds its charts, grids and Dock Manager on the same
base classes. An IL scan of its components against 0.4.0 found what it
cannot reach; this change provides it:

- InternalsVisibleTo the Infragistics-signed IgniteUI.Blazor assembly
  (keyed to eng/IG.publickey.hex, unconditional so release builds keep it).
- BaseCollection<T>, JsonSerializable, SerializationContext,
  SerializationFilter and IDataIntentAttribute are public again but
  [EditorBrowsable(Never)]: public full-package types derive from them, which
  friend access cannot allow (CS0060). DataIntentAttribute is public.
- SetPropertyValue is internal instead of private (used by the grids).
- RendererSerializer exposes its Context and ShouldWrite internally so the
  full package can serialize its drawing types as extension methods.
- Content-child host hook: components implementing the internal
  IContentChildHost cascade themselves to their child content, and
  IgbFormatSpecifier attaches to / detaches from the nearest host. Lets an
  IgbNumberFormatSpecifier declared inside a chart axis keep working.
  Non-host components render exactly as before.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The components called igSendMessage, igCheckReady, igWaitForLoaded,
igRequestLoad, igSetResourceString and igUnmarshalledDataSource* as
functions on window. Two Ignite UI packages on one page (Lite plus the full
IgniteUI.Blazor) would overwrite each other's, so the interop now follows
Blazor JS isolation:

- index.ts exports the functions instead of assigning them to window; a new
  fixed-name entry, interop.js, re-exports them, so importing it reuses the
  instance the initializer loaded. igSendMessages (dead: it called an
  undefined window.sendMessage), igConvertReturnValue and the raisingEvent
  flag are gone.
- InteropModule imports the module once per runtime and path. Calls made
  while it is still importing are queued and dispatched in call order, so
  fire-and-forget messages keep their order; synchronous calls (WASM) use
  the imported IJSInProcessObjectReference, available once EnsureReady has
  completed (a precondition of sync invokes already). The runtime service
  disposes its modules.
- Unmarshalled data (WASM) goes to the module on every TFM: a synchronous
  in-process call with the address of the local holding the columns. The
  net8-only InvokeUnmarshalled fast path needed a window global and is
  dropped; the client now dereferences the address with whichever heap
  reader the runtime has (getValue on net8, Blazor.runtime on net9+).
- Tests: the bUnit harness records the module interop; the unmarshalled
  channel records through an IUnmarshalledColumnSink module double.
  static-web-assets tests pin interop.js's exports and assert no interop
  function is put on window.

Verified: unit suite (net8/9/10, only the pre-existing locale-dependent
RatingTests.Methods_FollowContract failure), JS tests, and the Playwright
integration suite (89/89, Server + WASM).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Builds on the interop module: a component asks for its module through the
overridable (private protected) InteropModulePath, so the full
IgniteUI.Blazor package's components can talk to their own client runtime
while this package's components keep using interop.js. Nothing in either
runtime is shared through window.

- BaseRendererControl.InteropModulePath (defaults to Lite's interop.js)
  selects the module for messages, readiness, resource strings and the
  unmarshalled data channel (RuntimeHelper resolves it lazily).
- The runtime tracks client module load requests per interop module, so
  the same module name requested by two packages loads in both; internal
  ModuleLoader overloads take the interop module path.
- Tests: a component overriding the path sends only to its module, and load
  requests are routed and tracked per module.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The JS initializer is the whole bootstrap: a flat list of static imports that
Blazor awaits before it starts. It now includes interop.js next to the app
entry and api.js, so the module the components import through JS isolation
is already evaluated when the first component asks for it - its import()
resolves to that instance without another fetch, and no startup step depends
on window state (the former document.write / window-guard bootstrap of the
full package is not needed on this path).

static-web-assets tests assert the initializer covers interop.js.
Verified with the Playwright integration suite (89/89).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
docs/PACKAGE-CONTRACT.md lists what the full package relies on: friend
access pinned to the Infragistics key, the public-hidden base types, the
content-child host, the per-package interop module, the fixed asset names
under _content/IgniteUI.Blazor (interop.js exports, api.js, lit-html.js,
themes) and the absence of window interop state.

api.js's getRegisteredScript stays @internal (out of api.d.ts) but is now
documented as a stable cross-package export: the full package's runtime
resolves *Script parameters through it, so one registry serves both.
static-web-assets tests pin the fixed file names and that export.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Found by compiling the full IgniteUI.Blazor package against this package:

- MarshalByValueFactory.AddProvider: the full package registers its own
  by-value types (chart/grid value objects), consulted after this
  package's.
- BaseRendererControl.FindByName is internal virtual, like the element's,
  so generated types override both the same way.
- The shared client custom elements (igc-portal-*, igc-template-container,
  igc-template-content, and those registered through
  RegisterElementHelper) are defined only when not defined yet: with both
  packages' client runtimes on a page, the second definition threw and
  stopped that runtime from loading.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Documents, for the next person touching this, why Name intentionally has no
[Parameter] (per #436) and where the full package gets a Name markup
attribute instead: its generator shadows Name with 'new' + [Parameter] on
its own root, non-MarshalByValue BaseRendererElement-derived types (chart
axes, series, gauges), rather than this package reintroducing [Parameter]
on the shared base - which would retrigger analyzer BL0005 on this
package's own MarshalByValue types (e.g. IgbIconMeta) that still use plain
object-initializer syntax in tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…into dkamburov/full-package-extensibility
…into dkamburov/full-package-extensibility
@dkamburov dkamburov added the squash-merge Merge PR with "Squash and Merge" option label Oct 6, 2026
Comment thread src/src/index.ts
index: number,
columns: any,
) {
columns = derefColumns(columns);
Comment on lines +122 to +125
catch (Exception)
{
// Fire-and-forget calls: one failing must not keep the rest from being dispatched.
}
Comment on lines +149 to +151
catch (JSDisconnectedException)
{
}
Comment on lines +152 to +154
catch (JSException)
{
}
Comment on lines +155 to +157
catch (OperationCanceledException)
{
}
Comment on lines +158 to +160
catch (ObjectDisposedException)
{
}
@damyanpetev damyanpetev changed the title refactor!: move the client interop off window into interop.js refactor: move the client interop off window into interop.js Oct 6, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

squash-merge Merge PR with "Squash and Merge" option

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate remaining Blazor JS interop globals to JavaScript modules

1 participant