Articoli

Event aggregator in Blazor

da

Avere un accentratore di eventi, è estremamente utile quando si devono aggiornare diversi elementi dell’applicazione e magari da più fonti. Ma cos’è un event aggregator?

Event aggregator pattern

Quello che viene chiamato event aggregator è l’estensione del più noto pattern conosciuto come publisher-subscriber. In buona sostanza l’aggregatore di eventi è un punto centrale dell’applicazione atto a raccogliere tutti gli eventi emessi dai publishers e li rilancia a tutti i subscribers che si sono registrati per quel determinato evento.

Questo ci permette di disaccoppiare i publishers dai subscribers e magari anche comporre un evento verso i subscribers a seconda di più eventi di pubblicazione. Ci permette anche di rendere agnostico ogni subscriber senza fargli conoscere il o i publishers che generano uno o più eventi. Ci sono però delle controindicazioni: avere un solo accentratore di eventi ci espone ad un collo di bottiglia nella nostra applicazione per cui potrebbe essere più saggio suddividerlo in più accentratori a seconda del contesto.

Implementazione

Ora che conosciamo la teoria, possiamo implementare una prima versione dell’event-aggregator in C#.

Creiamo un’interfaccia, in modo da definire quali metodi vogliamo esporre, che chiameremo IEventAggregator con un eccesso di fantasia.

public interface IEventAggregator
{
    /// <summary>
    /// Publishes the specified event.
    /// </summary>
    /// <typeparam name="TEventType">The type of the event type.</typeparam>
    /// <param name="eventToPublish">The event to publish.</param>
    void Publish<TEventType>(TEventType eventToPublish);

    /// <summary>
    /// Register a subscribe to specified events.
    /// </summary>
    /// <param name="subscriber">The subscriber.</param>
    void Subscribe(Object subscriber);
}

Prima di passare all’implementazione, mi soffermerei sul parametro TEventType. Per poter distinguere tra un evento e l’altro, dovremmo “passare” al metodo Publish un certo tipo, ovvero una classe o un record. Nel nostro esempio creeremo due classi per trasportare due diversi valori:

  1. TimeValueChanged per trasportare un valore di DateTime sotto forma di stringa
  2. CounterValueChanged per trasportare il nuovo valore di un counter che creeremo

che verranno usare in fase di pubblicazione dell’evento da parte dei publisher.

Implementiamo l’EventAggregator

Creiamo a questo punto il nostro EventAggregator sotto forma di classe. Prenderemo anche in prestito una classe del framework poco usata: la classe WeakReference in modo da evitare un accoppiamento forte.

public class EventAggregator : IEventAggregator
{
    private readonly Dictionary<Type, List<WeakReference>> subscribersListByType = new Dictionary<Type, List<WeakReference>>();
    private readonly object lockSubscriberDictionary = new object();

    #region IEventAggregator
    public void Publish<TEventType>(TEventType eventToPublish)
    {
        var subsriberType = typeof(ISubscriber<>).MakeGenericType(typeof(TEventType));
        var subscribers = GetSubscriberList(subsriberType);
        List<WeakReference> subsribersToBeRemoved = new List<WeakReference>();
        foreach (var weakSubsriber in subscribers)
        {
            if (weakSubsriber.IsAlive)
            {
                var subscriber = (ISubscriber<TEventType>)weakSubsriber.Target;
                InvokeSubscriberEvent<TEventType>(eventToPublish, subscriber);
            }
            else
            {
                subsribersToBeRemoved.Add(weakSubsriber);
            }
        }

        if (subsribersToBeRemoved.Any())
        {
            lock (lockSubscriberDictionary)
            {
                foreach (var remove in subsribersToBeRemoved)
                {
                    subscribers.Remove(remove);
                }
            }
        }
    }

    public void Subscribe(object subscriber)
    {
        lock (lockSubscriberDictionary)
        {
            var subsriberTypes = subscriber.GetType().GetInterfaces()
                                    .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISubscriber<>));
            WeakReference weakReference = new WeakReference(subscriber);
            foreach (var subsriberType in subsriberTypes)
            {
                List<WeakReference> subscribers = GetSubscriberList(subsriberType);
                subscribers.Add(weakReference);
            }
        }
    }
    #endregion

    private void InvokeSubscriberEvent<TEventType>(TEventType eventToPublish, ISubscriber<TEventType> subscriber)
    {
        //Synchronize the invocation of method 
        SynchronizationContext syncContext;
        if (SynchronizationContext.Current is null)
        {
            syncContext = new SynchronizationContext();
        }
        else
        {
            syncContext = SynchronizationContext.Current;
        }
        syncContext.Post(s => subscriber.OnEventRaised(eventToPublish), null);
    }

    private List<WeakReference> GetSubscriberList(Type subsriberType)
    {
        List<WeakReference>? subsribersList = null;
        lock (lockSubscriberDictionary)
        {
            bool found = subscribersListByType.TryGetValue(subsriberType, out subsribersList);
            if (!found)
            {
                //First time create the list.
                subsribersList = new List<WeakReference>();
                subscribersListByType.Add(subsriberType, subsribersList);
            }
        }
        return subsribersList;
    }
}

In questa classe, oltre a pubblicare gli eventi in arrivo dai publishers, avremo una lista di subscribers suddivisa per tipo di dato che il subscriber intende sorvegliare.

Creiamo il progetto

Come progetto Blazor per l’implementazione di prova di tutto quanto esposto prima, creeremo un progetto Blazor Server solo per semplicità di esposizione dei concetti e per l’implementazione che ci nasconde la complessità delle notifiche.

Per poter visualizzare la data corrente ed i secondi, dobbiamo modificare le pagine sia MainLayout che Index in questo modo:

@page "/"
<h1>Hello, Event aggregator!</h1>
<h2>Blazor developers Italiani</h2>
<p>@DateTime</p>

e ovviamente nella parte di codice della pagina, Index.razor.cs, definiamo la variabile che servirà per visualizzare la data e l’ora.

public partial class Index
{
    [CascadingParameter] public string DateTime { get; set; }
}

Mentre la parte di MainLayout sarà fatto nel sequente modo:

@using BlazorAppServerEventAggregator.Models;
@using BlazorAppServerEventAggregator.Services;
@inherits LayoutComponentBase
@inject EventAggregatorService eventAggregatorService
@inject DateTimeManager dateTimeManager
<main>
    <CascadingValue Value="@ActualDateTime">
        @Body
    </CascadingValue>
</main>

Mentre la parte di codice

public partial class MainLayout
{
    /// <summary>
    /// Gets or sets the actual date time.
    /// </summary>
    /// <value>
    /// The actual date time.
    /// </value>
    public string ActualDateTime { get; set; }

    protected override Task OnInitializedAsync()
    {
        eventAggregatorService.OnTimeSecondsChanged += EventAggregatorService_OnTimeSecondsChanged;
        return base.OnInitializedAsync();
    }

    /// <summary>
    /// Events the aggregator service, on time seconds changed.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The e.</param>
    private void EventAggregatorService_OnTimeSecondsChanged(object? sender, string e)
    {
        ActualDateTime = e;
        InvokeAsync(() => StateHasChanged());
    }
}

EventAggregatorService

Avrete sicuramente notato l’istanza all’EventAggregatorService di cui non abbiamo ancora parlato.

Per poter consumare gli eventi provenienti dall’EventAggregator dobbiamo crearci un servizio che riceva gli eventi e li ritrasmetta alle pagine. Questo servizio deve implementare l’interfaccia ISubscriber<T> dove T è la classe di trasporto alla quale vogliamo abbonarci. Potremmo avere anche più di un servizio ciascuno che implementa una sottoscrizione diversa. Questo potrebbe servire ad alleggerire il traffico proveniente dall’EventAggregator che potrebbe essere suddiviso anche lui in diverse parti a seconda dei publisher. Ma questo esula dallo scopo di questo articolo e magari potremmo continuarlo in un prossimo articolo.

Il nostro servizio sarà fatto così:

public class EventAggregatorService : ISubscriber<TimeValueChanged>
{
    private readonly IEventAggregator eventAggregator;
    public event EventHandler<string>? OnTimeSecondsChanged;

    void ISubscriber<TimeValueChanged>.OnEventRaised(TimeValueChanged e)
    {
        OnTimeSecondsChanged?.Invoke(this, e.Value);
    }

    public EventAggregatorService(IEventAggregator ea)
    {
        eventAggregator = ea;
        eventAggregator.Subscribe(this);
    }
}

Questo sarà il servizio che andremo a iniettare tramite dependency injection nelle pagine razor che visualizzeranno i dati.

Questa è una prima implementazione che prende i dati da un servizio e li espone a video su una pagina. Ovviamente è un primo step che dovrebbe essere espanso e implementato in modo oculato per un’applicazione complessa. Il codice come al solito lo trovate qui su GitHub. Non esitate a fare commenti e pull requests sul codice.