Creare un’applicazione desktop multipiattaforma usando Electron e .NET Core

Electron love .NET Core

Una delle prime problematiche che si incontrano nella realizzazione di un applicativo desktop multipiattaforma è sicuramente la scelta delle librerie grafiche per la realizzazione dell’interfaccia.

In questo articolo viene proposto l’utilizzo di due strumenti: Electron e .NET Core, rispettivamente, per la gestione del front-end il primo e per la gestione della business logic dell’applicativo il secondo.

Che cos’è Electron?

Electron, citando il sito web, è un framework per la creazione di applicazioni native con tecnologie web come Javascript, HTML e CSS.

L’utilizzo di queste tecnologie permette di creare interfacce grafiche veloci, intuitive, piacevoli alla vista e soprattutto, multipiattaforma.

Tanto per rendere l’idea, Visual Studio Code è basato su Electron.

e .NET Core?

Arrivato alla versione 3, è un framework software gratuito e open source per i sistemi operativi Microsoft Windows, MacOS e Linux che consente la creazione di applicazioni Web ASP.NET Core, app da riga di comando, librerie e applicazioni Universal Windows Platform.

Il punto d’incontro di questi due framework, è Electron CGI, una libreria NodeJs rilasciata con licenza MIT che permette di stabilire un canale di comunicazione bidirezionale tra l’applicativo Electron ed un processo esterno .NET Core mediante uno scambio di messaggi tramite flussi stdin ed stdout.

Il risultato finale è un matrimonio perfetto, un’applicativo multipiattaforma che sfrutta tutte le potenzialità della piattaforma .NET Core insieme a quelle Electron per l’interfaccia grafica.

Strumenti di sviluppo

Per la realizzazione di questa applicazione di esempio e di questa guida, sono stati utilizzati i seguenti strumenti di sviluppo:

Visual Studio 2019 e Visual Studio Code non sono strettamente necessari in quanto, sia per quanto riguarda Electron, sia per .NET Core, si possono creare e gestire progetti anche da riga di comando.

Il progetto .NET Core

Una volta aperto VS2019, creare un nuovo progetto App console (.NET Core) selezionando il template che utilizza il linguaggio C#:

Creazione progetto console

Successivamente, aggiungere al progetto, il pacchetto Nuget ElectronCgi.DotNet:

Installazione pacchetto Nuget

Copiare quindi il codice seguente all’interno del file Program.cs:

using ElectronCgi.DotNet;
using System.Timers;

namespace ConsoleAppNETCore
{
    class Program
    {
        static void Main(string[] args)
        {
            var connection = new ConnectionBuilder()
                .WithLogging()
                .Build();

            connection.On<string, string>("getCarManufacturerFromModel", model =>
            {
                switch (model)
                {
                    case "Punto":
                    case "Tipo":
                    case "Bravo":
                        return "Fiat";
                    case "Fiesta":
                    case "Focus":
                        return "Ford";
                    case "Corsa":
                    case "Astra":
                        return "Opel";
                    case "Ibiza":
                    case "Leon":
                        return "Seat";
                    default:
                        return "Unknown";
                }
            });

            var counter = 0;

            var timer = new Timer(1000);
            timer.Elapsed += delegate
            {
                connection.Send<int>("sendCounter", counter);
                counter++;
            };
            timer.Start();

            // In ascolto per le richieste in entrata
            connection.Listen();
        }
    }
}

Esaminiamo ora il codice più nel dettaglio.

Prima di tutto è necessario importare la libreria:

using ElectronCgi.DotNet;

Successivamente, all’interno del metodo principale Main, si inizializza il canale di comunicazione:

var connection = new ConnectionBuilder()
    .WithLogging()
    .Build();

Impostiamo poi un messaggio “in ascolto” che chiamiamo getCarManufacturerFromModel. Questo, accetta in input una variabile stringa, nel nostro caso il modello di un’auto, e restituisce in output un’altra variabile stringa, nel nostro caso la casa costruttrice del modello di auto in ingresso:

connection.On<string, string>("getCarManufacturerFromModel", model =>
{
    switch (model)
    {
        case "Punto":
        case "Tipo":
        case "Bravo":
            return "Fiat";
        case "Fiesta":
        case "Focus":
            return "Ford";
        case "Corsa":
        case "Astra":
            return "Opel";
        case "Ibiza":
        case "Leon":
            return "Seat";
        default:
            return "Unknown";
    }
});

Impostiamo anche un Timer con un intervallo di un secondo che: aumenta un contatore ed invia un messaggio sendCounter tramite il canale di comunicazione con al suo interno il contatore progressivo:

var counter = 0; 
var timer = new Timer(1000); 
timer.Elapsed += delegate { 
    connection.Send<int>("sendCounter", counter); 
    counter++; 
}; 
timer.Start();

Questi due messaggi di esempio ci permetteranno di vedere all’opera una comunicazione bidirezionale fra questo applicativo .NET Core e quello che andremo a creare successivamente con Electron.

Infine, bisogna impostare il canale di comunicazione per iniziare l’ascolto dei messaggi in entrata:

connection.Listen();

A questo punto non ci resta che compilare il progetto per generare il file .dll e l’eseguibile .exe dell’applicativo console.

Progetto .NET Core compilato

Il progetto Electron

Creare una nuova cartella per il nuovo progetto Electron, ed al suo interno creare tre file vuoti:

  • index.html;
  • main.js;
  • package.json.

Cartella progetto Electron

Aprire quindi la cartella appena creata con VSCODE e, tramite il pannello del terminale (View, Terminal), lanciare il comando:

npm init

Questo comando effettua l’inizializzazione del progetto compilando il file package.json.

Verrà richiesto di impostare alcuni parametri come: il nome pacchetto, la versione, la descrizione, ecc….

Quando verrà richiesto di impostare il punto d’ingresso dell’applicativo (Entry point), è necessario inserire main.js.

Al termine della procedura si otterrà un package.json di questo tipo:

{
    "name": "com.electronapp.example",
    "version": "1.0.0",
    "description": "Esempio di applicazione Electron + .NET Core",
    "main": "main.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC"
}

All’interno del nodo scripts bisogna aggiungere la chiave start come segue, per trasformare l’applicazione Node appena creata in applicazione Electron:

"scripts": {
    "start": "electron .",
    "test": "echo \"Error: no test specified\" && exit 1"
},

Proseguire quindi con l’installazione del pacchetto Electron all’interno del progetto con questo comando:

npm install --save-dev electron

Compilare il file main.js come segue:

const { app, BrowserWindow } = require('electron')

function createWindow () {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        }
    });

    win.loadFile('index.html');
}
// Questo metodo viene chiamato quando Electron ha terminato l'inizializzazione
app.whenReady().then(createWindow);

// Evento invocato quando tutte le finestre sono chiuse
app.on('window-all-closed', () => {
    // Su macOS è comune che l'applicazione e la barra menù
    // restano attive finché l'utente non esce espressamente tramite i tasti Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

app.on('activate', () => {
    // Su macOS è comune ri-creare la finestra dell'app quando
    // viene cliccata l'icona sul dock e non ci sono altre finestre aperte.
    if (BrowserWindow.getAllWindows().length === 0) {
        createWindow()
    }
});

Le istruzioni sopra permettono di inizializzare l’applicativo Electron creando una nuova finestra (BrowserWindow) in cui viene caricato il file index.html che conterrà ciò che verrà renderizzato all’interno di essa.

Il template mostrato è basilare e fornisce gli elementi essenziali per caricare e gestire correttamente la finestra appena creata anche su altri sistemi operativi, come per esempio macOS.

Compiliamo ora il file index.html come segue:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Example App</title>
        <!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
        <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
    </head>
    <body>
        <h1>Esempio Invio</h1>
        <div>
            <input type="text" placeholder="Modello auto" />
            <button>Invia</button>
            <label>Risultato: </label>
        </div>
        <h1>Esempio Ricezione</h1>
        <div>
            <label>Contatore: 0</label>
        </div>
    </body>
</html>

Lanciando dal pannello terminale il comando:

npm start

Verrà avviata l’applicazione:

Primo esempio App Electron

Come si può vedere l’applicazione si presenta già con un menu predefinito ed il nostro codice HTML viene renderizzato correttamente.

Il matrimonio

Ora non ci resta che “legare” l’applicativo console .NET Core con l’applicativo Electron.

Per farlo dobbiamo innanzitutto aggiungere al nostro progetto Electron il pacchetto electron-cgi.

Per farlo possiamo utilizzare il comando:

npm install electron-cgi

Fatto ciò, bisogna ora aggiungere le istruzioni per inizializzare ed utilizzare il canale di comunicazione anche da questo applicativo.

Di seguito il file index.html precedente, con le dovute aggiunte:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Example App</title>
        <!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
        <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
    </head>
    <body>
        <h1>Esempio Invio</h1>
        <div>
            <input id="inputModello" type="text" placeholder="Modello auto" />
            <button onclick="onSendClick()">Invia</button>
            <label id="labelRisultato">Risultato: </label>
        </div>
        <h1>Esempio Ricezione</h1>
        <div>
            <label id="labelContatore">Contatore: 0</label>
        </div>
        <script>
            const { ConnectionBuilder } = require('electron-cgi');
            
            const inputModello = document.querySelector("#inputModello");
            const labelRisultato = document.querySelector("#labelRisultato");
            const labelContatore = document.querySelector("#labelContatore");

            const connection = new ConnectionBuilder()
                .connectTo('dotnet', 'run', '--project', '../ConsoleAppNETCore')
                .build();
           
            connection.onDisconnect = () => {
                console.log('Perdita di connessione al processo .NET');
            };

            async function onSendClick() {
                const modello = inputModello.value;
                const costruttore = await connection.send('getCarManufacturerFromModel', modello);
                labelRisultato.textContent = `Risultato: ${costruttore}`;
            }

            connection.on('sendCounter', res => {
                labelContatore.textContent = `Contatore: ${res}`;
            });
        </script>
    </body>
</html>

Spieghiamo ora il codice un po’ più nel dettaglio.

La prima cosa da fare, come per il progetto .NET Core, bisogna inizializzare il canale di comunicazione:

const { ConnectionBuilder } = require('electron-cgi');

const connection = new ConnectionBuilder()
    .connectTo('dotnet', 'run', '--project', '../ConsoleAppNETCore')
    .build();

Il metodo connectTo permette di “collegarsi” ad un processo esterno .NET Core.

Il processo esterno può essere collegato come nell’esempio sopra tramite la cartella del progetto, oppure, mediante il file .dll o l’eseguibile .exe compilato.

connection.onDisconnect = () => {
    console.log('Perdita di connessione al processo .NET');
};

L’evento onDisconnect permette di intercettare una disconnessione, volontaria o in caso di errore, fra il processo esterno .NET Core e l’applicativo Electron.

async function onSendClick() {
    const modello = inputModello.value;
    const costruttore = await connection.send('getCarManufacturerFromModel', modello);
    labelRisultato.textContent = `Risultato: ${costruttore}`;
}

All’evento onclick, sul pulsante Invia, viene prelevato il valore di input (il modello dell’auto) e viene inviato il messaggio getCarManufacturerFromModel all’interno del canale di comunicazione.

La risposta del messaggio, una variabile stringa contenente la casa costruttrice del modello di auto, viene poi stampata all’interno di un’oggetto label.

connection.on('sendCounter', res => {
    labelContatore.textContent = `Contatore: ${res}`;
});

Infine, intercettando il messaggio sendCounter, inviato dall’applicativo console .NET Core ogni secondo mediante un timer, viene stampato il risultato ricevuto all’interno della label predisposta.

Il risultato finale è il seguente:

Risultato finale applicativo

E su macOS?

Su macOS (dopo aver installato come su Window .NET Core SDK), basta aprire il progetto con VSCODE e lanciare il comando:

npm install

attendere il termine dell’installazione dei pacchetti e successivamente lanciare:

npm start

ed ecco il risultato:

Applicativo su macOS

Conclusione

In conclusione, l’accoppiata Electron e .NET Core risulta vincente sotto diversi aspetti:

  • La portabilità su altri sistemi operativi è immediata utilizzando questi due strumenti, come visto per esempio su macOS nell’esempio sopra.
  • Le tecnologie web utilizzate da Electron: Javascript, HTML e CSS permettono un’altissima personalizzazione grafica. E’ integrabile per esempio con strumenti come Bootstrap, oppure Material.
  • La community dietro al progetto Electron è molto ampia e la documentazione è completa, molte guide sono inoltre in Italiano.

L’esempio presente in questa guida è scaricabile a questo link.

Link utili

https://www.electronjs.org/docs/tutorial/first-app#scrivi-la-tua-prima-app-electron

https://www.nuget.org/packages/ElectronCgi.DotNet

https://www.blinkingcaret.com/2019/02/27/electron-cgi/

https://www.blinkingcaret.com/2019/11/27/electroncgi-a-solution-to-cross-platform-guis-for-net-core/

 

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *