8

I am making a chatroom application by Blazor server-side. I want to show the online state of each user.

I have asked a question for how to get the event while closing the page in How can I get the event while page close in blazor server-side?

Now it seems the CircuitHandler is the best choice for this.

When the user closes the page, I want to set a user state from online to offline in the database. And also, the primary key of each user is temporarily storing in the index.razor.

But now after the OnCircuitClosedAsync(Circuit, CancellationToken) runs, I don't know how to invoke a method to achieve this (I can't get the variable of Blazor front-end or invoke the Blazor method of front-end).

PS: Here is the code of the back-end:

using Microsoft.AspNetCore.Components.Server.Circuits;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;


namespace BlazorCircuitHandler.Services
{
    public class CircuitHandlerService : CircuitHandler
    {
        public ConcurrentDictionary<string, Circuit> Circuits { get; set; }

        public CircuitHandlerService()
        {
            Circuits = new ConcurrentDictionary<string, Circuit>();
        }

        public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)
        {
            Circuits[circuit.Id] = circuit;
            return base.OnCircuitOpenedAsync(circuit, cancellationToken);
        }

        public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken)
        {
            Circuit circuitRemoved;
            Circuits.TryRemove(circuit.Id, out circuitRemoved);
            return base.OnCircuitClosedAsync(circuit, cancellationToken);
        }

        public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
        {
            return base.OnConnectionDownAsync(circuit, cancellationToken);
        }

        public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
        {
            return base.OnConnectionUpAsync(circuit, cancellationToken);
        }
    }
}

And here is the front-end:

@page "/"

@using Microsoft.AspNetCore.Components.Server.Circuits
@inject CircuitHandler CircuitHandlerService

<h1>Hello, world!</h1>

Welcome to your new app.

<p>
    Number of Circuits: @((CircuitHandlerService as BlazorCircuitHandler.Services.CircuitHandlerService).Circuits.Count)

    <ul>
        @foreach (var circuit in (CircuitHandlerService as BlazorCircuitHandler.Services.CircuitHandlerService).Circuits)
        {
            <li>@circuit.Key</li>            
        }
    </ul>
    @{ 
        var PrimaryKey = "abcdefg";
    }
</p>

Would you please help me? Thank you.

poke
  • 369,085
  • 72
  • 557
  • 602
Melon NG
  • 2,568
  • 6
  • 27
  • 52

2 Answers2

12

This should be working, I guess ;)

CircuitHandlerService.cs

using Microsoft.AspNetCore.Components.Server.Circuits;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;


namespace BlazorCircuitHandler.Services
{
    public class CircuitHandlerService : CircuitHandler
    {
        public ConcurrentDictionary<string, Circuit> Circuits { get; 
            set; }
        public event EventHandler CircuitsChanged;

        protected virtual void OnCircuitsChanged()
             => CircuitsChanged?.Invoke(this, EventArgs.Empty);

        public CircuitHandlerService()
        {
             Circuits = new ConcurrentDictionary<string, Circuit>();
        }

        public override Task OnCircuitOpenedAsync(Circuit circuit, 
                             CancellationToken cancellationToken)
       {
             Circuits[circuit.Id] = circuit;
             OnCircuitsChanged();
             return base.OnCircuitOpenedAsync(circuit, 
                                   cancellationToken);
       }

       public override Task OnCircuitClosedAsync(Circuit circuit, 
                 CancellationToken cancellationToken)
      {
           Circuit circuitRemoved;
           Circuits.TryRemove(circuit.Id, out circuitRemoved);
           OnCircuitsChanged();
           return base.OnCircuitClosedAsync(circuit, 
                             cancellationToken);
      }

      public override Task OnConnectionDownAsync(Circuit circuit, 
                            CancellationToken cancellationToken)
      {
          return base.OnConnectionDownAsync(circuit, 
                           cancellationToken);
      }

      public override Task OnConnectionUpAsync(Circuit circuit, 
                          CancellationToken cancellationToken)
      {
          return base.OnConnectionUpAsync(circuit, cancellationToken);
      }
   }
 }

Usage

@page "/"

@using Microsoft.AspNetCore.Components.Server.Circuits
@using BlazorCircuitHandler.Services

@inject CircuitHandler circuitHandler
@implements IDisposable



<h1>Hello, world!</h1>

Welcome to your new app.

<p>
 Number of Circuits: @((circuitHandler as 
 BlazorCircuitHandler.Services.CircuitHandlerService).Circuits.Count)
 <ul>
    @foreach (var circuit in (circuitHandler as 
     BlazorCircuitHandler.Services.CircuitHandlerService).Circuits)
    {
        <li>@circuit.Key</li>
    }
 </ul>
</p>

@code {
   protected override void OnInitialized()
   {
       // Subscribe to the event handler
    (circuitHandler as CircuitHandlerService).CircuitsChanged += 
         HandleCircuitsChanged;
    
    }

 public void Dispose()
 {
    // Unsubscribe the event handler when the component is disposed
    (circuitHandler as CircuitHandlerService).CircuitsChanged -= 
      HandleCircuitsChanged;
   
 }

 public void HandleCircuitsChanged(object sender, EventArgs args)
 {
    // notify the component that its state has changed 
    // Important: You must use InvokeAsync
    InvokeAsync(() => StateHasChanged());
 }
}

Startup.cs

public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor();

            services.AddSingleton<CircuitHandler>(new CircuitHandlerService());
        }

Note: To verify that it is working run the app. Then open another two tabs. Now, close the first tab you open (left to right), then the second. Notice the display of the number of active circuits...

enet
  • 41,195
  • 5
  • 76
  • 113
  • Now it works well. However, I am still being curious about why Dispose method invokes while page just loaded. – Melon NG Jan 31 '20 at 13:48
  • I'll have to investigate it... However, this is only a guess, I'll have to verify it later: This is because render-mode attribute to the component tag helper is set to "ServerPrerendered" in the _Host.cshtml file. You can set it to "Server" instead like this: . Now I believe that the Dispose method won't be invoked this time as pre-rendering was deactivated. – enet Jan 31 '20 at 14:00
  • Does this solution work as of today? I'm getting 'There's no registered service of type CircuitHandlerService when I try to inject it... (of course, it is registered as Singleton in Startup.cs) – Vi100 Jun 04 '20 at 00:23
  • 3
    This method looks good to determine when a user disconnects from the server. I am wondering if it can also be useful to determine 'which' user disconnected from the server. For example is it possible to correlate the circuitKey with a given user? In my application I am interested to know when a given user disconnects, so I can remove them from my database. – Jason D Dec 02 '20 at 12:37
  • @JasonD did you figure out how to find the disconnected user belonging to the circuit? – rfcdejong Mar 10 '21 at 08:59
  • 1
    @rfcdejong, i posted my solution here: https://stackoverflow.com/a/66581485/10787774. Hope it can answer your question. – Jason D Mar 11 '21 at 11:19
1

You could just expose an event within your circuit handler:

public class CircuitHandlerService : CircuitHandler
{
    public event EventHandler CircuitsChanged;

    protected virtual void OnCircuitsChanged()
        => CircuitsChanged?.Invoke(this, EventArgs.Empty);

    public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        Circuits[circuit.Id] = circuit;
        OnCircuitsChanged();
        return Task.CompletedTask;
    }

    public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        Circuits.TryRemove(circuit.Id, out var circuitRemoved);
        OnCircuitsChanged();
        return Task.CompletedTask;
    }
}

And then, in your Razor component, you can subscribe to that event and refresh the UI as necessary:

@implements IDisposable
@inject CircuitHandler circuitHandler

<ul>
    @foreach (var circuit in Circuits)
    {
        <li>@circuit.Key</li>
    }
</ul>

@code {
    private IDictionary<string, Circuit> Circuits
        => (circuitHandler as CircuitHandlerService).Circuits;

    protected override void OnInitialized()
    {
        // register event handler
        (circuitHandler as CircuitHandlerService).CircuitsChanged += HandleCircuitsChanged;
    }

    public void Dispose()
    {
        // unregister the event handler when the component is destroyed
        (circuitHandler as CircuitHandlerService).CircuitsChanged -= HandleCircuitsChanged;
    }

    public void HandleCircuitsChanged(object sender, EventArgs args)
    {
        // notify the UI that the state has changed
        StateHasChanged();
    }
}
poke
  • 369,085
  • 72
  • 557
  • 602
  • It works. However, I add a breakpoint in OnInitialized/Dispose/HandleCircuitsChanged. While page just loaded, it will invoke the breakpoint of OnInitialized first and then invoke the Dispose. I don't know it will invoke the breakpoint of Dispose while just loaded. – Melon NG Jan 31 '20 at 12:46
  • And also, the breakpoint of HandleCircuitsChanged seems never invoke no matter I open a new page or close a exist page. – Melon NG Jan 31 '20 at 12:47