Articoli

Un componente per visualizzare data e ora

da

Ogni tanto capita di dover visualizzare un orologio sulla nostra pagina Blazor. Ed è quello che è successo a me qualche tempo fa. Mi sono, per cui, addentrato in un ginepraio. Ma andiamo per gradi.

Visualizzare l’ora

Se vogliamo solo visualizzare l’ora a video la cosa non è molto complicata. Creiamo un’applicazione console e mettiamo questo codice:

Task.Run(async() =>
{
    while (true)
    {
        Console.WriteLine(DateTime.Now.Hour.ToString("00" + ":" + DateTime.Now.Minute.ToString("00")));
        await Task.Delay(1000);
    }
});
Console.ReadLine();

Ecco. Sostituite la Console.WriteLine con qualunque output a video e… voilà! l’orologio è servito. Articolo finito e viviamo tutti tranquilli.

No… non è vero. Se siete come me, non vi accontenterete.

Aggiungiamo qualcosa: i secondi

Beh… non è molto complesso aggiungere i secondi. Però, una volta fatto, ci accorgiamo subito che un Task.Run con un ciclo infinito è molto dispendioso a livello di CPU. Soprattutto se abbassiamo il Task.Delay a 1 secondo per poter visualizzare anche i secondi.

Ma noi che siamo bravi, utilizziamo un bel Timer per gestire lo scadere dei secondi. Possiamo fare in questo modo: creiamo una classe SystemWatch in modo da separare la gestione del timer in vista del nostro componente.

public event EventHandler<DateTime>? SecondChangedEvent;      
private static System.Timers.Timer? aTimer;
        public SystemWatch(int interval = 10)
        {
            // Create a timer and set a two second interval.
            if (aTimer == null)
            {
                aTimer = new System.Timers.Timer();
                aTimer.Interval = interval;

                // Hook up the Elapsed event for the timer. 
                aTimer.Elapsed += ATimer_Elapsed!;

                // Have the timer fire repeated events (true is the default)
                aTimer.AutoReset = true;

                // Start the timer
                aTimer.Enabled = true;
            }
        }

        private void ATimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            SecondChangedEvent?.Invoke(this, e);
        }

Questo ci servirà per rilanciare l’evento ad ogni secondo per essere propagato all’interno dell’applicazione. Aggiungiamo la classe a Program.cs come singleton e lo passiamo al nostro componente Blazor. Otterremo questo:

Diciamo che non è male, ma possiamo fare di meglio.

Blinking

Come ogni orologio che si rispetti, il separatore tra le ore e i minuti dovrebbe lampeggiare. Quindi facciamo ancora qualche modifica al nostro componente proprio per questo scopo.

La prima modifica è aggiungere due campi

    private string separator = ":";
    private bool separatorActive = false;

la prima separator è il nostro separatore tra le ore e i minuti. Possiamo scegliere qualunque carattere, ed io ho scelto il due punti. La seconda sta ad indicare se il separatore dovrà essere visibile o no.

Ricordiamoci, però, di aggiungere la callback per l’evento sollevato dalla nostra classe SystemWatch. Questo si traduce nell’aggiungere watch.SecondChangedEvent += Sw_SecondChangedEvent; nel OnInitialize del nostro componente.

La callback conterrà questo:

private void Sw_SecondChangedEvent(object? sender, DateTime e)
{
    systemWatch = e.ToString("HH:mm ss", Culture);
    separatorActive = e.Second % 2 == 0;
}

A questo punto dobbiamo separare l’orario in due parti: la parte a sinistra del separatore e la parte destra. Dobbiamo separarle ed andare a sostituire quel separatore per poterlo far lampeggiare.

    private void FormatClock()
    {
        var regex = new Regex(Regex.Escape(":"));
        systemWatch = regex.Replace(systemWatch, separator, 1);

        watchRightPart = systemWatch.Substring(systemWatch.IndexOf(":", StringComparison.Ordinal) + 1);
        watchLeftPart = systemWatch.Substring(0, systemWatch.IndexOf(":", StringComparison.CurrentCulture));
        blinkStyleSeparator = separatorActive ? "" : "clock-separator";
    }

Ecco che questo codice lo mettiamo nella callback subito dopo il codice che abbiamo scritto prima.

private void Sw_SecondChangedEvent(object? sender, DateTime e)
{
  systemWatch = e.ToString("HH:mm ss", Culture);
  separatorActive = e.Second % 2 == 0;
  FormatClock()
}

Ed ecco il risultato. Non male, eh?

Parametrizziamo

Le opzioni da gestire per il nostro “semplice” orologio iniziano a crescere e con esse la complessità. Magari sarebbe meglio dare all’utente la possibilità di scegliere secondo le sue preferenze. Quindi per l’orario ho aggiunto un [Parameter] con un enum per poter gestire un po’ più agevolmente la cosa. Aggiungiamo quindi un enum fatto in questo modo

public enum WatchDisplayEnum
{
    None = 0x00,
    WithSeconds = 0x01,
    WithBlinking = 0x02,
    WithSecondsAndBlinking = 0x03,
    Undefined = 0xFF
}

In questi enum, io di solito aggiungo almeno due valori: None e Undefined per gestire gli estremi onde evitare assegnazioni non volute.

A questo punto il nostro componente avrà questo aspetto:

<div class="date-group">
    @switch (ClockDisplay)
    {
        case WatchDisplayEnum.None:
            <p class="default-watch">@systemWatch</p>
            break;
        case WatchDisplayEnum.WithSeconds:
            <p class="watch-with-date">@systemWatch</p>
            break;
        case WatchDisplayEnum.WithBlinking:
            <p class="default-watch">@watchLeftPart<e class="@blinkStyleSeparator">@separator</e>@watchRightPart</p>
            break;
        case WatchDisplayEnum.WithSecondsAndBlinking:
            <p class="default-watch">@watchLeftPart<e class="@blinkStyleSeparator">@separator</e>@watchRightPart</p>
            break;
        case WatchDisplayEnum.Undefined:
        default:
            <p class="watch-with-date">@systemWatch</p>
            break;
    }
</div>

Ovviamente avremo un [Parameter] chiamato CloclDisplay di tipo WatchDisplayEnum, e un campo systemWatch che servirà a visualizzare l’ora.

Orario 12/24 ore

Ci sono diversi modi per poter visualizzare l’orario, uno di questi è il formato europeo o anglosassone ovvero il formato 12/24 ore.

Il formato 12/24 ore però, è trasversale a tutte queste opzioni e, onde evitare duplicazioni inutili, l’ho tenuta separata gestendo direttamente il formato dell’ora in questo modo:

timeFormat = Is24H ? "HH:mm" : "hh:mm tt";
systemWatch = e.ToString(timeFormat, Culture);

La checkbox ci permetterà di visualizzare il formato da noi desiderato indipendentemente dalle altre opzioni generali agendo su un parametro del componente. Vi rimando al codice per ulteriori dettagli che per la nostra trattazione non sono così fondamentali.

Aggiungiamo la data

Abbiamo fatto tanto sin’ora ma… facciamo qualcosa in più. Aggiungiamo la possibilità di visualizzare la data sotto l’ora corrente e facciamo in modo che possa essere anch’essa una scelta.

Aggiungiamo al nostro componente un altro enum che ci servirà per gestire le opzioni di visualizzazione della data che per il momento sarà fatto in questo modo:

    public enum DateDisplayEnum
    {
        None = 0x00,
        WithShortDate = 0x01,
        WithLongDate = 0x02,
        Undefined = 0xFF
    }

Ecco che il nostro componente inizia ad essere un po’ più corposo e avrà, per ora, questo aspetto:

<div class="date-group">
    @switch (ClockDisplay)
    {
        case WatchDisplayEnum.None:
            <p class="default-watch">@systemWatch</p>
            break;
        case WatchDisplayEnum.WithSeconds:
            <p class="watch-with-date">@systemWatch</p>
            break;
        case WatchDisplayEnum.WithBlinking:
            <p class="default-watch">@watchLeftPart<e class="@blinkStyleSeparator">@separator</e>@watchRightPart</p>
            break;
        case WatchDisplayEnum.WithSecondsAndBlinking:
            <p class="default-watch">@watchLeftPart<e class="@blinkStyleSeparator">@separator</e>@watchRightPart</p>
            break;
        case WatchDisplayEnum.Undefined:
        default:
            <p class="watch-with-date">@systemWatch</p>
            break;
    }

    @switch (DateDisplay)
    {
        case DateDisplayEnum.None:
            break;
        case DateDisplayEnum.WithLongDate:
        case DateDisplayEnum.WithShortDate:
            <p class="date-field">@systemDate</p>
            break;
        case DateDisplayEnum.Undefined:
        default:
            <h1>@systemWatch</h1>
            break;
    }
</div>

Ovviamente avremo un [Parameter] chiamato DateDisplay di tipo DateDisplayEnum, un campo systemDate che servirà a visualizzare la data nel formato scelto.

Ed ecco qua quello che ci aspettavamo.

Gestire le opzioni

Ma come gestiamo tutte queste opzioni? Dobbiamo per forza farlo ad ogni “colpo di clock” del nostro timer principale.

    private void Sw_SecondChangedEvent(object? sender, DateTime e)
    {
        ClockDisplayMethod(e);
        FormatClock();
        DateDisplayMethod(e);
        StateHasChanged();
    }

Le varie funzioni che vedete richiamate sono proprio la gestione delle opzioni, sia per l’orario che per la data. Anche in questo caso vi invito a guardarlo su GitHub in modo da non caricare troppo l’articolo.

Blazor vs Javascript

Quando ho postato la domanda sul gruppo telegram di Blazor Developers Italiani un certo “Michele” mi disse che per così poco sarebbe più performante farlo in JavaScript.

Nei giorni successivi, mentre creavo il componente di cui sopra, questa frase ha continuato a ronzarmi nella testa e… ho dovuto farlo. Ho creato una cartella wwwroot nella libreria nella quale ho creato una cartella scripts dove mettere il file javascript warch.js fatto in questo modo:

function addZero(i) {
    if (i < 10) {
        i = "0" + i;
    }
    return i;
}
var SystemWatchCaller = SystemWatchCaller || {};
SystemWatchCaller.NewWatch = function (dotNetObject) {
    setInterval(function watch() {
        var d = new Date();
        var date = d.getDate();
        var hour = addZero(d.getHours());
        var min = addZero(d.getMinutes());
        var sec = addZero(d.getSeconds());
        var systemWatch = hour + ":" + min + ":" + sec;
        dotNetObject.invokeMethodAsync('UpdateWatch', systemWatch.toString());
    }, 1000);
};

La connessione tra i due mondi è fatta tramite l’oggetto dotNetObject dal lato javascript, mentre lato C# abbiamo l’oggetto DotNetObjectReference i quali serviranno per far scambiare informazioni ai due mondi. Nella fattispecie ci servirà richiamare una funzione JavaScript per “avviare” il timer, il quale ad ogni secondo chiamerà una funzione C# per aggiornare il nostro orologio.

Un parametro farà partire il timer C# o il timer JavaScript.

Li ho messi a confronto

Vorrei farvi notare lo sfasamento tra i due. Potrebbe essere dovuto allo sfasamento tra i due timer o magari a qualcos’altro che in questo momento non riesco ad identificare. In ogni caso le prestazioni sono molto simili. Anche a livello di CPU, thread attivi ecc…

Conclusioni

In questo progetto ho deciso di avere un solo parametro per gestire tutti i casi per le date e un parametro per gestire tutti i casi per l’orario. Ovviamente si potrebbe fare meglio, ma il progetto al momento può essere già utilizzabile (e lo sto utilizzando in produzione).

Per quanto riguarda la differenza tra il timer C# e JavaScript, sinceramente non ho visto problemi (oltre a quello evidenziato prima) né lato C# né lato JS, per cui ho scelto di utilizzare in produzione la versione C#.

Come ho già detto, tutto il codice è disponibile su GitHub. Fatene uso e, se volete, fate delle Pull Request e ne parliamo.

Scopri di più da Blazor Developer Italiani

Abbonati ora per continuare a leggere e avere accesso all'archivio completo.

Continue reading