[Windows 8.1] Activer la notification de Script dans la WebView WINRT en http
Depuis la sortie de Windows 8.1, le contrôle WebView a beaucoup évolué. Auparavant, il bénéficiait déjà de fonctionnalités permettant de faire communiquer l’intérieur de la WebView, via du JavaScript en utilisant la méthode window.external.notify vers l’applicatif, en s’abonnant à l’évènement ScriptNotify du contrôle. La restriction que l’on pouvait noter (en 8.0) était que l’on devait renseigner les URIs où l’on autorisait l’usage de notify dans la propriété AllowedScriptNotifyUris (toujours sur le contrôle).
A partir de Windows 8.1, cette propriété est dépréciée. La pratique imposée est la suivante : renseigner les URIs que l’on veut autoriser dans le manifeste de l’application. Cette pratique ne serait pas si dérangeante si, par mesure de sécurité, il n’était pas obligatoire de ne renseigner que des URIs en https. En effet, dès que l’on tente d’ajouter une URI telle que http://*.microsoft.com, l’erreur notifiée est « If content URIs start with a URI scheme, that scheme must be https:// ». Après avoir tenté de retirer le schème, cela ne fonctionne toujours pas.
Pour contourner ce problème, il existe tout de même une solution, certes pas très élégante, mais qui a le mérite d’être simple et fonctionnelle (en tout cas dans le cas que j’ai eu à traiter) : l’usage de l’UriToStreamResolver. Pour plus d’informations sur son usage : windows8-webview-navigatetolocalstreamuri-uritostreamresolver. En effet, dans ce cas, il n’y a pas de restriction sur l’usage du notify. L’UriToStreamResolver aura donc pour but de transformer les URIs locale en URIs Web, et via un HttpClient par exemple, de récupérer la ressource correspondante sur le Web.
Il convient donc d’implémenter un StreamResolver, comme suit :
public class Resolver : IUriToStreamResolver { public const string LocalPageUriAlias = "/toto.html"; private const string PageUri = "http://ici-l’uri-de-la-page-avec-du-notify"; private const string PageRoot = "http://ici-l’uri-du-dossier-de-la-page"; // à compléter avec d'autres extensions de fichiers texte private readonly string[] _extensionDocumentStrings = { "html", "xhtml", "xml", "json", "css", "js" }; private readonly HttpClient _client = new HttpClient(); public IAsyncOperation<IInputStream> UriToStreamAsync(Uri uri) { return LocalUriToHttpClientAsync(uri).AsAsyncOperation(); } }
Nous avons donc un UriToStreamResolver dont la méthode UriTostreamAsync sera appelée par la WebView dès que celle-ci souhaitera accéder à une ressource. Nous allons donc lui fournir cette ressource dans l’implémentation de la méthode LocalUriToHttpClientAsync, de la façon qui suit :
var path = uri.AbsolutePath; if (path == LocalPageUriAlias) { var du = new Uri(PageUri); var resp = await _client.GetAsync(du); var cont = await resp.Content.ReadAsStringAsync(); // ici il faut retravailler les liens absolu, sinon ça ne passe pas dans le stream resolver cont = cont.Replace("=\"//", "=\"fromWeb/"); cont = cont.Replace("=\"http://", "=\"fromWeb/"); return await GetTextAsFileStreamAsync(cont); }
Dans cette première partie, nous vérifions si la requête demandée correspond à l’alias de la page cible (s’il s’agit de la page qui nous pose problème), auquel cas, nous récupérons le html de la réponse sous forme de string, que retravaillons ensuite. En effet, si dans ce html ce trouve des liens absolues, ceux-ci sont ignorés par le resolver (ou la WebView). Il faut donc les transformer en liens relatifs, qui seront traités dans la suite du resolver. Ici, seuls les attributs des balises html, sont gérés, mais selon les cas, il faut peut-être réécrire les liens dans le javascript et/ou le css.
Si cette première condition est fausse, c’est que nous avons passé le stade la requête demandant l’accès à la page initiale. Il faut donc ensuite fournir les dépendances de celles-ci : fichiers css, javascript, images, etc :
var webUri = new Uri(PageRoot + path); if (path.StartsWith("/fromWeb/")) { path = path.Replace("/fromWeb/", "http://"); webUri = new Uri(path, UriKind.Absolute); } var extensions = path.Split('.').Last(); // si c'est du js, css ou html, il faut renvoyer une string if (_extensionDocumentStrings.Contains(extensions)) return await GetTextAsFileStreamAsync(await _client.GetStringAsync(webUri)); InMemoryRandomAccessStream ras = new InMemoryRandomAccessStream(); await ras.WriteAsync((await _client.GetByteArrayAsync(webUri)).AsBuffer()); return ras;
Dans un premier temps, l’URI de la ressource est reconstruite dans la variable webUri, puis nous vérifions s’il s’agissait à l’origine d’une URI absolue. Enfin, nous vérifions l’extension de la ressource demandée, car pour une raison que j’ignore, la WebView ne tolère pas que l’on renvoi le Stream d’un fichier texte sans en passer par là (de toute façon, il faut certainement refaire un traitement similaire à la première partie dans chacun de ces fichiers pour traiter les URIs absolues). Ensuite, dans les autres cas, pour les images par exemple, nous chargeons en mémoire la ressource pour la restituer à la WebView.
Vous aurez peut être remarqué l’usage de la méthode GetTextAsFileStreamAsync, dont voici le code :
using (var memoryStream = new InMemoryRandomAccessStream()) { using (var dataWriter = new DataWriter(memoryStream)) { // ici il faut gérer le bon encodage dataWriter.UnicodeEncoding = UnicodeEncoding.Utf8; dataWriter.ByteOrder = ByteOrder.LittleEndian; dataWriter.WriteString(txt); await dataWriter.StoreAsync(); await dataWriter.FlushAsync(); dataWriter.DetachStream(); } return memoryStream.GetInputStreamAt(0); }
Enfin, pour utiliser le StreamResolver, voici une procédure possible :
WebView.ScriptNotify += WebViewOnScriptNotify; var uri = WebView.BuildLocalStreamUri("plop", "toto.html"); WebView.NavigateToLocalStreamUri(uri, new Resolver());
Et vous constaterez que les notifications sont bien remontées J. Pour tester cela, vous pouvez insérer le code suivant (avant de faire le navigate) et vérifier que le handler WebViewOnScriptNotify est bien appelé avec le notifyEventArgs.Value à « test »:
WebView.DOMContentLoaded += (s, a) => WebView.InvokeScriptAsync("eval", new[] {"window.external.notify('test');"});
Voilà ! Je tiens tout de même à préciser que ce code a été monté un peu « à la va vite », dans l’urgence, et qu’il y a plusieurs optimisations possibles (certainement) et qu’il ne sera pas à 100% fonctionnel dans 100% des cas, mais cela fait peut être une bonne base de départ.
Commentaires