WebView, NavigateToLocalStreamUri & UriToStreamResolver

Windows 8.1 amène son lot de nouveautés pour le développement d’applications. Parmi elles, une WebView pour les applications HTML5 (en complément de l’IFRAME) et des améliorations pour le contrôle XAML. L’une des plus remarquables est sans doute l’UriToStreamResolver. Afin de comprendre son utilité, voici quelques informations supplémentaires :

Auparavant, la WebView XAML n’offrait que 2 modes de navigation :

  • NavigateToString : permettant d’afficher une chaîne de caractères HTML directement dans le contrôle
  • Navigate : permettant d’afficher une page HTML (locale ou distante) suivant une URL

Seul inconvénient, la WebView ne peut charger de dépendances locales (fichiers CSS, JavaScript) sous Windows 8. Pour être précis, afficher une page web distante fonctionnera bien, mais afficher une page web locale ne fonctionnera que si les dépendances se trouvent dans le répertoire d’installation de l’application (par conséquent le fichier HTML aussi). Impossible donc de télécharger une archive contenant une page web et toute ses dépendances, de la stocker dans le dossier temporaire ou le LocalState pour l’afficher ensuite. La raison invoquée par Microsoft jusqu’ici était le risque potentiel de télécharger du contenu malveillant.

A partir de Windows 8.1, cette limitation n’existe plus. Et justement, l’UriToStreamResolver permet de tirer parti de cela en offrant encore plus de contrôle sur le contenu que la WebView va charger.

 

Qu’est-ce que c’est ?

Concrètement, le resolver va permettre au développeur de mapper une URI sur le stream de son choix. Ainsi, la WebView ne fonctionnera plus simplement en résolvant l’URI de façon classique (schema/domain/…). Elle fera appel au resolver, lui-même instance d’une classe créée par le développeur.

Cette résolution n’a pas seulement lieu au moment de charger la page, mais aussi au moment de charger toutes les dépendances. Cela signifie concrètement, que la WebView n’est plus maître de ce qu’elle charge, elle se contente de consommer ce que le resolver lui donne.

Ainsi, il n’est plus obligatoire de structurer les dossiers de sa solution ou de son storage en fonction des urls des dépendances d’une page à afficher dans une WebView. Il n’est plus nécessaire de vérifier les urls des dépendances des fichiers HTML pour les réécrire si besoin avant de fournir le fichier à la WebView.

 

Comment l’utiliser ?

Il s’agit donc d’un nouveau mode de navigation, et en tant que tel, il donne lieu à une nouvelle méthode sur la WebView, à savoir NavigateToLocalStreamUri. Cette dernière prend en paramètre une URI, et une instance du resolver. Bien sûr, il y a quelques règles à respecter. Tout d’abord, l’objet Uri passé en paramètre doit être créé à partir de la méthode BuildLocalStreamUri de la WebView. Ensuite, le resolver doit implémenter l’interface IUriToStreamResolver, et par conséquent implémenter la méthode UriToStreamAsync.

Prenons le contexte suivant :

  • Le local storage est organisé comme sur l’image qui suit :  img1
  • La WebView est inscrite dans mon xaml comme suit :

  • Dans le code behind :
MyWebView.Navigate(new Uri("ms-appdata:///local/html/test.html"));
  • Le code du fichier test.html :
<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="test.css" type="text/css" />
</head>
    <body>
        <h1>Hello World</h1>
        <img class="fake" src="test.jpg" />
        <script type="text/javascript" src="test.js"></script>
    </body>
</html>

Cet exemple ne fonctionne pas du tout sous Windows 8, rien ne sera affiché. Pire, l’application vous proposera de chercher une application sur le store compatible avec cette url (le schéma ms-appdata n’est pas reconnu pour la WebView). Sous Windows 8.1, pas de souci majeur, le html est rendu, le CSS aussi, l’image est affichée. Pour le JavaScript aussi aucun problème.

Maintenant, pour utiliser un resolver d’uri, il est nécessaire de créer une classe implémentant l’interface. L’implémentation suivante offre une navigation vers les mêmes fichiers que l’exemple précédent :

public sealed class MyUriToStreamResolver : IUriToStreamResolver
{
    public IAsyncOperation<IInputStream> UriToStreamAsync(Uri uri)
    {
        return UriToStreamAsyncTask(uri).AsAsyncOperation();
    }

    private async Task<IInputStream> UriToStreamAsyncTask(Uri uri)
    {
        var local = ApplicationData.Current.LocalFolder;
        var html = await local.GetFolderAsync("html");
        // on retire le slash du path pour obtenir le nom du fichier
        StorageFile file = await html.GetFileAsync(uri.AbsolutePath.Remove(0, 1));
        using (var stream = await file.OpenAsync(FileAccessMode.Read))
            return stream.GetInputStreamAt(0);
    }
} 

L’appel de la méthode Navigate serait remplacé par :

var uri = MyWebView.BuildLocalStreamUri("test", "test.html");
MyWebView.NavigateToLocalStreamUri(uri, new MyUriToStreamResolver());

Il est possible d’utiliser un autre dossier racine que celui sélectionné par défaut dans le Resolver afin de customiser le comportement. Si l’on souhaite modifier à la volé un fichier il est envisageable de procéder ainsi :

private async Task<IInputStream> UriToStreamAsyncTask(Uri uri)
{
    var local = ApplicationData.Current.LocalFolder;
    var html = await local.GetFolderAsync("html");
    StorageFile file = await html.GetFileAsync(uri.AbsolutePath.Remove(0, 1));

    if (!uri.AbsolutePath.EndsWith(".css"))
        using (var stream = await file.OpenAsync(FileAccessMode.Read))
            return stream.GetInputStreamAt(0);
    
    using (var memoryStream = new InMemoryRandomAccessStream())
    {
        using (var dataWriter = new DataWriter(memoryStream))
        {
            dataWriter.UnicodeEncoding = UnicodeEncoding.Utf8;
            dataWriter.ByteOrder = ByteOrder.LittleEndian;
            using (TextReader r = new StreamReader(await file.OpenStreamForReadAsync()))
            {
                var txt = r.ReadToEnd();
                txt += "body { color: red; }";
                dataWriter.WriteString(txt);
                await dataWriter.StoreAsync();
                await dataWriter.FlushAsync();
                dataWriter.DetachStream();
            }
        }
        return memoryStream.GetInputStreamAt(0);
    }
}

Le code précédent rajoute une condition au premier pour rajouter une règle dans les fichiers CSS demandés par la WebView.

Bien entendu il est tout à fait possible de bénéficier des avantages du resolver dans une application WinJS. Il néanmoins impossible (en tout cas à ma connaissance) d’implémenter directement une interface WinRT en JavaScript et d’instancier un objet WinRT. En effet, les classes WinRT du Framework du même nom travaillent avec des objets transposables dans le type WinRT correspondant, ou bien avec des objets WinRT (instances d’une classe scellée et définie dans un composant WinRT en général). Or la WebView WinJS (qui est sensiblement la même qu’en XAML à l’utilisation) attend un objet implémentant l’interface. La seule façon que je connaisse à ce jour est de passer par un composant WinRT. La classe implémentant IUriToStreamResolver est donc directement définie dans le composant. Cela n’a rien de très complexe, à ceci près que cela ne peut être réalisé qu’en C# ou C++. Bien entendu le C++ aura l’avantage de ne pas nécessairement (ni inutilement) charger le Framework .NET pour lancer l’application WinJS, et sera donc bien moins gourmand en ressource.

Comme certains l’ont peut-être déjà remarqué, la classe MyUriToStreamResolver respecte déjà les règles nécessaires à sa définition publique dans un composant WinRT. Le code suivant utilise le resolver dans une application WinJS :

    
    

 

Cas d’utilisation

Le véritable intérêt du resolver est qu’il permet d’implémenter une logique dans la distribution des ressources que la WebView demande. Ainsi, il n’est pas obligatoire de renvoyer un fichier existant physiquement dans le storage. Il est tout à fait possible de créer le stream et son contenu à la demande. On peut aussi imaginer un processus de décryptage où des ressources ne seraient pas lisibles telles quelles par le resolver. Le resolver se chargerait de décrypter la ressource avant de la fournir à la WebView en clair, ce qui pourrait être fait en mémoire par exemple.

Le resolver prend aussi tout son intérêt lorsque l’arborescence des fichiers dans le storage ne correspond pas aux URIs reçues. Dans ce cas, le resolver peut extraire l’information pertinente de l’URI et aller chercher la bonne ressource dans le storage.

On peut aussi imaginer des scénarios de filtres. Par exemple, l’application peut vouloir afficher un fichier HTML dans une WebView, et autoriser la WebView à charger les fichiers CSS et images mais pas les fichiers JavaScript pour des raisons de sécurité. Ceci m’a été très utile pour empêcher une WebView de charger un fichier de type .otf (définition de police), car ceux-ci ne sont pas supportés par Internet Explorer. Pire que cela, ils font crasher la WebView. Le resolver, en l’occurrence, a permis de filtrer les requêtes vers ce type de fichier, sans avoir à modifier le fichier HTML original.

Enfin, il est possible d’ajouter ou de retirer des dépendances de façon dynamique dans un fichier HTML avant de le rendre, comme du code JavaScript personnel et un ensemble de variable dépendant du contexte de l’application (cela peut aussi être réalisé après chargement de la page avec la méthode InvokeAsync). Ici, à la différence du scénario précédant, le resolver ne filtre pas la dépendance demandé, il réécrit à la volée le document HTML pour en retirer les dépendances indésirables, ou même en rajouter.

Ce qui auparavant aurait nécessité de lancer un serveur embarqué au sein de l’application et de réécrire les urls des documents HTML de façon à pointer sur le serveur local, se fait maintenant de façon plus simple, plus souple et plus performante !

Pour finir, j’ajouterai que les WebViews de Windows 8.1 offrent un autre nouveau mode de navigation via la méthode NavigateWithHttpRequest ( juste pour être exhaustif :) ).

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus