WebView2 in .NET Framework 4.6 WinForms einbinden – ohne NuGet und ohne moderne Build-Pipeline

Ein PDF-Report soll durch eine interaktive Weboberfläche in einem WinForms-basierten ERP-System ohne Buildpipeline ersetzt werden. Ein Bericht über 4 Stolpersteine, die einem dabei im Weg liegen können.

Es gibt Projekte, bei denen man sich wünscht, einfach Install-Package Microsoft.Web.WebView2 in die Package Manager Console zu tippen und fertig zu sein. Und dann gibt es Projekte wie unseres: eine über Jahre gewachsene ERP-Anwendung auf Basis eines proprietären .NET-Framework-4.6-WinForms-Frameworks, verteilt per Terminal-Server-Deployment (RDS) an dutzende Benutzer gleichzeitig. Für die kundenspezifischen Forms ohne modernen NuGet-Workflow und ohne freien Zugriff auf die Projektstruktur in Visual Studio. Die Aufgabe: statische, seit Jahren gedruckte Reports durch interaktive, in Angular gebaute UIs ersetzen – direkt eingebettet im WinForms-Client. Der Weg dahin führte über WebView2. Aber wie rollt man die benötigten Komponenten aus, wenn man nicht einfach das Setup-Projekt neu erstellen kann?

Wer WebView2 schon mal in einem „normalen“ .NET-Projekt eingebunden hat, kennt den Standardweg: NuGet-Paket rein, Runtime ist meist schon auf dem Client vorhanden, ein WebView2Environment erstellen, fertig. In einer Legacy-Umgebung ohne Paketmanager-Zugriff und mit Dutzenden RDS-Sessions auf demselben Server ist praktisch jeder dieser Schritte anders.

Warum der Standardweg hier nicht funktioniert

Drei Rahmenbedingungen bestimmen die gesamte Lösung:

  1. Kein NuGet / kein Build-Prozess. Die kundenspezifischen Masken werden nicht kompiliert, sondern als C#-Quellcode in einem Ordner bereitgestellt und zur Laufzeit geladen. Die WebView2-Assemblies müssen also als statische DLL-Referenzen eingebunden werden.
  2. RDS-Mehrbenutzerbetrieb. Mehrere Benutzer arbeiten gleichzeitig auf demselben Terminal-Server. WebView2 legt standardmäßig ein userDataFolder an – ohne saubere Trennung würden sich die Sessions gegenseitig die Cookies, den Cache und den Zustand überschreiben.
  3. Keine Garantie, dass die WebView2-Runtime auf jedem Client vorinstalliert ist. Für ein zentral verteiltes RDS-Image braucht es eine kontrollierte, versionsfeste Bereitstellung statt der „Evergreen“-Runtime, die sich selbst aktualisiert und damit ein unkalkulierbares Risiko für ein produktives ERP-System darstellt.

Schritt 1: Fixed Version Runtime statt Evergreen

Für RDS-Umgebungen ist die Evergreen-Runtime die falsche Wahl – sie aktualisiert sich automatisch im Hintergrund, was auf einem zentral verwalteten Terminal-Server zu inkonsistentem Verhalten zwischen Sessions führen kann. Stattdessen kommt die Fixed Version Runtime zum Einsatz: eine konkrete WebView2-Runtime-Version wird einmal heruntergeladen, lokal auf dem Server abgelegt und im Code explizit referenziert.

var environmentOptions = new CoreWebView2EnvironmentOptions();
var browserExecutableFolder = @"C:\ErpSystem\WebView2Runtime\";
var environment = await CoreWebView2Environment.CreateAsync(
    browserExecutableFolder,
    userDataFolder,
    environmentOptions);

Der browserExecutableFolder muss exakt auf den Ordner zeigen, in dem die entpackte Fixed-Version-Runtime liegt – nicht auf ein übergeordnetes Verzeichnis und nicht auf die .exe selbst. Ein falscher Pfad an dieser Stelle ist einer der häufigsten Stolpersteine und äußert sich meist in einer wenig hilfreichen COMException beim Erstellen der Environment-Instanz.

Schritt 2: Statische DLL-Referenzierung ohne NuGet

Da wir keinen Paketmanager nutzen können, müssen die benötigten WebView2-Assemblies (Microsoft.Web.WebView2.Core.dll, Microsoft.Web.WebView2.WinForms.dll, Microsoft.Web.WebView2.Wpf.dll falls benötigt, sowie die passende WebView2Loader.dll für die Zielarchitektur) manuell aus einem NuGet-Paket extrahiert und als klassische Projektreferenzen eingebunden werden. Wichtig dabei:

  • Die Architektur (x86/x64) der WebView2Loader.dll muss zur Zielplattform des Projekts passen – ein Mismatch führt zu einer BadImageFormatException erst zur Laufzeit, nicht beim Kompilieren.
  • Die DLLs müssen als „Copy to Output Directory: Copy if newer“ markiert sein, damit sie im Deployment tatsächlich mitgeliefert werden.

Schritt 3: Session-Isolation über LocalApplicationData

Da mehrere Benutzer gleichzeitig auf demselben RDS-Host arbeiten, braucht jede Session ihren eigenen, isolierten userDataFolder. Der Fix: den Ordner dynamisch pro Windows-Benutzer unterhalb von LocalApplicationData anlegen.

string userDataFolder = Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
    "ErpSystemWebView2",
    Environment.UserName);

Directory.CreateDirectory(userDataFolder);

Ein null oder leerer userDataFolder führt hier zuverlässig zu einem E_ACCESSDENIED, sobald WebView2 versucht, in ein Verzeichnis zu schreiben, für das der Prozess unter der jeweiligen RDS-Benutzeridentität keine Schreibrechte hat. Die explizite, benutzerspezifische Pfadangabe behebt das zuverlässig.

Schritt 4: Mark-of-the-Web-Blockade

Ein Problem, das besonders in Deployment-Szenarien auftritt: Windows markiert Dateien, die per Netzwerkfreigabe oder aus dem Internet kopiert wurden, mit einem sogenannten „Mark of the Web“ (MOTW) – einem alternativen NTFS-Datenstrom, der die Datei als potenziell unsicher kennzeichnet. WebView2 verweigert unter dieser Markierung teilweise das korrekte Laden lokaler Inhalte.

Die zuverlässigste Lösung ist, das MOTW nach dem Deployment explizit von den relevanten Dateien zu entfernen (z. B. über Unblock-File in einem PowerShell-Deployment-Skript) statt die Sicherheitsmarkierung im Code zu umgehen.

Schritt 5: Bidirektionale Kommunikation zwischen C# und Angular

Das eigentliche Ziel – das interaktive UI – braucht einen Kommunikationskanal in beide Richtungen. Von C# nach JavaScript funktioniert das über PostWebMessageAsJson:

webView.CoreWebView2.PostWebMessageAsJson(jsonPayload);

Für größere Datenmengen (in unserem Fall rund 200 Bestellungen, etwa 100 KB) aus einer DataTable heraus hat sich ExecuteScriptAsync nach dem NavigationCompleted-Event als zuverlässiger erwiesen als PostWebMessageAsJson, da so sichergestellt ist, dass die Angular-Seite tatsächlich bereit ist, die Daten entgegenzunehmen.

Der Rückweg von JavaScript nach C# läuft über COM-sichtbare Objekte:

webView.CoreWebView2.AddHostObjectToScript("host", hostObject);

Wichtig für .NET Framework 4.6: Für die Serialisierung kommt hier JavaScriptSerializer statt System.Text.Json zum Einsatz, da Letzteres erst ab .NET Core/5+ beziehungsweise späteren .NET-Framework-Versionen zur Verfügung steht.

Auf der Angular-Seite wird der Host-Object-Zugriff Pull-basiert aus ngOnInit heraus initiiert, statt auf Push-Events zu warten – das macht den Ladezustand deterministischer und einfacher zu debuggen.

Zusammenfassung

Am Ende stand ein WebView2-Setup, das auf den ersten Blick unspektakulär aussieht – ein eingebettetes Browser-Steuerelement, das Daten mit Angular austauscht. Der Weg dahin war es nicht: Fixed-Version-Runtime statt Evergreen, statische DLL-Referenzen statt NuGet, benutzerspezifische userDataFolder statt Standardpfad, und ein manuell entferntes Mark-of-the-Web. Keiner dieser Schritte ist für sich genommen kompliziert – aber jeder einzelne bricht das Deployment bzw. wirft einen Fehler zur Laufzeit, wenn man ihn aus einem modernen .NET-Tutorial kopiert und in eine RDS-Legacy-Umgebung einsetzt, ohne die Rahmenbedingungen zu berücksichtigen. Der statische ausgedruckte Report ist nun eine interaktives Web-UI im Card-Design, das sich einfach viel moderner und reaktiver anfühlt, als einfach WinForms-Steuerelemente.

 

WordPress Cookie Hinweis von Real Cookie Banner