Intégrer WebView2 dans une application WinForms sous .NET Framework 4.6 – sans NuGet ni pipeline de build moderne

Il existe des projets pour lesquels on aimerait simplement taper Install-Package Microsoft.Web.WebView2 dans la console du gestionnaire de paquets et que tout soit terminé. Et puis il existe des projets comme le nôtre : une application ERP développée sur plusieurs années, reposant sur un framework propriétaire .NET 4.6 WinForms, distribuée via un déploiement Terminal Server (RDS) à des dizaines d’utilisateurs simultanément. Pour les formulaires spécifiques au client, sans workflow NuGet moderne et sans accès libre à la structure du projet dans Visual Studio. La mission : remplacer les rapports imprimés statiques depuis des années par des interfaces utilisateurs interactives développées en Angular – directement intégrées au client WinForms. Le chemin y menant passe par WebView2. Mais comment déployer les composants nécessaires quand on ne peut pas simplement recréer le projet d’installation ?

Quiconque a déjà intégré WebView2 dans un projet .NET « classique » connaît la voie standard : ajout du package NuGet, la runtime est généralement déjà présente sur le client, on crée un WebView2Environment, et c’est terminé. Dans un environnement legacy sans accès au gestionnaire de paquets et avec des dizaines de sessions RDS sur le même serveur, presque chaque étape se déroule différemment.

Pourquoi la solution standard ne fonctionne pas ici

Trois contraintes principales déterminent toute la solution :

  1. Pas de NuGet / pas de processus de build. Les masques spécifiques au client ne sont pas compilés, mais fournis sous forme de code source C# dans un dossier et chargés à l’exécution. Les assemblies WebView2 doivent donc être intégrés comme des références DLL statiques.
  2. Mode multi-utilisateurs RDS. Plusieurs utilisateurs travaillent simultanément sur le même serveur terminal. WebView2 crée par défaut un userDataFolder – sans séparation propre, les sessions se réécriraient mutuellement cookies, cache et état.
  3. Aucune garantie que la runtime WebView2 soit préinstallée sur chaque client. Pour une image RDS distribuée de manière centralisée, il faut un déploiement maîtrisé et versionné à la place de la runtime « Evergreen », qui se met à jour toute seule, ce qui représente un risque incontrôlable pour un système ERP en production.

Étape 1 : Fixed Version Runtime au lieu de Evergreen

Dans les environnements RDS, la runtime Evergreen n’est pas le bon choix – elle se met à jour automatiquement en arrière-plan, ce qui peut conduire à des comportements incohérents entre les sessions sur un serveur terminal centralisé. À la place, on utilise la Fixed Version Runtime : une version précise de la runtime WebView2 est téléchargée une fois, placée localement sur le serveur et référencée explicitement dans le code.

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

Le browserExecutableFolder doit pointer exactement sur le dossier où se trouve la Fixed Version Runtime décompressée – pas sur un répertoire parent, et pas sur le fichier .exe lui-même. Un chemin incorrect à ce niveau est l’un des pièges les plus fréquents, se manifestant généralement par une COMException peu parlante lors de la création de l’instance d’environnement.

Étape 2 : Référencement statique des DLL sans NuGet

Puisque nous ne pouvons pas utiliser de gestionnaire de paquets, les assemblies WebView2 nécessaires (Microsoft.Web.WebView2.Core.dll, Microsoft.Web.WebView2.WinForms.dll, Microsoft.Web.WebView2.Wpf.dll si besoin, ainsi que la WebView2Loader.dll correspondant à l’architecture cible) doivent être extraites manuellement depuis un package NuGet et ajoutées comme références classiques au projet. Points importants :

  • L’architecture (x86/x64) de la WebView2Loader.dll doit correspondre à la plateforme cible du projet – un mismatch provoquera une BadImageFormatException à l’exécution, pas à la compilation.
  • Les DLL doivent être marquées comme « Copy to Output Directory: Copy if newer » pour être réellement embarquées lors du déploiement.

Étape 3 : Isolation des sessions via LocalApplicationData

Puisque plusieurs utilisateurs travaillent en même temps sur le même hôte RDS, chaque session a besoin de son propre userDataFolder isolé. La solution : créer dynamiquement ce dossier pour chaque utilisateur Windows sous LocalApplicationData.

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

Directory.CreateDirectory(userDataFolder);

Un null ou un userDataFolder vide conduit systématiquement à une E_ACCESSDENIED dès que WebView2 tente d’écrire dans un répertoire où le processus, sous l’identité utilisateur RDS, n’a pas les droits en écriture. La spécification explicite et personnalisée du chemin utilisateur résout ce problème de manière fiable.

Étape 4 : Blocage par Mark-of-the-Web

Un problème qui apparaît surtout lors des déploiements : Windows marque les fichiers copiés via un partage réseau ou depuis Internet d’un « Mark of the Web » (MOTW) – un flux de données alternatif NTFS signalant le fichier comme potentiellement dangereux. WebView2 peut alors refuser de charger correctement certains contenus locaux marqués ainsi.

La solution la plus fiable est de supprimer explicitement le MOTW des fichiers concernés après le déploiement (par exemple via Unblock-File dans un script PowerShell de déploiement), au lieu de contourner le marquage de sécurité par le code.

Étape 5 : Communication bidirectionnelle entre C# et Angular

L’objectif principal – l’UI interactive – nécessite un canal de communication dans les deux sens. De C# vers JavaScript, cela fonctionne via PostWebMessageAsJson :

webView.CoreWebView2.PostWebMessageAsJson(jsonPayload);

Pour de plus gros volumes de données (dans notre cas environ 200 commandes, soit 100 Ko environ) extraits d’une DataTable, ExecuteScriptAsync après l’événement NavigationCompleted s’est révélé plus fiable que PostWebMessageAsJson, car cela garantit que la page Angular est réellement prête à recevoir les données.

Le chemin retour de JavaScript vers C# passe par des objets visibles par COM :

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

Important pour .NET Framework 4.6 : pour la sérialisation, on utilise ici JavaScriptSerializer au lieu de System.Text.Json, ce dernier n’étant disponible qu’à partir de .NET Core/5+ ou dans des versions ultérieures de .NET Framework.

Côté Angular, l’accès à l’objet host est initialisé de façon pull dans ngOnInit, au lieu d’attendre des événements push – cela rend l’état de chargement plus déterministe et plus facile à déboguer.

Résumé

Au final, on obtient une configuration WebView2 qui paraît banale au premier abord – un contrôle navigateur intégré, échangeant des données avec Angular. Mais ce n’était pas aussi simple : runtime Fixed Version au lieu d’Evergreen, références DLL statiques au lieu de NuGet, userDataFolder spécifique à l’utilisateur au lieu du chemin par défaut, et un Mark-of-the-Web supprimé manuellement. Chacune de ces étapes n’est pas particulièrement compliquée prise isolément – mais chacune d’elle brise le déploiement ou provoque une erreur à l’exécution si on la copie naïvement depuis un tutoriel .NET moderne pour l’appliquer à un environnement RDS legacy sans prendre en compte les spécificités. Le rapport imprimé, statique, est devenu désormais une interface web interactive en card design, nettement plus moderne et réactive que de simples contrôles WinForms.

 

Mentions de Cookies WordPress par Real Cookie Banner