Hinweis: Dieses Tutorial richtet sich an fortgeschrittene Entwickler.
Die hier gezeigten Techniken sind für sehr spezifische Anwendungsfälle gedacht und setzen fundierte Kenntnisse in der Programmierung voraus,
insbesondere in den Bereichen .NET, JavaScript und Webtechnologien.
Für die meisten GuiXT-Projekte sind diese Kommunikationswege nicht erforderlich.
Die Beispiele in diesem Tutorial sind bewusst einfach gehalten, da diese Strukturen in der Praxis schnell erheblich an Komplexität gewinnen können.
Es wird daher empfohlen, diese Ansätze nur dann einzusetzen, wenn es keine einfachere Alternative gibt.
In diesem Tutorial werden drei grundlegende Architekturen gezeigt, mit denen GuiXT, JavaScript, .NET und WebAssembly (WASM) miteinander kommunizieren können.
Jede Architektur legt fest, welche Komponente die Steuerung übernimmt und welche Verbindungen direkt bzw. indirekt sind.
Das folgende Übersichtsdiagramm zeigt alle möglichen Verbindungen auf einen Blick.
Eine durchgezogene Linie steht für eine direkte Verbindung, eine gestrichelte für eine optionale und eine gepunktete für eine indirekte Verbindung.
GuiXT kann direkt mit JavaScript (über CallJS) und mit .NET (über CallVB) sprechen.
JavaScript kann direkt mit WASM kommunizieren und optional auch mit .NET (z. B. über ein COM-Objekt).
WASM hingegen hat keine native direkte Verbindung zu GuiXT und ist immer auf einen Vermittler angewiesen.
Die drei Abschnitte unten zeigen je einen konkreten Kommunikationsweg mit Beispielcode.
GuiXT als Übersetzer
In dieser Architektur fungiert GuiXT als zentraler Vermittler zwischen JavaScript und .NET.
JavaScript und .NET kennen sich gegenseitig nicht; alle Daten fließen ausschließlich durch GuiXT-Befehle.
Der Ablauf ist vollständig sequenziell und wird immer durch eine Benutzeraktion in GuiXT ausgelöst.
1. Die Bildschirmmaske
Das GuiXT-Script legt einen WebView an, der eine lokale HTML-Seite (TestPage.html) lädt,
sowie einen Pushbutton, der bei Klick das InputScript greeting.txt ausführt.
Der WebView-Befehl öffnet die HTML-Seite im angegebenen Bereich und gibt ihr den internen Namen testview.
Über diesen Namen kann GuiXT später gezielt JavaScript-Funktionen in genau diesem WebView aufrufen.
Der Pushbutton löst das InputScript aus; erst dann beginnt die Kommunikation.
2. Der Ablauf: greeting.txt
Das InputScript enthält genau drei Zeilen und beschreibt den vollständigen Kommunikationsweg:
Zeile 1:CallJS jsval = <testview>.sendText
GuiXT ruft die JavaScript-Funktion sendText() im WebView auf und speichert den Rückgabewert in der GuiXT-Variable jsval.
In diesem Beispiel gibt die Funktion den String „Hello from JS!" zurück.
Zeile 2:CallVB netval = Greetings.Greeter.receiveGreeting "&V[jsval]"
GuiXT übergibt den gerade erhaltenen Wert (&V[jsval] wird durch den Inhalt der Variable ersetzt) an die .NET-Methode receiveGreeting
und speichert die Antwort in netval.
Zeile 3:CallJS <testview>.receiveText "&V[netval]"
GuiXT schickt die .NET-Antwort als Argument an die JavaScript-Funktion receiveText(),
die daraufhin den Text im WebView aktualisiert.
3. Die .NET-Klasse
Die .NET-Klasse Greeter enthält eine einzige Methode receiveGreeting, die einen String entgegennimmt und einen String zurückgibt.
Sie prüft, ob der Eingabewert exakt „Hello from JS!" lautet, und antwortet entsprechend.
Jede andere Eingabe liefert „Invalid Greeting", sodass im Beispiel klar erkennbar ist, ob die Daten korrekt übertragen wurden.
Namespace Greet
Public Class Greeter
Public Function receiveGreeting(text As String) As String
If text = "Hello from JS!" Then
Return "Hello from .NET!"
End If
Return "Invalid Greeting"
End Function
End Class
End Namespace
namespace Greet
{
public class Greeter
{
public string receiveGreeting(string text)
{
if (text == "Hello from JS!")
{
return "Hello from .NET!";
}
return "Invalid Greeting";
}
}
}
4. Der WebView-Inhalt: TestPage.html
Die HTML-Seite definiert zwei JavaScript-Funktionen, die GuiXT per CallJS aufrufen kann:
HTML
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>TestPage</title>
</head>
<body>
<script>
function receiveText(text) {
if (text === "Hello from .NET!") {
let node = document.getElementById("hellotext")
node.innerText = "Received Hello from .NET :)"
}
}
function sendText() {
return "Hello from JS!"
}
</script>
<p id="hellotext">No hello :(</p>
</body>
</html>
sendText() ist die Datenquelle: Sie gibt einfach den festen String „Hello from JS!" zurück,
den GuiXT in Zeile 1 des Scripts abholt.
receiveText(text) ist der Empfänger: GuiXT ruft sie in Zeile 3 mit der .NET-Antwort auf.
Wenn der Wert korrekt ist, ersetzt die Funktion den Platzhaltertext des <p>-Elements durch „Received Hello from .NET :)".
5. Ergebnis
Nach einem Klick auf den Pushbutton durchläuft GuiXT alle drei Zeilen des InputScripts.
Der WebView zeigt anschließend die Bestätigungsmeldung an:
Vorteile
• Einfachster Aufbau: kein zusätzlicher Konfigurationsaufwand, da GuiXT CallJS und CallVB von sich aus unterstützt.
• Klarer, leicht nachvollziehbarer Datenfluss: jeder Schritt ist eine eigene GuiXT-Zeile.
• Gut geeignet für einfache Anfrage-Antwort-Szenarien ohne Zustandsverwaltung.
Nachteile
• GuiXT ist der Flaschenhals: jede Interaktion muss durch ein GuiXT-Script ausgelöst werden.
• Keine direkte Kommunikation zwischen JS und .NET möglich; beide Seiten sind voneinander isoliert.
• Der Ablauf ist strikt synchron; asynchrone Muster (z. B. Events, Callbacks) lassen sich damit nicht abbilden.
Kommunikationsdreieck zwischen GuiXT, JS und .NET
In dieser Architektur können alle drei Komponenten (GuiXT, JavaScript und .NET) direkt miteinander kommunizieren.
Der Schlüssel dazu ist ein COM-Objekt, das .NET bereitstellt und dessen Referenz GuiXT einmalig an JavaScript übergibt.
Danach kann JavaScript die .NET-Methoden direkt aufrufen, ohne dass GuiXT noch einmal eingreifen muss.
Für die umgekehrte Richtung (von .NET zu JavaScript) wird ein Polling-Muster verwendet:
.NET schreibt Befehle in eine interne Warteschlange, die JavaScript regelmäßig abfragt.
1. Das COM-Objekt (.NET)
Damit GuiXT ein .NET-Objekt per CallVB erstellen und die Referenz an JavaScript weitergeben kann,
muss die Klasse als COM-Objekt sichtbar sein.
Das Interface IStringContainer ist mit [ComVisible(true)] und [InterfaceType(ComInterfaceType.InterfaceIsDual)] markiert.
InterfaceIsDual bedeutet, dass das Interface sowohl über COM-Dispatch (Late Binding, wie es GuiXT und ältere Skriptsprachen verwenden)
als auch über vtable (Early Binding) erreichbar ist.
Jede Methode erhält eine feste [DispId], damit die Zuordnung stabil bleibt, auch wenn sich die Reihenfolge der Methoden im Quellcode ändert.
Die Klasse StringContainer implementiert dieses Interface und enthält folgende Methoden:
• Store(s): speichert einen String im Objekt und gibt ihn zurück.
• Read(): gibt den gespeicherten String in Kleinbuchstaben zurück (demonstriert eine echte .NET-Verarbeitung).
• Create(): erzeugt eine neue Instanz und speichert sie statisch in _instance. GuiXT ruft diese Methode auf, um das Objekt zu initialisieren.
• polfunc(): gibt den nächsten Eintrag aus der internen Queue zurück, oder einen leeren String wenn die Queue leer ist. JavaScript ruft diese Methode im Polling-Intervall auf.
• WriteLine(s): fügt einen writeln:<text>-Befehl in die Queue ein. .NET kann damit JavaScript auffordern, Text in das Dokument zu schreiben.
Imports System.Collections.Generic
Imports System.Runtime.InteropServices
<ComVisible(True)>
<InterfaceType(ComInterfaceType.InterfaceIsDual)>
Public Interface IStringContainer
<DispId(1)>
Function Store(s As String) As String
<DispId(2)>
Function Read() As String
<DispId(3)>
Function Create() As IStringContainer
<DispId(4)>
Function polfunc() As String
End Interface
<ComVisible(True)>
<ClassInterface(ClassInterfaceType.None)>
Public Class StringContainer
Implements IStringContainer
Private mycontent As String = ""
Private queue As New Queue(Of String)()
Private Shared _instance As StringContainer
Public Function Store(s As String) As String Implements IStringContainer.Store
mycontent = s
Return mycontent
End Function
Public Function Read() As String Implements IStringContainer.Read
Return mycontent.ToLower()
End Function
Public Function Create() As IStringContainer Implements IStringContainer.Create
_instance = New StringContainer()
Return _instance
End Function
Public Function polfunc() As String Implements IStringContainer.polfunc
If queue.Count > 0 Then
Return queue.Dequeue()
Else
Return ""
End If
End Function
Public Sub Test()
Dim command As String = "console"
Dim msg As String = "Success!"
_instance.queue.Enqueue(command & ":" & msg)
End Sub
Public Sub WriteLine(s As String)
Dim command As String = "writeln"
Dim msg As String = s
_instance.queue.Enqueue(command & ":" & msg)
End Sub
End Class
using System.Collections.Generic;
using System.Runtime.InteropServices;
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IStringContainer
{
[DispId(1)]
string Store(string s);
[DispId(2)]
string Read();
[DispId(3)]
IStringContainer Create();
[DispId(4)]
string polfunc();
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class StringContainer : IStringContainer
{
private string mycontent = "";
private Queue<string> queue = new Queue<string>();
private static StringContainer _instance;
public string Store(string s)
{
mycontent = s;
return mycontent;
}
public string Read()
{
return mycontent.ToLower();
}
public IStringContainer Create()
{
_instance = new StringContainer();
return _instance;
}
public string polfunc()
{
if (queue.Count > 0)
return queue.Dequeue();
else
return "";
}
public void Test()
{
string command = "console";
string msg = "Success!";
_instance.queue.Enqueue(command + ":" + msg);
}
public void WriteLine(string s)
{
string command = "writeln";
string msg = s;
_instance.queue.Enqueue(command + ":" + msg);
}
}
2. Der WebView-Inhalt: HTML & JavaScript
Die HTML-Seite enthält die JavaScript-Logik für drei Aufgaben: Objekt empfangen, Objekt direkt nutzen und eingehende Nachrichten verarbeiten.
HTML
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>GuiXT JavaScript functions, package myhtml</title>
</head>
<body>
<script>
// .NET object
var dotnetobject = null;
function receiveObject(objname) {
dotnetobject = guixthost.get(objname);
}
function saveString(s) {
dotnetobject.Store(s);
}
function displayString() {
document.body.innerHTML = dotnetobject.Read();
}
function dotnetHandler(i) {
let arr = i.split(":");
switch (arr[0]) {
case "writeln": console.log("wrote line");
document.body.innerHTML += arr[1]; break;
case "console": console.log(arr[1]); break;
default: console.log("Unsupported operation: " + i);
}
}
setInterval(function () {
if (dotnetobject == null)
return;
let i;
do {
i = dotnetobject.polfunc();
if (i !== "")
dotnetHandler(i);
} while (i !== "")
}, 30);
</script>
</body>
</html>
receiveObject(objname) wird von GuiXT per CallJS aufgerufen.
Der Parameter objname ist der interne Name der COM-Instanz (hier "sc").
guixthost.get(objname) ist eine GuiXT-spezifische Funktion, die im WebView-Kontext verfügbar ist
und die gespeicherte .NET-Objektreferenz anhand des Namens zurückgibt.
Ab diesem Moment kann JavaScript alle Methoden des .NET-Objekts direkt aufrufen, ohne GuiXT zu involvieren.
saveString(s) und displayString() demonstrieren genau das: sie rufen dotnetobject.Store(s)
bzw. dotnetobject.Read() direkt auf.
Read() gibt den gespeicherten String in Kleinbuchstaben zurück; das .ToLower() wird dabei im .NET-Prozess ausgeführt, nicht im Browser.
Das setInterval mit 30 ms ist der Polling-Mechanismus für den umgekehrten Weg (.NET zu JS).
Solange dotnetobject gesetzt ist, ruft JavaScript in jeder Runde polfunc() auf.
Gibt die Methode einen String zurück, verarbeitet dotnetHandler ihn:
ein writeln:-Präfix hängt Text ans Dokument an, ein console:-Präfix schreibt in die Browser-Konsole.
Die Schleife läuft weiter, bis die Queue leer ist (Rückgabe "").
3. GuiXT-Scripts
Das Layoutscript öffnet den WebView und den Auslöseknopf, genau wie in Abschnitt 1:
Das InputScript übernimmt die Einrichtung des Kommunikationskanals und zeigt danach, wie GuiXT und JavaScript das .NET-Objekt gemeinsam nutzen:
GuiXT
CallVB sc = JavaScriptCom.stringcontainer.create
//Queues commands
CallVB JavaScriptCom.stringcontainer.WriteLine "1"
CallVB JavaScriptCom.stringcontainer.WriteLine "2"
CallVB JavaScriptCom.stringcontainer.WriteLine "3"
// pass .NET object to JS
CallJS <testview>.receiveObject "sc"
// set string as content into .NET object
CallJS <testview>.saveString "Hello from .NET!"
// display string in lowercase, string read from .NET object
CallJS <testview>.displayString
Zeile 1:CallVB sc = JavaScriptCom.stringcontainer.create
GuiXT ruft die COM-Methode Create() auf, die eine neue StringContainer-Instanz anlegt, intern als _instance speichert und zurückgibt.
GuiXT registriert das Objekt intern unter dem Namen sc.
Zeilen 3–5:CallVB ... WriteLine "1" etc.
Noch bevor JavaScript das Objekt kennt, fügt GuiXT drei Einträge in die Queue ein.
Dies demonstriert, dass .NET Nachrichten vorausschicken kann; JavaScript wird sie beim ersten Polling-Durchlauf vorfinden.
Zeile 8:CallJS <testview>.receiveObject "sc"
GuiXT übergibt den Namen "sc" an JavaScript. Dort holt guixthost.get("sc") die Referenz auf das .NET-Objekt;
ab jetzt läuft der Polling-Timer und JavaScript kann das Objekt direkt ansprechen.
Zeilen 11 und 14 demonstrieren den direkten Zugriff von JS auf .NET:
saveString schreibt „Hello from .NET!" in das Objekt via Store(),
und displayString liest es mit Read() zurück. Der Browser zeigt den Text in Kleinbuchstaben an,
weil Read() intern .ToLower() aufruft.
Vorteile
• JavaScript und .NET können direkt miteinander kommunizieren, ohne dass jede Nachricht durch ein GuiXT-Script geleitet werden muss.
• Das Polling-Muster ermöglicht es .NET, jederzeit Nachrichten an JavaScript zu senden und schafft so einen einfachen asynchronen Kanal.
• Flexibler Aufbau: GuiXT übernimmt nur die initiale Einrichtung; danach agieren JS und .NET weitgehend eigenständig.
Nachteile
• Höherer Einrichtungsaufwand: die .NET-Assembly muss als COM-Objekt registriert sein ([ComVisible(true)], COM-Registration).
• Das Polling verursacht dauerhaften Overhead, da der Timer auch dann alle 30 ms läuft, wenn keine Nachrichten anstehen.
• COM ist eine Windows-spezifische Technologie; dieser Ansatz ist nicht auf andere Plattformen übertragbar.
• Die Fehlersuche ist anspruchsvoller, da Daten über mehrere Schichten und einen asynchronen Mechanismus fließen.
JavaScript als Übersetzer
In dieser Architektur läuft .NET vollständig im Browser, und zwar als WebAssembly-Modul.
C#-Code wird zu WASM kompiliert; der Browser führt ihn mit nahezu nativer Geschwindigkeit aus, ohne dass ein Server nötig ist.
JavaScript übernimmt die Rolle des Vermittlers: es startet die WASM-Runtime, stellt ihr Browser-Funktionen bereit ([JSImport])
und macht C#-Methoden für HTML-Ereignisse zugänglich ([JSExport]).
GuiXT muss nichts weiter tun, als die fertige HTML-Seite in einem WebView zu öffnen.
Für .NET WASM wird C# empfohlen.
VB.NET wird von der WASM-Runtime zwar grundsätzlich unterstützt, ist jedoch nicht für den produktiven Einsatz vorgesehen
und sollte nur dann verwendet werden, wenn es unbedingt erforderlich ist.
Alle Beispiele in diesem Abschnitt sind daher ausschließlich in C# gehalten.
1. Voraussetzungen: .NET WASM installieren
Zunächst wird geprüft, ob das .NET SDK in einer aktuellen Version vorhanden ist.
Für .NET WASM wird mindestens .NET 7 benötigt; empfohlen ist .NET 10:
Als nächstes wird geprüft, welche Workloads bereits installiert sind.
Der Befehl dotnet workload list zeigt alle aktiven Erweiterungen des SDK:
Falls wasm-tools noch nicht in der Liste erscheint, wird es mit folgendem Befehl installiert.
Dieser Workload enthält den WASM-Compiler und alle nötigen Build-Targets:
Danach werden die WASM-Projektvorlagen installiert.
Das Paket Microsoft.NET.Runtime.WebAssembly.Templates fügt dem dotnet new-Befehl drei neue Vorlagen hinzu;
die für dieses Beispiel relevante ist wasmbrowser (WebAssembly Browser App):
2. Projekt erstellen und bauen
Mit dotnet new wasmbrowser -o MyWasmApp wird ein neues Projekt aus der Vorlage erzeugt.
Die Vorlage enthält bereits das fertige Stoppuhr-Beispiel mit Program.cs, main.js und index.html:
dotnet build -c Release MyWasmApp kompiliert den C#-Code zu einer WASM-Assembly.
Das Ergebnis ist eine .dll-Datei, die der Browser-Runtime übergeben wird.
Der Konfigurationsparameter -c Release sorgt für eine optimierte, kleinere Ausgabe:
dotnet publish -c Release MyWasmApp erzeugt den fertigen statischen Ordner unter
MyWasmApp\bin\Release\net10.0\publish\wwwroot\.
Dieser Ordner enthält index.html, main.js sowie den Unterordner _framework\
mit der WASM-Runtime (dotnet.js, dotnet.wasm und der kompilierten Assembly).
Dieser Ordner wird in den GuiXT-Websites-Pfad kopiert:
3. Der C#-Code: Program.cs
Program.cs enthält die gesamte Stoppuhr-Logik in einer einzigen partiellen Klasse StopwatchSample.
Der Einstiegspunkt ist die implizite Main-Methode am Dateianfang:
WASM (C#)
using System;
using System.Diagnostics;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
Console.WriteLine("Hello, Browser!");
if (args.Length == 1 && args[0] == "start")
StopwatchSample.Start();
while (true)
{
StopwatchSample.Render();
await Task.Delay(1000);
}
partial class StopwatchSample
{
private static Stopwatch stopwatch = new();
public static void Start() => stopwatch.Start();
public static void Render() => SetInnerText("#time",
stopwatch.Elapsed.ToString(@"mm\:ss"));
[JSImport("dom.setInnerText", "main.js")]
internal static partial void SetInnerText(string selector,
string content);
[JSExport]
internal static bool Toggle()
{
if (stopwatch.IsRunning)
{
stopwatch.Stop();
return false;
}
else
{
stopwatch.Start();
return true;
}
}
[JSExport]
internal static void Reset()
{
if (stopwatch.IsRunning)
stopwatch.Restart();
else
stopwatch.Reset();
Render();
}
[JSExport]
internal static bool IsRunning() => stopwatch.IsRunning;
}
Der Code startet mit dem Argument "start" (das JavaScript per .withApplicationArguments("start") übergibt)
und ruft dann in einer Endlosschleife alle 1000 ms Render() auf, sodass die Anzeige sekündlich aktualisiert wird.
[JSImport("dom.setInnerText", "main.js")] deklariert, dass C# die JavaScript-Funktion dom.setInnerText
aufrufen darf, die in main.js registriert wird. Diese Funktion setzt den Text eines DOM-Elements per CSS-Selektor.
C# ruft sie einfach als normale statische Methode auf; der WASM-Interop-Layer übersetzt den Aufruf automatisch.
[JSExport] macht C#-Methoden für JavaScript aufrufbar.
Toggle() startet oder stoppt die Stoppuhr und gibt zurück, ob sie gerade läuft.
JavaScript nutzt diesen Rückgabewert, um den Button-Text zwischen „Pause" und „Start" umzuschalten.
Reset() setzt die Stoppuhr zurück und aktualisiert sofort die Anzeige.
4. Der JavaScript-Einstiegspunkt: main.js
main.js ist ein ES-Modul und übernimmt das Bootstrapping der WASM-Runtime sowie die Verdrahtung der Schaltflächen:
JavaScript
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import { dotnet } from './_framework/dotnet.js'
const { setModuleImports, getAssemblyExports, getConfig, runMain }
= await dotnet
.withApplicationArguments("start")
.create();
setModuleImports('main.js', {
dom: {
setInnerText: (selector, time) =>
document.querySelector(selector).innerText = time
}
});
const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
document.getElementById('reset').addEventListener('click', e => {
exports.StopwatchSample.Reset();
e.preventDefault();
});
const pauseButton = document.getElementById('pause');
pauseButton.addEventListener('click', e => {
const isRunning = exports.StopwatchSample.Toggle();
pauseButton.innerText = isRunning ? 'Pause' : 'Start';
e.preventDefault();
});
// run the C# Main() method and keep the runtime process running
// and executing further API calls
await runMain();
dotnet.create() lädt und initialisiert die .NET-WASM-Runtime aus dem _framework/-Ordner.
.withApplicationArguments("start") entspricht dem Übergeben von args[0] an die C#-Main-Methode.
setModuleImports registriert die JavaScript-Implementierung für alle [JSImport]-Deklarationen in C#.
Hier wird dom.setInnerText definiert: ein einfacher Arrow-Function, der document.querySelector(selector).innerText setzt.
C# kann diese Funktion danach wie eine native Methode aufrufen.
getAssemblyExports liefert ein JavaScript-Objekt, das alle mit [JSExport] markierten C#-Methoden enthält.
Die Schaltflächen-Listener rufen exports.StopwatchSample.Reset() und exports.StopwatchSample.Toggle() direkt auf,
als wären es normale JavaScript-Funktionen, obwohl der eigentliche Code in C# läuft.
runMain() startet die C#-Main-Methode und hält die Runtime am Laufen.
Da Main eine while(true)-Schleife enthält, läuft der Render-Timer dauerhaft,
und weitere [JSExport]-Aufrufe bleiben jederzeit möglich.
5. Die HTML-Seite: index.html
Das HTML ist bewusst minimal gehalten; die Logik steckt vollständig in C# und JavaScript:
HTML
<!DOCTYPE html>
<!-- Licensed to the .NET Foundation under one or more agreements. -->
<!-- The .NET Foundation licenses this file to you under the MIT license. -->
<html>
<head>
<title>WASMTest</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preload" id="webassembly" />
<script type="importmap"></script>
<script type='module' src="main#[.{fingerprint}].js"></script>
</head>
<body>
<h1>Stopwatch</h1>
<p>
Time elapsed in .NET is <span id="time"><i>loading...</i></span>
</p>
<p>
<button id="pause">Pause</button>
<button id="reset">Reset</button>
</p>
</body>
</html>
Wichtig ist <script type='module' src="main#[.{fingerprint}].js">:
Das type='module' ist zwingend, da main.js ES-Module-Syntax (import) verwendet.
Der {fingerprint}-Platzhalter wird beim Publish durch einen Build-generierten Hash ersetzt,
der Cache-Busting sicherstellt: Bei jedem Rebuild ändert sich der Dateiname.
Der <span id="time"> ist der Ziel-Selektor, den C# per SetInnerText("#time", ...) befüllt.
6. GuiXT bindet die Seite ein
GuiXT muss in dieser Architektur nichts weiter tun, als den wwwroot/-Ordner als WebView zu öffnen.
Die gesamte Kommunikation zwischen C# und JavaScript läuft innerhalb des Browsers; GuiXT ist nicht beteiligt:
Nach dem Öffnen des WebViews startet die WASM-Runtime automatisch, lädt die C#-Assembly
und beginnt mit der sekündlichen Zeitanzeige. Die Schaltflächen Start/Pause und Reset
rufen direkt C#-Methoden auf:
Vorteile
• Die gesamte Logik läuft im Browser, kein serverseitiges .NET ist erforderlich. GuiXT muss lediglich einen WebView öffnen.
• Starke Typsicherheit zwischen C# und JavaScript dank [JSExport] und [JSImport]; keine manuellen String-Konvertierungen.
• Moderne, zukunftsorientierte Architektur: .NET WASM ist plattformunabhängig und wird aktiv weiterentwickelt.
• C# kann direkt auf Browser-APIs zugreifen, ohne dass GuiXT als Mittelsmann agieren muss.
• WASM ist nicht auf .NET beschränkt: Auch andere Sprachen wie Rust, Python, Go oder C++ können zu WASM kompiliert und im Browser ausgeführt werden. Der gleiche Ansatz lässt sich damit auf viele Technologie-Stacks übertragen.
Nachteile
• Größter Einrichtungsaufwand: ein .NET WASM-Projekt muss gebaut und als statische Dateien bereitgestellt werden (_framework/dotnet.js usw.).
• Der erste Seitenaufruf ist langsam, da die WASM-Runtime (mehrere Megabyte) in den Browser geladen werden muss.
• WASM kann nicht direkt mit GuiXT kommunizieren; jede Interaktion mit GuiXT muss über JavaScript als Brücke laufen.
• Debugging ist aufwendiger, da C#-Code im Browser-Kontext läuft und gesonderte Werkzeuge (z. B. Browser-Devtools mit WASM-Support) benötigt.
Kombinierte Architektur: WASM, JS, .NET und GuiXT
Achtung: Diese Architektur ist die komplexeste der hier gezeigten Strukturen
und in der Praxis nahezu immer unnötig. Sie wird hier ausschließlich der Vollständigkeit halber gezeigt.
Bevor dieser Ansatz in Betracht gezogen wird, sollte sehr sorgfältig geprüft werden,
ob einer der drei einfacheren Wege aus den vorangegangenen Abschnitten nicht ausreicht.
Dieses Diagramm kombiniert den Kommunikationsdreieck-Ansatz aus Abschnitt 2 mit dem WASM-Ansatz aus Abschnitt 3.
Alle vier Komponenten sind gleichzeitig aktiv: GuiXT orchestriert die SAP-Oberfläche,
.NET übernimmt die GuiXT-nahe Logik über das COM-Objekt,
JavaScript fungiert als zentraler Knotenpunkt
und WASM erledigt rechenintensive oder rein browser-seitige Aufgaben.
Warum zwei getrennte .NET-Runtimes?
Der entscheidende Grund, warum diese Architektur überhaupt existiert, liegt in einem grundlegenden technischen Unterschied:
.NET in GuiXT basiert auf dem .NET Framework und kommuniziert über die guinet.dll,
die als Brücke zwischen GuiXT-Scripts und .NET-Code dient.
.NET WASM hingegen verwendet .NET Core (ab .NET 5 aufwärts), eine vollständig separate Runtime,
die nicht im selben Prozess wie das .NET Framework laufen kann.
Beide Runtimes sind inkompatibel und können nicht direkt miteinander kommunizieren.
Wenn ein Projekt also sowohl GuiXT-Zugriff über guinet.dll (.NET Framework) als auch
Berechnungen oder DOM-Operationen in WASM (.NET Core) benötigt, ist diese Vier-Komponenten-Architektur
technisch gesehen die einzige Möglichkeit, beide gleichzeitig zu nutzen.
JavaScript ist dabei der einzige Vermittler, der mit allen anderen Komponenten sprechen kann.
Mögliche Aufgabenteilung
• WASM: rechenintensive Aufgaben (z. B. Datenverarbeitung, mathematische Berechnungen),
komplexe DOM-Manipulationen im WebView oder Funktionen, die bewusst im Browser bleiben sollen.
Zu beachten ist, dass .NET Framework DOM-Manipulationen prinzipiell ebenfalls durchführen kann,
etwa über das in Abschnitt 2 gezeigte Polling-Muster; dies erfordert jedoch, dass der JavaScript-Code
für jede gewünschte DOM-Operation erst explizit geschrieben und in der Queue registriert werden muss,
was bei vielen oder komplexen Operationen deutlich aufwendiger ist als die direkte WASM-Lösung.
• JavaScript: zentrale Drehscheibe; leitet Ergebnisse von WASM weiter an GuiXT (über Rückgabewerte bei CallJS)
oder an .NET (über das COM-Objekt via guixthost.get).
• .NET Framework: übernimmt alle Operationen, die guinet.dll voraussetzen,
also direkte GuiXT-Interaktion, SAP-Feldmanipulation und ähnliche GuiXT-spezifische Aufgaben.
• GuiXT: steuert die übergeordnete Ablauflogik der SAP-Oberfläche und löst bei Bedarf Kommunikation aus.
Fazit
Diese Struktur ist technisch möglich, aber sie kombiniert die Komplexität aller drei vorangegangenen Abschnitte gleichzeitig.
Der Wartungsaufwand, die Fehlersuche über vier verschiedene Runtimes hinweg und die nötige Tiefe des Verständnisses aller beteiligten Technologien
machen diesen Ansatz in realen Projekten kaum praktikabel.
In fast allen Fällen lässt sich das eigentliche Problem mit einer der einfacheren Architekturen aus den vorangegangenen Abschnitten lösen,
ohne dass alle vier Komponenten gleichzeitig benötigt werden.
Empfehlungen: Wann welche Architektur?
Abschnitt 1 (GuiXT als Übersetzer) ist der empfohlene Ausgangspunkt für die meisten Anwendungsfälle.
Wenn GuiXT Daten zwischen JavaScript und .NET austauschen soll und der Ablauf klar sequenziell ist,
reicht dieser Ansatz vollständig aus. Er ist leicht verständlich, erfordert keinen zusätzlichen Einrichtungsaufwand
und lässt sich schnell umsetzen. Für einfache Anfrage-Antwort-Szenarien zwischen WebView und .NET ist er die erste Wahl.
Abschnitt 2 (Kommunikationsdreieck) ist sinnvoll, wenn JavaScript und .NET regelmäßig und
unabhängig voneinander kommunizieren müssen, ohne dass jede Interaktion durch ein GuiXT-Script ausgelöst werden soll.
Das Polling-Muster ermöglicht es .NET, jederzeit Nachrichten in den WebView zu schicken,
was für Szenarien mit kontinuierlichem Datenaustausch oder asynchronen Rückmeldungen aus .NET heraus nützlich ist.
Dieser Ansatz erfordert mehr Einrichtungsaufwand als Abschnitt 1, bleibt aber noch gut handhabbar.
Abschnitt 3 (JavaScript als Übersetzer / WASM) ist vor allem dann interessant,
wenn rechenintensive Aufgaben oder komplexe Datenverarbeitung rein browser-seitig erledigt werden sollen,
ohne einen .NET-Server zu benötigen.
Ein weiterer Vorteil ist die Sprachenvielfalt: Da JavaScript selbst viele Funktionen der guinet.dll direkt ansprechen kann,
lassen sich über die WASM-Brücke auch andere Programmiersprachen wie Rust, Python oder Go indirekt mit GuiXT verbinden.
WASM kompiliert die jeweilige Sprache in ein browserfähiges Modul, JavaScript reicht die Ergebnisse an GuiXT weiter.
Dieser Ansatz erfordert gute Kenntnisse der WASM-Toolchain und ist mit einem höheren Einrichtungsaufwand verbunden,
kann aber in spezialisierten Szenarien ein wertvolles Werkzeug sein.
Abschnitt 4 (Kombinierte Architektur) sollte nicht eingesetzt werden,
außer es besteht eine zwingende, technisch nachgewiesene Notwendigkeit, alle vier Komponenten gleichzeitig zu betreiben.
In der Praxis lässt sich nahezu jedes Problem mit einem der ersten drei Ansätze lösen.
Die Komplexität, die mit dem gleichzeitigen Betrieb von .NET Framework, .NET Core WASM, JavaScript und GuiXT einhergeht,
übersteigt den Nutzen in den allermeisten Fällen bei weitem.