Skip to content

Conversation

@ArgoZhang
Copy link
Member

@ArgoZhang ArgoZhang commented Jul 13, 2025

Link issues

fixes #6406

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Add a new network monitoring service with JS interop, refactor the network module and indicator component to use this service, and include sample demos and tests for real-time network state updates.

New Features:

  • Introduce INetworkMonitorService interface and DefaultNetowrkMonitorService implementation to monitor network status and expose current connection details.
  • Add getNetworkInfo JS utility method to retrieve browser network connection data.
  • Enhance NetworkMonitorIndicator component to leverage INetworkMonitorService for registering state change callbacks and implement IDisposable.

Enhancements:

  • Refactor wwwroot/modules/net.js to use registerBootstrapBlazorModule and query indicators by CSS class, removing manual data storage and disposal logic.
  • Register INetworkMonitorService in dependency injection and update service collection extensions.
  • Add sample page and corresponding CSS for NetworkMonitor demos and include a new 'NetworkMonitor' menu item in the site navigation.

Documentation:

  • Update HomeLayout footer text formatting.

Tests:

  • Add unit tests for DefaultNetworkMonitorService.GetNetworkMonitorState.
  • Update NetworkMonitorIndicator tests to verify service callback registration and invocation.

@bb-auto bb-auto bot added the enhancement New feature or request label Jul 13, 2025
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jul 13, 2025

Reviewer's Guide

This PR introduces a new INetworkMonitorService abstraction with a default JS-interop implementation to centralize network state monitoring and callback management, refactors the existing client-side module and indicator component to use the service, and adds sample pages, DI registration, menu integration, and updated tests.

Sequence diagram for registering a network state change callback

sequenceDiagram
    participant Component
    participant INetworkMonitorService
    participant DefaultNetowrkMonitorService
    participant JSRuntime
    participant JS (Browser)

    Component->>INetworkMonitorService: RegisterStateChangedCallback(this, callback)
    INetworkMonitorService->>DefaultNetowrkMonitorService: RegisterStateChangedCallback(this, callback)
    DefaultNetowrkMonitorService->>JSRuntime: LoadModuleByName("net")
    JSRuntime-->>DefaultNetowrkMonitorService: JSModule
    DefaultNetowrkMonitorService->>JS (Browser): init({Invoke, OnNetworkStateChangedCallback})
    JS (Browser)-->>DefaultNetowrkMonitorService: (on network state change) TriggerNetworkStateChanged(state)
    DefaultNetowrkMonitorService->>Component: callback(state)
Loading

ER diagram for NetworkMonitorState changes

erDiagram
    NETWORK_MONITOR_STATE {
        string NetworkType
        double Downlink
        int RTT
    }
Loading

Class diagram for INetworkMonitorService and DefaultNetowrkMonitorService

classDiagram
    class INetworkMonitorService {
        +Task<NetworkMonitorState> GetNetworkMonitorState(CancellationToken token)
        +Task RegisterStateChangedCallback(IComponent component, Func<NetworkMonitorState, Task> callback)
        +void UnregisterStateChangedCallback(IComponent component)
    }
    class DefaultNetowrkMonitorService {
        -JSModule? _module
        -JSModule? _networkModule
        -IJSRuntime _runtime
        -DotNetObjectReference<DefaultNetowrkMonitorService> _interop
        -ConcurrentDictionary<IComponent, Func<NetworkMonitorState, Task>> _callbacks
        -bool _init
        -SemaphoreSlim _semaphoreSlim
        +DefaultNetowrkMonitorService(IJSRuntime jsRuntime)
        +Task<NetworkMonitorState> GetNetworkMonitorState(CancellationToken token)
        +Task RegisterStateChangedCallback(IComponent component, Func<NetworkMonitorState, Task> callback)
        +void UnregisterStateChangedCallback(IComponent component)
        +Task TriggerNetworkStateChanged(NetworkMonitorState state)
        +ValueTask DisposeAsync()
    }
    INetworkMonitorService <|.. DefaultNetowrkMonitorService
Loading

Class diagram for NetworkMonitorIndicator refactor

classDiagram
    class NetworkMonitorIndicator {
        +INetworkMonitorService NetworkMonitorService
        -NetworkMonitorState _state
        -string _networkTypeString
        -string _downlinkString
        -string _rttString
        +Task OnInitializedAsync()
        +Task OnNetworkStateChanged(NetworkMonitorState state)
        +void Dispose()
    }
Loading

File-Level Changes

Change Details Files
Add network monitor service interface and default implementation
  • Define INetworkMonitorService with methods to get state and manage callbacks
  • Implement DefaultNetworkMonitorService using JSModule for getNetworkInfo and init
  • Maintain a concurrent callback registry and invoke callbacks via JSInvokable method
  • Implement disposal of JS modules and interop reference
src/BootstrapBlazor/Services/INetworkMonitorService.cs
src/BootstrapBlazor/Services/DefaultNetworkMonitorService.cs
Refactor client-side JS modules for network monitoring
  • Replace manual Data store and dispose logic with registerBootstrapBlazorModule
  • Simplify init signature and use querySelectorAll('.bb-nt-indicator') for indicators
  • Remove id-based DOM lookups and dispose function
  • Add getNetworkInfo helper in utility.js
src/BootstrapBlazor/wwwroot/modules/net.js
src/BootstrapBlazor/wwwroot/modules/utility.js
Update NetworkMonitorIndicator component to use the new service
  • Inject INetworkMonitorService and switch to async OnInitializedAsync
  • Register and unregister state-changed callbacks instead of nested NetworkMonitor component
  • Implement IDisposable to clean up callbacks
  • Convert OnNetworkStateChanged to async and use InvokeAsync(StateHasChanged)
src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorIndicator.razor.cs
src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorIndicator.razor
src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitor.cs
Register service in DI and integrate menu/layout
  • Add INetworkMonitorService in BootstrapBlazorServiceCollectionExtensions
  • Insert new 'NetworkMonitor' menu item in MenusLocalizerExtensions
  • Adjust footer text in HomeLayout.razor
src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs
src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs
src/BootstrapBlazor.Server/Components/Layout/HomeLayout.razor
Add sample page and styles for network monitor demo
  • Create NetworkMonitors.razor sample with console logger
  • Implement code-behind to register/unregister service callbacks
  • Add accompanying CSS for demo indicators
src/BootstrapBlazor.Server/Components/Samples/NetworkMonitors.razor
src/BootstrapBlazor.Server/Components/Samples/NetworkMonitors.razor.cs
src/BootstrapBlazor.Server/Components/Samples/NetworkMonitors.razor.css
Update and add unit tests for indicator and service
  • Simplify NetworkMonitorIndicatorTest to synchronous assertion
  • Rename and extend indicator test to cover service registration
  • Add DefaultNetworkMonitorServiceTest to verify getNetworkInfo interop
test/UnitTest/Components/NetworkMonitorIndicatorTest.cs
test/UnitTest/Services/DefaultNetworkMonitorServiceTest.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#6406 Add INetworkMonitorService service to the BootstrapBlazor library.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@bb-auto bb-auto bot added this to the 9.8.0 milestone Jul 13, 2025
@ArgoZhang ArgoZhang merged commit 8daec76 into main Jul 13, 2025
3 of 4 checks passed
@ArgoZhang ArgoZhang deleted the doc-net branch July 13, 2025 09:38
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ArgoZhang - I've reviewed your changes - here's some feedback:

  • Rename the typo in DefaultNetowrkMonitorService to DefaultNetworkMonitorService for consistent naming across the API and file names.
  • Unify the casing of the RTT property in getNetworkInfo (rTT vs rtt) to match the NetworkMonitorState model and avoid potential mapping issues.
  • The _indicators field in NetworkMonitorIndicator is no longer used after moving to the service-based model—consider removing it to clean up unused state.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Rename the typo in DefaultNetowrkMonitorService to DefaultNetworkMonitorService for consistent naming across the API and file names.
- Unify the casing of the RTT property in getNetworkInfo (rTT vs rtt) to match the NetworkMonitorState model and avoid potential mapping issues.
- The _indicators field in NetworkMonitorIndicator is no longer used after moving to the service-based model—consider removing it to clean up unused state.

## Individual Comments

### Comment 1
<location> `src/BootstrapBlazor/wwwroot/modules/net.js:29` </location>
<code_context>
-    Data.set(id, {
-        onlineStateChanged,
-        offlineStateChanged
+    registerBootstrapBlazorModule("NetworkMonitor", null, () => {
+        EventHandler.on(window, 'online', onlineStateChanged);
+        EventHandler.on(window, 'offline', offlineStateChanged);
</code_context>

<issue_to_address>
Event handler registration is now deferred to module registration.

Please verify that deferring event handler registration does not cause race conditions or missed events if the module registers after the page load.
</issue_to_address>

### Comment 2
<location> `src/BootstrapBlazor/wwwroot/modules/net.js:49` </location>
<code_context>
     updateState(navigator.connection);
 }
-
-export async function dispose(id) {
-    const nt = Data.get(id);
-    Data.remove(id);
-
-    if (nt) {
-        const { onlineStateChanged, offlineStateChanged } = nt;
-        EventHandler.off(window, 'online', onlineStateChanged);
-        EventHandler.off(window, 'offline', offlineStateChanged);
-    }
-}
</code_context>

<issue_to_address>
The 'dispose' function has been removed, which may lead to event handler leaks.

Without 'dispose', 'online' and 'offline' event handlers are never unregistered, which can cause memory leaks if the module is re-initialized. Please assess if a cleanup method is still required.
</issue_to_address>

### Comment 3
<location> `test/UnitTest/Components/NetworkMonitorIndicatorTest.cs:11` </location>
<code_context>
 {
     [Fact]
-    public async Task NetworkMonitorIndicator_Ok()
+    public void NetworkMonitorIndicator_Ok()
     {
         var cut = Context.RenderComponent<NetworkMonitorIndicator>(pb =>
</code_context>

<issue_to_address>
Test method NetworkMonitorIndicator_Ok is now synchronous but previously tested async state changes.

Ensure tests still simulate network state changes and verify UI updates, as the previous async checks are no longer present.
</issue_to_address>

### Comment 4
<location> `src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorIndicator.razor.cs:86` </location>
<code_context>
+        return Task.CompletedTask;
+    }
+
+    private void Dispose(bool disposing)
+    {
+        if (disposing)
</code_context>

<issue_to_address>
Consider simplifying the Dispose implementation by removing the Dispose(bool) pattern and finalizer suppression, and directly unregistering the callback in Dispose().

```csharp
// Remove the full Dispose(bool) pattern and finalizer suppression.
// Just implement IDisposable.Dispose() and call UnregisterStateChangedCallback.

public partial class NetworkMonitorIndicator : IDisposable
{
    // ... existing code ...

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        await NetworkMonitorService.RegisterStateChangedCallback(this, OnNetworkStateChanged);
    }

    private async Task OnNetworkStateChanged(NetworkMonitorState state)
    {
        _state = state;
        await InvokeAsync(StateHasChanged);
    }

    // Replace this:
    // private void Dispose(bool disposing)
    // {
    //     if (disposing)
    //     {
    //         NetworkMonitorService.UnregisterStateChangedCallback(this);
    //     }
    // }
    //
    // public void Dispose()
    // {
    //     Dispose(true);
    //     GC.SuppressFinalize(this);
    // }

    // With this:
    public void Dispose()
    {
        NetworkMonitorService.UnregisterStateChangedCallback(this);
    }
}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Data.set(id, {
onlineStateChanged,
offlineStateChanged
registerBootstrapBlazorModule("NetworkMonitor", null, () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Event handler registration is now deferred to module registration.

Please verify that deferring event handler registration does not cause race conditions or missed events if the module registers after the page load.

navigator.connection.onchange = e => {
updateState(e.target);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The 'dispose' function has been removed, which may lead to event handler leaks.

Without 'dispose', 'online' and 'offline' event handlers are never unregistered, which can cause memory leaks if the module is re-initialized. Please assess if a cleanup method is still required.

{
[Fact]
public async Task NetworkMonitorIndicator_Ok()
public void NetworkMonitorIndicator_Ok()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): Test method NetworkMonitorIndicator_Ok is now synchronous but previously tested async state changes.

Ensure tests still simulate network state changes and verify UI updates, as the previous async checks are no longer present.

await InvokeAsync(StateHasChanged);
}

private void Dispose(bool disposing)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider simplifying the Dispose implementation by removing the Dispose(bool) pattern and finalizer suppression, and directly unregistering the callback in Dispose().

// Remove the full Dispose(bool) pattern and finalizer suppression.
// Just implement IDisposable.Dispose() and call UnregisterStateChangedCallback.

public partial class NetworkMonitorIndicator : IDisposable
{
    // ... existing code ...

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        await NetworkMonitorService.RegisterStateChangedCallback(this, OnNetworkStateChanged);
    }

    private async Task OnNetworkStateChanged(NetworkMonitorState state)
    {
        _state = state;
        await InvokeAsync(StateHasChanged);
    }

    // Replace this:
    // private void Dispose(bool disposing)
    // {
    //     if (disposing)
    //     {
    //         NetworkMonitorService.UnregisterStateChangedCallback(this);
    //     }
    // }
    //
    // public void Dispose()
    // {
    //     Dispose(true);
    //     GC.SuppressFinalize(this);
    // }

    // With this:
    public void Dispose()
    {
        NetworkMonitorService.UnregisterStateChangedCallback(this);
    }
}

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(INetworkMonitorService): add INetworkMonitorService service

2 participants