6

I have a Blazor webasemmbly app, it's using asp.net core as backend and Blazor wasm as frontend. I have a class that can check the HTTP issues like notfound, BadReqest, and ...

  public class HttpInterceptorService
    {
        private readonly HttpClientInterceptor _interceptor;
        private readonly NavigationManager _navManager;
        
        private readonly RefreshTokenService _refreshTokenService;
        
        public HttpInterceptorService(HttpClientInterceptor interceptor,
            NavigationManager navManager, 
            RefreshTokenService refreshTokenService)
        {
            _interceptor = interceptor;
            _navManager = navManager;               
            _refreshTokenService = refreshTokenService;
        }

        public void RegisterEvent() => _interceptor.AfterSend += HandleResponse;
        public void RegisterBeforeSendEvent() =>
            _interceptor.BeforeSendAsync += InterceptBeforeSendAsync;

        public void DisposeEvent()
        {
            _interceptor.AfterSend -= HandleResponse;
            _interceptor.BeforeSendAsync -= InterceptBeforeSendAsync;
        }

        private async Task InterceptBeforeSendAsync(object sender,
            HttpClientInterceptorEventArgs e)
        {
            var absolutePath = e.Request.RequestUri.AbsolutePath;

            if (!absolutePath.Contains("token") && !absolutePath.Contains("account"))
            {
                var token = await _refreshTokenService.TryRefreshToken();
                if (!string.IsNullOrEmpty(token))
                {
                    e.Request.Headers.Authorization =
                        new AuthenticationHeaderValue("bearer", token);
                }
            }
        }

        private void HandleResponse(object sender, HttpClientInterceptorEventArgs e)
        {
            if (e.Response == null)
            {
                _navManager.NavigateTo("/PageError");
                throw new HttpResponseException("Server not available.");
            }

            var message = "";

            if (!e.Response.IsSuccessStatusCode)
            {
                
                switch (e.Response.StatusCode)
                {
                    case HttpStatusCode.NotFound:
                        _navManager.NavigateTo("/Page404");                 
                        break;
                    case HttpStatusCode.BadRequest:                                 
                        break;
                    case HttpStatusCode.Unauthorized:
                        _navManager.NavigateTo("/unauthorized");                    
                        break;
                    default:
                        _navManager.NavigateTo("/PageError");                   
                        break;
                }

                throw new HttpResponseException(message);
            }
        }       
    }

this HTTP Interceptor does a great job, but the issue is when the client app (wasm) loses the connection to the server (for any reason like no internet, or server stop running and ...), it doesn't work and is not going to be useful. when the server doesn't run as well .

so searched I found that we have to check the signalR connection status but I couldn't find an example or tutorial on how to implement that.

I want to add it globally to the app.

Ali
  • 300
  • 4
  • 11
  • I'm not sure if signalr could meet your requirement, but if you would try it, I think [this document](https://learn.microsoft.com/en-us/aspnet/core/tutorials/signalr-blazor?view=aspnetcore-5.0&tabs=visual-studio&pivots=webassembly#add-razor-component-code-for-chat-2) will help. – Tiny Wang Oct 22 '21 at 07:54
  • I tried it before but It didn't help me. – Ali Oct 22 '21 at 09:53

4 Answers4

6

The SingnalR way of checking if the internet is available brings many disadvantages.

  1. You dont distinguish between lost internet connection or just unreachable server.
  2. You have usually limited amount of concurrent connections to your server and this approach seems to me like a waste of resources.

There is also another way how to know if user is offline by using a piece of JS code:

window.navigator.onLine

More discussed here: Detect the Internet connection is offline? But with this approach you should be able to know most cases when user is offline.

How to implement this into your code ? Create a component named Connection.razor and put there:

@inject IJSRuntime _jsRuntime;
@implements IAsyncDisposable

@if (IsOnline)
{
    @Online
}
else
{
    @Offline
}

@code {

    [Parameter]
    public RenderFragment Online { get; set; }

    [Parameter]
    public RenderFragment Offline { get; set; }

    public bool IsOnline { get; set; }

    [JSInvokable("Connection.StatusChanged")]
    public void OnConnectionStatusChanged(bool isOnline)
    {
        if (IsOnline != isOnline)
        {
            IsOnline = isOnline;
        }

        StateHasChanged();
    }

    protected override async Task OnInitializedAsync() {
        await base.OnInitializedAsync();

        await _jsRuntime.InvokeVoidAsync("Connection.Initialize", DotNetObjectReference.Create(this));
    }

    public async ValueTask DisposeAsync() {
        await _jsRuntime.InvokeVoidAsync("Connection.Dispose");
    }

}

Then create a JS file Connection.js with code:

let handler;

window.Connection = {
    Initialize: function (interop) {

        handler = function () {
            interop.invokeMethodAsync("Connection.StatusChanged", navigator.onLine);
        }

        window.addEventListener("online", handler);
        window.addEventListener("offline", handler);

        handler(navigator.onLine);
    },
    Dispose: function () {

        if (handler != null) {

            window.removeEventListener("online", handler);
            window.removeEventListener("offline", handler);
        }
    }
};

then link this JS inside your index.html:

<body>
    <div id="app" style="height: 100%;">Loading...</div>
    <script src="_framework/blazor.webassembly.js"></script>
    <script src="js/Connection.js"></script>
</body>

and finally use this component for example inside your MainLayout.razor like:

<Connection>
    <Online>
        <h1 style="color: green">Online</h1>
    </Online>
    <Offline>
        <h1 style="color: red">Offline</h1>
    </Offline>
</Connection>

and thanks to this approach you are able to inform user if his connection is unavailable.

Src:

https://www.patrickrobin.co.uk/articles/showing-connection-status-in-blazor-webassembly/
https://www.neptuo.com/blog/2019/12/blazor-network-status/
https://code-maze.com/use-browser-functionalities-with-blazor-webassembly/
Kebechet
  • 1,461
  • 15
  • 31
5

Firstly, create a hub in your server project.

https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-5.0

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR();
}

public void Configure(IApplicationBuilder app)
{
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<MyHub>("/myhub");
    });
}

MyHub

public class MyHub : Hub
{
}

Then, in your WASM client create a service to manage hub connection:

public class SignalRClientService
{
    HubConnection MyHubConnection;
    NavigationManager NavigationManager;

    public SignalRClientService(NavigationManager navigationManager)
    {
        NavigationManager = navigationManager;

        OpenHub();
    }

    public void OpenHub()
    {
        MyHubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/myhub")
            .Build();

        MyHubConnection.Closed += async (error) =>
        {
            // Do what you need to do ...
            // e.g. 1) Inject this service into your razor component
            //      2) Raise an event from here that connection closed
            //      3) Listen for event in razor component
            //      4) Tell user that connection is closed.

            // You could then try to reinitialize the connection here
            // and raise and event that connection is reestablished.
        }
    }
}

Register service in Program.cs

builder.Services.AddSingleton<SignalRClientService>();
Neil W
  • 7,670
  • 3
  • 28
  • 41
2

Based on @ssamko's great answer, I created a service that provides an event to subscribe to when the connection state is changed, as well as a property that indicates whether there is a connection.

Thanks to this, other services can start working the moment there is a connection available (such as synchronization services).

I have modified the component to use that service.

I have first created the IConnectionStatusDetectorService interface to comply with best practices:

public interface IConnectionStatusDetectorService
{
    event EventHandler<bool>? ConnectionStatusChanged;
    bool IsOnline { get; }
}

I have then implemented the ConnectionStatusDetectorService class:

public class ConnectionStatusDetectorService : IConnectionStatusDetectorService, IAsyncDisposable
{
    private readonly IJSRuntime _jsRuntime;

    public event EventHandler<bool>? ConnectionStatusChanged;
    public bool IsOnline { get; private set; }

    public ConnectionStatusDetectorService(IJSRuntime runtime)
    {
        _jsRuntime = runtime;

        Task.Run(async () =>
        {
            await _jsRuntime.InvokeVoidAsync("Connection.Initialize", DotNetObjectReference.Create(this));
        });
    }

    public async ValueTask DisposeAsync() => await _jsRuntime.InvokeVoidAsync("Connection.Dispose");

    [JSInvokable("Connection.StatusChanged")]
    public void OnConnectionStatusChanged(bool isOnline)
    {           
        if (IsOnline != isOnline)
        {
            IsOnline = isOnline;
        }

        ConnectionStatusChanged?.Invoke(this, isOnline);
    }
}

To use it, register it in Program.cs:

services.AddSingleton<IConnectionStatusDetectorService, ConnectionStatusDetectorService>();

Then, if you want a component like @ssamko's you have to modify it to use the service:

@inject IConnectionStatusDetectorService ConnectionStatus
@implements IDisposable

@if (ConnectionStatus.IsOnline)
{
    @Online
}
else
{
    @Offline
}

@code {
    [Parameter]
    public RenderFragment Online { get; set; }

    [Parameter]
    public RenderFragment Offline { get; set; }

    protected override void OnInitialized() => ConnectionStatus.ConnectionStatusChanged += (_, _) => StateHasChanged();
    public void Dispose() => ConnectionStatus.ConnectionStatusChanged -= (_, _) => StateHasChanged();
}

Finally I've created a NuGet package: Blazor.ConnectionStatusDetector

Add the package to your project:

dotnet add package Blazor.ConnectionStatusDetector --version 1.0.0

To use it, link to connection.js in your index.html file:

<script src="_content/Blazor.ConnectionStatusDetector/connection.js"></script>

Then register the service in yout Program.cs:

services.AddSingleton<IConnectionStatusDetectorService, ConnectionStatusDetectorService>();

To render different fragments based on the connection status, import the namespace in you _Imports.razor file:

@using Blazor.ConnectionStatusDetector

add a Connection component in your razor files, like:

<Connection>
    <Online>
        <h1 style="color: green">Online</h1>
    </Online>
    <Offline>
        <h1 style="color: red">Offline</h1>
    </Offline>
</Connection>
joseangelmt
  • 2,018
  • 18
  • 32
  • 1
    in OnConnectionStatusChanged method ,your code is wrong! it is better code: [JSInvokable("Connection.StatusChanged")] public void OnConnectionStatusChanged(bool isOnline) { if (IsOnline != isOnline) { IsOnline = isOnline; ConnectionStatusChanged?.Invoke(this, isOnline); } } – hamid reza shahshahani Jun 12 '23 at 10:04
  • It's true @hamidrezashahshahani, I've solved it. Thanks. – joseangelmt Jun 12 '23 at 19:25
0

best solution is checked a url on internet with setInterval js function according to this article