Implémentation d’une solution fondée sur les Azure App Services : Consommation du service de recherche par une Application Universelle

Voici donc le cinquième d’une série d’articles consacrée à l'implémentation d’une solution fondée sur les Azure App Services et qui sera organisée de la façon suivante :
Solution et choix d’implémentation
Mise en œuvre d’un service de recherche
Consommation du service de recherche par une application Web
Exposition du service de recherche
Consommation du service de recherche par une application Universelle
Gestion d’identité
Code et configuration de l’application

L’objectif est de développer une application pouvant s’exécuter sur Windows 10 ou Windows 10 Mobile, en utilisant l’« API App » décrite dans mon précédent article pour accéder aux services Backend.

Description fonctionnelle de l’application

Sur le plan fonctionnel, l’application doit permettre de naviguer entre une page de recherche et une page pour le stockage. Nous nous limiterons à l’implémentation de la partie recherche, la page liée au stockage pouvant être développée de façon similaire.
La page « Recherche » offre la possibilité de saisir une chaîne de caractères pour lancer une requête sur l’ensemble des attributs marqués comme étant « searchable » de l’index d’« Azure Search ». Elle doit permettre d’afficher le résultat de cette requête de telle sorte que l’on puisse downloader par un simple clic le document souhaité (avec là encore, incrément du compteur de téléchargement).

La plateforme UWP
Une plateforme universelle

Avec Windows 10 et la plateforme UWP (Universal Windows Platform), il est maintenant possible de créer des applications qui s’exécutent, avec le même package, dans un store unique, sur de multiples types de device (Windows 10 mobile, Windows 10, Xbox, Hololens, device IoT,…).

univapp
Précédemment, avec les versions Windows 8 et Windows Phone 8, même s’il était possible de partager du code, ce code contenait des directives de compilation permettant de prendre en compte les spécificités de telle ou telle plateforme. En outre, le package était spécifique à chaque plateforme.
L’exécution d’un même code sur PC et sur appareils mobiles est maintenant rendue possible grâce à la convergence des APIs mises à disposition dans une librairie baptisée « Universal Windows ».  
image
Cette librairie « Universal Windows » apparaît dans la liste des références du projet Visual Studio.

image

Les spécificités de l’application universelle « Doc.Search.App »

L’application « Doc.Search.App » doit fonctionner aussi bien sur un device mobile que sur un PC Windows. Ce choix se traduit par deux pré-requis.

En premier lieu, l’application doit être en mesure de détecter le type de device et de s’adapter en conséquence. Par exemple, l’application offrira la possibilité de faire des retours arrière sur la navigation avec une option de menu « Back » qui n’apparait que sur l’application Windows 10. Cette option offrira le même type de comportement que la touche « Back » sur Windows 10 mobile.

Le second pré-requis est de permettre à l’application d’adapter automatiquement son affichage aux différentes tailles d'écran. La copie d’écran suivante illustre les deux affichages possibles de la même application en fonction du device qui l’exécute. 
UIApp
Nous allons maintenant étudier les différentes techniques permettant de satisfaire à ces deux pré-requis.

Adaptation au device

L’environnement de développement d’application universelle permet, si on le souhaite, d’écrire du code qui qui ne s’exécute que sous certaines conditions (type de device, version d’API d’extension de la plateforme). Pour ce faire, il suffit d’ouvrir la boîte de dialogue « Gestionnaire de références » et de choisir l'extension du SDK pour la plateforme « Universal Windows » en fonction des familles de devices que le code doit cibler. Bien entendu, l’utilisation de ces extensions de plateforme n'invalide pas les binaires sur les devices qui n’en sont pas la cible.imageAinsi, pour pouvoir utiliser le bouton « Back » sur un device de type Windows 10 Mobile, dans l’application « Doc.Search.App », il faut gérer un évènement de type « BackPressed », ce qui d’après le MSDN, requiert le contrat « Windows.Phone.PhoneContract ».

image
Pour utiliser cette API, le projet « Doc.Search.App » doit donc référencer les extensions Windows Mobile pour la plateforme UWP.

image

Adaptation de l’interface utilisateur

Comme nous l’avons vu, l’application « Doc.Search.App » doit également proposer une interface adaptée à la taille et à la disposition du device sur lequel elle s’exécute. Pour faciliter ce type d’implémentation, la plateforme UWP offre de multiples évolutions.
De nouveaux contrôles sont venus enrichir la plateforme, comme le « RelativePanel » (un container qui permet de définir une zone dans laquelle on peut positionner et aligner les éléments d'interface les uns par rapport aux autres) ou le « SplitView » un contrôle visuel de navigation. Le « ViewStateManager » a également été amélioré pour permettre de changer facilement la mise en page en fonction des dimensions de l’écran et éviter d’avoir à gérer séparément de multiples projets et définitions d’interface utilisateur.

« ViewStateManager » - « AdaptiveTrigger »

En utilisant la classe « AdaptiveTrigger », lorsque la fenêtre est suffisamment réduite, on peut  basculer sur une interface correspondant à celle d’un téléphone mobile et à l’inverse changer d’interface pour s’adapter à un écran plus grand.
Pour ce faire, la classe « AdaptiveTrigger » permet de créer des règles qui déclenchent automatiquement un changement d’état (le « VisualState ») lorsque la hauteur ou la largeur d’une fenêtre dépasse une valeur spécifique.
Lorsque l’on utilise « AdaptiveTrigger » dans le balisage XAML, il n’est plus maintenant plus nécessaire de gérer par code l'évènement « Window.SizeChanged » afin de pouvoir appeler la méthode « VisualStateManager.GoToState ». De plus les effets de ces changements adaptatifs sont directement visibles dans le designer Visual Studio 2015.
Par exemple, pour changer l’affichage d’un mode grille « Paysage » à un mode liste « Portrait », il suffit de déclarer les deux états correspondant à ces « VisualState ».  

imageCe mécanisme peut être étendu pour permettre d’adapter l’interface de l’application en fonction d’autres critères, comme par exemple la présence d’un bouton « Back » sur le device. C’est ce que propose Morten Nielsen avec ses « WindowsStateTriggers » dont le code source est disponible sur GitHub
imagePour utiliser cette extension, il suffit alors de la référencer dans l’entête de la page. 
image
Il devient alors possible de l’employer dans la déclaration des éléments participant au déclenchement du changement d’état...

image

Le contrôle « SplitView »

« SplitView » est un contrôle visuel qui permet d’offrir une gestion de la navigation des applications Windows similaire à celle des applications web et autres plates-formes mobiles. Ce contrôle permet d’afficher un menu que l’on peut modifier pour créer un menu « hamburger » comme le propose Jerry Nilson dans son blog.
Le contrôle « SplitView » expose deux propriétés. Les boutons de menu et de navigation de l’application sont déclarés dans la propriété « Pane ». La propriété « Content » est, comme son nom l’indique, un « ContentPresenter » pour le « Frame » de l’application. Le contenu de la page est donc directement injecté dans le contrôle lui-même, via la propriété « ContentControl » que l’on définit via un mécanisme de binding.
Ce binding est configuré au niveau de la déclaration XAML du contrôle. 
imageIl est implémenté dans le code la page « Shell.xaml ». 
imageIl est préférable d’utiliser le contrôle « Splitview » dans une Page dédiée (c’est le rôle de la page « Shell.xaml ») plutôt qu’à la racine (« app.xaml ») : on bénéficie ainsi d’une  meilleure expérience de conception dans Visual Studio. Pour la page « SearchPage.xaml », l’arbre XAML sera donc :
Window.Current.Content > Shell > SplitView > Frame > SearchPage

Le contrôle « SplitView » offre plusieurs niveaux d'affichage correspondant à la propriété « DisplayMode ». Dans l’application, en fonction de la taille de l’écran deux modes ont été utilisés : le mode « Compact Overlay « (lorsque le volet de menu est ouvert, il se superpose sur la page contenue dans le contrôle) et le mode « Compact Inline » (lorsque le volet de menu est ouvert, la page contenu se décale et revient à sa position d’origine à la fermeture du volet). Dans les deux cas, le volet reste visible en mode « Compact » lorsqu’il est fermé.
Les changements d’état d’affichage du « SplitView » sont également gérés via le « VisualStateManager ».

image

Déploiement dans le Store

Avant de distribuer une application depuis le Store, il faut s’assurer de son bon fonctionnement en mode « .NET Native ». « .NET Native » est une technologie de pré-compilation incluse dans Visual Studio 2015 pour les applications universelles. Le principe est de compiler le code « IL » pour obtenir une librairie native avant la mise à disposition de l’application sur le device cible. Cette innovation apporte plusieurs avantages : optimisation des performances, réduction de l’occupation mémoire, indépendance vis-à-vis du runtime .NET… La contrepartie est une durée de compilation qui est plus élevée. Le fonctionnement par défaut (que l’on peut modifier) est donc de réserver l’option de compilation mode « .NET Native » aux configurations de type « Release ». 
image
Une fois validé le bon fonctionnement de l’application générée en mode « .NET Native », on peut alors créer un package « .appxupload », en précisant les différentes configurations cibles (x86, x64, ARM).
image 
Ce package est généré directement depuis Visual Studio, depuis l’option « Store » accessible par un clic droit sur le projet cible « Universal Windows ». Il contient les fichiers binaires MSIL, mais aussi une référence explicite à la version de la configuration « .NET Native » que l’application utilise et qui est déclarée dans le fichier « AppxManifest.xml ».
Il ne reste plus alors qu’à publier le package sur le portail développeur (https://dev.windows.com). Il sera alors compilé sur le store avec la même version de l’environnement de compilation « .NET Native » que la version référencée dans Visual Studio.

image

Implémentation de l’Application Universelle Windows

Pour implémenter l’application « Doc.Search.App », nous pouvons capitaliser sur les acquis des plateformes précédentes (WPF, Silverlight…). L’application est implémentée selon le respect de modèles éprouvés comme le « pattern M-V-VM » ou le « pattern Command ». Elle est développée en C# et la description de l'interface graphique (fenêtres, bouton, champs de texte, contrôle d’affichage de type liste ou grille) est réalisée en XAML (« Extensible Application Markup Language »), comme nous avons déjà pu voir dans la déclaration du contrôle « SplitView » ou du « VisualStateManager ».

« Le pattern M-V-VM »

Le « pattern M-V-VM » (« Model-View-ViewModel ») est dérivé du « pattern MVC ». Dans la logique de décomposition proposée par le « pattern M-V-VM », le « Model » (ou « DataModel ») est responsable des données, la « View » représente la partie visuelle de l'application tandis que le « ViewModel » expose les données et les comportements (notamment les commandes) pour la « View ».
 
image

Ainsi la « View » peut utiliser le « ViewModel » dans son « DataContext » pour le binding afin de transformer les données en éléments visuels avec les « DataTemplates ». Le « ViewModel » quant à lui ne fait aucune supposition sur la façon dont va s'opérer le binding. C’est sur ce principe que sont alimentés les contrôles « ListView » et « GridView » que nous verrons par la suite.
Pour afficher la « View » avec les informations souhaitées, il suffit d’alimenter le « ViewModel ». Ce dernier est spécifique pour chaque vue. D’un point de vue technique, il s’agit d’une classe qui implémente le pattern « Observable ».Plus concrètement, il s’agit d’implémenter l’interface « INotifyPropertyChanged » et il est préférable de définir une classe abstraite « BaseViewModel » dont hériteront l’ensemble des « ViewModels » pour y gérer le « PropertyChangedEventHandler ».

imageDe même, les propriétés de la classe « ViewModel » implémentent également « INotifyPropertyChanged » (ou « DependencyObject ») ou « ObservableCollection » suivant qu’il s’agit d’objets ou de collections, afin de prévenir la vue lors de tout changement des données, comme par exemple la collection de « DocumentProperties » correspondant aux éléments issus de la requête sur le service de recherche.
Lorsque ce « pattern » est mis en œuvre, la « View » ne contient quasiment pas de « code-behind ». Le code réside dans le « ViewModel » et est facile à tester de façon unitaire, ce qui présente un intérêt supplémentaire pour cette approche.

« Le pattern Command »

Le « pattern Command » permet de séparer complètement le code initiateur de l'action, du code de l'action elle-même. Très utilisé dans le développement d'interface utilisateur, il permet d'associer plusieurs éléments visuels à la même commande, et de les rendre activables ou non, selon des règles spécifiques (par exemple, le bouton « Rechercher » ne sera actif que si la zone de saisie inclue un critère permettant de filtrer la recherche...).

Plus concrètement, l’implémentation de ce « pattern » se traduit par la déclaration d’une « Command » qui, généralement est réalisée dans le « ViewModel », comme c’est le cas pour la commande « SearchCommand ». 
image
Il ne reste plus alors qu’à faire le binding de cette commande dans la « View » sur le contrôle avec lequel on souhaite l’associer.
 
Les commandes sont une implémentation de l'interface « ICommand ». Cette interface expose trois membres :

  • La méthode « Execute(object) » est appelée lorsque la commande est actionnée. Elle comporte un paramètre, qui peut être utilisé pour passer des informations supplémentaires de l'appelant à la commande..
  • La méthode « CanExecute(object) » retourne une valeur booléenne. Si la valeur de retour est « true », cela signifie que la commande peut être exécutée. Le paramètre est le même que pour la méthode « Execute ». Le contrôle sera automatiquement désactivé si la méthode « CanExecute » retourne la valeur false.
  • Le gestionnaire d'événements « CanExecuteChanged » doit être déclenché par l'implémentation de la commande lorsque la méthode « CanExecute » est réévaluée. En XAML, lorsqu'une instance de « ICommand » est liée à une propriété du contrôle commande via une liaison de données, déclencher l'événement « CanExecuteChanged » appelle automatiquement la méthode « CanExecute », et le contrôle va être activé ou désactivé en conséquence.

L’application « Doc.Search.App » utilise une implémentation « classique » de l’interface « ICommand » avec la classe « RelayCommand ». Cette classe permet d’injecter et de relayer la logique des commandes dans le constructeur de « RelayCommand » via un « delegate », ce qui permet de vérifier si elle peut être exécutée et de la déclencher.

image

La déclaration d’une « Command » peut se faire à un autre niveau que celui du « ViewModel ». Par exemple, la commande « UploadCommand », qui permet d’incrémenter le compteur de téléchargement d’un document, doit pouvoir être déclenchée pour chaque « Document » de la collection « DocumentProperties ». Elle doit donc être associée à un contrôle, ici un « HyperlinkButton », lui-même déclaré dans un « DataTemplate ». 
image
Dans le « ViewModel », lors de l’initialisation de la collection « DocumentProperties », il suffit donc de passer en paramètre une lambda expression permettant de décrire l’action souhaitée lors du déclenchement de la commande.
Dans notre contexte, il s’agit d’appeler l’API de mise à jour du compteur et de relancer la recherche pour que l’affichage soit cohérent avec les nouvelles valeurs (celles du compteur de téléchargement incrémenté mais également les mises à jour pouvant avoir eu lieu en parallèle, l’application n’étant pas mono-utilisateur).

imageLa classe « DocumentProperties » étant comme nous allons le voir un peu plus loin, une classe automatiquement générée à partir de la description Swagger de l’« API App », le passage de ce paramètre « Action » sur la classe « DocumentProperties » suppose donc le développement d’une extension sur cette classe incluant la définition de la commande « UploadCommand ».

image

L’organisation du code de l’application « Doc.Search.App »

L’organisation est des plus classiques, un dossier pour les « Views », un pour les « ViewModels », et un pour les extensions.

image

Les contrôles d’affichage des données

En fonction des dimensions de l’écran, la page « SearchPage » utilise deux contrôles d’affichage, en mode liste ou en mode grille.

Le contrôle « ListView »

Le contrôle « ListView » est nativement conçu pour afficher une énumération d’objets. Par conséquent, il est tout à fait adapté pour restituer la liste des « DocumentProperties ».

image
Il suffit donc de le déclarer en XAML, d’établir le binding avec la propriété « Documents », et de définir le modèle d’affichage de chacun de ses éléments (l’ « ItemTemplate »). 
imageL’ « ItemTemplate » est déclaré dans les ressources de la même page. 
image
Il utilise deux objets de type « Converter », l’un pour afficher la tailler du document en unités octales, l’autre pour afficher le nombre de téléchargement (avec ou sans « s », suivant la valeur du compteur…)
image 
Le contrôle « ListeView » utilise également un « ItemContainerStyleSelector » pour permettre d’alterner le style des lignes affichées dans la liste (pour l’instant sur UWP, il n’y a pas de « Triggers » sur le « DataTemplate » pour alterner les styles comme on pouvait le faire en WPF…). 
image
Pour utiliser cette classe ou les classes de type « Converter », il suffit de les déclarer dans les ressources de la page.

image

Le contrôle « GridView »

Lorsque l’écran le permet, il est plus approprié de pouvoir étendre l’affichage sur une grille, d’où l’intérêt d’utiliser le contrôle « GridView ». 
image
De même que pour le contrôle « ListView », il suffit de déclarer une instance de la « GridView » en XAML, d’établir le binding avec la propriété « Documents », de définir le modèle d’affichage de chacun de ses éléments (l’« ItemTemplate ») et définir un « ItemContainerStyleSelector » pour alterner le style des lignes (on utilisera d’ailleurs la même classe que pour la « ListView »).
image
L’ « ItemTemplate » est déclaré dans les ressources de la même page. A la différence de l’ « ItemTemplate » défini pour la « ListView », il utilise un « StackPanel » horizontal pour positionner les différents « TextBlock » ainsi que le « HyperlinkButton ».

Interaction avec l’« API App »

L’application universelle « Doc.Search.Api » doit pouvoir appeler l’ « API App » exposée sur Azure avec une URL similaire à https://microsoft-apiapp###############.azurewebsites.net. Il s’agit d’une API REST, on pourrait donc naturellement utiliser la classe « HttpClient » incluse dans l’espace de nommage « System.Net.Http ». Mais l’utilisation combinée d’« API App » et Visual Studio propose un modèle de développement nettement plus productif.

En effet, après installation du SDK Azure, Visual Studio offre une fonction d’ajout « Azure API App Client », qui permet de générer un proxy d’invocation de cette API App, avec les modèles associés (dont la classe « DocumentProperties » que nous avons dû étendre pour les raisons précédemment expliquées). Cette fonction est accessible à travers un assistant permettant de choisir l’ « API app » en ligne ou à partir d’un fichier Swagger local.

image
Cet assistant télécharge les métadonnées de l’API App et génère une interface typée pour pouvoir gérer les appels à l’API. Une fois la génération de code achevée, un nouveau dossier du nom de l’API App est ajouté à la solution. Il contient le code correspondant à l’implémentation des classes « client » et des modèles de données.

image 
L’appel à l’« API App » depuis le code de l’application Universelle est alors grandement simplifié grâce à la mise à disposition de l’objet « DocSearchAPI », qui permet d’accéder à un objet de type « SearchOperations » offrant des méthodes comme « SearchbyNameAsync » permettant de lancer une recherche sur un champ texte ou « Update » assurant la mise à jour du compteur de téléchargement. 
imageDans le cas de la méthode « Update », il a été nécessaire de reprendre le code généré dans la classe « SearchOperationsExtensions » pour permettre une invocation asynchrone.
Le code d’origine était le suivant. 
imageLe nouveau code est celui-ci :
image

Nous disposons maintenant d’une application universelle capable d’échanger avec une « API App » déployée dans Azure, cette API App permettant d’exposer l’accès au service de recherche « Azure Search ». Dans le prochain article, nous étudierons comment intégrer des mécanismes permettant d’assurer la gestion d’identité de l’ensemble de la solution.