Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 12 de mayo de 2020
Blazor En Blazor Server, cada usuario que esté utilizando la aplicación establece una conexión SignalR para enviar al servidor sus interacciones con la página, así como para retornar de vuelta al usuario las modificaciones del DOM realizadas desde el servidor. Esta conexión, junto con la información del estado del cliente almacenada en el servidor, es lo que en Blazor Server se denomina un "circuito".

Y ya que el servidor es consciente en todo momento de los usuarios conectados, en teoría debería ser posible contarlos para, por ejemplo, mostrarlos en pantalla como en la siguiente captura:

Aplicación Blazor mostrando el número de usuarios conectados

En este post vamos a ver cómo conseguir precisamente eso: incluir en las páginas del proyecto un contador de usuarios conectados que irá actualizándose en tiempo real, para, por el camino, profundizar un poco en el ciclo de vida de los circuitos de Blazor Server.

1. El servicio contador de usuarios

Antes de profundizar en las particularidades de Blazor Server, vamos a implementar un servicio bastante sencillito, que nos ayudará a mantener en memoria el contador de usuarios conectados y nos ofrecerá un par de métodos para gestionar los cambios.

El código, como se puede comprobar, habla por sí mismo:
public class UserCountService
{
    public event EventHandler<int> OnChanged;
    private int _counter = 0;
    public int CurrentUsersCount => _counter;

    public void Increment()
    {
        Interlocked.Increment(ref _counter);
        OnChanged?.Invoke(this, _counter);
    }

    public void Decrement()
    {
        Interlocked.Decrement(ref _counter);
        OnChanged?.Invoke(this, _counter);
    }
}
No hay nada más allá de lo normal, aunque quizás lo que más os llame la atención la definición del evento OnChanged que utilizamos para notificar a los consumidores de este servicio de que se han producido cambios en el número de usuarios conectados. Más adelante veremos por qué es necesario esto.

Este servicio deberíamos registrarlo en el método ConfigureServices() como Singleton, pues sólo necesitaremos una instancia compartida durante todo el tiempo de vida de la aplicación:
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<UserCountService>()
    ... // More service registrations here
}

2. ¿Cómo nos enteramos cuando un usuario conecta o desconecta?

Blazor permite introducir lógica personalizada en el ciclo de vida de los circuitos, gracias a la abstracción CircuitHandler. Esta clase abstracta define cuatro métodos que permiten tomar el control en distintos momentos:
  • OnCircuitOpenedAsync(): invocado cuando nuevo circuito es creado
  • OnConnectionUpAsync(): cuando la conexión ha sido establecida o restablecida
  • OnConnectionDownAsync(): la conexión con un cliente se cerró
  • OnCircuitClosedAsync(): notifica que el circuito ha sido eliminado
En nuestro caso, simplemente heredaremos de CircuitHandler e implementaremos OnCircuitOpenedAsync() y OnCircuitClosedAsync() para incrementar y decrementar, respectivamente, el contador de usuarios conectados, utilizando para ello el servicio que hemos creado algo más arriba:
public class UserCountCircuitHandler: CircuitHandler
{
    private readonly UserCountService _userCountService;

    public UserCountCircuitHandler(UserCountService userCountService)
    {
        _userCountService = userCountService;
    }

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

    public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        _userCountService.Decrement();
        return base.OnCircuitClosedAsync(circuit, cancellationToken);
    }
}
Para hacer que Blazor Server conozca la existencia de nuestra clase UserCountCircuitHandler es neceario registrarla en el inyector de dependencias asociada a la clase abstracta CircuitHandler, de nuevo como Singleton porque sólo necesitaremos una instancia:
services.AddSingleton<CircuitHandler, UserCountCircuitHandler>();
Durante los cambios de estado del circuito, Blazor Server invocará los métodos correspondientes en todas las clases registradas como CircuitHandler.

Con lo que hemos desarrollado hasta este momento, nuestra aplicación Blazor Server ya irá registrando las aperturas y cierres de circuitos, manteniendo el contador de usuarios sincronizado. Veamos ahora cómo mostrar esta información y actualizarla en tiempo real sobre la página.

3. ¿Cómo mostramos los usuarios conectados?

Sin duda, lo más sencillo es crear un componente Blazor reutilizable que muestre en todo momento del valor de la propiedad CurrentUsersCount del servicio UserCountService, algo como lo siguiente:
@* File: UserCount.razor *@
@inject UserCountService UserCountService

<span @attributes="Attributes">
    @UserCountService.CurrentUsersCount
</span>

@code {

    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object> Attributes { get; set; }
}
Observad que estamos utilizando CaptureUnmatchedValues en el parámetro Attributes, con objeto de expandir estos atributos en la etiqueta <span>. Puedes leer más sobre ello en el post "Capturar todos los parámetros enviados a un componente Blazor".
De esta forma, podríamos utilizarlo desde cualquier componente de la siguiente forma:
<UserCount style="font-weight: bold" />
Sin probamos el código anterior, veremos que funcionará bien... al menos la primera vez. Si abrimos varias ventanas, podremos comprobar que este valor no se va modificando en tiempo real conforme se vayan abriendo nuevos circuitos.

Pero si lo pensamos, tiene su lógica: UserCountService.CurrentUsersCount es modificado de forma asíncrona por otros hilos cuando otros circuitos cambian su estado, y la interfaz de usuario no es notificada para que actualice el valor. Por tanto, el contador que veremos siempre en pantalla será el valor inicial del contador en el momento de cargar el componente.

4. Actualizar los usuarios conectados en tiempo real

¿Y cómo podemos notificar a la interfaz de usuario que el valor ha cambiado? Pues aquí es donde entra en juego el evento OnChanged que recordaréis que habíamos declarado en UserCountService y que lanzábamos cada vez que el contador era modificado:
public class UserCountService
{
    public event EventHandler<int> OnChanged;
    ...
    public void Increment()
    {
        ...
        OnChanged?.Invoke(this, _counter);
    }

    public void Decrement()
    {
        ...
        OnChanged?.Invoke(this, _counter);
    }
}
Para ser notificado de los cambios, el componente UserCount.razor debe suscribirse a este evento, y utilizarlo para refrescar la interfaz. El siguiente código muestra el código ya completo, y lo comentamos justo abajo:
@* File: UserCount.razor *@
@implements IDisposable
@inject UserCountService UserCountService

<span @attributes="Attributes">
    @UserCountService.CurrentCount
</span>

@code {

    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object> Attributes { get; set; }

    protected override void OnInitialized()
    {
        UserCountService.OnChanged += UpdateCounter;
        base.OnInitialized();
    }

    private async void UpdateCounter(object sender, int counter)
    {
        await InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        UserCountService.OnChanged -= UpdateCounter;
    }
}
Lo primero a tener en cuenta, es que en OnInitialized() nos suscribimos al evento, pero debemos desuscribirnos cuando éste sea eliminado para evitar fugas de memoria. Para ello, hacemos que implemente IDisposable usando la directiva @implements, y utilizamos Dispose() para eliminar la suscripción.

Por otra parte, observaréis que hemos implementado en UpdateCounter() el handler del evento lanzado cuando se producen cambios. En su interior, vemos que mediante InvokeAsync() aseguramos que el contexto de sincronización de Blazor, y en él ejecutamos StateHasChanged() para notificar al componente que su estado cambió, por lo que debe refrescar la interfaz.

¡Y eso es todo! Una vez unidas las piezas ya tendremos nuestro flamante componente <UserCount> capaz de mostrar en tiempo real el número de usuarios conectados a nuestra aplicación Blazor Server.

Blazor counter en acción

Publicado en Variable not found.

2 Comentarios:

Esther dijo...

Está genial el ejemplo, yo lo que quisiera hacer es obtener al mismo tiempo una Lista de los usarios conectados, pero no se cómo.

José María Aguilar dijo...

Hola,

ese es un tema distinto y el framework no ofrece herramientas para ello.

Aunque no es algo excesivamente complicado, tiene su trabajo. La historia sería mantener en la memoria del servidor una lista para almacenar todos los usuarios conectados; ya luego, cada usuario debería añadirse o eliminarse al entrar y salir de la aplicación. Pero ya te digo, es un tema totalmente personalizado.

Saludos!