Comment “cuisiner” une application Windows 8 avec XAML et C# en une semaine–Jour 2
Au menu aujourd’hui nous allons nous intéresser aux sujets suivants:
- Internationalisation
- Gestion de l’activation du mode offline
- Intégration d’une flipView pour la page des cartes
- Gestion des vues “snapped”
- Adaptation aux différentes résolutions
Néanmoins, puisque la Release Preview est disponible, j’ai porté l’intégralité de ce 2ieme jour sur cette version, ce qui m’a pris en
gros une bonne demi-journée. Les modifications étant importante dans les styles XAML, j’ai opté pour la stratégie, qui consistait à repartir d’un nouveau
projet en mode grille et de refaire toutes les vues XAML. Avec des corrections
mineures dans le Code C#.
A partir de maintenant, il vous faudra faire les exercices sur cette version ainsi que sur les outils associés que vous
trouverez ici.
Téléchargement de la release Preview et des outils.
Vous retrouverez le code source liée à cet article ici : https://aka.ms/oitxqi
Internalisation
Comme nous visons toutes les places de marchés supportées par le store Windows 8,
nous allons devoir internationaliser notre application. Pour ce faire, XAML
pour Windows 8 Metro fournit un service très simple.
Pour commencer, nous allons créer dans la solution, un répertoire Resources,
dans lequel nous allons créer deux sous répertoires "en" et "fr"
Comme illustré sur la figure suivante :
Remarque :
Windows gère non seulement la culture qui symbolise la langue parlée, mais
également la région. Par exemple, pour symboliser la langue Française parlée en
France, la chaine complète sera "fr-FR",pour le Québec, "fr-CA",pour la Belgique "fr-BE"
et ainsi de suite.
Si aucune ressource n'est disponible pour la langue Française région France
("fr-FR") le gestionnaire de ressources, ira chercher la culture de
plus haut niveau "fr". Dans notre cas ici juste la langue Française.
Si ce fichier de ressources n’est pas présent, il ira chercher le fichier de
ressources par défaut
Ensuite, il faut ajouter dans chaque repertoire (ie "en" et "fr") un
fichier de ressources
Laissez le nom Resources.resw, car par défaut c'est ce nom de fichier qui sera utilisé par le système de gestion de ressources de XAML.
Une ressource est définie par, le nom de la ressource, et par la valeur de la ressource, comme illustré :
Exemple pour le fichier de ressources Anglais
Name | Value
Number.Text | Number:
Power.Text | Power:
Pour celui en Français
Name | Value
Number.Text | Numéro :
Power.Text | Puissance :
Dans le nom de la ressource, j'indique également le nom de la propriété pour que le gestionnaire de ressources
sache sur qu'elle propriété du contrôle il faut associer la valeur de la ressource. Ici la propriété Text, mais comme vous le
remarquerez en examinant les fichiers de ressources associées aux code de cet article, on peut également indiquer une
propriété comme la propriété Width concernant la largeur d'un contrôle.
Pour la langue Anglaise
CboColor.Width 120
Pour la langue Française
CboColor.Width 196
En effet, il faut bien garder à l'esprit que la longueur de la chaine "All colors", est plus petite, que la chaine "Toutes les couleurs". Ces une des
manières de pouvoir adapter un contrôle à l'écran automatiquement.
Ensuite, pour faire la liaison entre la ressource et le contrôle XAML , il faut utiliser son
attribut x:Uid avec le nom de la ressource, mais sans utiliser le préfixe type propriété .Text.
Par exemple dans notre vue Card, nos contrôles de type TexBlock qui affichent le libellé des caractéristiques d'une carte.
<TextBlock Style="{StaticResource UrzaCardLabelTextBlock}" x:Uid="Number"/>
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding number}" Style="{StaticResource UrzaCardTextBlock}" ></TextBlock>
<TextBlock Grid.Row="1" Style="{StaticResource UrzaCardLabelTextBlock}" x:Uid="Type" />
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding type}" Style="{StaticResource UrzaCardTextBlock}" ></TextBlock>
<TextBlock Grid.Row="3" Style="{StaticResource UrzaCardLabelTextBlock}" x:Uid="Power" />
...
Ou dans la vue Expansions pour que le contrôle Combobox adapte sa longueur automatiquement.
<ComboBox Grid.Column="1" x:Name="cboFilterByColors" ItemsSource="{Binding Path=FilterByColors}" Margin="4,0,0,8" Width="120" x:Uid="CboColor"
Style="{StaticResource ComboBoxStyle}" Background="{x:Null}" Template="{StaticResource ComboBoxControlTemplateFilter}"/>
...
Remarque : Même si ici la propriété Width est indiquée, c'est bien la longueur définie dans le fichier de ressources qui sera pris en compte. Le gestionnaire
de ressources fera le reste, il n'y a rien à faire d'autre.
L'autre scénario, c'est d'aller rechercher directement une ressource dans le fichier de ressources
pour l'affecter à la propriété Text d'un TexBlock par exemple.
Pour ce faire, nous allons utiliser Windows.ApplicationModel.Resources.ResourceLoader
ResourceLoader possède la méthode GetString() qui prend comme paramètre le nom de la ressource.
Par exemple .
String ressource = _resourceLoader.GetString("ReceiveBytesMessage");
Dans ce scénario, le nom de la ressource, n'a pas besoin d'être préfixée par une propriété,
de plus le fichier de ressources, n'est pas non plus forcement obligé de s'appeler Ressource.resw Du coup j'ai construit un classe statique qui n'expose que
des propriétés statiques en lecture seule, dont le nom est constitué du nom de la ressource et qui charge le fichier de ressources approprié.
Exemple ici je charge le fichier Strings.Resw.
static class UrzaResources
{
static ResourceLoader _resourceLoader = new ResourceLoader("Strings");
public static String ReceiveBytesMessage
{
get { return _resourceLoader.GetString("ReceiveBytesMessage"); }
}
public static String CreatingLocalResourcesMessage
{
get { return _resourceLoader.GetString("CreatingLocalResourcesMessage"); }
}
public static String ByName {
get { return _resourceLoader.GetString("ByName"); }
}
public static String ByNumber
{
get { return _resourceLoader.GetString("ByNumber"); }
}
...
}
De cette manière, on peut utiliser la ressource plus facilement, en utilisant
directement la propriété statique.
async void _dataSource_DownloadFileInProgress(object sender, JSONFileDownloadInProgressEventArgs e)
{
if (DownloadFileProgress != null)
{
if (e.TotalBytesToReceived > e.BytesReceived)
{
String Message = String.Format(UrzaResources.ReceiveBytesMessage,
e.BytesReceived.ToString(),
e.TotalBytesToReceived.ToString());
await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, new DispatchedHandler(() =>
{
DownloadFileProgress(this, new JSONFileDownloadInfoEventArgs(Message));
}));
}
}
}
En dernier lieu, il est possible de forcer par code le langage qui sera utilisé
Windows.Globalization.ApplicationPreferences.PreferredLanguage = "fr";
Mais le mieux, c'est encore d'utiliser l'interface de Windows et de laisser faire le
gestionnaire de ressources en modifiant la position du langage comme illustré
sur la figure ci-dessous.
Gestion de l’activation du mode offline
Certains utilisateurs ne souhaiteront peut-être pas utiliser de l'espace sur leur disque pour sauvegarder les images en local. Nous allons donc rajouter une propriété
dans les settings pour gérer l'activation du mode offline.
Tout d'abord, nous allons modifier notre classe Settings en lui rajoutant une méthode IsOffLineModeOn()
qui test si le mode OffLine est activé.
static public Boolean IsOffLineModeOn()
{
Boolean offlineMode = false;
if (ApplicationData.Current.RoamingSettings.Values.ContainsKey(UrzaResources.OffLineMode))
{
offlineMode = (Boolean)ApplicationData.Current.RoamingSettings.Values[UrzaResources.OffLineMode];
}
return offlineMode;
}
La propriété OffLineMode permettra de faire le pont avec le controle SettingsControl.xaml
(Rappelez vous nous passons dans son constructeur une instance de la classe Settings)
Puis enfin j'ajoute une méthode qui sauvegarde les paramètres itinérants.
public void SaveSettings(Double zoomfactor, Boolean offlinemode)
{
ApplicationData.Current.RoamingSettings.Values[UrzaResources.ZoomFactor] = zoomfactor;
ApplicationData.Current.RoamingSettings.Values[UrzaResources.OffLineMode] = offlinemode;
}
Ensuite nous allons ajouter deux contrôles de type RadioBouton dans notre contrôle
personnalisé SettingsControl.xaml
<RadioButton x:Name="rdOn" Content="On" Grid.Column="1" HorizontalAlignment="Left" Margin="10,98,0,0" Grid.Row="2" VerticalAlignment="Top" Height="48" Width="74" RenderTransformOrigin="-0.261999994516373,-0.0599999986588955" Background="White" Foreground="Black" Style="{StaticResource UrzaRadioButtonStyle}" />
<RadioButton x:Name="rdOff" Content="Off" Grid.Column="1" HorizontalAlignment="Left" Margin="129,98,0,0" Grid.Row="2" VerticalAlignment="Top" Height="29" Width="74" RenderTransformOrigin="-0.261999994516373,-0.0599999986588955" Background="White" IsChecked="True" Foreground="Black" BorderThickness="0" BorderBrush="Black" Click="CheckOffLineMode" Style="{StaticResource UrzaRadioButtonStyle}"/>
Puis nous allons lié nos contrôles avec la propriété OffLineMode de notre instance de classe Settings sur leur
propriété IsChecked
rdOff.IsChecked = !_roamingSettings.OffLineMode;
rdOn.IsChecked = _roamingSettings.OffLineMode;
et enfin sur le déchargement du contrôle nous sauvegardons les paramètres d'itinérance
void SettingsControl_Unloaded(object sender, RoutedEventArgs e)
{
_roamingSettings.SaveSettings(this.slider.Value, (Boolean)rdOn.IsChecked);
}
L'appel à la méthode URZASettings.IsOffLineModeOn() ce
fait à chaque fois qu'une ressource de type image doit être chargée
Intégration d'une FlipView pour la vue Cards
La vue Cards, ne permet de visualiser qu'une seule carte à la fois, avec le contrôle FlipView
nous allons pouvoir naviguer dans les différentes cartes, sans être obligé de
revenir à la vue Expansions
La bonne nouvelle, c'est qu'il n'y presque rien à faire, si ce n'est de modifier
légèrement le XAML pour prendre en compte le FlipView, et le code C# pour
binder les données correctement.
1) Le XAML qui permettait d'afficher nos données, nous allons l'encapsuler dans un contrôle <UserControl>.
2) Ensuite nous encapsulons ce contrôle comme étant le DataTemplate pour les items du FlipView. FlipView que nous lions avec la
propriété cards de notre VueData .
<FlipView Grid.Row="1" ItemsSource="{Binding cards}" x:Name="flipViewCards">
<FlipView.ItemTemplate>
<DataTemplate>
<UserControl>
<Grid Grid.Row="1" Margin="116,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="34*"/>
<RowDefinition Height="34*"/>
<RowDefinition Height="34*"/>
<RowDefinition Height="34*"/>
<RowDefinition Height="34*"/>
<RowDefinition Height="34*"/>
<RowDefinition Height="34*"/>
<RowDefinition Height="279*"/>
<RowDefinition Height="144*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100*"/>
<ColumnDefinition Width="500*"/>
<ColumnDefinition Width="500*"/>
</Grid.ColumnDefinitions>
<TextBlock Style="{StaticResource UrzaCardLabelTextBlock}" x:Uid="Number"/>
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding number}" Style="{StaticResource UrzaCardTextBlock}" ></TextBlock>
<TextBlock Grid.Row="1" Style="{StaticResource UrzaCardLabelTextBlock}" x:Uid="Type" />
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding type}" Style="{StaticResource UrzaCardTextBlock}" ></TextBlock>
<TextBlock Grid.Row="2" Style="{StaticResource UrzaCardLabelTextBlock}" x:Uid="Color" />
<TextBlock Grid.Column="1" Grid.Row="2" Text="{Binding color}" Style="{StaticResource UrzaCardTextBlock}" ></TextBlock>
<TextBlock Grid.Row="3" Style="{StaticResource UrzaCardLabelTextBlock}" x:Uid="Power" />
<TextBlock Grid.Column="1" Grid.Row="3" Text="{Binding power}" Style="{StaticResource UrzaCardTextBlock}" ></TextBlock>
<TextBlock Grid.Row="4" Style="{StaticResource UrzaCardLabelTextBlock}" x:Uid="Text" />
<TextBlock Grid.Column="1" Grid.Row="4" Text="{Binding text}" Style="{StaticResource UrzaCardTextBlock}" ></TextBlock>
<TextBlock Grid.Row="5" Style="{StaticResource UrzaCardLabelTextBlock}" x:Uid="Flavor" />
<TextBlock Grid.Column="1" Grid.Row="5" Text="{Binding flavor}" Style="{StaticResource UrzaCardTextBlock}" ></TextBlock>
<TextBlock Grid.Row="6" Style="{StaticResource UrzaCardLabelTextBlock}" x:Uid="Author" />
<TextBlock Grid.Column="1" Grid.Row="6" Text="{Binding author}" Style="{StaticResource UrzaCardTextBlock}" ></TextBlock>
<Image Stretch="Uniform" Source="{Binding ImageInfo, Converter={StaticResource pictureInfoConverter}}" Grid.Column="2" Grid.Row="0" Grid.RowSpan="8" d:LayoutOverrides="Margin" >
<Image.Transitions>
<TransitionCollection>
<AddDeleteThemeTransition/>
</TransitionCollection>
</Image.Transitions>
</Image>
<UrzaControl:UrzaImage Stretch="None" Source="{Binding bannerInfo}" Grid.Row="7" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="57,20,0,0" />
</Grid>
</UserControl>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
3) La modification du code ce résume, à changer la liaison du DataContext d'un
seul élément, a l'ensemble des éléments de l'expansion, et à sélectionner dans
le FlipView, la carte courante.
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
VueData vueData = (VueData)navigationParameter;
this.DataContext = vueData.CurrentExpansion;
this.flipViewCards.SelectedItem = vueData.CurrentCard;
this.flipViewCardsSnapped.SelectedItem = vueData.CurrentCard;
}
C'est tout !!!
Gestion des vues "Snapped"
Windows 8 Metro impose le support de la vue “snapped”
pour chacun des écrans de l’application. Ce mode s’active quand votre application est mise sur le côté d’une application principale. Elle va alors
occuper une largeur de 320 pixels.
Comme sur la figure suivante :
Il faut donc décider dans le cadre du design de son application, ce que devra
contenir ce mode pour chacun des écrans.
Dans le cas de UrzaGatherer, j’ai pris le parti de fournir une expérience
quasi-complète en repensant mes écrans sans enlever de fonctionnalités.
Il faut voir le mode snapped, comme une autre manière d’afficher les données dans la vue.
En gros comme ça marche ?
Par défaut dans nos vues Home et Expansions, nous avons des GridViews pour afficher et naviguer
dans nos données en mode Horizontal. Comme le mode snapped est plutôt vertical, nous allons y ajouter une ListView et qui sera liée à nos données
de la même manière que la GridView, mais qui s’adaptera à une taille de 320
pixels. Visual Studio fournit d’ailleurs un outil bien pratique, qui permet de
visualiser les différents états de la vue, vous permettant ainsi de les mettre
au point.
Par défaut cette ListView n’est pas visible, mais lorsque la vue snapped
est activée, le gestionnaire VisualStateManager (que nous aborderons un peu plus loin) la passera dans un état visible, alors
que la GridView sera dans un état non visible. Rien de magique ici, on rend visible/invisible un contrôle en fonction de l’état de la vue.
Exemple dans notre vue Expansions, j’ai ajouté une ListView (nommée CardsListView) et qui est invisible par
défaut.
<GridView
x:Name="CardsItemsGridView"
SelectionMode="None"
Grid.Row="2"
Margin="0,-3,0,0"
Padding="116,0,40,46"
IsItemClickEnabled="True"
ItemClick="cardsItemsGridView_ItemClick"
ItemsSource="{Binding Source={StaticResource
groupedCardsItemsViewSource}}">
<GridView.ItemTemplate>
<DataTemplate>
<Button Style="{StaticResource
UrzaExpansionGridViewStyle}" Width="{Binding Source={StaticResource
urzaSettings},Path=Zoom.Width}"
Height="{Binding Source={StaticResource
urzaSettings},Path=Zoom.Height}"
></Button>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
<!-- Vertical scrolling
list only used when snapped -->
<ListView
IsItemClickEnabled="True"
ItemClick="cardsItemsGridView_ItemClick"
SelectionMode="None"
x:Name="CardsListView"
Grid.Row="2"
Visibility="Collapsed"
Margin="0,-10,0,0"
Padding="10,0,0,60"
ItemsSource="{Binding Source={StaticResource
groupedCardsItemsViewSource}}"
ItemTemplate="{StaticResource UrzaExpansionsListView}">
</ListView>
Comme indiqué plus haut, c’est le VisualStateManager qui fera la liaison entre chaque état de la vue. Mais pour cela, il faut lui
indiquer la manière de se comporter, lorsqu’on passe d’un état à un autre. On va donc définir en XAML des groupes VisualStateGroups,
d’états Visuelles VisualState qui vont permettre d’afficher ou pas des contrôles.
Dans l’exemple suivant, en mode FullScreenLandscape, et ou Filled, on laisse tel que par
contre si on passe en mode Snapped, on rend invisible avec une animation la GridView, et on rend Visible la
ListView.
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ApplicationViewStates">
<VisualState x:Name="FullScreenLandscape"/>
<VisualState x:Name="Filled"/>
<VisualState x:Name="Snapped">
<Storyboard>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="CardsListView"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame
KeyTime="0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="CardsItemsGridView"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame
KeyTime="0" Value="Collapsed"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
La dernière étape est d’indiquer au VisualStateManager dans quel état visual l’application veut
s’afficher, afin qu’il puisse faire la liaison avec ce que nous venons d’ajouter en XAML.
Pour récupérer l’état en cours, nous allons utiliser la classe Windows.UI.ViewManagement.ApplicationView qui nous fournit entre autre la propriété
Value, qui récupère l’état visuel en cours.
string visualState = DetermineVisualState(ApplicationView.Value);
à positionner sur l’évènement WindowSizeChanged de la vue.
ApplicationView.Value, retournera selon le cas une chaine "Snapped", "Fill",
"FullScreenLandscape", "FullScreenPortrait"
Puis nous allons forcer l’état. VisualStateManager.GoToState(layoutAwareControl,
visualState, false);
Néanmoins la bonne nouvelle, c’est que vous aurez remarqué que toutes nos vues héritent de
la classe LayoutAwarePage (grâce au modèle par défaut de Visual Studio) qui nous fournit déjà cette mécanique de
changement d’état visuel, il n’y a donc rien à coder, tout est déjà fait pour
nous ;-)
Plus d’infos
Guidelines for
snapped and fill views (Metro style apps)
Adaptation aux différentes résolutions
Enfin il faut que nos images et plus particulièrement celle de la vue Cards s'adaptent aux différentes résolutions.
Pour ce faire vous pouvez utiliser le contrôle ViewBox, pour ma part , dans la vue cards j'ai juste décidé d'utiliser un espacement proportionnel. C'est à dire que notre image prendra la place de 8 row en taille proportionnelle définie par le symbole "*" sur 1 colonne à taille proportionnelle
<Image Stretch="Uniform" Source="{Binding ImageInfo, Converter={StaticResource pictureInfoConverter}}" Grid.Column="2" Grid.Row="0" Grid.RowSpan="8" >
Image en 1920*1080
Image en 1366*768
Image en 1024*768
Image en 2560*1440
Plus d’infos
Guidelines for scaling to screens (Metro style apps)
https://msdn.microsoft.com/fr-fr/library/windows/apps/hh465424#ancrage_et_mise___l__chelle
Pour la suite de notre menu, nous verrons :
- Gestion du contrat Search
- Gestion du contrat Share
- Gestion du contrat FileOpenPicker
- Gestion des vignettes dynamiques et secondaires
Eric Vernié