Cross-Plattform-Desktopsoftware mit ASP.NET Core

Electron.NET – das Oberflächen-API
1 Kommentar

Das klassische .NET Framework wurde von Microsoft komplett neu entwickelt. Das Ergebnis heißt jetzt .NET Core. Das besondere an der Wiedergeburt ist, dass es auf allen gängigen Plattformen wie Windows, Mac und Linux läuft. Nur bietet Microsoft hierbei keine GUI-Lösung. Was nun?

Der Nachfolger von WPF, die Universal Windows Apps, laufen nun mal nur auf Windows 10. Eine Einschränkung, die das Potenzial von .NET Core nicht ausschöpft. Inspiriert vom Open-Source-Framework Electron, war das die Geburtsstunde von Electron.NET.

Das GitHub-Team hat nicht nur eine tolle Open-Source-Plattform ins Leben gerufen, sondern auch eigene interessante Open-Source-Projekte. Eines davon ist Electron. Ursprünglich hieß es Atom Shell und diente als Cross-Platform-Desktoplösung für ihre hauseigene Entwicklungsumgebung Atom. Diese sollte auf Windows, Mac und Linux laufen. Die innovative Architektur und Performance steigerten die Beliebtheit von Electron. Microsoft selbst setzt darauf und nutzt es für die neue Entwicklungsumgebung Visual Studio Code. Ebenfalls beteiligen sich Microsoft und viele andere große Firmen an der Weiterentwicklung von Electron. Kurz zusammengefasst: Electron ist eine solide und ausgereifte Technologie für Cross-Platform-Desktoplösungen.

Von Electron zu Electron.NET

Die Laufzeitumgebung von Electron ist Node.js. Somit werden die Prozesse hauptsächlich in JavaScript geschrieben – ein Problem für den leidenschaftlichen .NET-Entwickler. Das erkannten die beiden Autoren, und das war die Geburtsstunde von Electron.NET, das jetzt kostenlos und Open Source zur Verfügung steht. Dabei erfindet Electron.NET das Rad nicht neu, sondern baut auf dem soliden Electron auf. Das bringt einige Vorteile mit sich, wie zahlreich vorhandene API-Funktionen, Stabilität und eine große Community.

Die Anatomie

Aus Architektursicht hostet eine fertige Electron-Anwendung das ASP.NET-Core-Projekt. Über das Electron.NET API von Electron.NET kann dann auf das Standard-Electron-API zugegriffen werden. Dieses beinhaltet zahlreiche Funktionen für den Zugriff auf plattformspezifische Funktionen, wie zum Beispiel das Auslösen von Benachrichtigungen (Push Notifications), Anzeige eines Tray-Icons, Zugriff auf den Zwischenspeicher und vieles mehr. Funktionen für das Management des Lebenszyklus der Anwendung sind ebenfalls zahlreich vorhanden. Der .NET-Entwickler hingegen spürt überhaupt nichts aus dieser JavaScript-Welt. Er kann wie gewohnt mit C# arbeiten und dennoch auf die ganze Vielfalt von Electron zugreifen.

Hallo Electron.NET-Welt!

Gehen wir gemeinsam auf die Reise und erkunden Schritt für Schritt die Möglichkeiten von Electron.NET. Die Systemvoraussetzungen entsprechen denen, die .NET Core 2 vorgibt. So wird eine Installation von .NET Core 2 SDK und mindestens Node.js v8.6.0 benötigt. Die Entwicklungsumgebung ist Visual Studio 2017. Aus den Projektvorlagen wählen wir eine ASP.NET Core Web Application. Der Name lautet hierbei „HalloElectronNET“ (Abb. 1).

Abb. 1: Ein neues ASP.NET-Core-Projekt anlegen

Abb. 1: Ein neues ASP.NET-Core-Projekt anlegen

Der nächste Dialog bietet die Auswahl der unterschiedlichen Architekturstile. Hierbei ist relevant, ob eine Server-side-App oder Client-side-App zum Einsatz kommen soll. Im Prinzip unterstützt Electron.NET beides. Regulär wird für eine Electron-App eine Client-side-App, auch Single Page Application, bevorzugt. Das liegt daran, dass bei einer Server-side-App die Anwendung ein kurzes Flackern erhält, wenn zwischen den unterschiedlichen Seiten navigiert wird. Der Effekt kommt daher, dass der Server nochmal die komplette Webseite neu lädt. Für Client-side-Apps gibt es auch zwei Frameworks, die sich stark etabliert haben: Angular und React.

Diese beiden werden ebenfalls im Dialog angeboten. Bei einem realen Projekt sollte man sich dann für eines der beiden entscheiden. Gehen wir davon aus, dass Sie bereits eine ASP.NET-Core-MVC-Webseite haben, dann unterstützt Electron.NET auch einen Hybridsupport. Wählen wir daher Web-Application (Model-View-Controller) aus. Wichtig: Achten Sie ebenfalls darauf, dass im Dialog oben .NET Core in Version 2 ausgewählt ist (Abb. 2).

Abb. 2: MVC-Projektvorlage auswählen mit .NET Core 2

Abb. 2: MVC-Projektvorlage auswählen mit .NET Core 2

Das Electron.NET API einbinden

Für die Unterstützung von nativen Electron-API-Befehlen und den Zugriff darauf wird die Electron.NET API Library benötigt. Diese wird über NuGet zur Verfügung gestellt, die Installation erfolgt daher wie gewohnt mit dem NuGet Package Manager: PM> Install-Package ElectronNET.API

Nach dem Einbinden der Electron.NET API Library muss der vorhandene Webhoster in der Program.cs-Datei mit einer UseElectron WebHostBuilder-Extension erweitert werden (Listing 1).

using ElectronNET.API;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace HalloElectronNET
{
  public class Program
  {
    public static void Main(string[] args)
    {
      BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
      WebHost.CreateDefaultBuilder(args)
        .UseStartup()
        .UseElectron(args)
        .Build();
  }
}

Im eigenen Desktopfenster anzeigen

Die ASP.NET-Core-Anwendung ist jetzt mit Electron.NET erweitert worden. Beim Ausführen des Projekts wird die Anwendung noch wie gewohnt im Browser ausgeführt. Für eine tatsächliche Integration von Electron wird dann zusätzlich das Electron.NET CLI benötigt. Dazu später mehr.

Würde die Anwendung jetzt über Electron ausgeführt werden, sähe der Benutzer nichts. Das liegt daran, dass ASP.NET Core hier als Hintergrundprozess läuft, was auch für einige Anwendungsfälle interessant sein kann. Als Beispiel wäre hierbei SnagIt zu erwähnen. Dieses Programm läuft im Hintergrund und ist aufrufbar über eine Tastenkombination oder über das eingeblendete Tray-Icon. Wird es über eine Tastenkombination ausgelöst, kann man unter Windows einen Screenshot erstellen, anschließend öffnet sich erst ein Desktopfenster zum Bearbeiten des Screenshots.

Bei unserem Beispiel soll gleich beim Ausführen der Anwendung ein Desktopfenster erscheinen. Hierfür wird in der Startup.cs-Datei am Ende der Configure-Methode das Hauptfenster über die Electron.WindowManager.CreateWindowAsync()-Methode geöffnet. In Listing 2 wird der dazugehörige Code angezeigt.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
  if (env.IsDevelopment())
  {
    app.UseBrowserLink();
    app.UseDeveloperExceptionPage();
  }
  else
  {
    app.UseExceptionHandler("/Home/Error");
  }

  app.UseStaticFiles();

  app.UseMvc(routes =>
  {
    routes.MapRoute(
      name: "default",
      template: "{controller=Home}/{action=Index}/{id?}");
  });

  // Open the Electron-Window here
  Task.Run(async () => await Electron.WindowManager.CreateWindowAsync());
}

Der Hybridsupport

Wird die Anwendung jetzt wie gewohnt mit dem Tastendruck F5 gestartet, wird man eine Exception beim Aufruf des Hauptfensters erhalten. Das liegt daran, dass das Electron.NET API versucht, auf das native Electron zuzugreifen. Soll die Anwendung später ebenfalls über einen Webserver und den Browser benutzt werden können, wird eine zusätzliche if-Abfrage mit HybridSupport.IsElectronActive benötigt. Andernfalls kann man diese getrost weglassen. Der Code aus Listing 2 müsste dann angepasst werden, wie in Listing 3 gezeigt.

if(HybridSupport.IsElectronActive)
{
  // Open the Electron-Window here
  Task.Run(async () => await Electron.WindowManager.CreateWindowAsync());
}

Das Electron.NET CLI

Neben dem API gibt es noch das Electron.NET CLI, das eine Erweiterung zum normalen .NET CLI darstellt. Das CLI bietet eine Reihe von Befehlen an, um sein Projekt zu initialisieren oder zum Starten und Bauen der eigentlichen ausführbaren Applikation. Anders als das Electron.NET API kann das CLI nicht über den normalen NuGet-Installationsbefehl hinzugefügt werden, sondern muss über einen entsprechenden Eintrag in der .csproj hinzugefügt werden.

<ItemGroup>
  <DotNetCliToolReference Include="ElectronNET.CLI" Version="0.0.9" />
</ItemGroup>

Nachdem man die Projektdatei mit der Änderung abgespeichert hat, kümmert sich Visual Studio bzw. das .NET Core Tooling über ein dotnet restore darum, dass das Electron.NET-CLI-Tool heruntergeladen wird und für das reguläre .NET CLI ausführbar ist. Das Tooling erreicht man nun, indem man eine Shell öffnet und dotnet electronize aufruft.

Das Projekt initialisieren

Der erste Befehl, den man ausführen sollte, ist der Befehl dotnet electronize init. Dieser legt eine electron.manifest.json-Datei im Projektverzeichnis an. In dieser Datei steht aktuell nur der Name der ausführbaren Datei. Diese Information wird später beim Starten der Anwendung benötigt, damit wir den .NET-Core-Prozess einfacher starten können. Das ursprüngliche Problem lag darin, dass es zwar auf Windows sehr leicht ist, eine ausführbare Datei durch die Dateiendung .exe zu erkennen, dies aber auf macOS oder Linux nicht so einfach ist.

Das Ausführen der Electron.NET-Anwendung

Um seine ASP.NET-Core-Anwendung nun als Electron-Anwendung auszuführen, kann einfach der Befehl dotnet electronize start genommen werden. Dieser baut die ASP.NET-Core-Anwendung und startet die Electron Anwendung mit den nötigen Parametern und Pfaden zur ASP.NET-Core-Anwendung (Abb. 3).

Abb. 3: Die ASP.NET-Core-Anwendung als Desktopanwendung

Abb. 3: Die ASP.NET-Core-Anwendung als Desktopanwendung

Debuggen

Für das Debuggen der Electron.NET-Anwendung muss aktuell nur die Anwendung mit dem Electron.NET CLI über den Befehl start ausgeführt werden, und mit Visual Studio wird dann auf den laufenden Prozess zugegriffen. Dazu klickt man in Visual Studio auf den Menüpunkt Debug und dann auf Attach to Process… . Es öffnet sich ein Dialog, bei dem rechts nach dem Projektnamen sortiert wird. Ein Doppelklick auf die .exe-Datei und Visual Studio debuggt das aktuelle Projekt (Abb. 4).

Abb. 4: An den laufenden Electron.NET-Prozess hängen und in Visual Studio debuggen

Abb. 4: An den laufenden Electron.NET-Prozess hängen und in Visual Studio debuggen

Für das Debuggen vom JavaScript-Code innerhalb vom Desktopfenster können auch die Chrome Developer Tools mit der Tastenkombination STRG+SHIFT+1 geöffnet werden. Im Source-Tab sind dann die geladenen JavaScript-Dateien sichtbar. Die Chrome Developer Tools bieten eine Vielzahl von Möglichkeiten, wie zum Beispiel das Messen der Performance, Debuggen von JavaScript-Code, einen Netzwerkmonitor und vieles mehr (Abb. 5). Diese Funktionalität wird nur beim Ausführen über das Electron.NET CLI bereitgestellt. Wird die Anwendung später zu einer ausführbaren Datei erzeugt, muss man selbst eine Logik implementieren, um die Chrome Developer Tools zu öffnen: Electron.WindowManager.BrowserWindows.First().WebContents.OpenDevTools();

Abb. 5: Das Debuggen innerhalb der Desktop-App

Abb. 5: Das Debuggen innerhalb der Desktop-App

Die Kommunikation zwischen den Prozessen

Die Electron-Architektur baut auf unterschiedlichen Prozessen auf. Der Hauptprozess wird hierbei als Main-Prozess bezeichnet. Dieser basiert auf Node.js und läuft im Hintergrund des Systems. Bei Electron.NET ist es ein Mix aus Node.js und ASP.NET Core. So läuft der Code von der Startup.cs– und der Program.cs-Datei im Main-Prozess.

Für jedes einzelne Desktopfenster wird ein eigener Prozess namens Renderer-Prozess erzeugt. Das Management dazu übernimmt der Main-Prozess. Der Vorteil: Stürzt ein Renderer-Prozess ab, leidet nicht die gesamte Anwendung darunter und kann durch den Main-Prozess wieder ins Leben gerufen werden. Bei Electron.NET ist der Main-Prozess sogar doppelt geschützt, da der eigentliche Main-Prozess in Node.js nochmal geschützt ist. Also wenn ebenfalls unser ASP.NET-Core-Prozess abstützen sollte, kümmert sich das „richtige“ Electron um eine Wiederbelebung.

Die Kommunikation zwischen diesen beiden Prozessen wird per IPC (Inter-process Communication) ermöglicht. Diese ist nachrichtenbasiert und funktioniert somit in beide Richtungen (bidirektional). Diese Lösung ist ein fester Bestandteil der Electron-Architektur und somit auch bei Electron.NET ein wichtiger Baustein.

Bei ASP.NET Core MVC wird als Standard eine Kommunikation über einen Action-Befehl an den Controller gesendet. Das hat einen erheblichen Nachteil: Der Controller gibt sein Ergebnis als View-Instanz zurück, was ein Neuladen der Seite zur Folge hat. Es entsteht eine unübliche User Experience. Das ist auch der Hauptgrund dafür, am besten auf eine Single-Page-App-Architektur zu setzen. Außerdem ist der Action-Aufruf nicht immer einfach zu implementieren. Hier bietet Electron.NET eine erheblich einfachere Kommunikation an.

Erweitern wir also unsere bisherige „Hallo Electron.NET“-Anwendung, um die IPC-Kommunikation genauer verstehen zu können. Auf der Oberfläche wird jetzt ein Button platziert, der beim Klick vom Renderer- zum Main-Prozess eine Methode auslöst, die vom Main-Prozess die aktuelle CPU-Information der eigenen Anwendung abfragt und permanent an den Renderer-Prozess pusht.

In der Index.cshtml-Datei aus dem Verzeichnis Views/Home werden innerhalb von Zeile 75 eine Überschrift und ein Button eingefügt. Am Ende folgt ein Embedded Script Part, wodurch direkt via JavaScript auf das API des Renderer-Prozess zugegriffen wird (Listing 5).

<h3 id="cpuHeader"></h3>

<button id="cpuBtn">Show CPU activity</button>
<script>

  (function () {
    const { ipcRenderer } = require("electron");

    document.getElementById("cpuBtn").addEventListener("click", () => {
      ipcRenderer.send("getCpuActivity", 'my possible args');
    });

    ipcRenderer.on('cpuActivityReply', (event, arg) => {
      document.getElementById('cpuHeader').innerHTML = arg + "%";
    });
  }());

</script>

Der ipcRenderer ermöglicht eine Push-Subscribe-Kommunikation. So Pushen wir in den IPC-Kanal eine Nachricht an getCpuActivity. Zusätzlich können zur Nachricht ein paar Argumentwerte überreicht werden. Für dieses Beispiel wäre diese allerdings nicht relevant. Dann möchten wir im IPC-Kanal darauf lauschen, wenn eine Nachricht an cpuActivityReply gepusht wird. Hier nehmen wir die Daten entgegen und schreiben dies an das Überschriftelement H3. Listing 5 zeigt den dazugehörigen Code.

Jetzt wird nach dem gleichen Prinzip eine Kommunikation im Main-Prozess hergestellt. Nur durch ein anderes API und in C#. Dazu gehen wir bei der ersten Index-Methode in den HomeController. Die Anwendung soll im Hybridmodus noch funktionieren, daher wird erst wieder die if-Abfrage von HybridSupport.IsElectronActive benötigt. Die IPC-Kommunikation erfolgt hier über das Electron.NET API mit Electron.IpcMain. Hier lauschen wir mit der On-Methode, um zu erkennen, wenn eine Nachricht an getCpuActivity eintrifft.

Dann soll in einer Schleife abgefragt werden, wie viel Prozent CPU unsere Desktopanwendung beansprucht. Die CPU-Prozessinformationen erhalten wir über die Electron.App.GetAppMetricsAsync()-Methode.
Beim Senden der IPC-Nachricht muss der gewünschte Renderer-Prozess angegeben werden.

Das Hauptfenster befindet sich hierbei an erster Stelle, und wir lassen uns das von Electron.WindowManager.BrowserWindows.First() geben. Mit der Electron.IpcMain.Send-Methode werden dann die BrowserWindow-Instanz und der Channel cpuActivityReply der eigentlichen Nachricht aufgerufen. Der Beispielcode dazu ist in Listing 6 zu sehen.

public IActionResult Index()
{
  if (HybridSupport.IsElectronActive)
  {
    Electron.IpcMain.On("getCpuActivity", async (args) =>
    {
      while (true)
      {
        var processes = await Electron.App.GetAppMetricsAsync();
        var firstCpuPercentUsage = processes.First().Cpu.PercentCPUUsage;

        var mainWindow = Electron.WindowManager.BrowserWindows.First();
        Electron.IpcMain.Send(mainWindow, "cpuActivityReply", firstCpuPercentUsage);

        Thread.Sleep(1000);
      }

    });
  }

  return View();
}

Wenn Sie die Anwendung jetzt wieder mit dem Electron.NET CLI starten und den Button klicken, wird auf der Oberfläche die CPU-Auslastung unserer Anwendung in Prozent angezeigt. Die Auslastung sehen Sie in Abbildung 6 und die eingesetzte IPC-Kommunikation in Abbildung 7.

Abb. 6: Die Beispielanwendung mit der CPU-Auslastung

Abb. 7: Die IPC-Kommunikation mit Electron.NET

Abb. 7: Die IPC-Kommunikation mit Electron.NET

Der Build zur fertigen Desktopanwendung

Um eine richtige Electron-Anwendung zu bauen, kommt wieder das Electron.NET CLI mit dem build-Befehl ins Spiel. Es ist wichtig zu verstehen, dass wir beim Bauen eigentlich zwei verschiedene Applikationen erstellen. Einerseits wird die ASP.NET-Core-Anwendung samt der .NET Core Runtime für ein bestimmtes Betriebssystem gebaut. Die daraus resultierende Anwendung wird über den Electron Packer als normale Electron-Anwendung paketiert. Der Electron Packer selbst kann wiederum über Parameter entsprechend eine Windows-typische .exe erzeugen oder eine macOS- bzw. Linux-Anwendung.

Ruft man dotnet electronize build ohne weiteren Parameter auf, wird die Anwendung für das aktuell ausführende Betriebssystem gebaut, d. h. auf Windows wird die ASP.NET-Core-Anwendung samt .NET Core Runtime für Windows gebaut, und der Packer nutzt ebenfalls die Windows-Einstellung. Das Electron.NET CLI bietet für das Bauen aktuell win für Windows, osx für macOS und linux für Linux. Möchte man auf Windows z. B. auch eine Linux-Anwendung erzeugen, muss man noch den Parameter linux wie folgt angeben: dotnet electronize build /target linux. In der Theorie wäre es möglich, dass man auf Windows auch alle drei Varianten bauen lässt, allerdings gibt es ein Problem mit dem macOS Build beim Electron Packer. Auf einem Linux allerdings sollte es möglich sein, alle drei Varianten zu bauen.

Für die nächste Electron.NET-CLI-Version ist zudem geplant, dass man neben den drei definierten Varianten noch direkt die Build-Optionen für die ASP.NET-Core-Anwendung oder den Electron Packer angeben kann, um auch weniger gebräuchliche Konfigurationen zu unterstützen, so beispielsweise, um eine Windows-Anwendung in 32 bit zu erzeugen.

Fazit

Mit nur wenigen Schritten konnten wir eine ASP.NET-Core-Anwendung als Desktopsoftware verpacken. Das Besondere daran ist ebenfalls, dass diese nun auf Windows, Mac und Linux läuft. Im wahrsten Sinne des Wortes konnten wir hier fast nur an der Oberfläche kratzen. Denn das Electron.NET API stellt so viele weitere interessante API-Funktionen zur Verfügung. Diese können Sie genauer in einer kleinen Beispielanwendung „Electron.NET API Demos“ auf GitHub erkunden. Ein YouTube-Video und ein Blogartikel geben ebenfalls nochmal einen Einstieg. Auch eine kleine Geschichte zur Entstehung von Electron.NET gibt es als Blogartikel.

Wenn Sie eine Lösung mit Electron.NET entwickelt haben, sind die Autoren sehr stark daran interessiert. Wir wünschen viel Spaß mit Electron.NET, und bei weiteren Fragen können Sie sich gerne direkt an die Autoren wenden.

Unsere Redaktion empfiehlt:

Relevante Beiträge

X
- Gib Deinen Standort ein -
- or -