Ce tutoriel vise à développer une application Open Data pour Windows 8, ou plus exactement Windows 8 Consumer Preview.

La première partie de ce tutoriel s’est intéressée à la vue d’ensemble de l’application, les axes de conception ainsi qu’à la connectivité aux données, dans le cas présent, celles du département de Saône-et-Loire via son portail Open Data 71 qui tire bénéfice du kit de démarrage OGDI (Open Government Data Initiative) pour mettre à disposition un service exposant les données.

Au cours de cette seconde partie du tutoriel, nous nous concentrons sur les éléments de code importants de l’application Metro en tant que telle.

Ecriture du code de l’application Métro

L’objectif n’est pas de faire ici un copier/coller de tout le code de l’application au risque de paraître rébarbatif (d’autant que celui-ci est fourni en pièce jointe) ; nous préférons en illustrer les éléments principaux.

Dans un premier temps, nous abordons la création d’une tuile dynamique permettant d’illustrer les cartes postales, puis, dans un second temps, nous montrons comment charger les données de façon asynchrone dans la page principale et, finalement, nous voyons par quel procédé on peut implémenter une logique de tri sur des collections à l’aide du contrôle GridView.

Ecriture de la tuile dynamique

Pour rajouter une petite touche d’originalité à notre application et la différencier des albums photos classiques, nous nous proposons de représenter les photos sous forme de tuile dynamique avec un panneau animé qui fait surface de temps en temps sur la photo pour afficher des informations liées à la photo (ville et description notamment). Comme un tel contrôle n’existe pas, il va nous falloir le créer par nous- même. On disposera ainsi d’un contrôle qui passera par plusieurs états visuels :

  1. Tuile cachée,
  2. Tuile partiellement visible,
  3. Et tuile totalement visible.

Pour ce faire, il suffit tout simplement de :

  1. créer un UserControl réutilisable,
  2. définir l’animation,
  3. et lier les éléments graphiques aux propriétés de l’objet CartePostaleModel qui représente le contexte de données du contrôle.

Voici donc le code correspondant à ce contrôle :

PostalCardTile.xaml :

<UserControl

    x:Class="CartesPostales.Metro.Views.PostalCardTile"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="using:CartesPostales.Metro.Views"

    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

    mc:Ignorable="d">

   

    <UserControl.Resources>

        <Storyboard x:Key="TileAnimation">

            <DoubleAnimationUsingKeyFramesStoryboard.TargetName="CommuneStackPanel"

                                           Storyboard.TargetProperty="(Canvas.Top)"

                                           RepeatBehavior="Forever">

                <EasingDoubleKeyFrameKeyTime="00:00:01" Value="250">

                    <!-- Tuile devient partiellement visible-->

                    <EasingDoubleKeyFrame.EasingFunction>

                        <CircleEaseEasingMode="EaseOut"></CircleEase>

                    </EasingDoubleKeyFrame.EasingFunction>

                </EasingDoubleKeyFrame>

                <EasingDoubleKeyFrameKeyTime="00:00:04" Value="250">

                    <!-- Tuile partiellement visible-->

                    <EasingDoubleKeyFrame.EasingFunction>

                        <CircleEase EasingMode="EaseOut" >< /CircleEase>

                    </EasingDoubleKeyFrame.EasingFunction>

                </EasingDoubleKeyFrame>

                <EasingDoubleKeyFrameKeyTime="00:00:06" Value="310">

                    <!-- Tuile devient cachée-->

                    <EasingDoubleKeyFrame.EasingFunction>

                        <CircleEase EasingMode="EaseOut" >< /CircleEase>

                    </EasingDoubleKeyFrame.EasingFunction>

                </EasingDoubleKeyFrame>

                <EasingDoubleKeyFrameKeyTime="00:00:08" Value="310">

                    <!-- Tuile cachée-->

                    <EasingDoubleKeyFrame.EasingFunction>

                        <CircleEaseEasingMode="EaseOut" ></CircleEase>

                    </EasingDoubleKeyFrame.EasingFunction>

                </EasingDoubleKeyFrame>

                <EasingDoubleKeyFrameKeyTime="00:00:11" Value="150">

                    <!-- Tuile devient totalement visible-->

                    <EasingDoubleKeyFrame.EasingFunction>

                        <CircleEase EasingMode="EaseOut" >< /CircleEase>

                    </EasingDoubleKeyFrame.EasingFunction>

                </EasingDoubleKeyFrame>

                <EasingDoubleKeyFrameKeyTime="00:00:13" Value="150">

                    <!-- Tuile totalement visible-->

                    <EasingDoubleKeyFrame.EasingFunction>

                        <CircleEase EasingMode="EaseOut" >< /CircleEase>

                    </EasingDoubleKeyFrame.EasingFunction>

                </EasingDoubleKeyFrame>

                <EasingDoubleKeyFrameKeyTime="00:00:16" Value="310">

                    <!-- Tuile devient cachée-->

                    <EasingDoubleKeyFrame.EasingFunction>

                        <CircleEase EasingMode="EaseOut" >< /CircleEase>

                    </EasingDoubleKeyFrame.EasingFunction>

                </EasingDoubleKeyFrame>

                <EasingDoubleKeyFrameKeyTime="00:00:16" Value="310">

                    <!-- Tuile cachée-->

                    <EasingDoubleKeyFrame.EasingFunction>

                        <CircleEase EasingMode="EaseOut" >< /CircleEase>

                    </EasingDoubleKeyFrame.EasingFunction>

                </EasingDoubleKeyFrame>

            </DoubleAnimationUsingKeyFrames>

        </Storyboard>

    </UserControl.Resources>

   

    <Grid>

      <Canvas Height="250" Width="408" Background="White">

            <Canvas.Clip>

                <RectangleGeometryRect="0,0,458,250"/>

            </Canvas.Clip>

            <Image Canvas.Top="0" Canvas.Left="0" Source="{Binding fichier}" Height="250" Width="408" />

            <StackPanel Canvas.Top="310" Canvas.Left="0" Height="250" Width="408" Background="#B5000000" Name="CommuneStackPanel" Orientation="Vertical">

                <TextBlock Text="{Binding commune}" Foreground="#FFFF" FontFamily="Segoe UI SemiBold" FontSize="20" Padding="5"></TextBlock>

                <TextBlock Text="{Binding titre1}" Foreground="#FFFF" FontFamily="Segoe UI Semilight" FontSize="11" Padding="5,25,5,5" TextWrapping="Wrap"></TextBlock>

            </StackPanel>

        </Canvas>

    </Grid>

</UserControl>

 

PostalCardTile.xaml.cs :

using System;

using System.Linq;

using Windows.UI.Xaml;

using Windows.UI.Xaml.Controls;

using Windows.UI.Xaml.Media.Animation;

 

namespaceCartesPostales.Metro.Views

{

    public sealed partial class PostalCardTile : UserControl

    {

        ///< summary>

        /// Objet permettant de générer des animations aléatoires.

        ///< /summary>

        private static Random random = new Random();

       

        public PostalCardTile()

        {

            this.InitializeComponent();

            this.Loaded += new RoutedEventHandler(PostalCardTile_Loaded);

        }

 

        void PostalCardTile_Loaded(objectsender, RoutedEventArgs e)

        {

            //Initialisation de l'animation

            Storyboard tileStoryboard = this.Resources["TileAnimation"] as Storyboard;

            tileStoryboard.BeginTime = new TimeSpan(0, 0, random.Next(2, 20));

 

            EasingDoubleKeyFrame lastKeyFrame = (tileStoryboard.Children.First() as DoubleAnimationUsingKeyFrames).KeyFrames.Last() as EasingDoubleKeyFrame;

            lastKeyFrame.KeyTime = new KeyTime();

            lastKeyFrame.KeyTime.TimeSpan.Add(new TimeSpan(0, 0, random.Next(10, 20)));

            tileStoryboard.Begin();

        }

    }

}

Comme vous pouvez le constater, il n’y a rien de compliqué dans ce code ! Nous définissons juste un scénarimage (story-board) qui permettra d’animer la tuile. (Il convient de noter au passage l’utilisation des fonctions appliquées aux différents points de l’animation permettant  d’incurver  la courbe temporelle de l’animation et rendant, par conséquent, cette dernière moins linéaire et donc plus naturelle).

Pour éviter que toutes les tuiles s’animent en même temps, on utilise un objet de type Random permettant d’obtenir pour chaque tuile des durées d’animations aléatoires.

Chargement des données de façon asynchrone

Nous allons maintenant illustrer le code de la méthode LoadNextPage de la classe HomePage permettant :

  • De télécharger les données de façon asynchrone sous forme d’objets de type CartePostaleModel,
  • Et ensuite d’affecter ces objets aux éléments de la GridView chargée d’afficher les cartes postales.

HomePage.xaml.cs :

/// <summary>

/// Méthode permettant de charger les données de la page suivante.

/// Algorithme =>

///     SI les données n'ont pas déjà été téléchargées et placées dans le catalogue

///     ALORS on les télécharge de façon asynchrone.

///     SINON on va les chercher directement depuis le catalogue.

///< /summary>

public voidLoadNextPage()

{

   itemGridView.Items.Clear();

   if (_currentPageNumber == _catalog.Count)

   {

      _isLoading = true;

      _catalog.Add(_currentPageNumber, new List<CartePostaleModel>());

      Task t = new Task(new Action(() =>

      {

         bool loadMore = true;

         List<CartePostaleModel> cartes = new List<CartePostaleModel>();

         // Chargement des données par groupe de 5 pour une fluidité maximale.

         while (_ogdiConsumer.LoadNextDataChunk(5, cartes).Result == true && loadMore == true)

         {

            Dispatcher.Invoke(CoreDispatcherPriority.Normal, (sender, e) =>

            {

               var newData = e.Context as IEnumerable<CartePostaleModel>;

               foreach (var item in newData)

               {

                  _catalog[_currentPageNumber].Add(item);

                  itemGridView.Items.Add(item);

               }

               // On se limite à 30 éléments par page

               if (itemGridView.Items.Count % 30 == 0)

               {

                  loadMore = false;

                  _currentPageNumber++;

               }

            }, this, cartes);

            cartes.Clear();

        }

        _isLoading = false;

      }));

      t.Start();

   }

   else

   {

      ICollection<CartePostaleModel> tmp = _catalog[_currentPageNumber];

      _currentPageNumber++;

      foreach (var item in tmp)

      {

         itemGridView.Items.Add(item);

      }

   }

}

Comme vous pouvez le constater, on fait appel à la bibliothèque TPL (Task Parallel Library) et notamment à la classe Task pour exécuter du code de façon asynchrone. On charge ainsi les données en tâche de fond par groupe de 5 pour un maximum de fluidité pour l’utilisateur.

Vu que les contrôles de l’interface utilisateur ont une affinité avec le Thread UI (, c’est-à-dire qu’ils ne peuvent être accédés qu’à partir du Thread qui les a créés, dans ce cas le Thread UI,) on utilise le Dispatcher pour invoquer, sur le Thread UI depuis la tâche de fond, le code permettant de mettre à jour les éléments de la GridView. On en profite également pour actualiser le catalogue gardant en cache les cartes postales déjà téléchargées.

Pour compléter cette illustration, voici le code XAML qui crée la GridView restituant les cartes postales :

HomePage.xaml :

<Page.Resources>

   <SolidColorBrush x:Key="PageBackgroundBrush" Color="White" />

   <Style x:Key="PageHeaderTextStyle"

          BasedOn="{StaticResourcePageHeaderTextStyle}" TargetType="TextBlock">

     <Setter Property="Foreground" Value="Black" />

   </Style>

   <DataTemplate x:Name="GridViewPostalCardTileTemplate">

      <views:PostalCardTileDataContext="{Binding}" />

   </DataTemplate>

      

</Page.Resources>

<Grid Background="{StaticResourcePageBackgroundBrush}">

   <Grid.RowDefinitions>

      <RowDefinitionHeight="140"/>

      <RowDefinitionHeight="*"/>

   </Grid.RowDefinitions>

 

   <!-- Back button and page title -->

   <Grid>

      <Grid.ColumnDefinitions>

         <ColumnDefinitionWidth="Auto"/>

         <ColumnDefinitionWidth="*"/>

      </Grid.ColumnDefinitions>

      <Button x:Name="backButton" Click="GoBack"

              IsEnabled="{Binding Frame.CanGoBack, ElementName=pageRoot}"

              Style="{StaticResourceBackButtonStyle}"/>

      <TextBlock x:Name="pageTitle" Grid.Column="1" Text="{StaticResource AppName}"

                 Style="{StaticResourcePageHeaderTextStyle}"/>

   </Grid>

 

   <!-- Horizontal scrolling gridused in most view states -->

   <GridView x:Name="itemGridView"

             Grid.Row="1"

             AutomationProperties.AutomationId="ItemsGridView"

             AutomationProperties.Name="Items"

             Margin="116,0,116,46"

             ItemTemplate="{StaticResourceGridViewPostalCardTileTemplate}"

             SelectionChanged="itemGridView_SelectionChanged" />  

</Grid>

 

Mise en œuvre d’une logique de tri avec le contrôle GridView

Parmi les nouveautés apportées par le modèle Metro, on retrouve notamment la possibilité de constituer à l’intérieur d’une GridView des groupes d’éléments.

On peut, par exemple, grouper les cartes postales par période historique, ce qui donnerait le résultat suivant :

image

Pour arriver à ce résultat, il convient de se servir de l’API de groupement des applications Metro. Tout d’abord, il faut intégrer sur l’élément GridView différents éléments visuels. Il faut définir :

  1. En premier lieu le HeaderTemplate qui permet de définir l’apparence visuelle de chaque en-tête de groupe ;
  2. Dans un second temps l’élément GroupStyle.Panel qui permet de spécifier la disposition des éléments graphiques dans chaque groupe ;
  3. Ensuite bien entendu un ItemTemplate qui permet de spécifier l’apparence graphique de chaque élément individuel ;
  4. Et enfin la propriété ItemsSource qui pour rendre le tri effectif doit impérativement être une liste d’éléments contenus dans une CollectionViewSource avec l’attribut IsSourceGrouped à True.

Pour commencer, voici déjà l’extrait de code XAML de la page principale permettant de définir les éléments graphiques de l’interface :

HomePage.xaml :

<Page.Resources>

  

   <CollectionViewSource x:Name="PeriodeHistoriqueCvs" IsSourceGrouped="True" />

  

</Page.Resources>

<GridView Name="PeriodeHistoriqueGridView" Grid.Row="1" Visibility="Collapsed" Margin="35,0,0,0"

                  ItemsPanel="{StaticResourceGridViewItemsPanelTemplate}"

                  ItemsSource="{Binding Source={StaticResourcePeriodeHistoriqueCvs}}"

                  ItemTemplate="{StaticResourceGridViewPostalCardTileTemplate}"

                  SelectionChanged="itemGridView_SelectionChanged">

   <GridView.GroupStyle>

      <GroupStyle>

         <GroupStyle.HeaderTemplate>

            <DataTemplate>

               <GridMargin="0">

                  <TextBlock Text='{Binding Key}' Foreground="Gray" FontSize="42" Margin="5" />

               </Grid>

            </DataTemplate>

         </GroupStyle.HeaderTemplate>

         <GroupStyle.Panel>

            <ItemsPanelTemplate>

               <VariableSizedWrapGrid  Orientation="Vertical" Margin="0,0,80,0" />

            </ItemsPanelTemplate>

         </GroupStyle.Panel>

      </GroupStyle>

   </GridView.GroupStyle>

</GridView>

Nous devons maintenant créer une classe qui hérite de List<object> et possédant une propriété Key qui permettra d’afficher le nom du groupe dans le HeaderTemplate de la GridView. Voici une solution possible avec la classe GroupInfoList :

GroupInfoList.cs :

using System.Collections.Generic;

 

namespaceCartesPostales.Metro.Helpers

{

   /// <summary>

   /// Hérite d'une liste d'objets et contient une propriété Key permettant l'affichage du

   /// groupe dans le HeaderTemplate de la GridView

   ///< /summary>

   /// <typeparamname="T"></typeparam>

   public class GroupInfoList<T> : List<object>

   {

      public object Key { get; set; }

 

      public new IEnumerator<object> GetEnumerator()

      {

         return (System.Collections.Generic.IEnumerator<object>)base.GetEnumerator();

      }

   }

}

Et voici enfin le code de la méthode GroupByPeriodehistorique dans la classe HomePage :

HomePage.xaml.cs :

/// <summary>

/// Méthode permettant de grouper les éléments de la page par période historique.

/// Algorithme =>

///     Récupération de tout les élements de la page.

///     Consitution des groupes.

///     Liaison des groupes à la source de données de la GridView correspondante.

///< /summary>

public async voidGroupByPeriodeHistorique()

{

   groups.Clear();

   await Task.Run(() =>

   {

      ICollection<CartePostaleModel> tmp;

      bool result = _catalog.TryGetValue(_currentPageNumber - 1, out tmp);

      if (result == true)

      {

         var query = from item in tmp

                     orderby item.periode_historique

                     group item by item.periode_historique into g

                     select new { GroupName = g.Key, Items = g };

        

         foreach (var g in query)

         {

            GroupInfoList<object> info = new GroupInfoList<object>();

            info.Key = g.GroupName;

           

            foreach (var item in g.Items)

            {

               info.Add(item);

            }

           

            Dispatcher.InvokeAsync(CoreDispatcherPriority.Low, (s, e) =>

            {

               groups.Add(info);

            }, this, this);

         }

        

         Dispatcher.InvokeAsync(CoreDispatcherPriority.Low, (s, e) =>

         {

            itemGridView.Visibility = Visibility.Collapsed;

            AnneeGridView.Visibility = Visibility.Collapsed;

            CommuneGridView.Visibility = Visibility.Collapsed;

            PeriodeHistoriqueGridView.Visibility = Visibility.Visible;

            PeriodeHistoriqueGridView.SelectionChanged -= itemGridView_SelectionChanged;

            PeriodeHistoriqueCvs.Source = groups;

            PeriodeHistoriqueGridView.SelectedItem = null;

            PeriodeHistoriqueGridView.SelectionChanged += itemGridView_SelectionChanged;

         }, this, this);

      }

   });

}

Vous pouvez noter une fois de plus l’exécution asynchrone du code afin de maximiser la réactivité de l’application.

Conclusion

Voilà, ce premier tutoriel sur la création d’une application Open Data Metro avec Windows 8 Consumer Preview est maintenant terminé.

Nous espérons avoir bien illustré les spécificités des applications Metro qui ont été présentées ici, à savoir :

  • L’omniprésence de l’asynchronisme afin d’avoir des applications les plus réactives possibles,
  • La création d’interfaces riches et avec la meilleure expérience utilisateur possible grâce au code XAML et la présence de contrôles enrichis comme la GridView qui permettent de mettre en valeur les données présentées avec un contrôle maximal sur la disposition de ces dernières.

Comme vous venez de le voir, les applications Metro sont des applications très graphiques et visuelles, ce qui s’avère des plus intéressants dans le contexte des données ouvertes et de leur « mise en scène » mais ces applications sont aussi très connectées au Cloud et aux nouveaux services Microsoft comme la plateforme de développement SkyDrive. Un prochain billet viendra compléter cette application pour illustrer l’utilisation du service SkyDrive dans ce contexte.