Comment “cuisiner” une application Windows 8 avec XAML et C# en une semaine–Jour 1

Dans ce second volet, nous allons nous concentrer sur les vues des détails des cartes, ainsi que la mise en place du support déconnecté, c'est à dire la création d'un cache local.

Le design global et l'enchainement des écrans est le suivant :

La page d'accueil s'affiche :

En sélectionnant "Homelands" par exemple, toutes les cartes s'affichent.

Pour accéder aux détails d'une carte il suffit alors de la sélectionner.

La solution associé à cet article est disponible ici : Day 1

Par rapport au jour 0, j'ai légèrement modifié l'architecture de l'application.

Chaque vue n'étant plus reliée directement à la source de données (UrzaGathererDataSource), mais à une classe que j'ai nommée VueData. C'est elle qui fera le pont entre la source de données et les vues en les notifiant de la disponibilités des données.

 

L'écran des extensions :

Cet écran (Expansions.xaml) est constitué d'une Grille affichant sur une ligne des contrôles Combobox pour le filtrage des données, et d'une simple GridView Horizontal pour l'affichage des cartes.

Lorsqu'on clique sur une image d'une expansion, on passe l'instance de VueData qui possède une propriété CurrentExtention, que nous raccrochons au DataContext de la vue Expansion

Binding des données.

Les combobox affiche dynamiquement des données liées , via la classe VueData par sa propriété DisplayedGroupFilter de type Filters

class Filters

    {

public int ByNameOrByNumber { get; set; }

public CardColorFilter Color { get; set; }

public CardAuthorFilter Author { get; set; }

public CardRarityFilter Rarity { get; set; }

public CardTypeFilter Type { get; set; }

public CardTextTilter Text { get; set; }

}

Exemple :  

 <ComboBox x:Name="cboFilterByNameOrByNumber" ItemsSource="{Binding Path=FilterByNameOrByNumber}" Margin="4,0,0,8" Width="120"                    

Style="{StaticResource ComboBoxStyle}" Background="{x:Null}"/>

<ComboBox Grid.Column="1" x:Name="cboFilterByColors" ItemsSource="{Binding Path=FilterByColors}" Margin="4,0,0,8" Width="120"

Style="{StaticResource ComboBoxStyle}" Background="{x:Null}" />

....

 La GridView quand à elle est liée à une Collection (ObservableCollection<URZACard>) de cartes alimentée par un contrôle de type CollectionViewSource.

<UserControl.Resources>

<common:ZoomSetting x:Key="zoomSetting"/>

<CollectionViewSource

   x:Name="groupedItemsViewSource"

   Source="{Binding Expansions}"

   IsSourceGrouped="False"/>

</UserControl.Resources>

<GridView

 IsItemClickEnabled="True"

x:Name="cardsGridView"

ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"

....

Les éléments de la GridView, sont accrochés à un DataTemplate qui prendra la forme d'un bouton, contenant lui même comme enfant un UserControl personnalisé (UrzaImage que nous détaillerons un peu plus loin) liée à la propriété pictureInfo de la classe URZACard.

<GridView.ItemTemplate>

       <DataTemplate>

                      <Button Style="{StaticResource ExpansionGridViewStyle}" Width="{Binding Source={StaticResource zoomSetting},Path=Zoom.Width}"

                                                                       Height="{Binding Source={StaticResource zoomSetting},Path=Zoom.Height}"/>

    </DataTemplate>

</GridView.ItemTemplate>

 

Style des contrôles :

La majorité des styles sont définies dans le fichier StandardStyles.xaml, mais il est bien évidement possible d'avoir des styles directement dans une vue.

Le style par défaut de la combobox ne me satisfaisant pas, il est possible de le modifier facilement en en créant une copie.

Pour créer une copie d'un Style, il suffit de sélectionner n'importe quel contrôle dans le designer, cliquer sur le bouton droit et éditer le Template comme illustré sur la figure suivante :

Ensuite il suffit d'aller dans le fichier StandardStyles.XML, ou vous retrouverez le style ComboBoxStyle, et modifier son apparence comme vous le souhaitez.

Pour la GridView, je n'ai fait que modifier le style standard du bouton, que j'ai nommé ExpansionGridViewStyle, afin d'y ajouter une légère transformation, lorsque l'élément est sélectionné et d'y afficher comme contenu un contrôle URZAImage 

<Style x:Key="ExpansionGridViewStyle" TargetType="Button">

<Setter Property="Template">

<Setter.Value>

<ControlTemplate TargetType="Button">

      <Border x:Name="Border" Background="Transparent" BorderThickness="2" BorderBrush="Black" >

<Border.RenderTransform >

<CompositeTransform x:Name="Transformation" />                       

</Border.RenderTransform>

<urzaGatherer:UrzaImage Source="{Binding pictureInfo}" />

....

 Le contrôle URZAImage :

Je me suis amusé à développer un UserControl, pour afficher un état d'attente sous forme d'un ProgressRing, et lorsque l'image est disponible, elle active une animation de type fondue/enchainée

 Néanmoins au delà de cet aspect de feedback utilisateur, ce contrôle va plus loin. Car il test la disponibilité de l'image en locale, et si elle ne l'est pas, il la télécharge et la sauvegarde dans le cache locale.

Couplé avec la Virtualization de la GridView, qui ne monte en mémoire que les contrôles visibles à l'écran, on gagne ainsi en performance et en fluidité, en évitant que toutes les images soient téléchargées d'un seul bloc lors de l'activation de la vue.

URZAImage, implémente une DependencyProperty nommée Source et lorsque cette propriété change à l'aide du DataBinding, la méthode OnSourceChanged est invoquée afin de télécharger ou pas l'image qui lui est associée.

public static readonly DependencyProperty SourceProperty =DependencyProperty.Register(

               "Source",typeof(Object),

               typeof(UrzaImage),

               new PropertyMetadata(null, OnSourceChanged));

....

 private async static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

       {

          URZAImageInfo imageInfo=(URZAImageInfo)e.NewValue;

          UrzaImage currentControl = d as UrzaImage;

         currentControl.imageSource.Source = await LoadPictureAsync(imageInfo);                       

      }

public static async Task<BitmapImage> LoadPictureAsync(URZAImageInfo imageInfo)

        {
            String uri;

            BitmapImage bmp=null;

         if (!await Helper.IsFileExistAsync(imageInfo.LocalFolder, imageInfo.FileName))

           {

                     var rawData = await Helper.DownloadPictureAsync(imageInfo.RemotePath);

                     Helper.SavePictureAsync(rawData, imageInfo.LocalFolder, imageInfo.FileName);

                     bmp =new BitmapImage();

                      InMemoryRandomAccessStream ms = new InMemoryRandomAccessStream();

                      IBuffer buffer = rawData.AsBuffer();

                      await ms.WriteAsync(buffer);
                     bmp.SetSource(ms);

                }

    }

else

    {

                uri = imageInfo.LocalPath;

                bmp =new BitmapImage(new Uri(uri));

      }

    return bmp;

   }

 

Remarques : Je tiens à attirer votre attention ici, sur la chose suivante. Très souvent, certaines méthodes de WinRT demande par exemple comme paramètre une interface, qu'il n'est pas forcement facile à implémenter.

Par exemple ici, la méthode WriteAsync du InMemoryStreamRandomAccess, demande un IBuffer comme paramètre. Ici je la crée avec AsBuffer du tableau de bytes rawdata qui m'est retourné par la méthode DowloadPictureAsync().

Or cette méthode, est une méthode d'extension disponible dans l'espace de nom System.Runtime.InteropServices.WindowsRuntime , qui permet de faire le pont entre le monde .NET et WinRT, n 'oubliez donc pas de l'ajouter systématiquement à vos projets.

 Gérer les niveaux de zoom

UrzaGatherer doit pouvoir gérer plusieurs tailles d'affichages pour les cartes, donc pouvoir modifier les propriétés Width et Height du bouton qui affiche les éléments de la GridView.

Comme vous avez pu le constater j'ai lié ces deux propriétés à une ressource statique de la classe ZoomSetting.

 <Button Style="{StaticResource ExpansionGridViewStyle}" Width="{Binding Source={StaticResource zoomSetting},Path=Zoom.Width}"

                                                                       Height="{Binding Source={StaticResource zoomSetting},Path=Zoom.Height}"/>

 

Cette classe ZoomSetting, à en faite plusieurs rôle,

1) C'est elle qui est en charge de sauvegarder/lire le facteur de zoom comme étant un réglage itinérant (https://msdn.microsoft.com/en-us/library/windows/apps/hh465094.aspx)

void SaveZoomSetting(Double zoomfactor)

        {

           ApplicationData.Current.RoamingSettings.Values[ZOOM_FACTOR] = zoomfactor;

        }

private Double ReadZoomSetting()

        {

      if (ApplicationData.Current.RoamingSettings.Values.ContainsKey(ZOOM_FACTOR))

             {

                    Double zoomFactor = (Double)ApplicationData.Current.RoamingSettings.Values[ZOOM_FACTOR];

      }

}

2) De calculer Width et Height

3) De définir un facteur de Zoom Maximum et Minimum en fonction de la résolution de l'écran

3) De signaler que la valeur du facteur de zoom a été modifiée

private Double _zoomFactor;

public Double ZoomFactor {

get {

                     return _zoomFactor ;

        }

set {

                       _zoomFactor =value;

                        Width = ZOOM_100_WIDTH * _zoomFactor / 100;

                        Height = ZOOM_100_HEIGHT * _zoomFactor / 100;

                       ApplicationData.Current.SignalDataChanged();

         }

}

Au lieu de modifier directement les propriétés Width et Height du bouton, nous modifierons directement la propriété ZoomFactor de l'objet ZoomSetting qui calculera Width et Height

et le Binding fera le reste pour nous. L'avantage ici, c'est que l'objet ZoomSetting est plus facilement transportable d'un contexte à un autre (Comme on le verra dans la section suivante) , plutôt que de manipuler un élément d'affichage tel que le bouton.

 

L'astuce pour récupérer l'instance de la classe ZoomSetting, contenu dans nos ressources statique de la vue Expansion réside dans la ligne de code suivante :

ZoomSetting zoomSetting = this.Resources["zoomSetting"] as ZoomSetting;

 

Pour être signalé de la modification du ZoomFactor la vue n'a plus qu'à s'abonner à l'évènement :

ApplicationData.Current.DataChanged+=Current_DataChanged;

 

Remarque : Vous remarquerez que dans le code joint, je redemande l'application des filtres (que nous verrons plus loin) pour que la

GridView recalcule le bon espacement entre chaque élément.

void Current_DataChanged(ApplicationData sender, object args)

        {

           if (cardsGridView != null)

            {

                _vueData.AppliedFilters();

            }

        }

Ajout des réglages de l'application

Pour configurer le niveau de zoom, l’écran de réglages fournit par Windows est bien évidemment le meilleur endroit. En effet à chaque fois que vous avez des réglages généraux pour votre application, il faut les mettre dans cet écran (https://msdn.microsoft.com/en-us/library/windows/apps/Hh780611.aspx):

La vue Setting est en faite un UserControl (SettingsControls), que nous afficherons à l'aide de la méthode ShowFyout, définie dans la classe SettingsFlyout.

Nous utiliserons la classe Popup, comme container de notre SettingsControl. Le faite de choisir cette méthode, nous évite ainsi de devoir ajouter directement le contrôle SettingsControl à notre vue Expansions

Ce contrôle prend dans son constructeur l'instance de type ZoomSetting de notre vue Expansion , et a pour rôle de définir la plage Minimum et Maximum autorisé pour les valeurs du slider. Cette plage étant calculée directement par la classe ZoomSetting en fonction de la résolution de l'écran comme vu précédemment.

 

Le facteur de Zoom sera modifié dans l'évènement ValueChanged du slider et de faite la vue Expansion sera notifiée du changement

void slider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)

        {          

            _zoomSetting.Zoom.ZoomFactor = e.NewValue; 

        }

En fermant la vue setting, on sauvegarde la dernière valeur du slider

 void SettingsControl_Unloaded(object sender, RoutedEventArgs e)

        {

            _zoomSetting.SaveZoomSetting(this.slider.Value);           

        }

 

Mode déconnecté

Lorsque l'application démarre, le fichier all.json est téléchargé la 1ere fois c'est la méthode DownloadJSONFileInBackground, qui est en charge de télécharger le fichier. A la différence du Day0, je n'utilise plus HTTPClient, pour télécharger ce fichier volumineux mais directement le BackgroundDownloader qui me permet de travailler sur de plus gros fichier et d'avoir une meilleur maitrise de ce qui se passe. Par exemple, je notifie désormais VueData du nombre d'octets encours de téléchargement (Le code étant un peu alambiqué, je vous laisse le découvrir dans le code source

Une fois le fichier téléchargé, je crée quelques répertoires locaux, et télécharge les logos pour les blocks

.

Remarque : Cette dernière opération étant un peu longue à mon gout la 1er fois, je travail sur une version un peu plus optimisée, car cela ne me satisfait pas dans l'état actuel. Une fois le cache local rempli, les performances sont meilleur bien sur.

Enfin de parcours, l'évènement BlockAvailable est levé afin de notifier VueData qu'elle peu commencer à consommer les données.

Toutes les cartes font en moyenne 150 Ko. De ce fait, il est important de fournir un moyen de ne les télécharger qu’une fois et de pouvoir les sauver en local.

 

Une expansion est symbolisé par la classe URZAExpansion, cette classe possède entre autre, une propriété cards de type ObservableCollection<URZACard> qui sera remplie à la demande.

Lorsqu'une Expansion est sélectionnée, VueData, invoque la méthode CreateCardsAsync qui ne fait que remplir la collection cards et créer les bons répertoires ou vont atterrir les images, mais en aucun cas ne télécharge les images. Elle ne fait que remplir une structure de données URZAImageInfo contenant entre autre le chemin distant à l'image de la carte, ainsi que le chemin local ou sera sauvegardé la carte. (Rappelez-vous c'est en faite le contrôle URZAImage qui téléchargera et sauvegardera en locale l'image de la carte. Bien évidement nous aurions pu choisir une autre stratégie, sur laquelle je suis d'ailleurs entrain de réfléchir).

Le code de téléchargement et de sauvegarde des images se trouve dans le fichier Helper.cs

 

Ajout des filtres

Lorsque vous naviguerez sur la vue Expansion (Evènement NavigateTo), vous vous apercevrez que la liaison de données ne se fait pas à ce niveau. En réalité elle se fait toujours sur l'évènement VueData.FilteredData qui est déclenché après l'appel à la méthode VueData.AppliedFilters(), méthode qui est elle même déclenchée, lorsqu'on sélectionne un filtre dans une des combobox.

L'application des filtres est assez simple, j'utilise dans la VueData, un ensemble de requêtes LINQ que j'applique séquentiellement. Comme le temps de réponse est satisfaisant, je n'ai pas travaillé sur un système plus complexe de requête LINQ, c'est à vous de voir. Sur un volume de données plus important par contre il faudra sans doute y consacrer un peu plus de temps. Une fois tous les filtres appliqués, je lève l'évènement FilteredData

private List<URZACard> AppliedFilterColors(List<URZACard> cards)

        {

           IEnumerable<URZACard> query = null;

           if (CardsFilter.Color.index == All)

            {

                query =from card in cards select card;

            }

           else

            {

                query =from card in cards where card.color == CardsFilter.Color.value select card;

            }

           return query.ToList<URZACard>();

        }

 

 public void AppliedFilters()

        {

           var cards = CurrentExpansion.cards;

           var filter1 = AppliedFilterByNameOrByNumber(cards);

           var filter2 = AppliedFilterColors(filter1);

           var filter3 = AppliedFilterAuhtors(filter2);

           var filter4 = AppliedFilterRarities(filter3);

           var filter5 = AppliedFilterTypes(filter4);

           var filter6 = AppliedFiltersByNameByTextAndByFlavor(filter5);

            RaiseFilteredData(filter6);

        }

       private void RaiseFilteredData(List<URZACard> cards)

        {

           URZAExpansion expansion = CurrentExpansion.Clone();

           foreach (URZACard card in cards)

            {

                expansion.cards.Add(card);

            }

           if (FilteredData != null)

            {

                FilteredData(this, new ExpansionFilterEventArgs(expansion));

            }

        }
 

 Vous noterez que je fais un clone des données, afin de garder en mémoire un jeu entier de carte pour pouvoir éviter de reparser les données JSON qui prend quand même plus de temps à l'exécution.

L'écran de présentation des cartes

Cet écran est relativement simple :

 

Très peu de code tout est fait par le DataBinding. Lorsqu'on clique sur une image, on passe l'instance de VueData qui possède une propriété CurrentCard, que nous raccrochons au DataContext de la vue Card

L'image de la carte est liée à un contrôle de type Image de base, auquel j'y est accroché en tant que Binding la propriété pictureInfo de ma carte. Comme le contrôle Image ne sais pas lire pictureInfo, je lui ai crée un Converter, qui ce chargera de faire la liaison pour moi. J'aurais pu utiliser mon contrôle UrzaImage, mais il me semblait bon de vous montrer ce à quoi un Converter pouvait servir.

public class PictureInfoConverter : IValueConverter

    {

       public object Convert(object value, Type targetType, object parameter, string language)

        {

           URZAImageInfo imageInfo = (URZAImageInfo)value;

           return new BitmapImage(new Uri(imageInfo.LocalPath));

        }

       public object ConvertBack(object value, Type targetType, object parameter, string language)

        {

           throw new NotImplementedException();

        }

    }

La second image, comme pour les autres images est téléchargée et sauvegardée à la volée.

Pour les contrôles de type TexBlock, c'est le DataBinding qui fait la liaison également.

<TextBlock Grid.Column="1" Grid.Row="6" Text="{Binding author}" Style="{StaticResource CardTextBlock}" ></TextBlock>

 

A suivre :

Le prochain article introduira les points suivants :

  • Localisation
  • Vues Snapped
  • Plus de réglages
  • Adaptation aux différentes résolutions