Archives du mot-clé Windows 8

Des applications Windows 8 moins gourmandes…

Mon post précédent traitait de l’espace disque utilisé par les applications Windows 8 et montrait que bien souvent elles étaient trop gourmandes dans leur utilisation du stockage local. Parmi ces applications, je parlais notamment des applications Dior Mag et Techdays 2013 qui, sur mon laptop, utilisaient respectivement 1,27Go et 21Mo.
Sachez qu’en fin de semaine des mises à jour étaient disponible pour ces 2 applications et que ce bug a été corrigé. Après installation des mises à jour et 2 jours d’utilisation, l’application Techdays 2013 occupe maintenant 5Mo et Dior Mag 28Mo.

Bravo aux développeurs pour leur réactivité. Cela montre qu’ils sont à l’écoute de ce que les utilisateurs peuvent dire de leur travail. Ainsi, n’hésitez pas à noter et commenter, en bien ou en mal, avec toute objectivité bien évidemment, les différentes applications du Store, ça ne fera qu’améliorer la qualité des contenus proposés.

Je continuerai tout de même à penser que ce genre de données à toute sa place dans le répertoire temporaire car :

  • le contenu peut être téléchargé à tout moment,
  • le mode de consommation de ces données par l’utilisateur est purement consultatif,
  • le stockage temporaire n’empêche pas le fonctionnement Offline,
  • le stockage temporaire permet à l’utilisateur de mieux gérer son espace disque, sans avoir à désinstaller des applications

Et vous, où pensez-vous qu’il vaut mieux stocker ces données de type Cache ?

Les applications Windows 8 sont souvent trop gourmandes en espace disque

Il y a de plus en plus d’applications disponible sur le Store et si vous êtes comme moi, curieux de voir ce que peuvent développer les autres, vous installez un tas d’applications. Vous les installez afin de les analyser, d’un point de vue design, ergo, etc… Ensuite vous ne les désinstallez pas forcément, pour pouvoir les montrer ou vous inspirer des plus réussies ou encore avoir des exemples de ce qu’il ne faut pas faire.

Dans ce post, je n’attaque personne en particulier, et je ne connais pas forcément le ou les développeurs ayant travaillés sur les applications que je prendrai en exemple. D’ailleurs mon objectif n’est pas de faire la critique d’une ou plusieurs applications, mais de sensibiliser les développeurs d’applications Windows 8 sur un point qui est jusqu’ici très souvent mis de côté, l’espace disque.

Cet espace disque, comme vous le savez, n’est pas infini, et l’est encore moins sur une Surface RT 32Go. Or je constate que beaucoup d’applications stockent des mégaoctets de données voir même pour certaines des gigaoctets dans le répertoire locale de l’application et que ce n’est pas toujours justifié.

Nous avons parlé lors du DevCamp Back to basics de la semaine dernière des différentes API de stockage ou d’accès aux données. Ce n’est que la veille de ce DevCamp que j’ai constaté que les applications que nous installons occupent souvent plusieurs mégaoctets d’espace disque et que l’espace occupé par ces applications grandit au fil des jours et de leur utilisation. Or j’ai du mal à comprendre comment une application pour laquelle je ne produis pas de données, mais que je ne fais qu’en consommer, qui plus est, une infime partie de ce qu’elle me propose, prenne aujourd’hui 10Mo, 100Mo, 500Mo, voir 1Go… Ma pauvre Surface risque de ne pas s’en remettre! Ce constat est intervenu trop tard pour que j’en fasse la démo au DevCamp, mais c’est tout de même pour cette raison que j’ai abordé dans mes slides la notion de TemporaryFolder en réexpliquant à quoi sert ce répertoire…

Mise en Cache et TemporaryFolder

Il existe une API disponible pour les applications Windows 8 qui s’appelle TemporaryFolder (ApplicationData.Current.TemporaryFolder). Cette API est disponible au même endroit que le LocalFolder ou encore que les RoamingSettings. Ce fameux TemporaryFolder sert à stocker des données dans un répertoire temporaire, comme son nom le laisse penser. Cela signifie que l’utilisateur a la possibilité de supprimer le contenu de ces répertoires temporaires via l’outil de nettoyage de disque, ou encore que le système peut lui aussi supprimer ce contenu pour récupérer de la place.

Cela signifie également que si l’on stocke des données dans ce répertoire, on ne peut être sur qu’elles seront encore là au prochain lancement de l’application. C’est en général, le principe de base d’un Cache. Un Cache est souvent utilisé pour stocker des données de manière temporaire. Il a besoin d’être invalidé et rafraichit au bout d’un certain temps, et peut même être supprimé si on n’y a pas accédé depuis un certain laps de temps. Ce qui permet de ne pas occuper d’espace disque inutilement (ou même d’espace mémoire dans le cadre d’un cache mémoire).

Quand doit-on utiliser ce répertoire temporaire ou comment peut-on mieux gérer l’espace occupé par les contenus, souvent éphémères et considéré comme du Cache ?

On peut quasiment toujours utilisé le stockage temporaire. Lorsque j’inspecte les applications installées sur mon poste (je me rends dans le répertoire LocalState des différents packages), je vois très souvent un répertoire nommé “Cache”. Pourquoi dans ce cas ne pas le mettre dans le stockage temporaire ?

Il faut également différencier mise en cache et fonctionnement Offline.

La mise en cache est généralement utilisé pour éviter de devoir télécharger plusieurs fois un même contenu, afin d’améliorer la fluidité de l’application, dans la navigation, les animations, les transitions et éventuellement le chargement.

Prenons l’exemple d’une application de type News/Actus. Il est beaucoup plus agréable en terme d’UX de télécharger les différentes Unes et articles du moment en local, ainsi que les différents médias associés tels que les images. Cependant, a-t-on besoin que ces différents articles soient disponible “à vie” ? Quel serait le soucis si ces données étaient stockées dans le répertoire temporaire ? On aurait les avantages du stockage local, avec en plus l’avantage pour l’utilisateur de gérer son espace disque. Et si le système est en manque de ressources de stockage, il peut lui aussi décider de nettoyer ces données, qui ne sont clairement pas vitales pour l’utilisateur.

Vérifier l’espace occupé par les applications Windows 8

Lorsqu’on est utilisateur de Windows 8, il est possible de vérifier l’espace occupé par les applications depuis les paramètres du PC et ainsi trouvé les applis “Gloutons” :

windows-live-writer_eebaed15233e_99a2_image_thumb

Analyse de quelques applications

Ci-dessous quelques exemples d’applications pour lesquelles j’ai analysé le contenu et la taille du répertoire local. Mon choix s’est arrêté sur ces quelques applications pour plusieurs raisons. Premièrement car je les avais installé lorsque j’ai fait ces analyses. Deuxièmement, car elles étaient représentatives du problème soulevé avec des scénarios de stockage différents. Il y a bien sûr d’autres applications sur le Store avec des problèmes de stockage. Tout ça pour dire que le choix des apps n’est pas arbitraire.

L’application Télé7

Prenons tout d’abord l’exemple de l’application Télé7, qui, si vous ne l’avez jamais testé, l’essayer, c’est l’adopter. Elle fonctionne avec une base de données locale de type SQL LITE, qui pèse environ 24Mo. Cette base est téléchargée à l’ouverture de l’application si la base actuelle a plus de 3 jours. Ceci permet à l’application de fonctionner en mode Offline, vous avez donc accès à votre grille des programmes TV sans avoir besoin d’internet. C’est une très bonne idée, et je l’ai d’ailleurs installé sur tous mes devices (laptop, Slate 7 et Surface).

windows-live-writer_eebaed15233e_99a2_tele7_thumb

Par contre en regardant ce qu’il se passe dans le répertoire LocalState je m’aperçois qu’il contient toutes les bases de données téléchargées depuis l’installation de l’application. Une base de données fait environ 24Mo. Pour l’instant j’en suis à 9 base de données locales, donc 216Mo.

Dans le cas de Télé7, on peut effectivement se dire qu’il est préférable de stocker la base de données en local, et non dans le Temp. Mais il faut dans ce cas une stratégie de rétention des données de l’application, afin de supprimer les anciennes bases. Je suis sûr que ce problème sera rapidement corrigé…

L’application Dior Mag

Prenons un autre exemple, cette fois dans une autre dimension… Le luxe a un cout, et pas seulement au niveau du porte monnaie mais également en stockage. La marque Dior a son application Windows 8, Dior Mag, application très jolie soit dit au passage. Attention si vous l’installez sur une Surface RT 32Go, elle prendra bientôt plus de place que l’OS!

windows-live-writer_eebaed15233e_99a2_diormag_thumb

Actuellement sur mon poste l’application prend pas moins de 1,27Go (11 972 fichiers de type jpeg et text/json), avec en local des fichiers datés d’octobre à janvier. Je vous rassure tout de même, l’utilisateur a la possibilité depuis le Charm des paramètres de l’application de supprimer le Cache. Mais l’utilisateur est-il invité à un moment ou à un autre d’utiliser ce Charm ? D’autre part, l’utilisateur de cette belle application a-t-il besoin d’avoir accès en local à tous les catalogues depuis son installation ?

Si vous voulez faire un petit test, vous pouvez supprimer tout le contenu du répertoire LocalState de ce package, ainsi que le fichier settings.dat. Une fois ces fichiers supprimés, au lancement de l’application, tous ces fichiers seront téléchargés. La logique de présence des fichiers est bien là. Pour les mettre dans le répertoire temporaire, il suffit de changer au niveau du code LocalFolder par TemporaryFolder.

L’application Bing

Un autre exemple, l’application Bing. Cette fois nous revenons dans des tailles de stockage très raisonnable, mais un choix de stockage vraiment incompréhensible.

windows-live-writer_eebaed15233e_99a2_bing_thumb

Si nous inspectons le répertoire de stockage de cette application, nous nous rendons compte qu’elle stocke les images de background utilisée par Bing. Or ces images changent régulièrement, parfois plusieurs fois par jours, et ne sont apparemment jamais supprimées.

Bien sûr ici l’espace de stockage utilisé par ces images n’est que de 4,5Mo (32 images depuis octobre, pour moins d’une dizaine d’ouverture de l’application). Par contre ceci est vraiment incompréhensible, dans le sens où les quelques images du jour ne pèsent que 300Ko et que toutes les autres images ne serviront plus jamais. Et pour cette application, on ne peut pas dire qu’il y a un mode Offline… Ici les images auraient dû être stockées dans le répertoire temporaire.

L’application Techdays 2013

L’application Techdays 2013, que je trouve très réussie, fluide et agréable à utiliser, est idéale pour gérer votre agenda de sessions pour cet évènement.

windows-live-writer_eebaed15233e_99a2_techdays2013_thumb

Cette application est fonctionnelle en mode Offline. Je désactive mon accès internet, et j’ai accès à toutes les sessions, les fiches des speakers, les photos, mon agenda… Nous avons donc en local toutes les informations nécessaires au bon fonctionnement de l’application. De ce côté c’est parfait.

Par contre, si je regarde ce qu’il se passe du côté du stockage local, ça fait peur… Dans un répertoire nommé Cache, j’ai pas moins de 625 fichiers (json), qui pour la plupart ont exactement le même contenu. On s’aperçoit que, quasiment à chaque lancement de l’application, tout le contenu est re-téléchargé en local si connexion internet il y a. Et que l’ancien contenu n’est pas supprimé.

Le fait d’avoir tout le contenu en local est évidemment une bonne idée, et c’est entre autre pour cette raison que l’application se charge rapidement et qu’elle est fluide et agréable à utiliser. Mais d’une part, je ne pense pas qu’il y ait besoin de télécharger et restocker ce contenu à chaque lancement de l’application (un numéro de version dans le json permettrait de palier à ce problème), et d’autre part il faudrait que les contenus précédemment téléchargés soient supprimés. Ce matin j’étais à 15Mo de stockage pour cette application. Cet après midi, après 4 ou 5 ouvertures de l’application, je suis à 21Mo. Et pourtant, le contenu est le même, pas de sessions ou de speakers supplémentaires.

Ici on voit bien que ces données sont utilisées comme un cache local pour fluidifier la navigation de l’application et à la fois pour offrir un mode Offline. Ces données pourraient être placées dans le TemporaryFolder, sauf pour l’agenda, qui doit rester local ou distant. Si l’on persiste à enregistrer ces données dans le LocalFolder, il faut mettre en place une politique de rétention des fichiers.

Conclusion

Je pense que l’API TemporaryFolder est vraiment méconnue des développeurs ou qu’ils n’en connaissent pas l’utilité. Mais ce n’est pas une excuse. Le fait que des anciens fichiers, qui ne serviront plus et qui vont rester dans le stockage local jusqu’à désinstallation de l’application n’est pas normal. L’utilisateur n’a pas d’autre choix que de désinstaller l’application pour nettoyer ce stockage. Vous pouvez vous même faire l’expérience suivante : rendez-vous dans le répertoire UsersYOUR_ACCOUNTAppDataLocalPackages. Parmi tous ces packages, effectuez une recherche des répertoires TempState, puis LocalState et comparez la taille totale utilisé par chacun de ces répertoires.

Parmi les applications que j’ai installé sur mon laptop, celle qui est la moins jolie, mais qui reste fonctionnelle, c’est l’application Windows Phone, qui permet de transférer/synchroniser des données entre son téléphone et son PC. Et bien parmi toutes les applications que j’ai actuellement (68 applications au total), c’est la seule qui possède du contenu dans le répertoire temporaire… Comme quoi, on peut faire des applications moches, mais fonctionnelles, et techniquement bien pensées.

Si vous pensez que votre application doit finalement utiliser ce stockage temporaire plutôt que le stockage local, il vous suffit simplement de changer dans votre code LocalFolder par TemporaryFolder. Et si ce n’est pas le cas, n’oubliez pas de supprimer les anciens fichiers, sinon ce sont les utilisateurs qui supprimeront votre application.

Retour du DevCamp Back to basics Win 8 et WP8

image
En attendant que les webcasts soient disponibles, je vous propose en téléchargement les ressources de ma session. Dans cette archive vous trouverez les slides ainsi que les démos.

Cette session portait sur les différentes API de stockage ainsi que les stratégies à adopter sur les plateformes Windows 8 (App Store) et Windows Phone 8.

Après avoir rappelé ce que l’on entend par accès aux données, nous avons abordé les notions suivantes :

  • Accès aux données locales de l’utilisateur (Contacts, Calendrier, Librairies…) avec en démonstration l’accès à la librairie de musique de l’utilisateur,
  • Accès aux données distantes de l’utilisateur (Hotmail, Skydrive, Messenger) avec en démonstration l’utilisation du Live SDK depuis une application Windows Phone 8,
  • Accès aux données locales de l’application, avec pour rappel les API Windows Phone 8 (IsolatedStorage et ApplicationData) et les API Windows 8 (LocalFolder, LocalSettings, TemporaryFolder),
  • Accès aux données itinérantes (Roaming) des applications Windows 8 avec l’API ApplicationData (RoamingFolder et RoamingSettings),
  • Accès aux données distantes de l’application/service. Données pouvant être stockées dans le Cloud ou sur un serveur OnPremise, avec 3 démonstrations :
    • Azure Mobile Services depuis une application Windows 8 et Windows Phone 8
    • Azure Blob Storage depuis une application Windows 8
    • WCF Data Services depuis des applications clientes Windows 8 et Windows Phone 8

Ci-dessous, quelques informations concernant la configuration et l’exécution des différentes démos que vous trouverez dans l’archive.

Démo MusicLibrary

Pas de configuration ou d’installation particulière. C’est une application Windows 8 qui explore votre librairie de musique. Voici l’article qui explique ce projet : Windows 8 Développement d’un lecteur audio.

Démo Live SDK

  • Télécharger et installer le  Live SDK v5.3.
  • Créer une application Live sur cette page et récupérer le Client ID.

Une fois l’application créée, il vous faut récupérer le Client ID, éditer le fichier Main.xaml du projet Windows Phone LiveSDK, et modifier la propriété ClientId du contrôle SignInButton.

Pour plus d’exemple de code du Live SDK rendez-vous ici : https://github.com/liveservices/LiveSDK

Et pour la documentation en ligne : http://msdn.microsoft.com/fr-fr/library/live/hh826532.aspx

Démo Azure Mobile Service

Pour les projets MobileServiceApp Windows 8 et Windows Phone 8, il vous faut au préalable vous rendre sur le portail Azure et créer un service Mobile. Une fois le service créé, vous pouvez soit télécharger les solutions Windows 8 et Windows Phone 8 proposées par le portail ou éditer les projets de la solution.

Pour configurer les projets MobileServiceApp Windows 8 et Windows Phone 8, éditer le fichier App.xaml.cs et lors de l’instanciation de l’objet MobileServiceClient, renseigner l’URL vers votre Azure Mobile Service ainsi que sa clé.

Démo Azure Blob Storage

Ce projet nécessite l’accès à un compte de stockage sur votre abonnement Azure. Depuis le portail Azure, vous pouvez créer un compte de stockage, puis récupérer son nom et sa clé.

Dans le projet AzureStorageExplorerApp, ouvrir le fichier App.xaml.cs et renseigner les valeurs des constantes StorageAccountName et StorageKey avec les données récupérées sur le portail Azure.

Pour plus de détails sur le code de ce projet, voici l’article à consulter : Windows 8 Azure Storage Explorer

Démo WCF Data Services

  • Télécharger et installer WCF Data Services 5.0 for OData V3
  • Télécharger et installer WCF Data Services Tools for Windows Store Apps
  • Télécharger et installer OData Client Tools for Windows Phone Apps
  • Dans le projet Common/WCFDataServices, créer l’application dans IIS (après l’avoir configurer pour WCF).
  • Dans le projet WCFDataServicesWP8Client, ouvrir le fichier NotesViewModel.cs, puis dans la méthode LoadNotes modifier l’URL avec l’adresse IP de votre machine. Ceci permet de faire fonctionner l’appel au Web Service depuis l’émulateur Windows Phone ou depuis votre device de développement.
  • Il vous faudra éventuellement modifier les règles de trafic de votre pare-feu pour autoriser les requêtes HTTP entrantes

Back to basics sur les données et le stockage pour Windows 8 et Windows Phone 8

image Ce jeudi 10 janvier, Microsoft organise le premier après midi du développement autour des données sur les plateformes Windows 8 et Windows Phone 8.

L’évènement aura lieu au Centre de Conférence à Issy Les Moulineaux à partir de 13h30, et pour ma part je vous présenterai les différentes possibilités et API de stockage sur ces plateformes.

Si vous n’êtes pas encore inscrits, dépêchez-vous de le faire… et si vous hésitez encore sachez qu’il y aura une tablette Asus Vivo Tab à gagner…

Pour les inscriptions, suivez ce lien : https://msevents.microsoft.com/CUI/EventDetail.aspx?EventID=1032521905&Culture=fr-FR

HeightSquare – Partie 3 : Le contrat Contact Picker

Dans les 2 premières parties nous avons expliqué les concepts d’authentification OAuth et la création d’un composant Windows Runtime. Nous terminons cette série avec la mise en place du contrat Contact Picker.

Dans ce scénario notre application devient fournisseur des informations de nos amis Foursquare aux autres applications.

Et voici pour le code complet de cette exemple d’application : HeightSquareApp.zip

Il vous faudra éditer les constantes ClientId et CallbackUri dans la classe App correspondant à votre application Foursquare (voir la première partie pour plus de détails).

Déclaration et activation du contrat

Nous en avons maintenant l’habitude, l’implémentation de contrats se déclare d’abord au niveau du manifeste. Dans l’onglet Declarations, on ajoute le Contact Picker.

windows-live-writer_756c95d55058_109a2_declarations_2
L’activation du contrat intervient au niveau de la classe App et de la méthode OnActivated :

  1. protected override void OnActivated(IActivatedEventArgs  args)
  2. {
  3.     if (args.Kind == ActivationKind.ContactPicker)
  4.     {
  5.         var frame = Window.Current.Content as Frame;
  6.         if (frame == null)
  7.             frame = new Frame();
  8.         Window.Current.Content = frame;
  9.         frame.Navigate(typeof(ContactPickerPage), args);
  10.         Window.Current.Activate();
  11.     }
  12.     else
  13.     {
  14.         base.OnActivated(args);
  15.     }
  16. }

Affichage des contacts Foursquare

Nous allons afficher la photo et le nom de nos contacts Foursquare. Pour cela nous définissons le DataTemplate suivant :

  1. <Page.Resources>
  2.     <DataTemplate x:Key= »ContactTemplate »>
  3.         <Grid Width= »200″ Height= »250″>
  4.                 <Grid.RowDefinitions>
  5.                     <RowDefinition Height= »150″ />
  6.                     <RowDefinition Height= »Auto » />
  7.                 </Grid.RowDefinitions>
  8.                 <Image Source= »{Binding Photo} » Width= »150″ Height= »120″ VerticalAlignment= »Center »/>
  9.                 <TextBlock Grid.Row= »1″ Text= »{BindingFullName} » VerticalAlignment= »Center » HorizontalAlignment= »Center »/>
  10.         </Grid>
  11.     </DataTemplate>
  12. </Page.Resources>

 

Et pour terminer une GridView pour afficher et sélectionner des contacts :

  1. <GridView Grid.Row= »1″ ItemsSource= »{Binding}«  SelectionChanged= »GridView_SelectionChanged » IsItemClickEnabled= »True » SelectionMode= »Multiple » ItemTemplate= »{StaticResource ContactTemplate} » />

Chargement des contacts

Le chargement des contacts Foursquare nécessite un jeton d’authentification OAuth. Il faut ensuite prévoir de les filtrer en fonction de ce qu’e l’application appelante désire comme informations.

Stockage du jeton d’authentification dans le Cloud

Nous allons améliorer notre application de sorte que ce jeton soit stocké dans les paramètres de l’application. Cela évitera de demander une authentification à chaque ouverture de l’application.

Pour stocker des paramètres d’application on peut utiliser la classe ApplicationData. Cette classe possède notamment une propriété RoamingSettings qui va nous permettre de stocker notre jeton. Et grâce aux paramètres d’itinérance l’utilisateur sera authentifier sur toute les machines où il ouvrira une session via son compte Microsoft.

Nous utilisons simplement le dictionnaire Values pour accéder à ses paramètres :

  1. public async static Task<string> GetFrousquareToken()
  2. {
  3.     object oToken = Windows.Storage.ApplicationData.Current.RoamingSettings.Values[« AccessToken »];
  4.     if (oToken == null)
  5.     {
  6.         string t = await FoursquareService.AuthenticateAsync(App.clientId, App.callbackFoursquareUri);
  7.         Windows.Storage.ApplicationData.Current.RoamingSettings.Values.Add(« AccessToken », t);
  8.         oToken = t;
  9.     }
  10.     return (string)oToken;
  11. }

Récupération des contacts

Dans la méthode LoadState de notre Page nous ajoutons le code permettant de récupérer les contacts :

  1. string token = await App.GetFrousquareToken();
  2. var friends = await FoursquareService.GetMyFriendsAsync(token);

Filtrage des contacts en fonction des désirs de l’appelant

Lors de l’activation du contrat ContactPicker nous avons reçu un paramètre de type ContactPickerActivatedEventArgs. Cette classe expose un objet de type ContactPickerUI. Cet objet fournit des informations sur ce qui intéresse l’application. Cet objet va également nous permettre d’envoyer nos contacts à l’application appelante.

La classe ContactPickerUI nous permet de connaitre les champs (email, numéro de tel…) dont a besoin l’application. Il nous suffit de parcourir la propriété DesiredFields et de filtrer notre liste de contact :

Toujours dans la méthode LoadState de notre page, voici comment récupérer la classe ContactPickerUI et filtré sur les champs désirés :

  1. var args = navigationParameter as ContactPickerActivatedEventArgs;
  2. if (args == null || args.ContactPickerUI == null)
  3.     return;
  4. var contactPickerUI = args.ContactPickerUI;
  5. foreach  (var field in contactPickerUI.DesiredFields)
  6. {
  7.     switch (Windows.ApplicationModel.Contacts.KnownContactField.ConvertNameToType(field))
  8.     {
  9.         case Windows.ApplicationModel.Contacts.ContactFieldType.Email:
  10.             friends = friends.Where(f => !string.IsNullOrEmpty(f.Email)).ToList();
  11.             break;
  12.         case Windows.ApplicationModel.Contacts.ContactFieldType.InstantMessage:
  13.             // friends =
  14.             break;
  15.         case Windows.ApplicationModel.Contacts.ContactFieldType.Location:
  16.             // friends =
  17.             break;
  18.         case Windows.ApplicationModel.Contacts.ContactFieldType.PhoneNumber:
  19.             // friends =
  20.             break;
  21.         case Windows.ApplicationModel.Contacts.ContactFieldType.Custom:
                // friends =
  22.             break;
  23.         default :
  24.             break;
  25.     }
  26. }

Ajouter des contacts au ContactPickerUI

Lorsque l’on sélectionne des contacts via le Contact Picker, ils s’ajoutent dans le bas du contrôle, comme illustré sur l’image suivante :


image

Pour ajouter des contacts, il suffit de créer des objets de type Windows.ApplicationModel.Contacts.Contact et de les ajouter via la méthode Add de l’objet ContactPickerUI. L’ajout de champs à un contact se fait via la liste Fields.

Le code ci-dessous illustre l’ajout des contacts sélectionnés au ContactPickerUI :

  1. private async void GridView_SelectionChanged(object sender, SelectionChangedEventArgs  e)
  2. {
  3.     foreach  (var item in e.AddedItems.OfType<FoursquareApi.Entities.Contact>())
  4.     {
  5.         var contact = new Windows.ApplicationModel.Contacts.Contact() { Name = item.FullName };
  6.         contact.Fields.Add(new Windows.ApplicationModel.Contacts.ContactField(item.Email, Windows.ApplicationModel.Contacts. ContactFieldType.Email));
  7.         contact.Fields.Add(new Windows.ApplicationModel.Contacts.ContactField (item.Email, Windows.ApplicationModel.Contacts.ContactFieldType.Email));
  8.         HttpClient client = new HttpClient();
  9.         var stream = await client.GetStreamAsync(item.Photo);
  10.         Windows.Storage.Streams.InMemoryRandomAccessStream memory = new Windows.Storage.Streams.InMemoryRandomAccessStream();
  11.         await Windows.Storage.Streams.RandomAccessStream .CopyAsync(stream.AsInputStream(), memory);
  12.         var streamReference = Windows.Storage.Streams.RandomAccessStreamReference.CreateFromStream(memory);
  13.         contact.Thumbnail = streamReference;
  14.         contactPickerUI.AddContact(item.Id, contact);
  15.     }
  16. }

Pour aller plus loin

Dans cet article nous n’avons pas abordé le processus inverse qui consiste à utiliser le picker depuis notre application afin de consommer des contacts provenant d’une autre application. Pour plus de détail sur ce sujet je vous invite à consulter la documentation de la classe ContactPicker qui fournit un exemple d’utilisation.

Et n’oubliez pas les différents évènements Windows 8 organisés par Microsoft et la communauté pour cette rentrée. Voici les liens :

HeightSquare – Partie 2 : Développement d’un composant Windows Runtime

HeightSquarePicker Cet article fait suite à une 1ère partie dans laquelle nous avons expliqué comment nous authentifier auprès des services Foursquare et plus généralement auprès de services qui implémentent le protocole OAuth.

Cette 2ème partie est un peu plus “Roots”, puisqu’elle est consacrée au développement de composants Windows Runtime dans laquelle nous répondons notamment à la question : comment Visual Studio génère un composant WinRT ? Nous terminons cet article par quelques notions d’asynchronisme avec notamment les types IAsyncInfo et IAsyncOperation<T>.

Comme je l’avais déjà précisé dans le 1er article, le code complet de cette application sera disponible à la fin de cette série, et donc au prochain article 😉

Création et compilation d’un composant Windows Runtime

Lorsque l’on veut développer une API pour WinRT et la rendre accessible quel que soit le langage utilisé (C#, VB.NET, C++, Javascript), cette API doit être compilée en tant que composant Windows Runtime.

Avec Visual Studio rien de plus simple… La première étape consiste à créer un projet de type Windows Runtime Component :

HeightSquare1

Si vous avez travaillé avec les versions Developer ou Consumer Preview de VS2012 , ce type de projet n’existait pas. Il fallait créer un projet de type Class Library, puis dans les propriétés du projet, changer le type de sortie en WinMD.

Compilation d’un projet Windows Runtime

Le fait d’ajouter un projet de type Windows Runtime Component permet à Visual Studio de l’identifier en tant que tel, et de le compiler différemment des autres projets.

Si l’on ouvre le fichier csproj avec l’éditeur XML, on peut voir que le type de sortie est winmdobj :

image

 

Visual Studio va appeler le compilateur C# avec cet argument. On peut retrouver cet argument en ouvrant une invite de commande VS2012 et en consultant l’aide du compilateur via la commande csc.exe /? :

image

Ensuite Visual Studio va exporter le fichier winmdobj résultant de cette compilation en fichier winmd. Cette opération se fait via la tache MSBuild WinMdExp, que l’on retrouve dans la documentation de la nouvelle version de Windows.Build.Tasks .

Cette tache MSBuild génère en sortie un fichier d’extension WINMD. C’est ce type de fichier qui représente un composant Windows Runtime. Un composant avec une extension WINMD est réutilisable dans les applications de type Windows Store quel que soit le langage utilisé.

Un composant WinMD est pour les applications Windows Store ce qu’une assembly .Net est aux applications Desktop.

Un fichier WinMD contient le code compilé et les métadonnées. La MSDN nous précise que ce sont ces métadonnées qui ont un format différent entre .Net et WinRT. C’est donc la raison pour laquelle nous devons exporter la sortie du compilateur csc.exe vers un composant WinMD via WinMDExp.exe.

Prenons par exemple la classe SampleClass définie dans le fichier SampleClass.cs. Ouvrons une invite de commande Visual Studio 2012 et tapons la ligne de commande suivante : csc /target:winmdobj SampleClass.cs

image

Nous obtenons en sortie un fichier d’extension winmdobj :

image

 

Ensuite appelons l’outil winmdexp sur le fichier winmdobj. Il faut ajouter 3 références. 2 références .Net, mscorlib.dll et System.Runtime.dll, et une référence vers le Windows Runtime, Windows.winmd. Voici la ligne de commande à exécuter :

winmdexp.exe /reference: »%ProgramFiles(x86)%\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\mscorlib.dll » /reference: »%ProgramFiles(x86)%\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\System.Runtime.dll »  /reference: »%ProgramFiles(x86)%\Windows Kits\8.0\References\CommonConfiguration\Neutral\Windows.winmd » SampleClass.winmdobj

image

Comme nous l’indique l’invite de commande, nous obtenons en sortie un fichier d’extension WINMD :

image

 

L’outil WinMDExp a exporté le code de notre classe dans un fichier de nom SampleWinMD.winmd. Il a simplement utilisé le nom de l’espace de noms racine dans lequel se trouve la classe.

En effet l’une des règles lorsque l’on écrit un composant WIndows Runtime est de mettre toutes les classes que nous exposons dans un même espace de noms racine. On peut bien évidemment les “ranger” dans des sous espaces de noms.

Maintenant que notre composant est compilé et exporté, nous pouvons, depuis un projet WinJS par exemple, l’ajouter en référence :

image

 

Et il ne reste plus qu’à appeler notre classe :

 

image

 

Quelques règles de développement d’un composant Windows Runtime

Nous allons ici détailler quelques règles de base. L’ensemble des règles à respecter lorsque vous développez une composant Windows Runtime est décrit dans cet article de la MSDN .

Exposition de classes “sealed”

Toutes les classes exposées (donc publiques) doivent être scellées (mot clé sealed). Vous pouvez bien évidemment définir ou implémenter des interfaces, mais il n’est pas possible d’utiliser les concepts de l’abstraction ou du polymorphisme par le biais de l’héritage de type pour les classes que vous exposez. Ceci est une restriction qui peut s’avérer assez contraignante, mais nous n’avons pas le choix…

Les types du Windows Runtime

Seuls les types du Windows Runtime, en plus des vôtres (qui respectent les règles) peuvent être exposés, que ce soit pour des valeurs de retour ou des paramètres d’entrée. Vous pouvez exposer certains types primitifs du Framework .Net, comme par exemple les Value types (en fait ils sont également définis dans le WinRT). Par exemple le type DateTime du Framework .Net ne peut pas être exposé depuis un composant WinRT. Il faudra dans ce cas utiliser le type DateTimeOffset. La classe ObservableCollection est également un type .Net et donc non exposable…

Dans cet article de la MSDN vous retrouvez la correspondance des types primitifs du Windows Runtime selon le langage utilisé.

Un autre exemple sur les types .Net que l’on ne peut pas exposer, c’est le type Task, que nous utilisons de plus en plus dans nos développements. Au lieu d’un objet de type Task, il faut exposer un objet de type IAsyncOperation. C’est pour cette raison qu’il existe aujourd’hui une méthode d’extension AsAsyncOperation() qui converti un objet Task en IAsyncOperation. L’inverse existe également, sur un objet IAsyncOperation on peut appeler la méthode AsTask. Thomas nous en parle dans ce post de blog (dans la langue de Shakespeare) et nous y reviendrons également dans la section suivante.

Limitations dans les surcharges des méthodes et des constructeurs

Le concept de surcharge d’une méthode en langage objet est le fait de définir une même méthode plusieurs fois avec des paramètres différents. La surcharge se définit sur le nombre et/ou sur le type des paramètres. Le type de retour reste identique d’une surcharge à l’autre.

Par exemple en C# on peut donc définir les surcharges suivantes :

  1. public void SampleMethod()
  2. {
  3. }
  4. public void SampleMethod( int i)
  5. {
  6. }
  7. public void SampleMethod( string i)
  8. {
  9. }
  10. public void SampleMethod( int i, int j)
  11. {
  12. }

 

Dans un composant Windows Runtime ce code ne compilera pas. En effet en javascript, on fait un peu ce que l’on veut en terme de paramètres… Par exemple, on peut appeler une fonction qui demande un “int” en entrée en lui passant une variable dans laquelle on a mis un “string”.

Si nous voulons persister à écrire ce genre de surcharge, nous devons indiquer quelle est la surcharge par défaut, c’est à dire celle qui sera appelée en cas de doute (en javascript donc…) :

  1. public void SampleMethod()
  2. {
  3. }
  4. [Windows.Foundation.Metadata. DefaultOverload ]
  5. public void SampleMethod( int i)
  6. {
  7. }
  8. public void SampleMethod( string i)
  9. {
  10. }
  11. public void SampleMethod( int i, int j)
  12. {
  13. }

 

Par contre cette règle ne s’applique pas aux constructeurs. Dans un composant WinRT il est tout simplement impossible de définir 2 constructeurs avec le même nombre d’arguments.

Pas de paramètre optionnel

Autre restriction, il n’est pas possible de définir de paramètre par défaut comme ci-dessous :

  1. public void SampleMethod( string s = « hello world » )
  2. {
  3. }

 

Dans ce cas il faudra utiliser des surcharges. Par exemple on peut utiliser les surcharges en “public” et le paramètre optionnel en “private” :

  1. public void SampleMethod()
  2. {
  3. PrivateSampleMethod();
  4. }
  5. public void SampleMethod( string s)
  6. {
  7. PrivateSampleMethod(s);
  8. }
  9. private void PrivateSampleMethod( string s = « Hello World » )
  10. {
  11. }

 

Un peu d’asynchronisme pour terminer

WindowsRuntimeSystemExtensions et les Mondes parallèles…

Nous le disions un peu plus haut, lorsque l’on développe un composant WIndows Runtime et que nous voulons exposer des méthodes asynchrones nous ne pouvons pas retourner un objet de type Task ou Task<TResult>. Nous devons utiliser les types IAsyncAction et IAsyncOperation<TResult> (et IAsyncActionWithProgress<TProgress> et IAsyncOperationWithProgress<TResult, TProgress> pour supporter la mise à jour de la progression).

Pour passer du monde .Net au monde WinRT il faut jeter un œil du côté de la classe WindowsRuntimeSystemExtensions. Cette classe définie des méthodes d’extension aux types Task, Task<T>, IAsyncInfo et IAsyncOperation<T>. On retrouve ces méthodes d’extension dans la classe WindowsRuntimeSystemExtensions :

image

Petite parenthèse au passage… on peut remarquer que des méthodes d’extension GetAwaiter existent pour les types IAsyncAction et IAsyncOperation<T>. C’est grâce à cette méthode qu’il nous est possible d’utiliser le mot clé await sur les composants WinRT. En effet souvenez-vous, les composants WinRT asynchrones ne retournent pas des objets de type Task ou Task<T> mais des objets de type IAsyncAction ou IAsyncOperation<T>. Or contrairement à l’objet Task qui possède bien une méthode GetAwaiter(), les 2 interfaces WinRT n’en définissent pas.

Pour information, l’IntelliSense ne vous montrera pas ces méthodes sur les types IAsyncAction et IAsyncOperation<T> car elles sont marquées par l’attribut EditorBrowsable(EditorBrowsableState.Never) :

  1. [ EditorBrowsable ( EditorBrowsableState .Never)]
  2. public static TaskAwaiter GetAwaiter( this IAsyncAction source)
  3. {
  4. return source.AsTask().GetAwaiter();
  5. }

 

Cet attribut a pour effet de cacher le membre aux différents éditeurs de Visual Studio, et notamment à l’éditeur de code. Cela ne nous empêche pas de l’appeler :

image

 

Donc finalement si nous avons une méthode qui nous retourne déjà un objet de type Task, il nous suffit de l’appeler, récupérer la tache résultante, et appeler la méthode AsAsyncOperation() (ou AsAsyncAction()).

L’exemple ci-dessous encapsule dans une méthode privée l’authentification OAuth aux services Foursquare et retourne un objet Task et une autre méthode publique est chargé de retourner l’objet IasyncOperation :

  1. private async static Task < string > AuthenticateTask( string clientId, string callbackUri)
  2. {
  3. string foursquareUriFormat = « https://foursquare.com/oauth2/authenticate?client_id={0}&response_type=token&redirect_uri={1} » ;
  4. Uri oauthFoursquareUri = new Uri ( string .Format(foursquareUriFormat, clientId, Uri .EscapeDataString(callbackUri)));
  5. Uri callbackFoursquareUri = new Uri (callbackUri);
  6. var authenticateFoursquareResult = await WebAuthenticationBroker .AuthenticateAsync( WebAuthenticationOptions .None, oauthFoursquareUri, callbackFoursquareUri);
  7. if (authenticateFoursquareResult != null && authenticateFoursquareResult.ResponseStatus == Windows.Security.Authentication.Web. WebAuthenticationStatus .Success)
  8. {
  9. Uri tokenFoursquareUri = new Uri (authenticateFoursquareResult.ResponseData.ToString());
  10. WwwFormUrlDecoder decoder = new WwwFormUrlDecoder (tokenFoursquareUri.Fragment);
  11. string accessToken = decoder.GetFirstValueByName( « #access_token » );
  12. return accessToken;
  13. }
  14. else
  15. throw new Exception ( « Authenticate fail… » );
  16. }
  17. public static IAsyncOperation < string > AuthenticateAsync( string clientId, string callbackUri)
  18. {
  19. return AsyncInfo .Run< string >((token) =>
  20. {
  21. return AuthenticateTask(clientId, callbackUri);
  22. });
  23. }

La classe AsyncInfo

La classe AsyncInfo nous offre une autre possibilité. Elle possède une méthode statique Run qui facilite la création d’objets IAsyncAction et IAsyncOperation. Elle supporte également l’annulation et la progression. Si on reprend l’exemple de l’authentification vu précédemment, on obtient le code suivant :

  1. public static IAsyncOperation < string > AuthenticateAsync( string clientId, string callbackUri)
  2. {
  3. return AsyncInfo .Run< string >((token) =>
  4. {
  5. return AuthenticateInternalAsync(clientId, callbackUri);
  6. });
  7. }

AsAsyncOperation() et AsyncInfo.Run : Même combat…

Que doit-on utiliser, y-a-t-il un choix plus judicieux que l’autre ?

Et bien pas vraiment, car les 2 font la même chose. Elles utilisent un objet de type TaskToAsyncOperationAdapter (ou TaskToAsyncActionAdapter) pour encapsuler la tâche. Ci-dessous la méthode AsAsyncOperation() :

  1. public static IAsyncOperation <TResult> AsAsyncOperation<TResult>( this Task <TResult> source)
  2. {
  3. if (source == null )
  4. {
  5. throw new ArgumentNullException ( « source » );
  6. }
  7. return new TaskToAsyncOperationAdapter<TResult>(source, null );
  8. }

 

Et ici la méthode AsyncInfo.Run :

  1. public static IAsyncOperation <TResult> Run<TResult>( Func < CancellationToken , Task <TResult>> taskProvider)
  2. {
  3. if (taskProvider == null )
  4. {
  5. throw new ArgumentNullException ( « taskProvider » );
  6. }
  7. return new TaskToAsyncOperationAdapter<TResult>(taskProvider);
  8. }

 

Finalement notre API Foursquare…

Nous savons maintenant implémenter un composant Windows Runtime asynchrone et réutilisable quel que soit le langage.

Et donc voici le code de notre API Foursquare :

  1. public sealed class FoursquareService
  2. {
  3. private const string versionDate = « 20120828 » ;
  4. public static IAsyncOperation < string > AuthenticateAsync( string clientId, string callbackUri)
  5. {
  6. return AsyncInfo .Run< string >((token) =>
  7. {
  8. return AuthenticateTask(clientId, callbackUri);
  9. });
  10. }
  11. public static IAsyncOperation < Contact > GetMyUserDetailsAsync( string token)
  12. {
  13. return GetUserDetailsTask(token).AsAsyncOperation();
  14. }
  15. public static IAsyncOperation < Contact > GetUserDetailsAsync( string token, string userId)
  16. {
  17. return GetUserDetailsTask(token, userId).AsAsyncOperation();
  18. }
  19. /// <summary>
  20. /// Returns a history of checkins for the authenticated user
  21. /// </summary>
  22. /// <param name= »token »> Access token </param>
  23. /// <returns></returns>
  24. public static IAsyncOperation < IEnumerable < Checkin >> GetMyCheckinsAsync( string token)
  25. {
  26. return GetCheckinsTask(token).AsAsyncOperation< IEnumerable < Checkin >>();
  27. }
  28. /// <summary>
  29. /// Returns a history of checkins for the authenticated user
  30. /// </summary>
  31. /// <param name= »token »> Access token </param>
  32. /// <param name= »limit »> Number of results to return, up to 250 </param>
  33. /// <returns></returns>
  34. public static IAsyncOperation < IEnumerable < Checkin >> GetMyCheckinsAsync( string token, int limit)
  35. {
  36. return GetCheckinsTask(token, « self » , limit).AsAsyncOperation< IEnumerable < Checkin >>();
  37. }
  38. /// <summary>
  39. ///
  40. /// </summary>
  41. /// <param name= »token »> Access token </param>
  42. /// <param name= »limit »> Number of results to return, up to 250 </param>
  43. /// <param name= »offset »> The number of results to skip. Used to page through results. </param>
  44. /// <returns></returns>
  45. public static IAsyncOperation < IEnumerable < Checkin >> GetMyCheckinsAsync( string token, int limit, int offset)
  46. {
  47. return GetCheckinsTask(token, « self » , limit, offset).AsAsyncOperation< IEnumerable < Checkin >>();
  48. }
  49. /// <summary>
  50. /// Returns badges for authenticated user
  51. /// </summary>
  52. /// <param name= »token »> Access token </param>
  53. /// <returns></returns>
  54. public static IAsyncOperation < IEnumerable < Badge >> GetMyBadgesAsync( string token)
  55. {
  56. return GetBadgesTask(token).AsAsyncOperation();
  57. }
  58. /// <summary>
  59. /// Returns badges for a given user
  60. /// </summary>
  61. /// <param name= »token »> Access token </param>
  62. /// <param name= »userId »> ID for user to view badges for </param>
  63. /// <returns></returns>
  64. public static IAsyncOperation < IEnumerable < Badge >> GetBadgesAsync( string token, string userId)
  65. {
  66. return GetBadgesTask(token, userId).AsAsyncOperation();
  67. }
  68. /// <summary>
  69. /// Returns friends of authenticated user
  70. /// </summary>
  71. /// <param name= »token »> Access token </param>
  72. /// <returns></returns>
  73. public static IAsyncOperation < IEnumerable < Contact >> GetMyFriendsAsync( string token)
  74. {
  75. return GetFriendsTask(token).AsAsyncOperation();
  76. }
  77. /// <summary>
  78. /// Returns friends of authenticated user
  79. /// </summary>
  80. /// <param name= »token »> Access token </param>
  81. /// <param name= »limit »> Number of results to return, up to 500. </param>
  82. /// <returns></returns>
  83. public static IAsyncOperation < IEnumerable < Contact >> GetMyFriendsAsync( string token, int limit)
  84. {
  85. return GetFriendsTask(token, « self » , limit).AsAsyncOperation();
  86. }
  87. /// <summary>
  88. /// Returns friends of authenticated user
  89. /// </summary>
  90. /// <param name= »token »> Access token </param>
  91. /// <param name= »limit »> Number of results to return, up to 500 </param>
  92. /// <param name= »offset »> Used to page through results </param>
  93. /// <returns></returns>
  94. public static IAsyncOperation < IEnumerable < Contact >> GetMyFriendsAsync( string token, int limit, int offset)
  95. {
  96. return GetFriendsTask(token, « self » , limit, offset).AsAsyncOperation();
  97. }
  98. /// <summary>
  99. /// Returns a list of a user’s friends
  100. /// </summary>
  101. /// <param name= »token »> Access token </param>
  102. /// <param name= »userId »> Identity of the user to get friends of </param>
  103. /// <returns></returns>
  104. public static IAsyncOperation < IEnumerable < Contact >> GetFriendsAsync( string token, string userId)
  105. {
  106. return GetFriendsTask(token, userId).AsAsyncOperation();
  107. }
  108. /// <summary>
  109. /// Returns a list of a user’s friends
  110. /// </summary>
  111. /// <param name= »token »> Access token </param>
  112. /// <param name= »userId »> Identity of the user to get friends of </param>
  113. /// <param name= »limit »> Number of results to return, up to 500 </param>
  114. /// <returns></returns>
  115. public static IAsyncOperation < IEnumerable < Contact >> GetFriendsAsync( string token, string userId, int limit)
  116. {
  117. return GetFriendsTask(token, userId, limit).AsAsyncOperation();
  118. }
  119. /// <summary>
  120. /// Returns a list of a user’s friends
  121. /// </summary>
  122. /// <param name= »token »> Access token </param>
  123. /// <param name= »userId »> Identity of the user to get friends of </param>
  124. /// <param name= »limit »> Number of results to return, up to 500 </param>
  125. /// <param name= »offset »> Used to page through results </param>
  126. /// <returns></returns>
  127. public static IAsyncOperation < IEnumerable < Contact >> GetFriendsAsync( string token, string userId, int limit, int offset)
  128. {
  129. return GetFriendsTask(token, userId, limit, offset).AsAsyncOperation();
  130. }
  131. /// <summary>
  132. /// Returns mayorships of authenticated user
  133. /// </summary>
  134. /// <param name= »token »> Access token </param>
  135. /// <returns></returns>
  136. public static IAsyncOperation < IEnumerable < Venue >> GetMyMayorshipsAsync( string token)
  137. {
  138. return GetMayorshipsTask(token).AsAsyncOperation();
  139. }
  140. /// <summary>
  141. /// Returns a user’s mayorships
  142. /// </summary>
  143. /// <param name= »token »> Access token </param>
  144. /// <param name= »userId »> Identity of the user to get mayorships for </param>
  145. /// <returns></returns>
  146. public static IAsyncOperation < IEnumerable < Venue >> GetMayorshipsAsync( string token, string userId)
  147. {
  148. return GetMayorshipsTask(token, userId).AsAsyncOperation();
  149. }
  150. /// <summary>
  151. /// Returns the leaderboard of authenticated user
  152. /// </summary>
  153. /// <param name= »token »> Access token </param>
  154. /// <returns></returns>
  155. public static IAsyncOperation < IEnumerable < LeaderboardItem >> GetLeaderboardAsync( string token)
  156. {
  157. return GetLeaderboardTask(token).AsAsyncOperation();
  158. }
  159. /// <summary>
  160. /// Returns the leaderboard of authenticated user
  161. /// </summary>
  162. /// <param name= »token »> Access token </param>
  163. /// <param name= »neighbors »> Number of friends’ scores to return that are adjacent to your score, in ranked order. The current user’s score is returned as well </param>
  164. /// <returns></returns>
  165. public static IAsyncOperation < IEnumerable < LeaderboardItem >> GetLeaderboardAsync( string token, int neighbors)
  166. {
  167. return GetLeaderboardTask(token, neighbors).AsAsyncOperation();
  168. }
  169. public static IAsyncOperation < IEnumerable < Checkin >> GetRecentCheckinsByFriends( string token)
  170. {
  171. return GetRecentCheckinsByFriendsTask(token).AsAsyncOperation();
  172. }
  173. public static IAsyncOperation < IEnumerable < Checkin >> GetRecentCheckinsByFriends( string token, int limit)
  174. {
  175. return GetRecentCheckinsByFriendsTask(token, limit).AsAsyncOperation();
  176. }
  177. public static IAsyncOperation < IEnumerable < Checkin >> GetRecentCheckinsByFriends( string token, int limit, double latitude, double longitude)
  178. {
  179. return GetRecentCheckinsByFriendsTask(token, limit, latitude, longitude).AsAsyncOperation();
  180. }
  181. public static IAsyncOperation < IEnumerable < Checkin >> GetRecentCheckinsByFriends( string token, double latitude, double longitude)
  182. {
  183. return GetRecentCheckinsByFriendsTask(token, null , latitude, longitude).AsAsyncOperation();
  184. }
  185. public static IAsyncOperation < IEnumerable < Contact >> FindUserAsync( string token, string name)
  186. {
  187. return FindUsersTask(token, name).AsAsyncOperation();
  188. }
  189. public static IAsyncOperation < IEnumerable < Contact >> FindUsersAsync( string token, string email)
  190. {
  191. return FindUsersTask(token, null , email).AsAsyncOperation();
  192. }
  193. public static IAsyncOperation < IEnumerable < Venue >> FindVenuesAsync( string token, double latitude, double longitude)
  194. {
  195. return FindVenuesTask(token, latitude, longitude, null ).AsAsyncOperation();
  196. }
  197. [ DefaultOverload ]
  198. public static IAsyncOperation < IEnumerable < Venue >> FindVenuesAsync( string token, double latitude, double longitude, int limit)
  199. {
  200. return FindVenuesTask(token, latitude, longitude, null , limit).AsAsyncOperation();
  201. }
  202. public static IAsyncOperation < IEnumerable < Venue >> FindVenuesAsync( string token, double latitude, double longitude, string queryVenue)
  203. {
  204. return FindVenuesTask(token, latitude, longitude, queryVenue).AsAsyncOperation();
  205. }
  206. public static IAsyncOperation < IEnumerable < Venue >> FindVenuesAsync( string token, double latitude, double longitude, string queryVenue, int limit)
  207. {
  208. return FindVenuesTask(token, latitude, longitude, queryVenue, limit).AsAsyncOperation();
  209. }
  210. public static IAsyncOperation < IEnumerable < Venue >> FindVenuesAsync( string token, double latitude, double longitude, string queryVenue, int limit, int radius)
  211. {
  212. return FindVenuesTask(token, latitude, longitude, queryVenue, limit, radius).AsAsyncOperation();
  213. }
  214. private async static Task < string > AuthenticateTask( string clientId, string callbackUri)
  215. {
  216. string foursquareUriFormat = « https://foursquare.com/oauth2/authenticate?client_id={0}&response_type=token&redirect_uri={1} » ;
  217. Uri oauthFoursquareUri = new Uri ( string .Format(foursquareUriFormat, clientId, Uri .EscapeDataString(callbackUri)));
  218. Uri callbackFoursquareUri = new Uri (callbackUri);
  219. var authenticateFoursquareResult = await WebAuthenticationBroker .AuthenticateAsync( WebAuthenticationOptions .None, oauthFoursquareUri, callbackFoursquareUri);
  220. if (authenticateFoursquareResult != null && authenticateFoursquareResult.ResponseStatus == Windows.Security.Authentication.Web. WebAuthenticationStatus .Success)
  221. {
  222. Uri tokenFoursquareUri = new Uri (authenticateFoursquareResult.ResponseData.ToString());
  223. WwwFormUrlDecoder decoder = new WwwFormUrlDecoder (tokenFoursquareUri.Fragment);
  224. string accessToken = decoder.GetFirstValueByName( « #access_token » );
  225. return accessToken;
  226. }
  227. else
  228. throw new Exception ( « Authenticate fail… » );
  229. }
  230. private async static Task < Contact > GetUserDetailsTask( string token, string userId = « self » )
  231. {
  232. string uriFormat = « https://api.foursquare.com/v2/users/{0}?v={1}&oauth_token={2} » ;
  233. Uri uri = new Uri ( string .Format(uriFormat, userId, versionDate, token));
  234. HttpClient client = new HttpClient ();
  235. var response = await client.GetAsync(uri);
  236. if (!response.IsSuccessStatusCode)
  237. throw new Exception ( « Error when requested Foursquare :  » + response.ReasonPhrase);
  238. string jsonResult = await response.Content.ReadAsStringAsync();
  239. JsonValue jsonValue;
  240. if (! JsonValue .TryParse(jsonResult, out jsonValue))
  241. throw new Exception ( « Unable to parse checkins response » );
  242. var userJson = jsonValue.GetObject()[ « response » ].GetObject()[ « user » ].GetObject();
  243. Contact contact = DeserializeUser(userJson);
  244. return contact;
  245. }
  246. private async static Task < IEnumerable < Venue >> FindVenuesTask( string token, double latitude, double longitude, string queryVenue, int ? limit = null , int ? radius = null )
  247. {
  248. string queryParam = string .IsNullOrEmpty(queryVenue) ? «  » : « &query= » + queryVenue;
  249. string limitParam = limit.HasValue ? « &limit= » + limit : «  » ;
  250. string radiusParam = radius.HasValue ? « &radius= » + radius : «  » ;
  251. string ll = latitude + « , » + longitude;
  252. string uriFormat = « https://api.foursquare.com/v2/venues/search?v={0}&ll={1}{2}{3}{4}&oauth_token={5} » ;
  253. Uri uri = new Uri ( string .Format(uriFormat, versionDate, ll, queryParam, limitParam, radiusParam, token));
  254. HttpClient client = new HttpClient ();
  255. var response = await client.GetAsync(uri);
  256. if (!response.IsSuccessStatusCode)
  257. throw new Exception ( « Error when requested Foursquare :  » + response.ReasonPhrase);
  258. string jsonResult = await response.Content.ReadAsStringAsync();
  259. JsonValue jsonValue;
  260. if (! JsonValue .TryParse(jsonResult, out jsonValue))
  261. throw new Exception ( « Unable to parse checkins response » );
  262. var venues = new List < Venue >();
  263. foreach ( var item in jsonValue.GetObject()[ « response » ].GetObject()[ « venues » ].GetArray().AsParallel())
  264. {
  265. var venueItem = item.GetObject();
  266. var venue = DeserializeVenue(venueItem);
  267. venues.Add(venue);
  268. }
  269. return venues;
  270. }
  271. private async static Task < IEnumerable < Contact >> FindUsersTask( string token, string name, string email = null )
  272. {
  273. string nameParam = string .IsNullOrEmpty(name) ? «  » : « &name= » + name;
  274. string emailParam = string .IsNullOrEmpty(email) ? «  » : « &email= » + email;
  275. string uriFormat = « https://api.foursquare.com/v2/users/search?v={0}{1}{2}&oauth_token={3} » ;
  276. Uri uri = new Uri ( string .Format(uriFormat, versionDate, nameParam, emailParam, token));
  277. HttpClient client = new HttpClient ();
  278. var response = await client.GetAsync(uri);
  279. if (!response.IsSuccessStatusCode)
  280. throw new Exception ( « Error when requested Foursquare :  » + response.ReasonPhrase);
  281. string jsonResult = await response.Content.ReadAsStringAsync();
  282. JsonValue jsonValue;
  283. if (! JsonValue .TryParse(jsonResult, out jsonValue))
  284. throw new Exception ( « Unable to parse checkins response » );
  285. List < Contact > users = new List < Contact >();
  286. foreach ( var item in jsonValue.GetObject()[ « response » ].GetObject()[ « results » ].GetArray().AsParallel())
  287. {
  288. var contactItem = item.GetObject();
  289. var contact = new Contact ();
  290. contact.Id = contactItem[ « id » ].GetString();
  291. if (contactItem.ContainsKey( « firstName » ))
  292. contact.FirstName = contactItem[ « firstName » ].GetString();
  293. if (contactItem.ContainsKey( « lastName » ))
  294. contact.LastName = contactItem[ « lastName » ].GetString();
  295. if (contactItem.ContainsKey( « photo » ))
  296. {
  297. var photoItem = contactItem[ « photo » ].GetObject();
  298. var prefix = photoItem[ « prefix » ].GetString();
  299. var suffix = photoItem[ « suffix » ].GetString();
  300. contact.Photo = prefix + suffix;
  301. }
  302. if (contactItem.ContainsKey( « gender » ))
  303. contact.Gender = contactItem[ « gender » ].GetString();
  304. if (contactItem.ContainsKey( « homeCity » ))
  305. contact.HomeCity = contactItem[ « homeCity » ].GetString();
  306. if (contactItem.ContainsKey( « bio » ))
  307. contact.HomeCity = contactItem[ « bio » ].GetString();
  308. var contactInfoItem = contactItem[ « contact » ].GetObject();
  309. if (contactInfoItem.ContainsKey( « email » ))
  310. contact.Email = contactInfoItem[ « email » ].GetString();
  311. if (contactInfoItem.ContainsKey( « facebook » ))
  312. contact.Facebook = contactInfoItem[ « facebook » ].GetString();
  313. if (contactInfoItem.ContainsKey( « twitter » ))
  314. contact.Twitter = contactInfoItem[ « twitter » ].GetString();
  315. var tipsItem = contactItem[ « tips » ].GetObject();
  316. contact.CountTips = Convert .ToInt32(tipsItem[ « count » ].GetNumber());
  317. users.Add(contact);
  318. }
  319. return users;
  320. }
  321. private async static Task < IEnumerable < Checkin >> GetRecentCheckinsByFriendsTask( string token, int ? limit = null , double ? latitude = null , double ? longitude = null )
  322. {
  323. string limitParam = limit.HasValue ? « &limit= » + limit : «  » ;
  324. string ll = latitude.HasValue && longitude.HasValue ? « &ll= » + latitude + « , » + longitude : «  » ;
  325. string uriFormat = « https://api.foursquare.com/v2/checkins/recent?v={0}{1}{2}&oauth_token={3} » ;
  326. Uri uri = new Uri ( string .Format(uriFormat, versionDate, ll, limitParam, token));
  327. HttpClient client = new HttpClient ();
  328. var response = await client.GetAsync(uri);
  329. if (!response.IsSuccessStatusCode)
  330. throw new Exception ( « Error when requested Foursquare :  » + response.ReasonPhrase);
  331. string jsonResult = await response.Content.ReadAsStringAsync();
  332. JsonValue jsonValue;
  333. if (! JsonValue .TryParse(jsonResult, out jsonValue))
  334. throw new Exception ( « Unable to parse checkins response » );
  335. List < Checkin > checkins = new List < Checkin >();
  336. foreach ( var item in jsonValue.GetObject()[ « response » ].GetObject()[ « recent » ].GetArray())
  337. {
  338. var checkinItem = item.GetObject();
  339. Checkin checkin = new Checkin ();
  340. //checkinItem[« distance »]
  341. // checkinItem[« isMayor »]
  342. // checkinItem[« createdAt »]
  343. // checkinItem[« shoot »]
  344. checkin.Id = checkinItem[ « id » ].GetString();
  345. if (checkinItem.ContainsKey( « distance » ))
  346. checkin.Distance = Convert .ToInt32(checkinItem[ « distance » ].GetNumber());
  347. if (checkinItem.ContainsKey( « isMayor » ))
  348. checkin.IsMayor = checkinItem[ « isMayor » ].GetBoolean();
  349. if (checkinItem.ContainsKey( « shoot » ))
  350. checkin.Shoot = checkinItem[ « shoot » ].GetString();
  351. if (checkinItem.ContainsKey( « createdAt » ))
  352. {
  353. var offset = checkinItem[ « timeZoneOffset » ].GetNumber();
  354. var da = new DateTimeOffset (1970, 1, 1, 0, 0, 0, TimeSpan .FromSeconds(offset));
  355. da = da.AddSeconds(checkinItem[ « createdAt » ].GetNumber());
  356. da = da.ToLocalTime();
  357. checkin.CreatedAt = da;
  358. }
  359. if (checkinItem.ContainsKey( « venue » ))
  360. {
  361. var venueItem = checkinItem[ « venue » ].GetObject();
  362. checkin.Venue = DeserializeVenue(venueItem);
  363. }
  364. if (checkinItem.ContainsKey( « user » ))
  365. {
  366. var userItem = checkinItem[ « user » ].GetObject();
  367. checkin.User = DeserializeUser(userItem);
  368. }
  369. checkins.Add(checkin);
  370. }
  371. return checkins;
  372. }
  373. private async static Task < IEnumerable < Checkin >> GetCheckinsTask( string token, string userId = « self » , int ? limit = null , int ? offset = null )
  374. {
  375. List < Checkin > checkins = new List < Checkin >();
  376. string limitParam = limit.HasValue ? « &limit= » + limit.Value : «  » ;
  377. string offsetParam = offset.HasValue ? « &offset= » + offset.Value : «  » ;
  378. string uriFormat = « https://api.foursquare.com/v2/users/{0}/checkins?v={1}{2}{3}&oauth_token={4} » ;
  379. Uri uri = new Uri ( string .Format(uriFormat, userId, versionDate, limitParam, offsetParam, token));
  380. HttpClient client = new HttpClient ();
  381. var response = await client.GetAsync(uri);
  382. if (!response.IsSuccessStatusCode)
  383. throw new Exception ( « Error when requested Foursquare :  » + response.ReasonPhrase);
  384. string jsonResult = await response.Content.ReadAsStringAsync();
  385. JsonValue jsonValue;
  386. if (! JsonValue .TryParse(jsonResult, out jsonValue))
  387. throw new Exception ( « Unable to parse checkins response » );
  388. foreach ( var item in jsonValue.GetObject()[ « response » ].GetObject()[ « checkins » ].GetObject()[ « items » ].GetArray().AsParallel())
  389. {
  390. var checkinItem = item.GetObject();
  391. Checkin checkin = new Checkin ();
  392. checkin.Id = checkinItem[ « id » ].GetString();
  393. var venueItem = checkinItem[ « venue » ].GetObject();
  394. checkin.Venue = DeserializeVenue(venueItem);
  395. checkins.Add(checkin);
  396. }
  397. return checkins;
  398. }
  399. private async static Task < IEnumerable < Contact >> GetFriendsTask( string token, string userId = « self » , int ? limit = null , int ? offset = null )
  400. {
  401. string limitParam = limit.HasValue ? « &limit= » + limit.Value : «  » ;
  402. string offsetParam = offset.HasValue ? « &offset= » + offset.Value : «  » ;
  403. string uriFormat = « https://api.foursquare.com/v2/users/{0}/friends?v={1}{2}{3}&oauth_token={4} » ;
  404. Uri uri = new Uri ( string .Format(uriFormat, userId, versionDate, limitParam, offsetParam, token));
  405. HttpClient client = new HttpClient ();
  406. var response = await client.GetAsync(uri);
  407. if (!response.IsSuccessStatusCode)
  408. throw new Exception ( « Error when requested Foursquare :  » + response.ReasonPhrase);
  409. string jsonResult = await response.Content.ReadAsStringAsync();
  410. JsonValue jsonValue;
  411. if (! JsonValue .TryParse(jsonResult, out jsonValue))
  412. throw new Exception ( « Unable to parse checkins response » );
  413. List < Contact > contacts = new List < Contact >();
  414. foreach ( var item in jsonValue.GetObject()[ « response » ].GetObject()[ « friends » ].GetObject()[ « items » ].GetArray().AsParallel())
  415. {
  416. var contactItem = item.GetObject();
  417. var contact = new Contact ();
  418. contact.Id = contactItem[ « id » ].GetString();
  419. if (contactItem.ContainsKey( « firstName » ))
  420. contact.FirstName = contactItem[ « firstName » ].GetString();
  421. if (contactItem.ContainsKey( « lastName » ))
  422. contact.LastName = contactItem[ « lastName » ].GetString();
  423. if (contactItem.ContainsKey( « photo » ))
  424. {
  425. var photoItem = contactItem[ « photo » ].GetObject();
  426. var prefix = photoItem[ « prefix » ].GetString();
  427. var suffix = photoItem[ « suffix » ].GetString();
  428. contact.Photo = prefix + « original » + suffix;
  429. }
  430. if (contactItem.ContainsKey( « gender » ))
  431. contact.Gender = contactItem[ « gender » ].GetString();
  432. if (contactItem.ContainsKey( « homeCity » ))
  433. contact.HomeCity = contactItem[ « homeCity » ].GetString();
  434. if (contactItem.ContainsKey( « bio » ))
  435. contact.HomeCity = contactItem[ « bio » ].GetString();
  436. var contactInfoItem = contactItem[ « contact » ].GetObject();
  437. if (contactInfoItem.ContainsKey( « email » ))
  438. contact.Email = contactInfoItem[ « email » ].GetString();
  439. if (contactInfoItem.ContainsKey( « facebook » ))
  440. contact.Facebook = contactInfoItem[ « facebook » ].GetString();
  441. if (contactInfoItem.ContainsKey( « twitter » ))
  442. contact.Twitter = contactInfoItem[ « twitter » ].GetString();
  443. var tipsItem = contactItem[ « tips » ].GetObject();
  444. contact.CountTips = Convert .ToInt32(tipsItem[ « count » ].GetNumber());
  445. contacts.Add(contact);
  446. }
  447. return contacts;
  448. }
  449. private async static Task < IEnumerable < Badge >> GetBadgesTask( string token, string userId = « self » )
  450. {
  451. string uriFormat = « https://api.foursquare.com/v2/users/{0}/badges?v={1}&oauth_token={2} » ;
  452. Uri uri = new Uri ( string .Format(uriFormat, userId, versionDate, token));
  453. HttpClient client = new HttpClient ();
  454. var response = await client.GetAsync(uri);
  455. if (!response.IsSuccessStatusCode)
  456. throw new Exception ( « Error when requested Foursquare :  » + response.ReasonPhrase);
  457. string jsonResult = await response.Content.ReadAsStringAsync();
  458. JsonValue jsonValue;
  459. if (! JsonValue .TryParse(jsonResult, out jsonValue))
  460. throw new Exception ( « Unable to parse checkins response » );
  461. List < Badge > badges = new List < Badge >();
  462. var badgeItems = jsonValue.GetObject()[ « response » ].GetObject()[ « badges » ].GetObject();
  463. foreach ( var badgeKey in badgeItems.Keys)
  464. {
  465. var badgeItem = badgeItems[badgeKey].GetObject();
  466. var badge = new Badge ();
  467. badge.Id = badgeItem[ « id » ].GetString();
  468. badge.BadgeId = badgeItem[ « badgeId » ].GetString();
  469. badge.Name = badgeItem[ « name » ].GetString();
  470. if (badgeItem.ContainsKey( « unlockMessage » ))
  471. badge.UnlockMessage = badgeItem[ « unlockMessage » ].GetString();
  472. if (badgeItem.ContainsKey( « description » ))
  473. badge.Description = badgeItem[ « description » ].GetString();
  474. if (badgeItem.ContainsKey( « hint » ))
  475. badge.Description = badgeItem[ « hint » ].GetString();
  476. var imageItem = badgeItem[ « image » ].GetObject();
  477. string prefix = imageItem[ « prefix » ].GetString();
  478. string name = imageItem[ « name » ].GetString();
  479. badge.ImageUri = prefix + « 114 » + name;
  480. if (! string .IsNullOrEmpty(badge.Name))
  481. badges.Add(badge);
  482. }
  483. return badges;
  484. }
  485. private async static Task < IEnumerable < Venue >> GetMayorshipsTask( string token, string userId = null )
  486. {
  487. string userParam = userId ?? « self » ;
  488. string uriFormat = « https://api.foursquare.com/v2/users/{0}/mayorships?v={1}&oauth_token={2} » ;
  489. Uri uri = new Uri ( string .Format(uriFormat, userParam, versionDate, token));
  490. HttpClient client = new HttpClient ();
  491. var response = await client.GetAsync(uri);
  492. if (!response.IsSuccessStatusCode)
  493. throw new Exception ( « Error when requested Foursquare :  » + response.ReasonPhrase);
  494. string jsonResult = await response.Content.ReadAsStringAsync();
  495. JsonValue jsonValue;
  496. if (! JsonValue .TryParse(jsonResult, out jsonValue))
  497. throw new Exception ( « Unable to parse checkins response » );
  498. List < Venue > venues = new List < Venue >();
  499. foreach ( var item in jsonValue.GetObject()[ « response » ].GetObject()[ « mayorships » ].GetObject()[ « items » ].GetArray().AsParallel())
  500. {
  501. var venueItem = item.GetObject()[ « venue » ].GetObject();
  502. var venue = DeserializeVenue(venueItem);
  503. venues.Add(venue);
  504. }
  505. return venues;
  506. }
  507. private async static Task < IEnumerable < LeaderboardItem >> GetLeaderboardTask( string token, int ? neighbors = null )
  508. {
  509. string uriFormat = « https://api.foursquare.com/v2/users/leaderboard?v={0}{1}&oauth_token={2} » ;
  510. string neighborsCount = neighbors.HasValue ? « &neighbors= » + neighbors.Value.ToString() : «  » ;
  511. Uri uri = new Uri ( string .Format(uriFormat, versionDate, neighborsCount, token));
  512. HttpClient client = new HttpClient ();
  513. var response = await client.GetAsync(uri);
  514. if (!response.IsSuccessStatusCode)
  515. throw new Exception ( « Error when requested Foursquare :  » + response.ReasonPhrase);
  516. string jsonResult = await response.Content.ReadAsStringAsync();
  517. JsonValue jsonValue;
  518. if (! JsonValue .TryParse(jsonResult, out jsonValue))
  519. throw new Exception ( « Unable to parse checkins response » );
  520. List < LeaderboardItem > leaderboard = new List < LeaderboardItem >();
  521. foreach ( var item in jsonValue.GetObject()[ « response » ].GetObject()[ « leaderboard » ].GetObject()[ « items » ].GetArray())
  522. {
  523. var lboardJson = item.GetObject();
  524. var leaderboardItem = new LeaderboardItem ();
  525. leaderboardItem.Rank = Convert .ToInt32(lboardJson[ « rank » ].GetNumber());
  526. var userJson = lboardJson[ « user » ].GetObject();
  527. leaderboardItem.User = new Contact ();
  528. leaderboardItem.User.Id = userJson[ « id » ].GetString();
  529. if (userJson.ContainsKey( « firstName » ))
  530. leaderboardItem.User.FirstName = userJson[ « firstName » ].GetString();
  531. if (userJson.ContainsKey( « lastName » ))
  532. leaderboardItem.User.LastName = userJson[ « lastName » ].GetString();
  533. leaderboardItem.User.Me = userJson[ « relationship » ].GetString() == « self » ;
  534. if (userJson.ContainsKey( « photo » ))
  535. {
  536. var photoItem = userJson[ « photo » ].GetObject();
  537. var prefix = photoItem[ « prefix » ].GetString();
  538. var suffix = photoItem[ « suffix » ].GetString();
  539. leaderboardItem.User.Photo = prefix + « original » + suffix;
  540. }
  541. var scoreJson = lboardJson[ « scores » ].GetObject();
  542. leaderboardItem.Score = new Score ();
  543. leaderboardItem.Score.Recent = Convert .ToInt32(scoreJson[ « recent » ].GetNumber());
  544. leaderboardItem.Score.Max = Convert .ToInt32(scoreJson[ « max » ].GetNumber());
  545. leaderboardItem.Score.CheckinsCount = Convert .ToInt32(scoreJson[ « checkinsCount » ].GetNumber());
  546. leaderboard.Add(leaderboardItem);
  547. }
  548. return leaderboard;
  549. }
  550. private static Contact DeserializeUser( JsonObject userJson)
  551. {
  552. Contact contact = new Contact ();
  553. contact.Id = userJson[ « id » ].GetString();
  554. if (userJson.ContainsKey( « firstName » ))
  555. contact.FirstName = userJson[ « firstName » ].GetString();
  556. if (userJson.ContainsKey( « lastName » ))
  557. contact.LastName = userJson[ « lastName » ].GetString();
  558. contact.Me = userJson[ « relationship » ].GetString() == « self » ;
  559. if (userJson.ContainsKey( « photo » ))
  560. {
  561. var photoItem = userJson[ « photo » ].GetObject();
  562. var prefix = photoItem[ « prefix » ].GetString();
  563. var suffix = photoItem[ « suffix » ].GetString();
  564. contact.Photo = prefix + « original » + suffix;
  565. }
  566. if (userJson.ContainsKey( « gender » ))
  567. contact.Gender = userJson[ « gender » ].GetString();
  568. if (userJson.ContainsKey( « homeCity » ))
  569. contact.HomeCity = userJson[ « homeCity » ].GetString();
  570. if (userJson.ContainsKey( « bio » ))
  571. contact.Bio = userJson[ « bio » ].GetString();
  572. if (userJson.ContainsKey( « contact » ))
  573. {
  574. var contactInfoItem = userJson[ « contact » ].GetObject();
  575. if (contactInfoItem.ContainsKey( « email » ))
  576. contact.Email = contactInfoItem[ « email » ].GetString();
  577. if (contactInfoItem.ContainsKey( « facebook » ))
  578. contact.Facebook = contactInfoItem[ « facebook » ].GetString();
  579. if (contactInfoItem.ContainsKey( « twitter » ))
  580. contact.Twitter = contactInfoItem[ « twitter » ].GetString();
  581. }
  582. if (userJson.ContainsKey( « tips » ))
  583. {
  584. var tipsItem = userJson[ « tips » ].GetObject();
  585. contact.CountTips = Convert .ToInt32(tipsItem[ « count » ].GetNumber());
  586. }
  587. return contact;
  588. }
  589. private static Venue DeserializeVenue( JsonObject venueItem)
  590. {
  591. Venue venue = new Venue ();
  592. venue.Id = venueItem[ « id » ].GetString();
  593. venue.Name = venueItem[ « name » ].GetString();
  594. var locationItem = venueItem[ « location » ].GetObject();
  595. venue.Location = DeserializeLocation(locationItem);
  596. var statsItem = venueItem[ « stats » ].GetObject();
  597. venue.VenueStatistics.CheckinsCount = Convert .ToInt32(statsItem[ « checkinsCount » ].GetNumber());
  598. venue.VenueStatistics.TipCount = Convert .ToInt32(statsItem[ « tipCount » ].GetNumber());
  599. venue.VenueStatistics.UsersCount = Convert .ToInt32(statsItem[ « usersCount » ].GetNumber());
  600. var categoryItems = venueItem[ « categories » ].GetArray();
  601. foreach ( var catItem in categoryItems)
  602. {
  603. var categoryItem = catItem.GetObject();
  604. var category = new VenueCategory ();
  605. category.Id = categoryItem[ « id » ].GetString();
  606. if (categoryItem.ContainsKey( « icon » ))
  607. {
  608. var iconJson = categoryItem[ « icon » ].GetObject();
  609. var prefix = iconJson[ « prefix » ].GetString();
  610. var suffix = iconJson[ « suffix » ].GetString();
  611. if (prefix.EndsWith( « _ » ))
  612. prefix = prefix.Substring(0, prefix.Length – 1);
  613. category.Icon = prefix + suffix;
  614. }
  615. category.Name = categoryItem[ « name » ].GetString();
  616. category.PluralName = categoryItem[ « pluralName » ].GetString();
  617. category.ShortName = categoryItem[ « shortName » ].GetString();
  618. venue.VenueCategory.Add(category);
  619. }
  620. return venue;
  621. }
  622. private static Location DeserializeLocation( JsonObject locationItem)
  623. {
  624. var location = new Location ();
  625. if (locationItem.ContainsKey( « city » ))
  626. location.City = locationItem[ « city » ].GetString();
  627. if (locationItem.ContainsKey( « address » ))
  628. location.Address = locationItem[ « address » ].GetString();
  629. if (locationItem.ContainsKey( « postalCode » ))
  630. location.PostalCode = locationItem[ « postalCode » ].GetString();
  631. location.Country = locationItem[ « country » ].GetString();
  632. location.CountryCode = locationItem[ « cc » ].GetString();
  633. location.Latitude = locationItem[ « lat » ].GetNumber();
  634. location.Longitude = locationItem[ « lng » ].GetNumber();
  635. if (locationItem.ContainsKey( « state » ))
  636. location.State = locationItem[ « state » ].GetString();
  637. return location;
  638. }
  639. }

 

Et ci-dessous un exemple d’utilisation de cette API.

  1. var token = await FoursquareService .AuthenticateAsync( App .clientId, App .callbackFoursquareUri);
  2. var friendsTask = FoursquareService .GetMyFriendsAsync(token, 9).AsTask();
  3. var badgesTask = FoursquareService .GetMyBadgesAsync(token).AsTask();
  4. var checkinsTask = FoursquareService .GetMyCheckinsAsync(token, 5).AsTask();
  5. var userDetailsTask = FoursquareService .GetMyUserDetailsAsync(token).AsTask();
  6. var leaderBoardTask = FoursquareService .GetLeaderboardAsync(token, 5).AsTask();
  7. var recentTask = FoursquareService .GetRecentCheckinsByFriends(token, 5).AsTask();
  8. var mayorshipsTask = FoursquareService .GetMyMayorshipsAsync(token).AsTask();
  9. await Task .WhenAll(friendsTask, badgesTask, checkinsTask, leaderBoardTask, recentTask, mayorshipsTask);
  10. TaskScheduler scheduler = TaskScheduler .FromCurrentSynchronizationContext();
  11. lvFriends.ItemsSource = friendsTask.Result;
  12. lvCheckins.ItemsSource = checkinsTask.Result.Select(c => c.Venue).ToList();
  13. lvBadges.ItemsSource = badgesTask.Result.Where(b => ! string .IsNullOrEmpty(b.UnlockMessage)).Take(9).ToList();
  14. lvLeaderboard.ItemsSource = leaderBoardTask.Result;
  15. lvFriendsActivity.ItemsSource = recentTask.Result;
  16. lvMayorships.ItemsSource = mayorshipsTask.Result.Take(6).ToList();

 

Dans ce 1er exemple, nous appelons différentes méthodes asynchrones de récupération de nos objets Foursquare et pour chacun on récupère la tâche associée.

Nous appelons ensuite la méthode Task.WhenAll avec le mot clé await qui permet d’attendre que toutes les tâches soient terminées. Puis nous faisons la liaison des résultats aux contrôles de la page.

Ci-dessous un autre exemple qui cette fois n’attend pas la fin de chacun des traitements pour effectuer les liaisons de données. Dès qu’une tache se termine elle continue avec la liaison de son résultat au contrôle de la page :

  1. var token = await FoursquareService .AuthenticateAsync( App .clientId, App .callbackFoursquareUri);
  2. var friendsTask = FoursquareService .GetMyFriendsAsync(token, 9).AsTask();
  3. var badgesTask = FoursquareService .GetMyBadgesAsync(token).AsTask();
  4. var checkinsTask = FoursquareService .GetMyCheckinsAsync(token, 5).AsTask();
  5. var userDetailsTask = FoursquareService .GetMyUserDetailsAsync(token).AsTask();
  6. var leaderBoardTask = FoursquareService .GetLeaderboardAsync(token, 5).AsTask();
  7. var recentTask = FoursquareService .GetRecentCheckinsByFriends(token, 5).AsTask();
  8. var mayorshipsTask = FoursquareService .GetMyMayorshipsAsync(token).AsTask();
  9. TaskScheduler scheduler = TaskScheduler .FromCurrentSynchronizationContext();
  10. friendsTask.ContinueWith(
  11. (task) =>
  12. {
  13. lvFriends.ItemsSource = task.Result;
  14. }, scheduler);
  15. badgesTask.ContinueWith(
  16. (task) =>
  17. {
  18. lvBadges.ItemsSource = task.Result.Where(b => ! string .IsNullOrEmpty(b.UnlockMessage)).Take(9).ToList();
  19. }, scheduler);
  20. checkinsTask.ContinueWith(
  21. (task) =>
  22. {
  23. lvCheckins.ItemsSource = task.Result.Select(c => c.Venue).ToList();
  24. }, scheduler);
  25. leaderBoardTask.ContinueWith(
  26. (task) =>
  27. {
  28. lvLeaderboard.ItemsSource = task.Result;
  29. }, scheduler);
  30. recentTask.ContinueWith(
  31. (task) =>
  32. {
  33. lvFriendsActivity.ItemsSource = task.Result;
  34. }, scheduler);
  35. mayorshipsTask.ContinueWith(
  36. (task) =>
  37. {
  38. lvMayorships.ItemsSource = task.Result;
  39. }, scheduler);

 

Aller plus loin

Du côté de l’asynchronisme je ne me suis pas étendu car le sujet est très vaste et je voulais aller à l’essentiel. Peut être que j’y reviendrai prochainement dans un article consacré à ce sujet. Mais si vous désirez en savoir un peu plus, je vous conseille vivement ces 2 articles, qui ont d’ailleurs été traduits :

Pour le développement de composants Windows Runtime, il faut garder à l’esprit qu’il existe quelques contraintes d’implémentation. Ces contraintes sont essentiellement imposées par le système de type du Windows Runtime et le fait de devoir supporter différents langages (et notamment le javascript…).

Dans le prochain article, nous nous finaliserons cette application et nous verrons comment implémenter le contrat ContactPicker.

HeightSquare – Partie 1 : Authentification OAuth avec le Windows Runtime

Foursquare est un service de géolocalisation qui vous permet d’indiquer auprès de vos amis les endroits dans lesquels vous vous trouvez. Cette série d’articles s’articule autour de ces services, car Foursquare met à disposition une API qui permet de développer sa propre application. Cette API se présente sous la forme de services REST avec une authentification OAuth. Elle vous permet, une fois authentifié, de récupérer vos “checkins”, vos badges, les informations de vos amis, etc…

Plusieurs API Foursquare .Net existent actuellement sur Codeplex, mais pour l’instant aucune pour les applications Windows Store (ou ModernUI, ou Metro, ou Windows 8…).

Ce service de géolocalisation se prête très bien pour analyser les concepts de développement WinRT suivants :

  • Authentification OAuth grâce à la classe WebAuthenticationBrocker,
  • Enregistrement du jeton d’authentification dans les paramètres Cloud de l’application,
  • Développement d’une API Foursquare sous forme d’un composant Windows Runtime réutilisable quel que soit le langage,
  • un peu d’asynchronisme au passage avec des Task, des IAsyncOperation, des AsyncInfo
    et des SynchronizationContext,
  • et enfin la mise en place du ContactPicker permettant à notre application Foursquare
    de partager les informations de nos amis vers d’autres applications.

La série d’articles sera découpée en 3 parties (normalement!). Et dans cette 1ère
partie je vous propose de découvrir comment nous allons facilement s’authentifier
auprès de services implémentant le protocole OAuth.

L’application finale de cette série d’articles que j’ai baptisé HeightSquare sera téléchargeable dans la dernière partie… 😉

En voici un aperçu :

image

Principe du protocole OAuth

De plus en plus de services Internet mettent en place une authentification basée sur le protocole OAuth. On retrouve notamment des services tels que Facebook, Twitter, Google+, FlickR, Foursquare…

Si vous désirez en connaitre la petite histoire, je vous conseille cet article de Wikipédia.

Le principe de l’authentification OAuth est assez simple. Nous allons en donner la définition, mais avant rappelons dans quel contexte un développeur peut rencontrer ce type d’authentification :

  • soit il veut consommer les services d’une API connectée et sécurisée,
  • soit il veut donner accès à sa plateforme via une API connectée et sécurisée.

C’est pour cette raison que des services comme Facebook & Co ont implémenté ce protocole.

Pour le principe d’authentification, il se décompose en étape suivante :

  • On affiche une page de Login du service connecté,
  • l’utilisateur saisie ces informations de connexion,
  • il autorise ou non l’accès à ces informations par l’application
  • le service connecté nous redirige vers une URL prédéfinie afin de nous renvoyer un
    jeton d’authentification

C’est ensuite grâce à ce jeton que nous accèderons aux données de l’utilisateur.

Selon les plateformes, ce jeton peut éventuellement expirer. Auquel cas, il faudra demander à l’utilisateur de s’authentifier une nouvelle fois.

Avec les API de Foursquare, le jeton d’authentification n’expire jamais.

Récemment avec Facebook, les jetons d’authentification de type “Offline Access” ne sont plus pris en charge…

Création de l’application sur le site Foursquare

De manière générale, ce genre d’authentification nécessite de déclarer notre application auprès de la plateforme mettant à disposition le jeton OAuth. Il nous fournir l’URL de callback de notre application et en échange elle nous fournira un identifiant, ou “ClientID”.

Avec Facebook, il faut en plus indiquer quelles sont les informations que notre application sera autorisée à accéder.

Du côté de Foursquare, il vous suffit de vous rendre à cette adresse et de créer votre application : https://fr.foursquare.com/oauth/

Il suffit de fournir au minimum un nom d’application, une adresse de page d’accueil ou de téléchargement et une adresse de callback :


image

Ensuite Foursquare fournit la description de son implémentation du protocole OAuth.

Celà consiste à interroger l’url suivante : https://foursquare.com/oauth2/authenticate?client_id=YOUR_CLIENT_ID&response_type=token&redirect_uri=YOUR_REDIRECT_URI

Vous remplacer bien évidemment les paramètres client_id et redirect_url avec vos informations et le tour est joué. Si l’utilisateur s’authentifie et autorise l’accès à ses informations, alors vous serez redirigé vers l’url suivante : https://YOUR_REDIRECT_URI/#access_token=ACCESS_TOKEN.

Il ne vous reste plus qu’à garder précieusement ce jeton pour interroger les différents services Foursquare. A chaque requête il vous suffit d’ajouter le paramètre oauth_token avec la valeur du jeton.

Par exemple pour récupérer la liste de vos amis il vous suffit d’appeler l’URL suivante : https://api.foursquare.com/v2/users/self/friends?&oauth_token=ACCESS_TOKEN.

Pour aller plus loin sur les API Foursquare, je vous conseille de consulter la documentation associée.

WebAuthenticationBroker et OAuth

Avec Windows 8 Microsoft a intégré un composant très utile, car plus besoin de se casser la tête pour ce type d’authentification… Ce composant porte le doux nom de WebAuthenticationBroker et se situe dans l’espace de nom Windows.Security.Authentication.Web.

Il s’utilise de manière extrêmement simple, en lui fournissant l’url d’authentification et l’URL de callback, comme l’illustre le code ci-dessous :

  1. string clientId = « YOUR_CLIENT_ID »;
  2. string callbackUri = « benoitlaut.wordpress.com »;
  3. string foursquareUriFormat = « https://foursquare.com/oauth2/authenticate?client_id={0}&response_type=token&redirect_uri={1} »;
  4. Uri oauthFoursquareUri = new Uri(string.Format(foursquareUriFormat, clientId, Uri.EscapeDataString(callbackUri)));
  5. Uri callbackFoursquareUri = new Uri(callbackUri);
  6. var authenticateFoursquareResult = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.None, oauthFoursquareUri, callbackFoursquareUri);

 

L’appel à la méthode AuthenticateAsync doit se faire dans le ThreadUI (nous y reviendrons dans la partie 2) car il nous affiche une UI. Ici le Windows Runtime fait tout pour nous, car il affiche la page Web d’authentification à l’utilisateur au sein de notre application :


image

Une fois que l’utilisateur se connecte, nous retrouvons le résultat de l’authentification dans l’objet retourné qui est de type WebAuthenticationResult.

Ce type possède une propriété ResponseStatus qui nous permet de vérifier que l’utilisateur s’est authentifier avec succès. Si c’est le cas, la propriété ResponseData contient l’url de callback avec en paramètre le jeton d’authentification OAuth, comme l’illustre le code ci-dessous :

  1. if (authenticateFoursquareResult != null && authenticateFoursquareResult.ResponseStatus == Windows.Security.Authentication.Web.WebAuthenticationStatus.Success)
  2. {
  3.     Uri tokenFoursquareUri = new Uri(authenticateFoursquareResult.ResponseData.ToString());
  4.     WwwFormUrlDecoder decoder = new WwwFormUrlDecoder(tokenFoursquareUri.Fragment);
  5.     string accessToken = decoder.GetFirstValueByName(« #access_token »);
  6.     return accessToken;
  7. }
  8. else
  9.     throw new Exception(« Unable to authenticate user! »);

 

A noter au passage, l’utilisation de la classe WwwFormUrlDecoder pour parser notre URI, en lieu et place de la classe HttpWebUtility que nous utilisions, entre autre, pour ce genre de chose.

Le code final de notre méthode d’authentification auprès des services Foursquare peut s’écrire ainsi :

  1. private async static Task<string> AuthenticateAsync(string clientId,  string callbackUri)
  2. {
  3.     string foursquareUriFormat = « https://foursquare.com/oauth2/authenticate?client_id={0}&response_type=token&redirect_uri={1} »;
  4.     Uri oauthFoursquareUri = new Uri(string.Format(foursquareUriFormat, clientId, Uri.EscapeDataString(callbackUri)));
  5.     Uri callbackFoursquareUri = new Uri(callbackUri);
  6.     var authenticateFoursquareResult = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.None, oauthFoursquareUri, callbackFoursquareUri);
  7.     if (authenticateFoursquareResult != null && authenticateFoursquareResult.ResponseStatus == Windows.Security.Authentication.Web.WebAuthenticationStatus.Success)
  8.     {
  9.         Uri tokenFoursquareUri = new Uri(authenticateFoursquareResult.ResponseData.ToString());
  10.         WwwFormUrlDecoder decoder = new WwwFormUrlDecoder(tokenFoursquareUri.Fragment);
  11.         string accessToken = decoder.GetFirstValueByName(« #access_token »);
  12.         return accessToken;
  13.     }
  14.     else
  15.         throw new Exception(« Unable to authenticate user! »);
  16. }

Pour aller plus loin

Nous avons découvert qu’avec le Windows Runtime il est très facile de s’authentifier auprès de services implémentant le protocole OAuth. Pour aller un peu plus loin vous pouvez commencer par étudier l’exemple que Microsoft met à disposition sur le DevCenter et la page MSDN de la classe WebAuthenticationBroker.

L’étape suivante dans le développement de notre application HeightSquare sera d’encapsuler notre API dans un composant Windows Runtime afin de la rendre accessible quel que soit le langage utilisé.

Windows 8 : Azure Storage Explorer

Alors que nous venons de passer (pour les abonnés MSDN!) en version RTM de Windows 8 et VS2012, nous n’avons pas encore de SDK Azure pour le Windows Runtime, en particulier pour l’accès au Blob storage. Actuellement pour y accéder et gérer le contenu nous pouvons utiliser les services REST de l’Azure Storage.

Dans cet article nous aborderons les sujets suivants :

  • Accès aux services REST de l’Azure Storage via la classe HttpClient et authentification des requêtes via les API Windows.Security.Cryptography
  • Utilisation des contrôles FileOpenPicker et FileSavePicker
  • Mise en place des contrats File Open Picker, File Save Picker et Cached File Updater

Vous pouvez télécharger le code source au format VS2012 RTM ici : AzureStorageExplorerApp.zip

Pour le repasser en VS2012 RC, je pense qu’il vous suffit d’éditer en XML le manifeste et de modifier les attributs OSMinVersion et OSMaxVersion à 6.2.0.

Pour l’exécuter il vous faudra éditer les constantes AccountStorageName et AccountKey de la classe App avec un compte de stockage Azure valide. Ci-dessous un visuel de l’application :

image

 

Accès aux services REST Azure

Pour accéder à un compte Windows Azure via les services REST, nous avons besoin d’une connexion internet bien sûr, d’un compte azure et de sa clé.

En terme d’API, nous utilisons la classe HttpClient pour envoyer nos requêtes. Et en fonction de l’opération, nous utilisons les méthodes suivantes :

  • Lister les éléments : GetStringAsync(Uri),
  • Télécharger un blob : GetStreamAsync(Uri)
  • Envoyer un blob : PutAsync(Uri), DeleteAsync(Uri)

Les entêtes Http

Toutes ces opérations nécessitent les entêtes Http suivantes :

  • x-ms-date : Date au format UTC
  • x-ms-version : numéro de version des services REST Azure
  • Authorization : clé signé d’authentification

Dans le cadre d’un envoi de blob il en faudra 3 supplémentaires :

  • x-ms-blob-type : type de blob à télécharger, “BlockBlob” ou “PageBlob”
  • Accept-Charset : identifie l’encodage
  • Content-Lenght : précise la taille du blob à envoyer

Et si nous envoyons un Page Blob alors il faut encore des informations supplémentaires sur sa taille et l’identification du numéro de page que l’on envoie :

  • x-ms-blob-content-lenght : représente la taille maximale d’une page de blob
  • x-ms-blob-sequence-number : numéro de séquence de la page utilisé par le développeur pour envoyer ses pages au service

D’autres entêtes peuvent être ajoutées mais sont optionnelles. Vous retrouverez tous les détails dans la documentation officielle des services REST Azure .

Le code ci-dessous illustre la construction d’une requête Http permettant de lister les containeurs d’un compte de stockage Azure :

  1. // Compte de stockage Azure
  2. var accountStorageName = « win8sample » ;
  3. var accountKey = « XXXXXXXXXXXXXXXXXXXXXXX » ;
  4. var now = DateTime .UtcNow;
  5. // Uri pour lister les containeurs
  6. Uri uri = new Uri ( « http:// &raquo; + storageAccountName + « .blob.core.windows.net/?comp=list » );
  7. var httpClient = new HttpClient ();
  8. // Ajout des Entetes Http
  9. httpClient.DefaultRequestHeaders.Add( « x-ms-date » , now.ToString( « R » , System.Globalization. CultureInfo .InvariantCulture));
  10. httpClient.DefaultRequestHeaders.Add( « x-ms-version » , « 2009-09-19 » );
  11. // Cration de la signature et ajout de l’entete d’authentification
  12. var signature = RequestHeadersHelper .GetSignature( « GET » , now, httpClient, uri, storageAccountName, storageKey);
  13. httpClient.DefaultRequestHeaders.Add( « Authorization » , « SharedKey  » + storageAccountName + « : » + signature);
  14. // Appel du service REST et recuperation du resultat sous forme de string
  15. var response = await httpClient.GetStringAsync(uri);

Authentification de la requête

L’entête Authorization permet de signer la requête. Dans notre cas elle est construite de la manière suivante : [Type de clé] [Compte de stockage]:[Signature]

Cette entête peut se construire différemment selon la version des services REST que nous utilisons (x-ms-version). Dans notre cas nous spécifions que nous utilisons la méthode SharedKey pour la signature.

Cette méthode consiste à créer une chaine de caractères représentant la signature de la requête. Une fois cette signature obtenue, il suffit de l’encoder avec l’algorithme HMAC-SHA256. La valeur de hachage ainsi obtenu représente la signature à ajouter dans l’entête d’authentification.

La signature se compose ainsi :

  1. string stringToSign = VERB + « \n » +
  2. Content- Encoding + « \n »
  3. Content-Language + « \n »
  4. Content-Length + « \n »
  5. Content-MD5 + « \n » +
  6. Content- Type + « \n » +
  7. Date + « \n » +
  8. If-Modified-Since + « \n »
  9. If-Match + « \n »
  10. If-None-Match + « \n »
  11. If-Unmodified-Since + « \n »
  12. Range + « \n »
  13. CanonicalizedHeaders +
  14. CanonicalizedResource;

 

Dans notre exemple, la signature de la requête permettant de lister les containeurs du compte Azure blaut est la suivante :

  1. string signature = « GET\n\n\n\n\n\n\n\n\n\n\n\nx-ms-date:Sun, 19 Aug 2012 16:43:57 GMT\nx-ms-version:2009-09-19\n/blaut/\ncomp:list » ;

 

Une fois que nous avons cette signature il nous faut l’encoder via l’algorithme HMAC-SHA256, que nous obtenons via la classe MacAlgorithmProvider dans l’espace de nom Windows.Security.Cryptography.Core :

  1. MacAlgorithmProvider hmacSha256 = MacAlgorithmProvider .OpenAlgorithm( MacAlgorithmNames .HmacSha256);

 

Nous avons besoin d’une clé de chiffrement de type CryptographicKey pour chiffrer la signature. Pour obtenir cette clé, il faut récupérer sous forme binaire la clé du compte de stockage Azure puis appeler la méthode CreateKey de l’algorithme de hachage :

  1. // Dcodage de la cl de stockage Azure
  2. IBuffer storageKeyBuffer = CryptographicBuffer .DecodeFromBase64String(storageKey);
  3. // Cration d’une cl de chiffrement partir de la cl de stockage Azure
  4. CryptographicKey cryptoKey = hmacSha256.CreateKey(storageKeyBuffer);

 

Il ne reste plus qu’à convertir la signature en binaire pour pouvoir la signer avec la clé. Puis convertir le binaire résultant en chaine de caractères Base64 :

  1. // Conversion de la signature en binaire
  2. IBuffer signatureBuffer = CryptographicBuffer .ConvertStringToBinary(signature, BinaryStringEncoding .Utf8);
  3. // Chiffrement de la signature
  4. IBuffer hashResult = CryptographicEngine .Sign(cryptoKey, signatureBuffer);
  5. // Recuperation de la valeur de hachage sous forme d’une string en Base64
  6. return CryptographicBuffer .EncodeToBase64String(hashResult);

AzureBlobStorageRestServices

Ci-dessous le code complet de la classe permettant d’accéder à son compte de stockage Azure : lister les containeurs, lister les blobs, ajouter, télécharger ou supprimer un blob :

  1. public static class AzureBlobStorageRestServices
  2. {
  3. public async static Task < string > ListContainersAsync( string storageAccountName, string storageKey)
  4. {
  5. var now = DateTime .UtcNow;
  6. // Uri pour lister les containeurs
  7. Uri uri = new Uri ( « http:// &raquo; + storageAccountName + « .blob.core.windows.net/?comp=list » );
  8. var httpClient = new HttpClient ();
  9. // Ajout des Entetes Http
  10. httpClient.DefaultRequestHeaders.Add( « x-ms-date » , now.ToString( « R » , System.Globalization. CultureInfo .InvariantCulture));
  11. httpClient.DefaultRequestHeaders.Add( « x-ms-version » , « 2009-09-19 » );
  12. // Cration de la signature et ajout de l’entete d’authentification
  13. var signature = AzureBlobStorageRestServices .GetSignature( « GET » , now, httpClient, uri, storageAccountName, storageKey);
  14. httpClient.DefaultRequestHeaders.Add( « Authorization » , « SharedKey  » + storageAccountName + « : » + signature);
  15. // Appel du service REST et recuperation du resultat sous forme de string
  16. var response = await httpClient.GetStringAsync(uri);
  17. return response;
  18. }
  19. public async static Task < string > ListBlobsAsync( string containerName, string storageAccountName, string storageKey)
  20. {
  21. var now = DateTime .UtcNow;
  22. // Uri pour lister les blobs d’un containeur
  23. Uri uri = new Uri ( « http:// &raquo; + storageAccountName + « .blob.core.windows.net/ » + containerName + « ?restype=container&comp=list » );
  24. var httpClient = new HttpClient ();
  25. httpClient.DefaultRequestHeaders.Add( « x-ms-date » , now.ToString( « R » , System.Globalization. CultureInfo .InvariantCulture));
  26. httpClient.DefaultRequestHeaders.Add( « x-ms-version » , « 2009-09-19 » );
  27. var signature = AzureBlobStorageRestServices .GetSignature( « GET » , now, httpClient, uri, storageAccountName, storageKey);
  28. httpClient.DefaultRequestHeaders.Add( « Authorization » , « SharedKey  » + storageAccountName + « : » + signature);
  29. var message = await httpClient.GetStringAsync(uri);
  30. return message;
  31. }
  32. public async static Task < Stream > GetBlobAsync( string blobUri, string storageAccountName, string storageKey)
  33. {
  34. Uri uri = new Uri (blobUri);
  35. var now = DateTime .UtcNow;
  36. var httpClient = new HttpClient ();
  37. httpClient.DefaultRequestHeaders.Add( « x-ms-date » , now.ToString( « R » , System.Globalization. CultureInfo .InvariantCulture));
  38. httpClient.DefaultRequestHeaders.Add( « x-ms-version » , « 2009-09-19 » );
  39. var signature = AzureBlobStorageRestServices .GetSignature( « GET » , now, httpClient, uri, storageAccountName, storageKey);
  40. httpClient.DefaultRequestHeaders.Add( « Authorization » , « SharedKey  » + storageAccountName + « : » + signature);
  41. var stream = await httpClient.GetStreamAsync(blobUri);
  42. return stream;
  43. }
  44. public async static Task PutBlobAsync( Stream stream, string blobUri, string storageAccountName, string storageKey)
  45. {
  46. Uri uri = new Uri (blobUri);
  47. var now = DateTime .UtcNow;
  48. HttpClient httpClient = new HttpClient ();
  49. httpClient.DefaultRequestHeaders.Add( « x-ms-date » , now.ToString( « R » , System.Globalization. CultureInfo .InvariantCulture));
  50. httpClient.DefaultRequestHeaders.Add( « x-ms-version » , « 2009-09-19 » );
  51. httpClient.DefaultRequestHeaders.Add( « x-ms-blob-type » , « BlockBlob » );
  52. httpClient.DefaultRequestHeaders.Add( « Accept-Charset » , « UTF-8 » );
  53. httpClient.DefaultRequestHeaders.TryAddWithoutValidation( « Content-Length » , stream.Length.ToString());
  54. var signature = GetSignature( « PUT » , now, httpClient, uri, storageAccountName, storageKey, stream.Length);
  55. httpClient.DefaultRequestHeaders.Add( « Authorization » , « SharedKey  » + storageAccountName + « : » + signature);
  56. System.Net.Http. StreamContent content = new StreamContent (stream);
  57. var response = await httpClient.PutAsync(blobUri.ToString(), content);
  58. if (response.StatusCode != System.Net. HttpStatusCode .Created)
  59. throw new Exception ( « Unable to upload blob » );
  60. }
  61. public async static Task DeleteBlobAsync( string blobUri, string storageAccountName, string storageKey)
  62. {
  63. var now = DateTime .UtcNow;
  64. var uri = new Uri (blobUri);
  65. HttpClient httpClient = new HttpClient ();
  66. httpClient.DefaultRequestHeaders.Add( « x-ms-date » , now.ToString( « R » , System.Globalization. CultureInfo .InvariantCulture));
  67. httpClient.DefaultRequestHeaders.Add( « x-ms-version » , « 2009-09-19 » );
  68. var signature = AzureBlobStorageRestServices .GetSignature( « DELETE » , now, httpClient, uri, storageAccountName, storageKey, 0);
  69. httpClient.DefaultRequestHeaders.Add( « Authorization » , « SharedKey  » + storageAccountName + « : » + signature);
  70. var response = await httpClient.DeleteAsync(blobUri);
  71. if (response.StatusCode != System.Net. HttpStatusCode .Accepted)
  72. throw new Exception ( « Uanble to delete blob » );
  73. }
  74. private static string GetSignature( string method, DateTime now, HttpClient httpClient, Uri uri, string storageAccountName, string storageKey, long contentLenght = 0)
  75. {
  76. string messageSignature = String .Format( « {0}\n\n\n{1}\n\n\n\n\n\n\n\n\n{2}{3} » ,
  77. method,
  78. (method == « GET » || method == « HEAD » ) ? String .Empty : contentLenght.ToString(),
  79. GetCanonicalizedHeaders(httpClient),
  80. GetCanonicalizedResource(uri, storageAccountName));
  81. // Rcupration de l’algorithme HMAC-SHA256
  82. MacAlgorithmProvider hmacSha256 = MacAlgorithmProvider .OpenAlgorithm( MacAlgorithmNames .HmacSha256);
  83. // Dcodage de la cl de stockage Azure
  84. IBuffer storageKeyBuffer = CryptographicBuffer .DecodeFromBase64String(storageKey);
  85. // Cration d’une cl de chiffrement partir de la cl de stockage Azure
  86. CryptographicKey cryptoKey = hmacSha256.CreateKey(storageKeyBuffer);
  87. // Conversion de la signature en binaire
  88. IBuffer signatureBuffer = CryptographicBuffer .ConvertStringToBinary(messageSignature, BinaryStringEncoding .Utf8);
  89. // Chiffrement de la signature
  90. IBuffer hashResult = CryptographicEngine .Sign(cryptoKey, signatureBuffer);
  91. // Recuperation de la valeur de hachage sous forme d’une string en Base64
  92. return CryptographicBuffer .EncodeToBase64String(hashResult);
  93. }
  94. private static string GetCanonicalizedHeaders( HttpClient httpClient)
  95. {
  96. List < string > headerNameList = new List < string >();
  97. StringBuilder sb = new StringBuilder ();
  98. foreach ( var heeader in httpClient.DefaultRequestHeaders)
  99. {
  100. if (heeader.Key.ToLowerInvariant().StartsWith( « x-ms-«  , StringComparison .Ordinal))
  101. {
  102. headerNameList.Add(heeader.Key.ToLowerInvariant());
  103. }
  104. }
  105. headerNameList.Sort();
  106. foreach ( string headerName in headerNameList)
  107. {
  108. StringBuilder builder = new StringBuilder (headerName);
  109. string separator = « : » ;
  110. foreach ( string headerValue in GetHeaderValues(httpClient.DefaultRequestHeaders, headerName))
  111. {
  112. string trimmedValue = headerValue.Replace( « \r\n » , String .Empty);
  113. builder.Append(separator);
  114. builder.Append(trimmedValue);
  115. separator = « , » ;
  116. }
  117. sb.Append(builder.ToString());
  118. sb.Append( « \n » );
  119. }
  120. return sb.ToString();
  121. }
  122. private static List < string > GetHeaderValues( HttpRequestHeaders headers, string headerName)
  123. {
  124. List < string > list = new List < string >();
  125. IEnumerable < string > values = headers.GetValues(headerName);
  126. if (values != null )
  127. {
  128. foreach ( string str in values)
  129. {
  130. list.Add(str.TrimStart( null ));
  131. }
  132. }
  133. return list;
  134. }
  135. private static string GetCanonicalizedResource( Uri address, string accountName)
  136. {
  137. StringBuilder str = new StringBuilder ();
  138. StringBuilder builder = new StringBuilder ( « / » );
  139. builder.Append(accountName);
  140. builder.Append(address.AbsolutePath);
  141. str.Append(builder.ToString());
  142. var param = address.Query;
  143. if ( string .IsNullOrEmpty(param))
  144. return str.ToString();
  145. param = param.Replace( « ? » , «  » );
  146. //List<KeyValuePair<string, string>> values2 = new List<KeyValuePair<string, string>>();
  147. Dictionary < string , string > values2 = new Dictionary < string , string >();
  148. foreach ( string str2 in param.Split( ‘&’ ))
  149. {
  150. List < string > list = new List < string >() { str2.Split( ‘=’ ).Last() };
  151. list.Sort();
  152. StringBuilder builder2 = new StringBuilder ();
  153. foreach ( object obj2 in list)
  154. {
  155. if (builder2.Length > 0)
  156. {
  157. builder2.Append( « , » );
  158. }
  159. builder2.Append(obj2.ToString());
  160. }
  161. values2.Add((str2 == null ) ? str2 : str2.ToLowerInvariant().Split( ‘=’ ).First(), builder2.ToString());
  162. }
  163. List < string > list2 = new List < string >(values2.Select(v => v.Key));
  164. list2.Sort();
  165. foreach ( string str3 in list2)
  166. {
  167. StringBuilder builder3 = new StringBuilder ( string .Empty);
  168. builder3.Append(str3);
  169. builder3.Append( « : » );
  170. builder3.Append(values2[str3]);
  171. str.Append( « \n » );
  172. str.Append(builder3.ToString());
  173. }
  174. return str.ToString();
  175. }
  176. }

 

Cette classe n’est pas complète et ne prend pas en charge toutes les fonctionnalités des services REST, mais elle représente un bon point de départ…

File Pickers UI

Windows 8 met à disposition les classes FileOpenPicker et FileSavePicker. Ces classes permettent depuis une application d’afficher une interface demandant à l’utilisateur de sélectionner un ou plusieurs fichiers dans la cas du FileOpenPicker, et de sélectionner une destination dans le cas du FileSavePicker.

Ces classes sont disponibles dans l’espace de nom Windows.Storage.Pickers.

Envoi de fichiers vers Azure avec le FileOpenPicker

Dans notre application, nous utilisons le FileOpenPicker pour donner la possibilité à l’utilisateur d’envoyer l’un de ses fichiers vers son compte Azure.

La classe FileOpenPicker permet de demander à l’utilisateur de sélectionner un ou plusieurs fichiers. On doit obligatoirement préciser le type de fichier à sélectionner via la propriété FileTypeFilter. Pour supporter tous les types de fichiers, il faut préciser le filtre “*”.

On peut personnaliser le libellé du bouton de validation du picker via la propriété CommitButtonText et on peut également préciser le répertoire de départ via la propriété SuggestedStartLocation.

Pour déclencher l’affichage du picker il faut appeler la méthode PickSingleFileAsync() ou PickMultipleFilesAsync() qui nous retournera les fichiers sélectionnés sous la forme d’objet StorageFile :

  1. private async void Upload_Click( object sender, RoutedEventArgs e)
  2. {
  3. FileOpenPicker fileOpenPicker = new FileOpenPicker ();
  4. fileOpenPicker.FileTypeFilter.Add( « * » );
  5. fileOpenPicker.CommitButtonText = « Upload » ;
  6. fileOpenPicker.SuggestedStartLocation = PickerLocationId .PicturesLibrary;
  7. var files = await fileOpenPicker.PickMultipleFilesAsync();
  8. if (files == null )
  9. return ;
  10. var container = DataContext as StorageItem ;
  11. foreach ( var file in files)
  12. {
  13. // Envoi dans l’Azure Storage
  14. }
  15. }

 

Le résultat de l’appel à la méthode PickMultipleFilesAsync :

image

Téléchargement de fichiers depuis Azure avec le FileSavePicker

Dans notre application le contrôle FileSavePicker est utilisé pour donner la possibilité à l’utilisateur de télécharger vers la destination de son choix un fichier provenant de son compte Azure.

L’utilisation de la classe FileSavePicker nécessite au minimum de spécifier les types de fichiers via la propriété FileTypeChoices.

Comme pour le FileOpenPicker, nous pouvons personnaliser le bouton de validation via la propriété CommitButtonText et spécifier un chemin de départ via la propriété SuggestedStartLocation.

Et enfin nous pouvons suggérer un nom de fichier de destination via la propriété SuggestedFileName :

  1. private async void Download()
  2. {
  3. FileSavePicker savePicker = new FileSavePicker ();
  4. savePicker.SuggestedFileName = blob.Name;
  5. savePicker.CommitButtonText = « Download » ;
  6. savePicker.SuggestedStartLocation = PickerLocationId .PicturesLibrary;
  7. savePicker.FileTypeChoices.Add( Path .GetExtension(blob.Name), new List < string >() { Path .GetExtension(blob.Name) });
  8. var storageFile = await savePicker.PickSaveFileAsync();
  9. if (storageFile != null )
  10. {
  11. // Telechargement du fichier Azure vers le storageFile
  12. }
  13. }

 

L’appel à la méthode PickSaveFileAsync() déclenche l’affichage du picker et nous renvoie un objet StorageFile dans lequel il ne reste plus qu’à écrire nos données :

image

Les contrats File Picker

Pour terminer cet article nous allons implémenter les contrats File Open Picker et File Save Picker. Ces contrats permettent à une application de devenir fournisseur de fichiers dans le cas du File Open Picker Contract et espace de stockage dans le cas du File Save Picker Contract. L’application Skydrive en est un exemple.

File Open Picker Contract

Le contrat File Open Picker est utilisé lorsque l’on veut que notre application devienne fournisseur de fichiers.

Par exemple, lors de la rédaction d’un nouveau mail depuis l’application Courrier, à l’ajout de pièces jointes, l’utilisateur aura la possibilité de sélectionner l’application Azure Explorer pour y sélectionner des fichiers provenant de son compte Azure.

La 1ère étape consiste à déclarer ce contrat dans le manifeste de l’application :

image

Il faut développer une vue permettant à l’utilisateur de sélectionner un fichier. Cette vue sera activée par la classe App en implémentant la méthode OnFileOpenPickerActivated :

  1. protected override void OnFileOpenPickerActivated( FileOpenPickerActivatedEventArgs args)
  2. {
  3. FileOpenPickerPage page = new FileOpenPickerPage ();
  4. page.Activate(args);
  5. }

 

Dans la méthode Activate de la vue, nous récupérons une instance du contrôle FileOpenPickerUI.

On peut personnaliser le titre de la fenêtre via sa propriété Title, récupérer les types de fichier supportés via sa propriété AllowedFileTypes, ou encore être notifier lorsque l’utilisateur supprime un fichier par l’évènement FileRemoved :

  1. public void Activate( FileOpenPickerActivatedEventArgs args)
  2. {
  3. fileOpenPickerUI = args.FileOpenPickerUI;
  4. fileOpenPickerUI.Title = storageAccountName +  » Azure Storage » ;
  5. fileOpenPickerUI.FileRemoved += fileOpenPickerUI_FileRemoved;
  6. Window .Current.Content = this ;
  7. this .OnNavigatedTo( null );
  8. Window .Current.Activate();
  9. }

 

On utilise ensuite la méthode Add(string, IStorageFile) pour ajouter des fichiers à la sélection de l’utilisateur :

  1. private async void GridView_ItemClick( object sender, ItemClickEventArgs e)
  2. {
  3. StorageItem item = e.ClickedItem as StorageItem ;
  4. var blob = await dataSource.DownloadBlob(( Blob )item);
  5. fileOpenPickerUI.AddFile(item.Name, blob);
  6. }

 

Pour tester il suffit de lancer l’application Skydrive, de cliquer sur télécharger et dans le FileOpenPicker sélectionner l’application Azure Explorer :

image

File Save Picker et Cached File Updater Contract

Le contrat File Save Picker est utilisé pour que l’application propose aux autres un espace de stockage. Lors de l’enregistrement d’un fichier via la fenêtre d’enregistrement (FileSavePicker), l’utilisateur peut sélectionner une destination local dans le système de fichiers, ou sélectionner une application qui implémente ce contrat.

Lorsqu’une application va nous demander une destination pour sauvegarder un fichier, nous devrons lui fournir un objet de type StorageFile dans lequel elle écrira les données. Nous allons créer un fichier dans le stockage local de notre application, puis, une fois les données écrites, nous l’enverrons vers Azure.

Dans ce scénario, nous avons besoin d’être notifié lorsque l’application appelante aura terminé d’écrire le fichier. Pour recevoir cette notification nous utiliserons le contrat Cached File Updater.

1ère étape, déclarer les contrats File Save Picker et Cached File Updater dans le manifeste :

 

image

 

Dans la classe App on implémente les méthodes d’activation de nos vues :

  1. protected override void OnFileSavePickerActivated( FileSavePickerActivatedEventArgs args)
  2. {
  3. FileSavePickerPage page = new FileSavePickerPage ();
  4. page.Activate(args);
  5. }
  6. protected override void OnCachedFileUpdaterActivated( CachedFileUpdaterActivatedEventArgs args)
  7. {
  8. CachedFilePage page = new CachedFilePage ();
  9. page.Activate(args);
  10. }

 

Du côté de la vue qui implémente le Save File Picker, on récupère l’instance du FileSavePickerUI et on s’abonne à l’évènement TargetFileRequested :

  1. private FileSavePickerUI fileSavePickerUI = null ;
  2. public void Activate( FileSavePickerActivatedEventArgs args)
  3. {
  4. // cache FileOpenPickerUI
  5. fileSavePickerUI = args.FileSavePickerUI;
  6. fileSavePickerUI.TargetFileRequested += fileSavePickerUI_TargetFileRequested;
  7. Window .Current.Content = this ;
  8. this .OnNavigatedTo( null );
  9. Window .Current.Activate();
  10. }

L’évènement TargetFileRequested est levé lorsque l’utilisateur valide la destination de son fichier. A ce moment là nous devons fournir à l’application appelante un objet StorageFile. Pour donner ce fichier, l’évènement nous envoie un argument de type TargetFileRequestedEventArs. Cet objet possède une propriété Request de type TargetFileRequest, qui lui même a un membre TargetFile de type StorageFile.

L’objet TargetFileRequest possède également une méthode, GetDeferral(), qui nous permet de récupérer un objet de type TargetFileRequestDeferral. Cet objet permet à notre application de s’exécuter de manière asynchrone et elle devra appeler la méthode Complete() pour indiquer à l’application appelante qu’elle a terminé :

  1. var deferral = args.Request.GetDeferral();
  2. var storageFile = await ApplicationData .Current.LocalFolder.CreateFileAsync(sender.FileName, CreationCollisionOption .ReplaceExisting);
  3. args.Request.TargetFile = storageFile;
  4. deferral.Complete();

 

Dans notre cas, nous créons le fichier dans le répertoire local de notre application et nous voulons être notifié lorsque l’application appelante aura écrit les données dans ce fichier, afin de l’envoyer sur Azure. Il faut utiliser la classe CachedFileUpdater et la méthode SetUpdateInformation afin de paramétrer notre notification. Voici le code complet :

  1. async void fileSavePickerUI_TargetFileRequested( FileSavePickerUI sender, TargetFileRequestedEventArgs args)
  2. {
  3. StorageItem storageItem = null ;
  4. await Dispatcher.RunAsync(Windows.UI.Core. CoreDispatcherPriority .Normal, async () =>
  5. {
  6. storageItem = DataContext as StorageItem ;
  7. });
  8. var deferral = args.Request.GetDeferral();
  9. var storageFile = await ApplicationData .Current.LocalFolder.CreateFileAsync(sender.FileName, CreationCollisionOption .ReplaceExisting);
  10. CachedFileUpdater .SetUpdateInformation(storageFile, storageItem.Uri, ReadActivationMode .NotNeeded, WriteActivationMode .AfterWrite, CachedFileOptions .DenyAccessWhenOffline);
  11. args.Request.TargetFile = storageFile;
  12. deferral.Complete();
  13. }

 

Du côté de la vue CachedFilePage, à l’activation de notre page, nous devons récupérer l’objet CachedFileUpdaterUI et s’abonner là l’évènement FileUpdateRequested. Cet évènement sera levé lorsque l’application appelante aura terminé l’écriture du fichier local :

  1. CachedFileUpdaterUI cachedFileUpdaterUI;
  2. internal void Activate(Windows.ApplicationModel.Activation. CachedFileUpdaterActivatedEventArgs args)
  3. {
  4. cachedFileUpdaterUI = args.CachedFileUpdaterUI;
  5. cachedFileUpdaterUI.FileUpdateRequested += cachedFileUpdaterUI_FileUpdateRequested;
  6. Window .Current.Activate();
  7. }

 

Et pour terminer il ne reste plus qu’à télécharger le fichier local vers Azure :

  1. async void cachedFileUpdaterUI_FileUpdateRequested( CachedFileUpdaterUI sender, FileUpdateRequestedEventArgs args)
  2. {
  3. var deferral = args.Request.GetDeferral();
  4. var request = args.Request;
  5. var name = request.File.Name;
  6. var file = await ApplicationData .Current.LocalFolder.GetFileAsync(name);
  7. var uri = request.ContentId;
  8. Uri blobUri = new Uri (uri + « / » + file.Name);
  9. var dataSource = new AzureStorageDataSource (storageAccountName, storageKey);
  10. await dataSource.UploadBlob(uri, file.Name, file);
  11. deferral.Complete();
  12. }

Conclusion

En l’absence de SDK Azure pour Windows 8 ou tout autre technologie, nous pouvons utiliser les services REST pour accéder aux services de stockage. Le code est bien sur un peu plus long à écrire que si nous avions un SDK, mais je ne doute pas que les équipes de développement Azure en fourniront un rapidement.

Nous ne sommes pas entré dans le détail des services REST de l’Azure Storage, mais nous pouvons bien évidemment faire plus : créer/supprimer un containeur, spécifier des permissions, récupérer les métadatas… Pour aller plus loin, je vous invite à consulter la documentation des services REST de l’Azure Storage.

Pour les contrats File Picker et Cached File Updater, je vous conseille cet article MSDN et les différentes ressources qui gravitent autour (Quickstart, Guidelines, Samples).

Concernant le File Save Picker Contract, je n’ai pas trouvé d’autre manière de l’implémenter, conjointement avec le Cached File Updater pour télécharger le fichier sur Azure. Pourtant, lorsque l’on utilise le File Save Picker de l’application Skydrive, on dirait bien que le fichier est téléchargé directement dans le Cloud, de manière synchrone, avant que le picker ne se referme. Si vous avez des pistes à ce sujet, n’hésitez pas!

Windows 8 et l’Advanced Query Syntax… ou comment tester l’existence d’un fichier

Mais qu’est-ce que l’AQS (Advanced Query Syntax) ?

L’AQS est une syntaxe permettant d’effectuer des recherches avancées sur le système de fichiers. Cette syntaxe a été introduite à partir de Windows 7.

Lorsque l’on effectue une recherche depuis le menu démarrer dans Windows 7, ou depuis le Charm de recherche sous Windows 8, nous pouvons utiliser l’AQS pour rechercher dans les fichiers indexés par Windows.

L’AQS fonctionne aussi bien avec les propriétés système des fichiers, que des mots clés (dans la langue de l’UI du système). Par exemple pour rechercher tous les fichiers appartenant à un auteur spécifique, nous pouvons utiliser les 3 filtres suivants :

  • system.author:benoit
  • authors:benoit
  • author:benoit


image

Tester l’existence d’un fichier avec l’AQS

Pour connaitre l’existence d’un fichier en Windows 8 il n’existe que des méthodes “alternatives”. En fait avec les API de WinRT il n’est pas possible de faire appel à File.Exist(string). La classe File est une classe .Net et n’est pas accessible en WinRT.

Richard C. nous expose sur son blog certaines méthodes alternatives permettant de tester l’existence d’un fichier.

Les 3 méthodes décrite dans ce post sont les suivantes :

  • Méthode 1 : Parcourir tous les fichiers d’un répertoire à la recherche d’un fichier spécifique
  • Méthode 2 : Ouvrir le fichier pour lecture. Cette méthode déclenche une exception dans le cas où le fichier n’existe pas
  • Méthode 3 : utiliser les API Win32

La méthode 1 n’est pas à mon gout, surtout lorsque l’on effectue souvent cette opération. Et plus il y a de fichiers dans le répertoire et moins ce sera performant.

Comme Richard, je qualifierai la méthode 2 de méthode brutale, puisque nous forçons le système à déclencher une exception en accédant à un fichier inexistant. A éviter si vous l’utiliser très souvent…

Et pour ma part je qualifierai la 3ème méthode de méthode barbue… C’est bien évidemment la meilleure méthode en terme d’efficacité et de performance, mais la plus complexe à développer (bien qu’il suffise de faire un copier/coller ;))

En utilisant l’AQS nous pouvons ajouter une méthode alternative pour rechercher l’existence d’un fichier, plus efficace que la 1ère méthode, non brutale comparée à la 2ème méthode, et plus simple à développer et à réécrire que la méthode barbue.

Avec l’AQS il est possible d’effectuer une recherche sur le nom du fichier en utilisant la propriété system.filename ou tout simplement le mot clé filename. Avec ceci, nous pouvons écrire la méthode FileExist comme suit :

  1. public async static Task<bool> FileExist(string filePath)
  2. {
  3.     string fileName = Path.GetFileName(filePath);
  4.     string directoryPath = Path.GetDirectoryName(filePath);
  5.     var storageFolder = await Windows.Storage.StorageFolder.GetFolderFromPathAsync(directoryPath);
  6.     var queryOpt = new QueryOptions();
  7.     queryOpt.ApplicationSearchFilter = « system.filename: » » + fileName + «  » »;
  8.     queryOpt.FolderDepth = FolderDepth.Shallow;
  9.     queryOpt.IndexerOption = IndexerOption.UseIndexerWhenAvailable;
  10.     var queryFile = storageFolder.CreateFileQueryWithOptions(queryOpt);
  11.     return  (await queryFile.GetFilesAsync()).Count > 0;
  12. }

 

Dans cet exemple de code nous utilisons la classe StorageFolder pour retrouver une instance vers le répertoire devant contenir le fichier rechercher. Puis nous utilisons un objet de type QueryOptions associé à la méthode CreateFileQueryWithOptions, qui va nous permettre de rechercher des fichiers selon certains critères.

Pour utiliser l’AQS il suffit de préciser le filtre sur le nom du fichier au niveau de la propriété ApplicationSearchFilter des options de recherche.

Finalement, il ne reste plus qu’à exécuter la requête et vérifier le nombre d’éléments retournés pour savoir si notre fichier existe ou non.

Pour aller plus loin

Au delà du nom d’un fichier, il est possible de rechercher des fichiers en filtrant sur bien des propriétés et mots clés, comme par exemple le nom d’un artiste ou d’un album, ou encore les dimensions d’une photo…

Pour plus d’infos sur l’AQS voici le lien vers la documentation officielle.

Windows 8 : développement d’un lecteur audio

L’objectif de cet article est de découvrir les API Windows 8 qui vont nous permettre :

  • d’accéder aux librairies de l’utilisateur (dans notre cas la librairie de musique),
  • lire un flux audio en mode arrière-plan
  • s’interfacer aux évènements du contrôle audio

Vous pouvez télécharger les sources complètes au format Visual Studio 2012 RC en suivant MusicLibrary.zip.

Voici un aperçu de l’application :
image

Accès aux librairies

Les librairies auxquelles nous pouvons accéder depuis les applications Metro sont les librairies Windows de documents, d’images, de vidéos et de musiques de l’utilisateur, comme nous les retrouvons dans Windows Explorer :
image

Les capacités de l’application

Pour accéder au contenu de ces librairies dans une application Windows 8 il faut en déclarer l’accès via les capacités, depuis le manifeste de l’application :
image

Dans le cas contraire, une exception de type UnauthorizedAccessException sera levée avec le message suivant : “Access to the specified location (MusicLibrary) requires a capability to be declared in the manifest”.

Les classes KnownFolders, StorageFolder et StorageFile

L’accès aux librairies se fait via la classe Windows.Storage.KnownFolders qui expose des propriétés de type StorageFolder :

  1. StorageFolder documentsFolder = KnownFolders.DocumentsLibrary;
  2. StorageFolder musicFolder = KnownFolders.MusicLibrary;
  3. StorageFolder picturesFolder = KnownFolders.PicturesLibrary;
  4. StorageFolder videosFolder = KnownFolders.VideosLibrary;

 

Depuis un objet StorageFolder, nous pouvons interroger le système de fichiers pour
récupérer la liste des genres musicaux, des artistes ou des albums via la méthode
GetFoldersAsync et également des fichiers via la méthode GetFilesAsync. Dans le code
ci-dessous nous récupérons la liste des genres musicaux. Par genre, nous parcourons
la liste des albums et enfin par album, la liste des fichiers :

  1. foreach (var genreFolder in genreFolders)
  2. {
  3.     IReadOnlyList<StorageFolder> albumFolders = await genreFolder.GetFoldersAsync(CommonFolderQuery.GroupByAlbum);
  4.     foreach  (var albumFolder in albumFolders)
  5.     {
  6.         IReadOnlyList<StorageFile> trackFiles = await albumFolder.GetFilesAsync();
  7.         foreach (var trackFile in trackFiles)
  8.         {
  9.         }
  10.     }
  11. }

 

L’interface IStorageFolderQueryOperations

Pour aller un peu plus loin dans la recherche de répertoires ou de fichiers, nous
pouvons utiliser les membres de l’interface IStorageFolderQueryOperations, implémentée
par StorageFolder et StorageFile.

Cette interface nous permet par exemple de spécifier des options de filtres supplémentaires
sur les fichiers : la méthode CreateFileQueryWithOptions nous retourne un objet StorageFileQueryResult.
De cette manière nous pouvons filtrer les fichiers d’extension mp3 et wma.

Elle permet également d’être notifier en cas de changement dans un répertoire, par
exemple l’ajout d’un album dans un genre musical. Dans ce cas nous passerons par la
méthode CreateFolderQuery qui renvoie un objet StorageFolderQueryResult et qui possède
un évènement ContentsChanged.

Le code ci-dessous illustre ces 2 exemples :

  1. foreach (var genreFolder in genreFolders)
  2. {
  3.     StorageFolderQueryResult albumsQuery = genreFolder.CreateFolderQuery(CommonFolderQuery.GroupByAlbum);
  4.     albumsQuery.ContentsChanged += albumsQuery_ContentsChanged;
  5.     IReadOnlyList<StorageFolder> albumFolders = await albumsQuery.GetFoldersAsync();
  6.     foreach (var albumFolder in albumFolders)
  7.     {
  8.         StorageFileQueryResult tracksQuery = albumFolder.CreateFileQueryWithOptions(new QueryOptions(CommonFileQuery.OrderByMusicProperties, new string[] {« .mp3 »« .wma »}));
  9.         IReadOnlyList<StorageFile> trackFiles = await tracksQuery.GetFilesAsync();
  10.         foreach (var trackFile in trackFiles)
  11.         {
  12.         }
  13.     }
  14. }

 

L’interface IStorageItemProperties

Les classes StorageFolder et StorageFile implémentent l’interface IStorageItemProperties qui nous donne accès aux propriétés étendues de ces éléments.

Par exemple pour un album, nous aimerions retrouver l’année de sortie ainsi que l’artiste, et pour une piste, le numéro de la piste et sa durée. Dans ce cas nous devons récupérer un objet MusicProperties via la méthodes GetMusicProperties de l’objet StorageItemContentProperties.

Cette interface expose également la méthode GetThumbnailAsync qui permet de retrouver la miniature d’un élément en spécifiant le type de miniature que nous voulons récupérer ainsi que sa taille.

Le code ci-dessous illustre l’utilisation de ces membres :

  1. // Album Properties
  2. MusicProperties albumProp = await albumFolder.Properties.GetMusicPropertiesAsync();
  3. string album = albumProp.Album;
  4. string artist = albumProp.Artist;
  5. uint year = albumProp.Year;
  6. // Music Thumbnail
  7. StorageItemThumbnail thumbnail = await albumFolder.GetThumbnailAsync(ThumbnailMode.MusicView, 250);
  8. // Track Properties
  9. MusicProperties trackProp = await trackFile.Properties.GetMusicPropertiesAsync();
  10. TimeSpan trackDuration = trackProp.Duration;
  11. uint trackNumber = trackProp.TrackNumber;

 

Lecture audio en mode arrière plan

La lecture audio en arrière-plan consiste à continuer la lecture d’un flux audio lorsque l’on navigue de page en page à l’intérieur de notre application, mais également lorsque l’on navigue vers une autre application. Les actions pour arriver à ce résultat sont décrites ci-dessous.

Déclaration d’une tâche de fond

La 1ère étape pour activer la lecture audio en arrière-plan consiste à le déclarer dans le manifeste de l’application. Il faut ajouter une déclaration Background Tasks de type Audio en spécifiant le point d’entrée applicatif :


image

Gestion de la navigation et du composant MediaElement

La 2ème étape est de déclarer la capacité de lecture en tache de fond au niveau de l’objet MediaElement. Le MediaElement est un composant déjà connu par les développeurs XAML qui permet d’effectuer la lecture de contenu multimédia. Il possède une propriété AudioCategory qui permet d’indiquer au système comment traiter ce flux audio :

  1. <MediaElement x:Name= »MediaPlayer » AudioCategory= »BackgroundCapableMedia » AudioDeviceType= »Multimedia » />

 

Une autre chose à savoir si l’on veut lire un flux audio en tache de fond lorsque l’on navigue entre les pages d’une application, est que le composant MediaElement doit être présent dans l’arbre de contrôles de toutes les pages.

L’une des solutions en Windows 8, est d’utiliser le système de Frame pour la navigation. Une même Frame est utilisée pour afficher les pages de notre application comme le montre le code ci-dessous :

  1. // Init frame
  2. var rootFrame = new Frame();
  3. rootFrame.Navigate(typeof(MainPage));
  4. Window.Current.Content = rootFrame;
  5. Window.Current.Activate();
  6. //Navigate to another page
  7. var frame = Window.Current.Content as Frame;
  8. frame.Navigate(typeof(PlayTracksPage));

 

Avec ce système il suffit d’attacher un composant MediaElement à l’objet Frame de façon à ce qu’il se retrouve dans l’arbre des contrôles quel que soit la page affichée. Nous pouvons dans ce cas déclarer un style contenant un objet MediaElement au niveau des ressources de l’application.

Voici le code de la déclaration du style :

  1. <Style x:Key= »MediaPlayerStyle » TargetType= »Frame »>
  2.     <Setter Property= »Template »>
  3.         <Setter.Value>
  4.             <ControlTemplate TargetType= »Frame »>
  5.                 <Grid>
  6.                     <MediaElement x:Name= »MediaPlayer » AudioCategory= »BackgroundCapableMedia » AudioDeviceType= »Multimedia » />
  7.                     <ContentPresenter />
  8.                 </Grid>
  9.             </ControlTemplate>
  10.         </Setter.Value>
  11.     </Setter>
  12. </Style>

 

Puis l’affectation du style à l’objet Frame lors de sa création :

  1. var rootFrame = new Frame();
  2. rootFrame.Style = Resources[« MediaPlayerStyle »as Style;

 

Lorsque nous sommes dans une page et que nous voulons récupérer l’instance du composant MediaElement, l’utilisation de la classe VisualTreeHelper s’impose :

  1. DependencyObject rootGrid = VisualTreeHelper.GetChild(Window.Current.Content, 0);
  2. MediaElement mediaElement = (MediaElement)VisualTreeHelper.GetChild(rootGrid, 0);

 

Lecture en arrière-plan en dehors de l’application

Les 2 étapes décrites ci-dessus permettent de mettre en place la lecture d’un flux multimédia en arrière-plan dans l’application. Si nous voulons que la lecture du flux audio continue hors des frontières de l’application (c’est à dire lorsque l’utilisateur navigue vers une autre application) il faut s’interfacer à la classe MediaControl. Le chapitre ci-dessous y est consacré.

Gestion du contrôle audio

Avec Windows 8, comme c’est déjà le cas avec Windows Phone 7, l’utilisateur peut contrôler un flux audio depuis n’importe quel endroit grâce à ce qu’on appelle le MediaControl. Le MediaControl est un panel permettant de visualiser et de contrôler la lecture en cours. Qu’il soit dans une application Windows 8, une application Desktop ou sur le Start Screen, l’utilisateur peut faire apparaitre le MediaControl en haut à gauche de son écran en appuyant par exemple sur les touches de volume de son clavier :


image

Avec les APIs Windows 8, nous pouvons recevoir les notifications de ce contrôle (Previous, Play, Pause, Stop, Next…) dans notre application et également lui envoyer des informations, telles que le nom de l’artiste, de la piste en cours ou encore la miniature de l’album.

Pour s’interfacer au MediaControl et activer la lecture en arrière-plan depuis une application, il faut au minimum s’abonner aux 4 évènements suivants : StopPressed, PlayPressed, PausePressed, PlayPauseTogglePressed :

  1. MediaControl.StopPressed += MediaControl_StopPressed;
  2. MediaControl.PlayPressed += MediaControl_PlayPressed;
  3. MediaControl.PlayPauseTogglePressed += MediaControl_PlayPauseTogglePressed;
  4. MediaControl.PausePressed += MediaControl_PausePressed;

 

Attention il vous faudra dispatcher vers le thread UI la modification de votre objet MediaElement :

  1. void MediaControl_PausePressed(object sender, object e)
  2. {
  3.     App.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,() =>
  4.                                    {
  5.                                        mediaPlayer.Pause();
  6.                                    });
  7. }
  8. void MediaControl_PlayPauseTogglePressed(object sender, object e)
  9. {
  10.     if (MediaControl.IsPlaying)
  11.         App.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
  12.                                    {
  13.                                        mediaPlayer.Pause();
  14.                                    });
  15.     else
  16.         App.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
  17.                                    {
  18.                                        mediaPlayer.Play();
  19.                                    });
  20. }
  21. void MediaControl_PlayPressed(object sender, object e)
  22. {
  23.     App.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
  24.                                    {
  25.                                        mediaPlayer.Play();
  26.                                    });
  27. }
  28. void MediaControl_StopPressed(object sender, object e)
  29. {
  30.     App.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
  31.                                    {
  32.                                        mediaPlayer.Stop();
  33.                                    });
  34. }

 

Pour activer/désactiver les boutons Previous et Next du MediaControl il suffit de s’abonner/se désabonner aux évènements PreviousTrackPressed et NetxTrackPressed. A vous de gérer la position courante et le fait de s’abonner et se désabonner. Par exemple, après avoir cliquer sur Next, on vérifie la position actuelle :

  1. if  (!CanGoNext)
  2.     MediaControl.NextTrackPressed -= MediaControl_NextTrackPressed;
  3. if (currentPosition == 1)
  4.     MediaControl.PreviousTrackPressed += MediaControl_PreviousTrackPressed;

 

L’état du bouton Play/Pause quant à lui se gère via la propriété IsPlaying :

  1. MediaControl.IsPlaying = true;

 

Et enfin, les informations concernant la lecture en cours se paramètrent via les propriétés
TrackName, ArtistName et AlbumArt :

  1. MediaControl.TrackName = album.CurrentTrack.Name;
  2. MediaControl.ArtistName = album.CurrentTrack.ArtistName;
  3. MediaControl.AlbumArt = new Uri(album.ThumbnailLocalStorageUri);

 

Petite remarque sur l’Uri du contenu de l’AlbumArt. Cette propriété accepte 2 types d’Uri, soit une URI vers une donnée locale de l’application (ms-appdata://), soit une ressource dans le package (ms-appx://).

Conclusion

Nous avons vu que le parcours et la recherche dans les librairies est très simple à prendre en main et à mettre en place dans son application Windows 8. L’utilisation des membres de l’interface IStorageItemProperties permet de simplifier la récupération d’informations spécifiques en fonction du type de contenu de la librairie. Pour plus d’informations à ce sujet je vous invite à consulter la documentation sur le site MSDN

La mise en place d’une lecture en arrière-plan est un peu plus complexe. Il faut d’une part s’assurer que le composant MediaElement soit présent dans l’arbre de contrôle de toutes les pages XAML pour qu’il continue sa lecture lors de la navigation InApp. Et d’autre part si la lecture doit continuer en dehors de l’application, il faut s’interfacer au MediaControl. Cependant ce scénario permet une très bonne intégration au système d’exploitation et aux évènements clavier. Pour plus d’infos sur ce dernier voici un article de la MSDN (en Html / Javascript).