Electron.NET

Neue Features, Tipps und Tricks
28
Okt

Electron.NET von A bis Z

Das Open-Source-Framework Electron.NET feiert seinen dreijährigen Geburtstag und in der Zwischenzeit hat sich einiges getan. Fast 80 000 Downloads auf NuGet, fast 5 000 Sterne auf GitHub, über 400 bekannte Projekte – und sogar Microsoft nutzt es für die Desktoplösung von Blazor. Dieser Artikel geht auf die neuen Features ein und zeigt zusätzliche Tipps und Tricks, die häufig nachgefragt werden.

Allgemeines zu Electron.NET

Das GitHub-Team hat 2013 das Electron Framework veröffentlicht, um mit JavaScript Cross-Platform-Desktopanwendungen bereitstellen zu können, die auf Windows, Mac und Linux laufen. Eine Vielzahl bekannter Anwendungen läuft auf diesem Framework, zum Beispiel Visual Studio Code, Microsoft Teams, Discord, Postman und viele weitere. Electron.NET nutzt unter der Haube das native Electron Framework, ermöglicht allerdings das Ausführen von .NET-Core-Anwendungen und stellt eine Brücke zum API mittels C# zur Verfügung. .NET-Entwickelnde bleiben damit in ihrem gewohnten Ökosystem und müssen sich nicht mit JavaScript beschäftigen. Ein kompletter Einstieg in Electron.NET wurde bereits im Windows Developer veröffentlicht und kann jetzt kostenlos online gelesen werden. Alternativ gibt es dazu passend eine Live-Stream-Aufzeichnung auf YouTube [3]. Dieser Artikel zeigt einige neue Features sowie wertvolle Tipps und Tricks.

Der Aufbau der Versionierung

Mit der Veröffentlichung von Electron.NET 5.22.12 haben wir eine neue Versionierung eingeführt. Die aktuelle Version lautet 9.31.2 (Stand 10.08.2020). Die erste Hauptversionsnummer mit der Zahl neun steht für die darunterliegende native Electron-Version. Die zweite Nebenversionsnummer mit der Zahl 31 steht für den Support von .NET Core 3.1. Die letzte Revisionsnummer enthält eigene Erweiterungen und Fehlerbehebungen. Diese wird automatisch auf eins zurückgesetzt, wenn eine neue native Electron-Hauptversion erscheint.

Verschaffen Sie sich den Zugang zur .NET- und Microsoft-Welt mit unserem kostenlosen Newsletter!

Der Splash-Screen-Support

Der Ladevorgang von Electron.NET benötigt etwas mehr Zeit, als man es von herkömmlichen Desktopanwendungen gewohnt ist. Ein Grund ist der lange Initialisierungsvorgang von ASP.NET Core. Electron selbst ist sehr schnell einsatzbereit, wobei Electron.NET die Wartezeit mit einem Begrüßungsbildschirm (Splash Screen) gefühlt verkürzen kann. Nebenbei bemerkt ist der Startvorgang von Electron.NET seit der Version 8.31.1 um bis zu ~25-36 Prozent schneller geworden – und wir arbeiten stetig daran, ihn noch weiter zu beschleunigen.

Für den Splash Screen benötigen wir eine Bilddatei. Der ideale Ort für die Bilddatei ist das wwwroot-Verzeichnis. Unterstützte Bildformate sind JPEG, WEBP, GIF, PNG, APNG, 2D Canvas, XBM, BMP und ICO. In der electron.manifest.json-Datei werden dann der Pfad und der Name der Bilddatei festgelegt (Listing 1). Electron.NET erkennt automatisch, dass ein Splash Screen hinterlegt wurde und zeigt diesen beim nächsten Start an. Die Splash-Screen-Größe wird automatisch der Bildgröße angepasst und kann aktuell nicht selbst verändert werden.

{
  "executable": "ElectronNetTest",
  "splashscreen": {
    "imageFile": "/wwwroot/img/my-splashscreen.jpg"
  },
  "singleInstance": false,
  ...
}

 

Sollte beim Startvorgang der ASP.NET-Core-Anwendung eine Exception ausgelöst werden, wird der Splash Screen nicht beendet und die Anwendung wird nicht geladen. Electron.NET erfährt jedoch nichts von dieser Exception und wartet darauf, bis die Anwendung fertig initialisiert wurde. Ist die Anwendung über das Terminal gestartet worden, hilft hierbei die Tastenkombination STRG + C, um sie manuell zu beenden. Alternativ klickt man auf den Splash Screen und beendet diesen mit der Tastenkombination ALT + F4. Eine eigene Lösung im Code kann diesem unschönen Verhalten entgegenwirken, wie Listing 2 zeigt.

 

try
{
  // ASP.NET Startup code...
  throw new Exception("No money!");
}
catch (Exception)
{
  Electron.App.Exit();
  throw;
}

 

Unterschiedliche Manifest-Dateien

Die electron.manifest.json-Datei ist ein wichtiger Bestandteil von Electron.NET. Sie beherbergt alle notwendigen Einstellungen, damit die beiden Welten .NET und das native Electron zusammenfinden. Zusätzlich konfiguriert man hier die Details einer App, wie zum Beispiel den Namen der Anwendung, die Herausgeberinformationen, die Version, den Splash Screen, das Anwendungsicon und vieles Weitere. Häufig soll eine Anwendung aus unterschiedlichen Konfigurationen erzeugt werden, z. B. eine Desktopanwendung mit einer Standard- und einer Professional-Edition. Dazu passend sollen auch die unterschiedlichen Splash Screens erscheinen.

Um eine weitere Manifest-Datei zu erzeugen, wird ebenfalls das Electron.NET CLI benötigt. Dazu geben wir in der Konsole innerhalb des Projektverzeichnisses folgenden Befehl ein:

electronize init /manifest test

In dem Projekt wurde jetzt neben der Standard-electron.manifest.json-Datei eine zusätzliche Datei namens electron.manifest.test.json angelegt. In Abbildung 1 wird gezeigt, dass diese beiden Dateien in Visual Studio eingebettet sind.

 

Abb. 1: Unterschiedliche Manifest-Dateien

 

Wenn wir jetzt in der neuen Manifest-Datei ein anderes Splash-Screen-Bild hinterlegen, wird dieses verwendet, wenn die Anwendung mit folgendem Befehl gestartet wird:

electronize start /manifest electron.manifest.test.json

Das Erzeugen der Desktopanwendung mit der neuen Datei wird dann über folgenden Befehl ermöglicht:

electronize build /target win /manifest electron.manifest.test.json

 

Eigenen TypeScript-Code ausführen

Der besondere Vorteil von Electron.NET ist, dass kein TypeScript/JavaScript-Code benötigt wird. Dennoch gibt es eine Vielzahl von npm-Paketen, die Electron oder das darunter laufende Node.js bereichern. Um diese ebenfalls in Electron.NET nutzen zu können, existiert das HostHook API. Tatsächlich war dieses Feature eins der gefragtesten in der Community.

HostHook aktivieren

Für den eigenen TypeScript/JavaScript-Code oder das zusätzliche Installieren von npm-Paketen wird ein eigenes isoliertes Webprojekt innerhalb der ASP.NET-Core-Anwendung benötigt. Über das Electron.NET CLI wird es durch electronize add hosthook mit einem eigenen Ordner namens ElectronHostHook angelegt.

Eigenen TypeScript-Code hinterlegen und ein npm-Paket installieren

Das ElectronHostHook-Verzeichnis kann jetzt mit einem Editor, wie zum Beispiel Visual Studio Code, geöffnet werden. Dieser bietet eine perfekte Unterstützung für die Webentwicklung von TypeScript. In einem kleinen Beispiel soll gezeigt werden, wie mittels TypeScript und Node.js eine Excel-Datei erzeugt wird. Für das Erzeugen von Excel-Dateien eignet sich die Node.js-Bibliothek ExcelJS. Sie wird installiert, indem wir per Terminal innerhalb des ElectronHostHook-Verzeichnisses npm install exceljs eingeben.

Wir legen dann eine neue TypeScript-Datei mit dem Namen excelCreator.ts an. Der Code aus Listing 3 erzeugt eine Excel-Datei mit einigen Beispieldaten.

import * as Excel from "exceljs";
import { Workbook, Worksheet } from "exceljs";

export class ExcelCreator {
  async create(path: string): Promise<string> {
    const workbook: Workbook = new Excel.Workbook();
    const worksheet: Worksheet = workbook.addWorksheet("My Sheet");
    worksheet.columns = [
      { header: "Id", key: "id", width: 10 },
      { header: "Name", key: "name", width: 32 },
      { header: "Birthday", key: "birthday", width: 10, outlineLevel: 1 }
    ];
    worksheet.addRow({ id: 1, name: "John Doe", birthday: new Date(1970, 1, 1) });
    worksheet.addRow({ id: 2, name: "Jane Doe", birthday: new Date(1965, 1, 7) });

    await workbook.xlsx.writeFile(path + "\\sample.xlsx");

    return "Excel file created!";
  }
}

 

Initialisierung und Kommunikation zum HostHook API von TypeScript

Im ElectronHostHook-Verzeichnis befindet sich die index.ts-Datei (Listing 4). Sie wird automatisch beim Starten des nativen Electron ausgeführt. Der Konstruktor stellt hierbei sogar die Verbindung zur Electron IPC (Inter-Process Communication) und Electron-App-Instanz zur Verfügung. Die Basisklasse Connector sorgt dafür, dass eine onHostReady()-Funktion aufgerufen wird, wenn der native Electron-Prozess einsatzbereit ist. Innerhalb dieser Funktion kann dann für die IPC TypeScript-/JavaScript-Code hinterlegt werden. In diesem Beispiel soll dann der Code zum Ausführen des eigenen ExcelCreator erfolgen. Ist die asynchrone Arbeit erledigt, wird das Ergebnis mittels der done-Callback-Funktion an die IPC zurückgeliefert.

// @ts-ignore
import * as Electron from "electron";
import { Connector } from "./connector";
import { ExcelCreator } from "./excelCreator";

export class HookService extends Connector {
  constructor(socket: SocketIO.Socket, public app: Electron.App) {
    super(socket, app);
  }

  onHostReady(): void {
    // execute your own JavaScript host logic here
    this.on("create-excel-file", async (path, done) => {
      const excelCreator: ExcelCreator = new ExcelCreator();
      const result: string = await excelCreator.create(path);

      done(result);
    });
  }
}

 

TypeScript mit C# und dem HostHook API ausführen

Über die statische Property Electron.HostHook kann in C# auf das HostHook API zugegriffen werden. Die CallAsync-Methode dient einer asynchronen Verarbeitung, bei der eine Antwort erwartet wird. Der Code aus Listing 5 zeigt, wie in C# der eigene TypeScript-/JavaScript-Code ausgelöst wird und wie wir an das Ergebnis gelangen.

var mainWindow = Electron.WindowManager.BrowserWindows.First();
var options = new OpenDialogOptions
{
  Properties = new OpenDialogProperty[] 
  {
    OpenDialogProperty.openDirectory
  }
};
var folderPath = await Electron.Dialog.ShowOpenDialogAsync(mainWindow, options);

var resultFromTypeScript = await Electron.HostHook.CallAsync<string>("create-excel-file", folderPath);
Electron.IpcMain.Send(mainWindow, "excel-file-created", resultFromTypeScript);

 

Parameter mit dem Command-Line-Support verarbeiten

Für das Setzen von Einstellungen in Desktopanwendungen gibt es die Möglichkeit, das Binary mittels Parametern zu starten. Seit Electron.NET 7.30.2 gibt es ebenfalls die Möglichkeit, mit Parametern zu arbeiten, und zwar mit dem Command-Line-Support. Bei einem meiner Kundeneinsätze gab es die Anforderung, dass sich die Chrome Developer Tools automatisch öffnen sollen, wenn die Anwendung mit dem folgenden Parameter gestartet wurde:

myapp.exe /args --debug=true

Die Chrome Developer Tools sind die Entwicklertools aus Chromium, die mit zahlreichen Funktionen zum Gestalten, Editieren, Testen, Analysieren und Korrigieren von Webanwendungen zur Verfügung stehen. In Listing 6 wird gezeigt, wie mit der statischen Property Electron.App.CommandLine auf die Parameter zugegriffen wird. Eine Überprüfung auf die Existenz eines Parameters wird mit der HasSwitchAsync-Methode durchgeführt. Anschließend erhalten wir den Wert mit der GetSwitchValueAsync-Methode. Zur Entwicklungszeit kann die App via electronize start /args –debug=true zusätzliche Parameter erhalten. In Abbildung 2 sehen wir die Desktopanwendung mit geöffneten Chrome Developer Tools.

Task.Run(async () => {
  var mainWindow = await Electron.WindowManager.CreateWindowAsync();

  if (await Electron.App.CommandLine.HasSwitchAsync("debug"))
  {
    string result = await Electron.App.CommandLine.GetSwitchValueAsync("debug");
    if (Convert.ToBoolean(result))
    {
      mainWindow.WebContents.OpenDevTools();
    }
  }
});

 

Abb. 2: Desktop-App startet mit den Chrome Developer Tools

 

Automatisch den Cache löschen

Ein natürliches Electron-Verhalten ist das Aufrechthalten des internen Browsercaches, wodurch ebenfalls einiges an Verarbeitungszeit eingespart wird. Das ist gerade zur Entwicklungszeit jedoch etwas unglücklich, da eigene Änderungen nicht ersichtlich werden. Sollte man über ein solches Problem stolpern, kann der Cache nun via Parameter automatisch gelöscht werden:

electronize start /clear-cache

Alternativ muss bei Electron oder in Kombination mit Electron.NET der folgende API-Befehl ausgeführt werden, der mit dem neuen clear-cache-Parameter erspart bleibt:

await browserWindow.WebContents.Session.ClearCacheAsync();

 

Ausbruch aus der Sandbox

Die Browser-Sandbox ist einer der Gründe, weshalb Webanwendungen so erfolgreich geworden sind und auch im Enterprise ein Zuhause gefunden haben. Allerdings ist sie auch eine Art Zwangsjacke für Entwickler, die in ihren Möglichkeiten stark beschränkt werden. Die Browser-Sandbox isoliert die Webanwendung vom eigentlichen Hostsystem, sodass auf keinen lokalen Prozess zugegriffen werden kann. Auch der Zugriff auf das Dateisystem ist nicht ohne Benutzeraktion ohne weiteres möglich. Weitere HTML5-Features wie die Standortermittlung via GPS, der Zugriff auf die Kamera und das Anzeigen von Benachrichtigungen sind ohne Benutzergenehmigung nicht möglich. Das ist für einige Businessanwendungen hinderlich, sodass auch keine Entscheidung für Progressive Web Apps gefällt werden kann. Das Besondere an Electron ist, dass Chromium die Option anbietet, die Sandbox zu deaktivieren. Wir können also wie ein Magier aus der Zwangsjacke ausbrechen.

Electron besteht aus unterschiedlichen Prozessen. Der Main-Prozess (Hauptprozess) ist eine Node.js-Instanz. Bei Electron.NET wird von Node.js unsere ASP.NET-Core-Anwendung gestartet und gemanagt. Im Prinzip haben wir hierbei keine Sandbox und können auf dem Hostsystem alles machen, was der angemeldete Benutzer ebenfalls darf. Sobald bei Electron ein sichtbares Fenster erzeugt und angezeigt wird, handelt es sich um einen getrennten Prozess namens Renderer-Prozess. Dieser ist die eigentliche Chromium-Instanz, die per Standard in der Sandbox aktiv ist. Soll jetzt eines der eben genannten HTML5-Features eingesetzt werden, hätten wir wieder unsere Einschränkungen. Deshalb wird beim Erzeugen eines neuen Fensters zusätzlich eine Option benötigt, um die Websecurity zu deaktivieren. In Listing 7 wird gezeigt, wie die notwendigen BrowserWindowOptions dazu aussehen. Selbstverständlich sollte die Sandbox auch nur dann deaktiviert werden, wenn wirklich ein Ausbruch aus der Sandbox vom Renderer-Prozess notwendig ist.

Task.Run(async () => {
  var options = new BrowserWindowOptions
  {
    WebPreferences = new WebPreferences
    {
      WebSecurity = false
    }
  };

  await Electron.WindowManager.CreateWindowAsync(options);
});

 

Das Beenden der App verhindern

In der Regel benötigt eine Desktopanwendung kein Fenster. Ein Screenshottool wie Snagit läuft beispielsweise im Hintergrund und wird erst durch globale Shortcuts aktiv. Das laufende Programm kann dann meist ebenfalls über das System Tray geöffnet werden.

Das Standardverhalten von Electron.NET ist, dass der Main-Prozess ohne Fenster im Hintergrund laufen kann, ohne dass sich das Programm gleich wieder beendet. Anders verhält es sich, wenn das erste Hauptfenster erzeugt wurde. Hier wird beim Beenden standardmäßig die gesamte Anwendung beendet. Um das Standardverhalten beeinflussen zu können, kann das Electron.App.WillQuit Event abonniert werden. Über die Event Arguments wird dann ein komplettes Beenden der Anwendung bei einem Aufruf der PreventDefault-Methode verhindert.

In Listing 8 wird ein Code gezeigt, der ein Hauptfenster öffnet. Wird dieses geschlossen, erhalten wir alle zehn Sekunden einen Abfragedialog, ob die Anwendung beendet werden soll. Bei der Auswahl von No wird der Beenden-Prozess gestoppt. Bei Yes wird sich die Anwendung komplett beenden.

Im Task Manager werden im Hintergrund die Prozesse mit einigen Electron-Instanzen sowie eine Instanz, die den gleichen Namen wie das eigene Programm trägt, angezeigt. Ist die Anwendung korrekt beendet worden, sind diese Instanzen nicht mehr ersichtlich. Es gibt Ausnahmefälle, die ein händisches Beenden erfordern, wenn das Standardverhalten automatisch beeinflusst wurde.

Task.Run(async () =>
{
  var mainWindow = await Electron.WindowManager.CreateWindowAsync();
  mainWindow.OnClose += () =>
  {
    var timer = new Timer();
    timer.Interval = 10000;
    timer.Elapsed += (s, e) => Electron.App.Quit();
    timer.Start();
  };

  Electron.App.WillQuit += async (e) => 
  {
    var options = new MessageBoxOptions("Exit the application?")
    {
      Type = MessageBoxType.question,
      Title = "Question",
      Buttons = new string[] { "Yes", "No" }
    };

    var result = await Electron.Dialog.ShowMessageBoxAsync(options);

    if (result.Response == 1)
    {
      e.PreventDefault();
    }
  };
});

 

Deployment

Das Erzeugen der nativen Binaries für die unterschiedlichen Plattformen erfolgt über das Electron.NET CLI mit dem Build-Befehl electronize build. Unter der Haube setzt Electron.NET auf das Open-Source-Projekt electron builder. Es erstellt nicht nur die notwendigen Binaries in beliebigen Formaten, sondern erzeugt ebenfalls die passende Installationsdatei gepackt als 7z, zip, tar.xz, tar.7z, tar.lz, tar.gz, tar.bz2 und dir (ungepacktes Verzeichnis) für alle Plattformen:

  • macOS: dmg, pkg, mas
  • Linux: AppImage, snap, Debian Package (deb), rpm, freebsd, pacman, p5p, apk
  • Windows: nsis (Installer), nsis-web (Web-Installer), portable (portable App ohne Installation), AppX (Windows Store), MSI, Squirrel.Windows

 

Ein weiteres interessantes Feature ist der integrierte Auto Update Manager, der es ermöglicht, die eigene App über einen Fileserver oder auch über GitHub-Releases zu aktualisieren. Ebenfalls kann hier automatisiert eine Codesignatur erfolgen.

Die offizielle Dokumentation von electron builder bietet eine Vielzahl von Konfigurationsmöglichkeiten. In der electron.manifest.json-Datei haben wir hierfür den Build-Bereich hinterlegt. Die dort enthaltenen Konfigurationen werden eins zu eins an den electron builder überreicht. Listing 9 zeigt die Standardkonfiguration, womit im bin\Desktop-Verzeichnis die fertige Desktopanwendung erzeugt wird. Darin wird per Standard die App ungepackt erzeugt, und für einen Windows-Build ein passender nsis Installer.

{
  "executable": "MyElectronNETApp",
  "splashscreen": {
    "imageFile": ""
  },
  "name": "MyElectronNETApp",
  "author": "",
  "singleInstance": false,
  "environment": "Production",
  "build": {
    "appId": "com.MyElectronNETApp.app",
    "productName": "MyElectronNETApp",
    "copyright": "Copyright © 2020",
    "buildVersion": "1.0.0",
    "compression": "maximum",
    "directories": {
      "output": "../../../bin/Desktop"
    },
    "extraResources": [
      {
        "from": "./bin",
        "to": "bin",
        "filter": [ "**/*" ]
      }
    ],
    "files": [
      {
        "from": "./ElectronHostHook/node_modules",
        "to": "ElectronHostHook/node_modules",
        "filter": [ "**/*" ]
      },
      "**/*"
    ]
  }
}

 

Fazit

Dieser Artikel hat nur einige der neuen Features vorgestellt und kleine Tipps gegeben, die häufig nachgefragt wurden. Im Detail hat die wachsende Community für zahlreiche API-Implementationen gesorgt, sodass noch mehr vom nativen Electron innerhalb unserer .NET-Welt zur Verfügung steht. Wir arbeiten weiterhin intensiv an einer noch schlankeren und performanteren Lösung. Dazu wird sich das Backend technisch noch so weit verändern, dass eine API-Kommunikation über WebSockets hinfällig wird. Ein großes Ziel meinerseits ist es, dass diese Optimierungen ohne Breaking Changes auskommen werden.

Das Open-Source-Projekt sucht natürlich weiterhin nach Verstärkung. Wenn Interesse besteht, an Electron.NET etwas zu verändern, zeigt unser YouTube-Video „Electron.NET – Contributing Getting Started“, wie einfach für Electron.NET entwickelt werden kann. Bei weiteren Fragen steht die Community via Chat auf Gitter rund um die Uhr zur Verfügung. Natürlich darf man auch den Autor jederzeit per E-Mail kontaktieren.

Ihr aktueller Zugang zur .NET- und Microsoft-Welt.
Der BASTA! Newsletter:

Behind the Tracks

.NET Framework & C#
Visual Studio, TFS, C# & mehr

Agile & DevOps
Best Practices & mehr

Web Development
Alle Wege führen ins Web

Data Access & Storage
Alles rund um´s Thema Data

HTML5 & JavaScript
Leichtegewichtig entwickeln

User Interface
Alles rund um UI- und UX-Aspekte

Microservices & APIs
Services, die sich über APIs via REST und JavaScript nutzen lassen

Security
Tools und Methoden für sicherere Applikationen

Cloud, Azure, Serverless
Cloud-basierte & Native Apps