Sometimes, a user logged out of your website but in the other browser tabs, the user is still logged in and create an inconsistency between browser tabs. That is when you need to share data between browsing context to avoid any unwanted inconsistency. In this tutorial, you will learn:
BroadcastChannel
.You can download the example code used in this topic on GitHub.
According to MDN Web Docs, a browsing context is the environment in which a browser displays a Document
. To make it simple to understand, a browsing context is a browser tab, a browser window, frame
, or an iframe
element. The following video demonstrates sharing data between browsing contexts:
A browsing context broadcaster is an implementation of publish/subscribe design pattern where a browsing context publishing a message to a channel and all the channel's subscribers will receive it and act accordingly. We will use the Broadcast Channel API as the underlying technique. The following image illustrates how a browsing context broadcaster works:
As we mentioned before, we will use the Broadcast Channel API. It has the BroadcastChannel
class, and we are going to use this class to implement a browsing context broadcaster.
BlazorSchoolBroadcast
(wwwroot/js/BlazorSchoolBroadcast.js) JavaScript module as follows:export function postMessage(channelName, message) { let channel = new BroadcastChannel(channelName); channel.postMessage(JSON.stringify(message)); } export function listenChannel(channelName, csharpBroadcaster) { let channel = new BroadcastChannel(channelName); channel.onmessage = (event) => { csharpBroadcaster.invokeMethodAsync("Notify", JSON.parse(event.data)); } }
You can read more about JavaScript module in the Add JavaScript to Blazor tutorial.
public record BroadcastData<T>(JsonElement Data) { public T? GetData() { return Data.Deserialize<T>(new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); } }
public abstract class BlazorSchoolBroadcasterBase<T> : IAsyncDisposable { private readonly IJSRuntime _jsRuntime; private Lazy<IJSObjectReference> _blazorSchoolBroadcastJsRef = new(); public abstract string ChannelName { get; } public event EventHandler<BroadcastData<T>> OnMessageReceived = (sender, args) => { }; public BlazorSchoolBroadcasterBase(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; } public async Task SendMessageAsync(T message) { await WaitForReference(); await _blazorSchoolBroadcastJsRef.Value.InvokeVoidAsync("postMessage", ChannelName, message); } public async Task ListenChannelAsync() { await WaitForReference(); var wrappedInstance = DotNetObjectReference.Create(this); await _blazorSchoolBroadcastJsRef.Value.InvokeVoidAsync("listenChannel", ChannelName, wrappedInstance); } [JSInvokable] public void Notify(JsonElement jsonData) { OnMessageReceived?.Invoke(this, new(jsonData)); } protected async Task WaitForReference() { if (_blazorSchoolBroadcastJsRef.IsValueCreated is false) { _blazorSchoolBroadcastJsRef = new(await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "/js/BlazorSchoolBroadcast.js")); } } public async ValueTask DisposeAsync() { if (_blazorSchoolBroadcastJsRef.IsValueCreated) { await _blazorSchoolBroadcastJsRef.Value.DisposeAsync(); } } }
In the above example,T
is the data you want to share between your component. It can be a primitive data likestring
,int
or a complex data likeExampleClass
.
This type of broadcaster allows you to subscribe to a channel only when you need it. To access the data, your components need to subscribe to the broadcaster first. When the component receives a notification, you need to act accordingly.
public class BlazorSchoolComponentBroadcaster1 : BlazorSchoolBroadcasterBase<string> { public BlazorSchoolComponentBroadcaster1(IJSRuntime jsRuntime) : base(jsRuntime) { } public override string ChannelName { get; } = "BlazorSchoolComponentBroadcaster1"; }
Program.cs
. For example:builder.Services.AddScoped<BlazorSchoolComponentBroadcaster1>();
This type of broadcaster allows you to access the data anywhere without subscribe to the broadcaster.
public class BlazorSchoolGlobalBroadcaster : BlazorSchoolBroadcasterBase<ExampleClass> { public override string ChannelName { get; } = "BlazorSchoolGlobalBroadcaster"; public BlazorSchoolGlobalBroadcaster(IJSRuntime jsRuntime) : base(jsRuntime) { } }
T
in the BlazorSchoolBroadcasterBase<T>
). For example, our BlazorSchoolCascadingGlobalBroadcastData.razor
:@implements IDisposable @inject BlazorSchoolGlobalBroadcaster BlazorSchoolGlobalBroadcaster <CascadingValue TValue="BroadcastData<ExampleClass>" Value="_currentData" ChildContent="ChildContent" Name="BlazorSchoolCascadingGlobalBroadcastData" /> @code { private BroadcastData<ExampleClass> _currentData = default!; [Parameter] public RenderFragment ChildContent { get; set; } = _ => { }; protected override async Task OnInitializedAsync() { BlazorSchoolGlobalBroadcaster.OnMessageReceived += OnGlobalBroadcasterNotify; await BlazorSchoolGlobalBroadcaster.ListenChannelAsync(); } public void OnGlobalBroadcasterNotify(object? sender, BroadcastData<ExampleClass> broadcastData) { _currentData = broadcastData; StateHasChanged(); } public void Dispose() { BlazorSchoolGlobalBroadcaster.OnMessageReceived -= OnGlobalBroadcasterNotify; } }
Program.cs
. For example:builder.Services.AddScoped<BlazorSchoolGlobalBroadcaster>();
App.razor
. For example:<BlazorSchoolCascadingGlobalBroadcastData> <Router AppAssembly="@typeof(App).Assembly"> ... </Router> </BlazorSchoolCascadingGlobalBroadcastData>
public class BlazorSchoolGlobalBroadcasterAccessor : ComponentBase { [CascadingParameter(Name = "BlazorSchoolCascadingGlobalBroadcastData")] public BroadcastData<ExampleClass> CurrentData { get; set; } = default!; [Parameter] public RenderFragment<BroadcastData<ExampleClass>>? ChildContent { get; set; } protected override void BuildRenderTree(RenderTreeBuilder builder) => builder.AddContent(0, ChildContent?.Invoke(CurrentData)); }
SendMessageAsync
method to publish a message. For example:@inject BlazorSchoolComponentBroadcaster1 BlazorSchoolComponentBroadcaster1 <form> <label> Enter your name: <input type="text" @bind-value="ExampleString" /> </label> <button type="button" @onclick="SendDataBroadcaster1Async">Sync other browsing contexts</button> </form> @code { public string ExampleString { get; set; } = "Blazor School"; private async Task SendDataBroadcaster1Async() { await BlazorSchoolComponentBroadcaster1.SendMessageAsync(ExampleString); } }
ListenChannelAsync
at the Initialize phase (see Component Lifecycle for more information). Next, subscribe for the OnMessageReceived
event and act accordingly. For example:@inject BlazorSchoolComponentBroadcaster1 BlazorSchoolComponentBroadcaster1 @implements IDisposable <div>The data is @ReceivedData</div> @code { public string ReceivedData { get; set; } = "Blazor School"; protected override async Task OnInitializedAsync() { BlazorSchoolComponentBroadcaster1.OnMessageReceived += OnBroadcaster1Notify; await BlazorSchoolComponentBroadcaster1.ListenChannelAsync(); } private void OnBroadcaster1Notify(object? sender, BroadcastData<string> broadcastData) { ReceivedData = broadcastData.GetData() ?? ""; StateHasChanged(); } public void Dispose() { BlazorSchoolComponentBroadcaster1.OnMessageReceived -= OnBroadcaster1Notify; } }
SendMessageAsync
method to publish a message. For example:@inject BlazorSchoolGlobalBroadcaster BlazorSchoolGlobalBroadcaster <EditForm Model="FormModel"> @if (FormModel.LoggedIn) { <label> Enter your name: <InputText @bind-Value="FormModel.Username" /> </label> <div> <button type="button" @onclick="SaveChangesAsync">Save changes</button> <button type="button" @onclick="LogoutAsync">Log out</button> </div> } else { <button type="button" @onclick="LoginAsync">Log in</button> } </EditForm> @code { public ExampleClass FormModel { get; set; } = new(); private async Task SaveChangesAsync() { await BlazorSchoolGlobalBroadcaster.SendMessageAsync(FormModel); } private async Task LoginAsync() { FormModel.LoggedIn = true; await SaveChangesAsync(); } private async Task LogoutAsync() { FormModel = new(); await SaveChangesAsync(); } }
<BlazorSchoolGlobalBroadcasterAccessor Context="broadcastData"> <a href="https://blazorschool.com" target="_blank">@broadcastData?.GetData()?.Username</a> </BlazorSchoolGlobalBroadcasterAccessor>
OnMessageReceived
event of the global broadcaster. For example:@inject BlazorSchoolGlobalBroadcaster BlazorSchoolGlobalBroadcaster @implements IDisposable ... @code { public ExampleClass FormModel { get; set; } = new(); protected override void OnInitialized() { BlazorSchoolGlobalBroadcaster.OnMessageReceived += OnGlobalBroadcasterUpdate; } private void OnGlobalBroadcasterUpdate(object? sender, BroadcastData<ExampleClass> broadcastData) { FormModel = broadcastData.GetData() ?? new(); StateHasChanged(); } public void Dispose() { BlazorSchoolGlobalBroadcaster.OnMessageReceived -= OnGlobalBroadcasterUpdate; } }
[CascadingParameter(Name = "BlazorSchoolCascadingGlobalBroadcastData")] public BroadcastData<ExampleClass> CurrentData { get; set; } = default!;
Component broadcaster is suitable for data that does not appear on every pages of your website. For example: the cart information is only useful in the checkout page or product page and not useful in the register or forgot password page. In this case, a component broadcaster is preferable.
Global broadcaster is suitable for data that appears on every or almost every pages. For example, the profile name displayed at the menu bar. In this case, a global broadcaster is preferable.
BroadcastChannel
BroadcastChannel
only works with the same origin (including protocol, host and port) and on a same browser (incognito/private count as different browser). The following image illustrates this characteristic of BroadcastChannel
:
string
then you should expect to receive a string
.ChannelName
should have a clear meaning.