loading
Warning:
Both origin checks are yours to make Anyone holding a reference to your window can post to it, and a message says nothing trustworthy about who sent it except Origin - which the browser sets and nobody can forge. So pass the origins you trust to Listen, and name the origin you expect when sending rather than "*": a frame may not still contain the document you loaded into it.

Support check

IsSupported

window.postMessage has existed since forever; the check is here so this API has the same shape as the rest. During prerender/SSR it returns false rather than throwing.

C#
@inject Bit.Butil.WindowMessaging windowMessaging

var supported = await windowMessaging.IsSupported();
Live sample
support check output
Results will appear here when you interact with the samples.

Talking to an iframe

Listen / Frame / PostMessage

The document in the frame below is a real separate document, so what crosses between them are real cross-document messages. A target is a description rather than a reference: the window behind an iframe is replaced on every navigation, so it is looked up on each send - which is also why a message sent before the frame has loaded goes nowhere rather than being queued.

@implements IAsyncDisposable
@inject NavigationManager navManager
@inject Bit.Butil.WindowMessaging windowMessaging

<iframe @ref="_frame" src="/frames/echo.html" title="messaging frame"></iframe>

@code {
    private ElementReference _frame;
    private ButilSubscription? _subscription;

    private string Origin => new Uri(navManager.BaseUri).GetLeftPart(UriPartial.Authority);

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender is false) return;

        // Receive. The allowed origins are the security boundary: a listener that accepts every
        // origin accepts messages from anyone who can get a reference to this window.
        _subscription = await windowMessaging.Listen(
            m => InvokeAsync(() => { /* m.Origin, m.Deserialize<T>() */ }),
            allowedOrigins: [Origin]);
    }

    // Send. A target is a description rather than a reference - the window behind an iframe is
    // replaced on every navigation - so a message sent before the frame has loaded goes nowhere
    // rather than being queued.
    private async Task Send() =>
        await windowMessaging.Frame(_frame)
            .PostMessage(new { hello = "frame" }, targetOrigin: Origin);

    public async ValueTask DisposeAsync()
    {
        if (_subscription is not null) await _subscription.DisposeAsync();
    }
}
Live sample
frame output
Results will appear here when you interact with the samples.

From messaging to a private channel

PostWithPorts / WindowMessage.Ports

This is the handshake worth knowing: post once with a port attached, and everything after that goes over the port instead - no origin argument on every message, and nobody else listening in. The ports are transferred, so the handles you pass stop working on this side. A message that arrives carrying ports hands them over in WindowMessage.Ports, and they deliver nothing until started.

@inject NavigationManager navManager
@inject Bit.Butil.MessageChannel messageChannel
@inject Bit.Butil.WindowMessaging windowMessaging

<iframe @ref="_frame" src="/frames/echo.html" title="messaging frame"></iframe>

@code {
    private ElementReference _frame;
    private MessageChannelHandle? _channel;

    private string Origin => new Uri(navManager.BaseUri).GetLeftPart(UriPartial.Authority);

    private async Task HandOverAPort()
    {
        _channel = await messageChannel.Create();
        await _channel!.Port1.OnMessage(m => InvokeAsync(StateHasChanged));
        await _channel.Port1.Start();

        // Port2 goes to the frame, and stops working here - that transfer is what makes the line
        // private.
        await windowMessaging.Frame(_frame)
            .PostWithPorts(new { op = "here is a line" }, Origin, [_channel.Port2]);
    }

    // From here on there is no origin argument and nobody else listening in.
    private async Task SendOverPort() =>
        await _channel!.Port1.PostMessage(new { over = "the port" });
}
Live sample
port output
Results will appear here when you interact with the samples.

A window you opened

OpenedWindow

Window.Open hands back an id; this addresses the popup behind it. Only while it is open - a closed one, or an id from a NoOpener open (which deliberately hands back no reference at all), posts nothing and returns false. That is the feature working, not a failure to report.

Razor
@inject Bit.Butil.Window window
@inject NavigationManager navManager
@inject Bit.Butil.WindowMessaging windowMessaging

@code {
    private string? _id;
    private string Origin => new Uri(navManager.BaseUri).GetLeftPart(UriPartial.Authority);

    // Must run inside a user gesture, or the popup is blocked.
    private async Task OpenPopup() =>
        _id = await window.Open("/frames/echo.html", "_blank", "popup=yes,width=420,height=320");

    // False once the popup is closed, and for an id from a NoOpener open.
    private async Task Post() =>
        await windowMessaging.OpenedWindow(_id!)
            .PostMessage(new { hello = "popup" }, targetOrigin: Origin);
}
Live sample
popup output
Results will appear here when you interact with the samples.

Upwards

Parent / Top / Opener

Parent is the document that embedded this one, Top the outermost of the frame tree, Opener the window that opened this one. In a top-level page opened by nobody, Parent and Top are this page itself - so a message posted there comes straight back to your own listener, which is what happens here.

Razor
@inject NavigationManager navManager
@inject Bit.Butil.WindowMessaging windowMessaging

@code {
    private string Origin => new Uri(navManager.BaseUri).GetLeftPart(UriPartial.Authority);

    private async Task PostUpwards()
    {
        await windowMessaging.Parent().PostMessage(new { up = true }, Origin);
        await windowMessaging.Top().PostMessage(new { up = true }, Origin);

        // A no-op on the other side of a NoOpener open, where there is deliberately no reference at
        // all - that is the feature working, not a failure to report.
        await windowMessaging.Opener().PostMessage(new { back = true }, Origin);
    }
}
Live sample
upwards output
Results will appear here when you interact with the samples.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes window.postMessage. Returns default (false) during prerender/SSR instead of throwing.
Listen
ValueTask<ButilSubscription> Listen(Action<WindowMessage> onMessage, string[]? allowedOrigins = null)
Listens for messages posted to this window. Anything from an origin not in the list is dropped before the callback sees it; an empty list accepts everything, which is almost never right.
Frame
WindowMessageTarget Frame(ElementReference iframe)
A target for the document inside an iframe, looked up on each send rather than held.
OpenedWindow
WindowMessageTarget OpenedWindow(string windowId)
A target for a window opened through Window.Open, addressed by the id it returned.
Parent / Top / Opener
WindowMessageTarget Parent(), Top(), Opener()
The embedding document, the outermost document of the frame tree, and the window that opened this one.
DisposeAsync
ValueTask DisposeAsync()
On scope/circuit teardown, removes every listener whose subscription was never disposed.
WindowMessageTarget.PostMessage
ValueTask<bool> PostMessage<T>(T value, string targetOrigin, JsonSerializerOptions? options = null)
Posts a message as JSON. False when the target window is not there - an unloaded frame, a closed popup, a withheld opener.
WindowMessageTarget.PostBytes
ValueTask<bool> PostBytes(byte[] data, string targetOrigin, bool transfer = true)
Posts raw bytes, moving the buffer to the receiver rather than copying it when transfer is true.
WindowMessageTarget.PostWithPorts
ValueTask<bool> PostWithPorts<T>(T value, string targetOrigin, MessagePortHandle[] ports, JsonSerializerOptions? options = null)
Posts a message carrying ports. They are transferred, so the handles passed here stop working.
WindowMessage
record (string Origin, bool IsBinary, string? Json, byte[]? Data, MessagePortHandle[] Ports) { T? Deserialize<T>() }
A received message. Origin is set by the browser and cannot be forged - it is the only thing identifying the sender. Ports carries whatever the sender transferred; where several listeners accept the same message, only the first one registered is given them, because a port has one owner.
An unhandled error has occurred. Reload 🗙