Plan du site  
français  English
pixel
pixel

Articles - Étudiants SUPINFO

Introduction

Dans ce chapitre seront dévoilées les principales fonctionnalités de Xamarin.Forms, ainsi que certaines différences entre la librairie et le développement natif d'applications universelles présentées depuis le premier chapitre de ce cours.

Xamarin.Forms est une librairie permettant d'abstraire l'ensemble des principes fondamentaux des différents systèmes d'exploitation pour fournir une expérience cohérente de développement quel que soit l'équipement ciblé. A ce titre, cette librairie possède des différences notables dans les noms de contrôles utilisés ou dans les fonctionnalités proposées. En effet, elle doit être le dénominateur commun entre toutes les plateformes, et de fait, ne permet pas d'utiliser l'ensemble des fonctionnalités fournies par iOS, Android et UWP, mais leurs fonctionnalités communes. Les fonctionnalités spécifiques feront l'objet de développements non-partagés, au sein des projets relatifs à chaque plateforme.

Contrôles Page

Xamarin.Forms propose de nombreuses possibilités de page, permettant de proposer différentes expériences de navigation. Pour définir ce qu'est exactement une instance de Xamarin.Forms.Page, la documentation officielle en présente une définition aussi claire que concise.

La classe Page est un élément visuel occupant la majorité ou l'entièreté de l'écran et contient une unique balise enfant. Une page représente un élément ViewController en iOS ou une Page en UWP. En Android, bien qu'une page s'étende sur tout l'écran, elle ne peut pas être considérée comme une activité. - Source

Au travers de ce chapitre, l'accent sera placé sur l'organisation et l'imbrication des différentes pages, sans s'attarder sur les contrôles utilisés en leur sein. Ceux-ci seront détaillés dans le chapitre suivant. Cependant, on retrouvera pour la majorité des exemples le fonctionnement des applications universelles détaillées jusqu'ici.

ContentPage

La page la plus simple sans aucune fonctionnalité particulière, c'est le modèle à utiliser pour partir d'une feuille blanche.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       x:Class="PagesTypes_PCL.MasterDetail.Detail"
       Title="Présentation ContentPage" Padding="10">
  <StackLayout>
    <Label Text="Contenu présenté" />
    <Label Text="au sein d'une page" />
    <Label Text="de type ContentPage" />
  </StackLayout>
</ContentPage>

Figure 1.1. Rendu de l'exemple précédent

Rendu de l'exemple précédent

MasterDetailPage

Représente une page avec un panneau de navigation et une zone de contenu. On peut voir ce modèle de page comme reprenant la structure du contrôle UWP SplitView et offrant une navigation au sein de plusieurs pages.

<!-- MasterDetail.xaml -->
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       x:Class="PagesTypes_PCL.MasterDetail.MasterDetail"
       xmlns:local="clr-namespace:PagesTypes_PCL.MasterDetail">
  <MasterDetailPage.Master>
    <local:Master />
  </MasterDetailPage.Master>
  <MasterDetailPage.Detail>
    <NavigationPage>
      <x:Arguments>
        <!-- Par défaut, la première page affichée sera Detail.xaml -->
        <local:Detail />
      </x:Arguments>
    </NavigationPage>
  </MasterDetailPage.Detail>
</MasterDetailPage>
<!-- Master.xaml -->
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       x:Class="PagesTypes_PCL.MasterDetail.Master"
       Title="Hello Master">
  <ListView x:Name="MasterListView" ItemSelected="MasterListView_ItemSelected">
    <ListView.ItemTemplate>
      <DataTemplate>
        <!-- 
          Il est également possible d'utiliser la balise ImageCell.
          <ImageCell Text="{Binding Label}" ImageSource="{Binding Icon}" />
          Cependant, cette balise n'est pas suffisante pour offrir un rendu
          hautement personnalisable.
        -->
        <ViewCell>
          <StackLayout Margin="10,5,10,5">
            <Grid>
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="30" />
                <ColumnDefinition Width="*" />
              </Grid.ColumnDefinitions>
              <Grid.RowDefinitions>
                <RowDefinition Height="30" />
              </Grid.RowDefinitions>
              <Image Source="{Binding Icon}" Aspect="AspectFit" 
                     Grid.Row="0" Grid.Column="0" Margin="5" />
              <Label Text="{Binding Label}" Grid.Row="0" 
                     Grid.Column="1" VerticalTextAlignment="Center"/>
            </Grid>
          </StackLayout>
        </ViewCell>
      </DataTemplate>
    </ListView.ItemTemplate>
  </ListView>
</ContentPage>
// Fichier Master.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class Master : ContentPage
{
  internal class Link
  {
    public Type Target { get; set; }
    public string Label { get; set; }
    public string Icon { get; set; }
  }
  public Master()
  {
    InitializeComponent();

    MasterListView.ItemsSource = new List<Link>()
    {
      new Master.Link() {
        // Lien vers la page Detail.xaml
        Target = typeof(Detail),
        Label = "Démarrage",
        Icon = "Assets/home.png"
      },
      new Master.Link() {
        // Lien vers la page Detail2.xaml
        Target = typeof(Detail2),
        Label = "Présentation",
        Icon = "Assets/video.png"
      },
      new Master.Link() {
        // Lien vers la page Detail3.xaml
        Target = typeof(Detail3),
        Label = "Informations",
        Icon = "Assets/people.png"
      }
    };
  }

  private void MasterListView_ItemSelected(object sender, SelectedItemChangedEventArgs e)
  {
    // La page courante, dans ce contexte, est le fichier MasterDetail.xaml,
    // étant de type MasterDetailPage.
    MasterDetailPage mdp = App.Current.MainPage as MasterDetailPage;
    mdp.IsPresented = false;
    Link selected = e.SelectedItem as Link;
    if (selected != null && selected.Target != null)
    {
      // Changement de la balise Detail, donc de la page visitée.
      mdp.Detail = new NavigationPage((Page)Activator.CreateInstance(selected.Target));
    }
  }
}

Figure 1.2. Rendu de l'exemple précédent

Rendu de l'exemple précédent

[Note]

Au sein de l'application UWP, les images devront être placées dans le dossier Assets comme indiquent les liens. Dans une application Android, le dossier "Assets" n'est pas pris en compte et les images devront être positionnées dans le dossier Resources/drawable.

Notons également que dans le cas des applicatons Android, l'option de Build (présentes dans les propriétés des fichiers images) doit être définie à AndroidResource pour être intégrée à l'application pendant le déploiement.

TabbedPage

Port à l'identique du contrôle Pivot, il permet d'afficher une page contenant plusieurs onglets.

<!-- MainTabbed.xaml -->
<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       x:Class="PagesTypes_PCL.Tabbed.MainTabbedPage">
  <ContentPage Title="Page1">
    <StackLayout>
      <Label Text="Présentation de la page 1" />
    </StackLayout>
  </ContentPage>
  <ContentPage Title="Page2">
    <StackLayout>
      <Label Text="Présentation de la page 2" />
    </StackLayout>
  </ContentPage>
</TabbedPage>

Figure 1.3. Rendu de l'exemple précédent

Rendu de l'exemple précédent

[Note]

Il est également possible de lier dans un contrôle TabbedPage des références à d'autres pages en séparant les fichiers comme ceci :

<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
      xmlns:local="clr-namespace:PagesTypes_PCL.Tabbed"
       x:Class="PagesTypes_PCL.Tabbed.MainTabbedPage">
  <!-- Fichiers Page1.xaml et Page2.xaml liés au contrôle TabbedPage -->
  <local:Page1 />
  <local:Page2 />
</TabbedPage>

CarouselPage

Port à l'identique du contrôle Hub, il permet d'afficher une page contenant plusieurs sections. On retrouvera d'ailleurs la même ressemblance entre Pivot et Hub qu'entre TabbedPage et CarouselPage. La navigation entre les pages se fait via un geste de défilement latéral sur l'écran.

<?xml version="1.0" encoding="utf-8" ?>
<CarouselPage xmlns="http://xamarin.com/schemas/2014/forms"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       x:Class="PagesTypes_PCL.Carousel.MainCarouselPage">
  <ContentPage Title="Page1">
    <StackLayout Margin="10">
      <Label Text="Page1 du contrôle CarouselPage" />
    </StackLayout>
  </ContentPage>
  <ContentPage Title="Page2">
    <StackLayout Margin="10">
      <Label Text="Page2 du contrôle CarouselPage" />
    </StackLayout>
  </ContentPage>
</CarouselPage>
[Note]

Tout comme les contrôles TabbedPage, on peut séparer les éléments ContentPage en plusieurs fichiers distincts.

Figure 1.4. Rendu de l'exemple précédent

Rendu de l'exemple précédent

Contrôles de mise en page

Au sein des pages, on retrouvera cinq systèmes principaux de mise en page. Comme pour les applications des différentes plateformes, leur intérêt réside dans leur combinaison pour obtenir l'effet visuel escompté.

Grid

Tout comme les applications UWP, Grid permet de construire une mise en page par lignes et colonnes ; la syntaxe est à ce sujet exactement la même.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:local="clr-namespace:PagesTypes_PCL"
       x:Class="PagesTypes_PCL.MainPage">
  <Grid Margin="20">
    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
      <RowDefinition Height="auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="3*" />
    </Grid.ColumnDefinitions>
    <Label Text="Volet gauche" Grid.Column="0" Grid.Row="0" />
    <Label Text="Volet droite" Grid.Column="1" Grid.Row="0" />
    <Label Text="Volet fixé en bas" Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2" />
  </Grid>
</ContentPage>
[Note]

Si le contrôle Grid n'a qu'une seule ligne ou qu'une seule colonne, il est cependant conseillé de la représenter explicitement via les éléments Grid.RowDefinitions et Grid.ColumnDefinitions.

Figure 1.5. Rendu de l'exemple précédent

Rendu de l'exemple précédent

StackLayout

Le contrôle StackLayout est un port identique du contrôle StackPanel des applications UWP. Il dispose de l'attribut Orientation qu'il est possible de placer aux valeurs Horizontal ou Vertical. Tout comme StackPanel, l'orientation par défaut est verticale.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:local="clr-namespace:PagesTypes_PCL"
       x:Class="PagesTypes_PCL.MainPage">
  <StackLayout Margin="10">
    <StackLayout>
      <Label Text="Contenus" />
      <Label Text="Positionnés" />
      <Label Text="Verticalement" />
    </StackLayout>
    <StackLayout Orientation="Horizontal">
      <Label Text="Contenus" />
      <Label Text="Positionnés" />
      <Label Text="Horizontalement" />
    </StackLayout>
  </StackLayout>
</ContentPage>

Figure 1.6. Rendu de l'exemple précédent

Rendu de l'exemple précédent

RelativeLayout

Ce contrôle permet le positionnement des éléments en son sein relativement aux éléments contigus ou au contrôle RelativeLayout lui-même. Son fonctionnement est cependant radicalement différent du contrôle RelativePanel en UWP, car il ne permet qu'un positionnement suivant les deux axes (X et Y) et non une modulation de la largeur ou de la hauteur.

Pour définir la marge entre le bord gauche du contrôle RelativeLayout et celui du contrôle ciblé, il faudra utiliser l'attribut RelativeLayout.XConstraint. De même, pour définir la marge entre le bord haut du contrôle RelativeLayout et celui du contrôle ciblé, on utilisera RelativeLayout.YConstraint.

Les contraintes possèdent la même syntaxe que les Bindings, avec cinq attributs :

  • Type : Peut être relatif au contrôle RelativeLayout avec la valeur RelativeToParent, ou relatif à un autre élément avec RelativeToView. Dans le cas d'une constante, on utilisera le type Constant.

  • ElementName : Dans le cas d'une relation avec un autre élément, ElementName permet de définir à quel élément celui-ci est relatif.

  • Property : Si relatif à RelativeLayout ou à un autre élément, on définira ici la propriété à prendre en compte (Width pour la largeur, Height pour la hauteur).

  • Factor : Une opération peut être effectuée, pour obtenir par exemple une marge égale à deux fois la taille d'un autre élément.

  • Constant : Une opération peut être effectuée, pour obtenir par exemple une marge égale à la taille d'un autre élément plus une valeur fixe.

Pour illustrer l'utilisation de ces constantes, voici quelques exemples :

<RelativeLayout>
  <!-- On crée un rectangle à fond rouge de largeur 20 et de hauteur 40,
       ayant pour marge horizontale une valeur de 10. -->
  <BoxView Color="Red" WidthRequest="20" HeightRequest="40" x:Name="RedBox"
      RelativeLayout.XConstraint ="{ConstraintExpression Type=Constant, 
                                                         Constant=10}" />
  <!-- On crée un rectangle à fond vert de largeur 50 et de hauteur 10,
       ayant pour marge horizontale un dixième de la largeur de RelativeLayout -->
  <BoxView Color="Green" WidthRequest="50" HeightRequest="10" 
      RelativeLayout.XConstraint ="{ConstraintExpression Type=RelativeToParent,
                                                         Property=Width, Factor=0.1}" />
  <!-- On crée un rectangle à fond bleu de largeur et de hauteur 100,
       centré au milieu du contrôle RelativeLayout -->
  <BoxView Color="Blue" WidthRequest="100" HeightRequest="100" 
      RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToParent, 
                                                        Property=Width, Constant=-50, 
                                                        Factor=0.5}"
      RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToParent, 
                                                        Property=Height, Constant=-50, 
                                                        Factor=0.5}" />
  <!-- On crée un rectangle à fond violet de largeur 100 et de hauteur 20,
       placé sous le contrôle portant le nom RedBox, plus une marge de 10 -->
  <BoxView Color="Purple" WidthRequest="100" HeightRequest="20" 
      RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToView, 
                                                        ElementName=RedBox, 
                                                        Property=Height, Constant=10}" />
</RelativeLayout>

Pour définir une largeur et une hauteur, on peut utiliser comme vu dans l'exemple précédent des tailles fixes, mais également se reposer sur les bindings pour reporter sur nos éléments les valeurs d'autres contrôles de la page ou du contrôle RelativeLayout.

<RelativeLayout x:Name="RelativeLayout">
  <!-- On définit un rectangle rouge appelé RedBox de largeur et de hauteur 200 -->
  <BoxView Color="Red" x:Name="RedBox" WidthRequest="200" HeightRequest="200" />
  <!-- On définit un rectangle vert de hauteur 100 et de largeur 50,
       ayant pour marge horizontale la largeur de l'élément RedBox
  <BoxView Color="Green" x:Name="Green" HeightRequest="100" WidthRequest="50"
      RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToView,
                                                        ElementName=RedBox,
                                                        Property=Width, Factor=1}" />
  <!-- On définit un rectangle jaune de hauteur 50 et de même largeur que RelativeLayout,
       ayant pour marge verticale la hauteur du contrôle RedBox -->
  <BoxView Color="Yellow" HeightRequest="50"
      WidthRequest="{Binding Source={x:Reference RelativeLayout}, Path=Width}" 
      RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToView,
                                                        ElementName=RedBox,
                                                        Property=Height, Factor=1}"/>
</RelativeLayout>
[Note]

De par son fonctionnement, RelativeLayout tout comme AbsoluteLayout (présenté ci-après) permet de superposer des éléments.

Figure 1.7. Rendu de l'exemple précédent

Rendu de l'exemple précédent

AbsoluteLayout

AbsoluteLayout permet de définir la position des éléments en son sein par rapport à ses bordures. Contrairement à RelativeLayout, ce contrôle dispose d'une gestion des positionnements (axes X et Y) et des tailles (largeur et hauteur). Ces valeurs peuvent être définies de manière absolue (en pixels effectifs) ou de manière proportionnelle par rapport à la taille du contrôle AbsoluteLayout.

Deux éléments sont à prendre en compte concernant la disposition des éléments : la stratégie de calcul et les bordures.

La stratégie de calcul se choisit avec l'attribut AbsoluteLayout.LayoutFlags et choisit entre un calcul absolu ou proportionnel. Cet attribut peut posséder les valeurs suivantes :

  • None : Option par défaut, toutes les valeurs sont calculées de manière absolue.

  • All : Interprète toutes les valeurs comme proportionnelles

  • WidthProportional : Interprète seulement la largeur comme proportionnelle, le reste étant déterminé de manière absolue

  • HeightProportional : Seule la hauteur est définie proportionnellement, le reste est déterminé de manière absolue

  • SizeProportional : Largeur et hauteur sont définies proportionnellement, les coordonnées sont spécifiées de manière absolue

  • XProportional : La position selon l'axe X est déterminé proportionnellement, le reste étant déterminé de manière absolue

  • YProportional : Seule la position selon l'axe Y est définie proportionnellement, le reste est déterminé de manière absolue

  • PositionProportional : Les positions selon les axes X et Y sont définies proportionnellement, la taille sera spécifiée de manière absolue

Une fois la stratégie de calcul définie, il suffira de spécifier les valeurs par le biais de l'attribut AbsoluteLayout.LayoutBounds. Celui-ci se construit par quatre valeurs séparées par des virgules dans l'ordre suivant : x, y, largeur, hauteur.

Voici quelques exemples d'utilisation d'AbsoluteLayout :

<AbsoluteLayout>
  <!-- Axe X : 90. Axe Y : 60. Largeur : 300. Hauteur : 20. -->
  <Label Text="Je suis placé de manière absolue"
      AbsoluteLayout.LayoutBounds="90,60,300,20" />
  <!-- Axe X : 50%. Axe Y : 50%. Largeur : 25%. Hauteur : 25% -->
  <BoxView Color="Olive"  AbsoluteLayout.LayoutBounds=".5,.5, .25, .25"
      AbsoluteLayout.LayoutFlags="All" />
  <!-- Axe X : 50%. Axe Y : 100%. Largeur : 100%. Hauteur : 10%.
       On notera l'attribut VerticalTextAlignment plaçant le texte au
       plus bas dans la hauteur du contrôle Label. -->
  <Label Text="Je suis placé en bas de l'affichage sur toute sa longueur."
      AbsoluteLayout.LayoutBounds=".5,1,1,.1" AbsoluteLayout.LayoutFlags="All"
      VerticalTextAlignment="End"/>
  <!-- Axe X : 50%. Axe Y : 0%. Largeur : 100. Hauteur : 25. -->
  <BoxView Color="Blue" AbsoluteLayout.LayoutBounds=".5,0,100,25"
      AbsoluteLayout.LayoutFlags="PositionProportional" />
</AbsoluteLayout>

Figure 1.8. Rendu de l'exemple précédent

Rendu de l'exemple précédent

[Note]

On notera la différence d'affichage entre Android et Windows 10 (Mobile). La barre de navigation est rétractable avec Windows 10 et s'affiche en sur-impression de notre application. Elle est cependant facile à cacher pour un utilisateur.

Il est possible de présenter à l'émulateur des boutons matériels et ne survolant pas l'affichage via l'onglet "Capteurs", accessible via l'icône ">>" dans la barre d'outils à droite de l'émulateur. De plus amples informations sont disponibles sur cette page (Anglais).

ScrollView

Tout comme pour les applications UWP, le contrôle ScrollView permet de contraindre les éléments en son sein à un affichage d'une taille donnée, en offrant à l'utilisateur la possibilité de faire défiler celui-ci.

<Grid Margin="10">
  <Grid.RowDefinitions>
    <RowDefinition Height="auto"/>
    <RowDefinition Height="*"/>
    <RowDefinition Height="auto"/>
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="*" />
  </Grid.ColumnDefinitions>
  <Label Grid.Row="0" Text="Affichage d'un élément au dessus de ScrollView" />
  <ScrollView Grid.Row="1">
    <!-- Un contrôle ScrollView ne doit contenir qu'un seul enfant -->
    <StackLayout>
      <!-- Gestion d'un texte de plusieurs lignes pour un contrôle Label -->
      <Label>
        <Label.Text>
          Lorem ipsum dolor sit amet[...]
          
          [...]pulvinar in, elementum sed erat.
        </Label.Text>
      </Label>
      <Label>
        <Label.Text>
          Lorem ipsum dolor sit amet[...]
          
          [...]pulvinar in, elementum sed erat.
        </Label.Text>
      </Label>
    </StackLayout>
  </ScrollView>
  <Label Grid.Row="2" Text="Affichage d'un élément en dessous de ScrollView" />
</Grid>

Figure 1.9. Rendu de l'exemple précédent

Rendu de l'exemple précédent

[Note]

Le contrôle ScrollView peut également piloter le défilement via le code-behind. Pour cela, il suffira d'utiliser la méthode ScrollToAsync(x, y, animations) de cette manière :

ElementScrollView.ScrollToAsync(0, 100, true); // Défile de 100 unités sur l'axe Y.

Différences notables entre UWP et Xamarin

Cette section recensera quelques différences d'utilisation entre les contrôles UWP et les contrôles Xamarin. Si les premiers ont été à n'en pas douter une source d'inspiration pour les seconds, il aura fallu s'adapter aux différents systèmes d'exploitation, équipements et systèmes de rendu. De fait, des différences se ressentent pour le développeur et nous en dressons une liste ici qui, si elle ne prétend à aucune exhaustivité, permettra d'illustrer ces divergences.

Balises équivalentes sous différents noms

Comme vu précédemment, certains contrôles Xamarin fonctionnent comme les balises UWP mais disposent de noms légèrement différents dans un souci de généricité. Voici une liste plus complète permettant de relier UWP à Xamarin facilement.

Dénomination UWP Dénomination Xamarin
Rectangle / Border BoxView
ToggleSwitch Switch
TextBox Entry (une ligne) / Editor (plusieurs lignes)
StackPanel StackLayout
HubSection CarouselPage
Pivot TabbedPage
Page ContentPage

Utilisation du contrôle ListView

Le contrôle ListView avec Xamarin nécessite comme élément unique de son modèle de données (DataTemplate) un contrôle Cell ou dérivés (ViewCell, ImageCell). Une autre différence notable est l'absence de la balise ListView.ItemContainerStyle permettant de définir des éléments visuels particuliers pour le contrôle ListViewItem que la liste génère. A l'inverse, deux propriétés supplémentaires font leur apparition dans Xamarin, SeparatorColor et SeparatorVisibility permettant de définir respectivement la couleur que prendra le séparateur entre chaque élément de la liste, et de déterminer si ce séparateur apparaît ou non.

[Warning]

Xamarin pour des raisons d'optimisation considèrera que tous les éléments d'un contrôle ListView possède la même hauteur. Si ce n'est pas le cas, il faudra impérativement spécifier l'attribut HasUnevenRows à la valeur true.

Autre différence cette fois côté C#, il n'est pas possible d'obtenir via ListView la propriété TemplatedItems représentant les cellules de la liste après leur génération. Il faudra pour cela créer son propre modèle de liste étendant ListView pour en offrir l'accès.

<ListView x:Name="ExempleListView" ItemSelected="ListView_ItemSelected"
          SeparatorColor="Red" SeparatorVisibility="Default">
  <ListView.ItemTemplate>
    <DataTemplate>
      <ViewCell>
        <Label Text="{Binding}" />
      </ViewCell>
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>
public MainPage()
{
  InitializeComponent();
  ExempleListView.ItemsSource = new List<string>()
  {
    "Orange", "Poire", "Pomme", "Mangue", "Cassis"
  };
}

private void ListView_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
  if(e.SelectedItem != null)
  {
    Debug.WriteLine(e.SelectedItem);
  }
}

Alignement horizontal et vertical

Déterminer la place d'un élément dans un espace donné se fait par les attributs VerticalOptions et HorizontalOptions. Ceux-ci acceptent huit valeurs basées sur quatre comportements décrits comme suit :

  • Start : L'élément sera placé au plus haut possible dans l'espace donné par son conteneur parent

  • Center : L'élément sera placé au centre

  • End : L'élément sera placé en bas

  • Fill : L'élément prendra à partir de sa position prévue le maximum de place possible jusqu'au prochain élément

Ces quatre comportements peuvent être aggrémentés d'une stratégie d'expansion s'il reste de la place. Dans ce cas, la place restante sera divisée entre tous les éléments disposant de cette stratégie d'expansion, et l'élément sera placé à l'intérieur. Pour assigner cette stratégie, il faudra utiliser les quatre autres valeurs :

  • StartAndExpand

  • CenterAndExpand

  • EndAndExpand

  • FillAndExpand

Pour illustrer ces options, voici une page présentant un contrôle StackLayout avec à l'intérieur huit contrôles Button disposant chacun d'une stratégie différente.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:local="clr-namespace:PagesTypes_PCL"
       x:Class="PagesTypes_PCL.MainPage">
  <StackLayout Orientation="Vertical">
    <Button VerticalOptions="Start" Text="Aligné en haut" />
    <Button VerticalOptions="Center" Text="Aligné au centre" />
    <Button VerticalOptions="End" Text="Aligné en bas" />
    <Button VerticalOptions="Fill" Text="Remplissage" />
    <Button VerticalOptions="StartAndExpand" Text="S'étend aligné en haut" />
    <Button VerticalOptions="CenterAndExpand" Text="S'étend aligné au centre" />
    <Button VerticalOptions="EndAndExpand" Text="S'étend aligné en bas " />
    <Button VerticalOptions="FillAndExpand" Text="S'étend avec remplissage" />
  </StackLayout>
</ContentPage>

On retrouvera dans l'image ci-dessous ces huit boutons, les quatre derniers disposant de la stratégie d'expansion pour lesquels la place allouée est encadrée en rouge. On remarquera que la place restante dans le contrôle StackLayout est répartie équitablement entre les quatre derniers boutons.

Figure 1.10. Rendu de l'exemple précédent

Rendu de l'exemple précédent

[Note]

Cet exemple, bien qu'il mette en application une orientation verticale, fonctionne de la même manière pour une orientation horizontale.

L'alignement des textes au sein d'un contrôle Label se construit via les attributs HorizontalTextAlignment et VerticalTextAlignment. Si via Xamarin la valeur "Stretch" est absente, les autres possibilités sont conservées malgré un changement de nom.

<Label HorizontalTextAlignment="Start" VerticalTextAlignment="End" Text="Mon texte Xamarin" />
<TextBlock HorizontalAlignment="Left" VerticalAlignment="Bottom" Text="Mon texte UWP" />

Titre des pages

Les applications iOS et Android disposent d'un en-tête pour leur page, présentant le titre de celle-ci. Pour satisfaire cette contrainte, Xamarin propose un attribut Title sur les contrôles de type Page.

<ContentPage Title="Titre de ma page" ...>
  <!-- Contenu omis -->
</ContentPage>

Ce titre prend tout son sens dans les contrôles CarouselPage ou TabbedPage, définissant le nom des sections ou onglets disponibles.

Exercice : Affichage du site DevRant - Partie 1

Le site internet DevRant a pour but d'offrir une tribune aux développeurs de tous horizons sur un système simple : chacun peut poster un Rant (diatribe) sur le site et chacun peut commenter et appuyer un propos. Le flux est accessible à cette adresse : https://www.devrant.io/feed

Le but de cet exercice sera d'afficher ce contenu dans une application Xamarin. Une classe d'obtention des données sera fournie pour permettre une focalisation sur les spécificités de Xamarin. Toutes les opérations devront à ce titre être réalisées dans le projet partagé.

Exercice corrigé : Affichage du site DevRant - Partie 1

1.1.

Pour commencer, téléchargez le fichier RantManager.cs disponible sur notre plateforme de contenus pédagogiques. Cette classe contient la représentation des différents objets que nous serons amenés à utiliser pendant ce cours ainsi qu'une classe de récupération des données via l'API DevRant.

Créez une nouvelle solution d'application multiplateforme de modèle "Application vide" utilisant Xamarin.Forms et ayant pour stratégie un projet partagé.

Au sein du projet partagé, créez un dossier ayant pour nom "Models". Ajoutez à ce dossier un élément existant et incluez le fichier RantManager.cs téléchargé précédemment.

1.2.

Nous allons pour cette première partie créer la page principale de notre application affichant les diatribes des utilisateurs.

Pour ce faire, créez un nouveau dossier ViewModels à la racine du projet partagé et ajoutez une nouvelle classe en son sein appelée MainPageViewModel.

Dans ce ViewModel, ajoutez la propriété suivante :

  • Rants : De type ObservableCollection<Rant>, cette collection représentera les diatribes

Ajoutez une méthode FillRants qui aura pour but de gérer le remplissage de la collection précédemment ajoutée. Cette méthode ne retournera aucune valeur. Au sein de cette méthode, appelez tout d'abord la méthode Clear de la collection. Suite à cet appel, déclenchez la méthode GetLatestRants() de la classe RantManager pour récupérer les éléments. Si ce retour n'est pas null, ajoutez un par un les nouveaux éléments à la collection.

Appelez la méthode FillRants récemment créée au constructeur de MainPageViewModel.

public class MainPageViewModel
{
  private ObservableCollection<Rant> _rants = new ObservableCollection<Rant>();
  public ObservableCollection<Rant> Rants { 
      get { return _rants; } 
      set { _rants = value; } 
  }
  
  public MainPageViewModel()
  {
    FillRants();
  }
  
  private void FillRants()
  {
    Rants.Clear();
    var data = DevRantReader.GetInstance().GetLatestRants();
    if (data != null)
    {
      foreach (Rant r in data)
      {
        Rants.Add(r);
      }
    }
  }
}

1.3.

Dans la classe MainPage, ajoutez une propriété publique et statique ViewModel de type MainPageViewModel. Au constructeur de MainPage, affectez à cette propriété une nouvelle instance de MainPageViewModel. Définissez également comme contexte de la page ce ViewModel via l'instruction suivante :

this.BindingContext = ViewModel;
public partial class MainPage : ContentPage
{
  public static MainPageViewModel ViewModel { get; set; }

  public MainPage()
  {
    InitializeComponent();
    ViewModel = new MainPageViewModel();
    this.BindingContext = ViewModel;
  }
}

1.4.

Dans la balise ContentPage de MainPage, ajoutez comme titre "New Dev Rants", puis réalisez l'affichage suivant :

Figure 1.11. Structure de la page principale

Structure de la page principale

Il est à noter que l'élément StackLayout à l'intérieur de ViewCell possède une marge de 10 dans toutes les directions.

La structure de chaque élément (colonnes du contrôle Grid et Binding) est représenté par le schéma suivant :

Figure 1.12. Structure d'un élément de contrôle ListView

Structure d'un élément de contrôle ListView

Les deux contrôles Label représentant les scores (diatribe et auteur) devront posséder 10 de marge à gauche et à droite, mais 0 en haut et en bas.

[Note]

Pour afficher la valeur de score de l'utilisateur précédée du texte "Score:", utilisez la syntaxe suivante :

<Label Text="{Binding User.Score, StringFormat='Score: {0:F0}'}" />

L'image de la diatribe étant facultative, bindez la propriété HasAttachedImage à l'attribut IsVisible.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DevRantApp"
             x:Class="DevRantApp.MainPage"
             Title="New Dev Rants"
             x:Name="MainContentPage">
  <ListView HasUnevenRows="True" ItemsSource="{Binding Rants}">
    <ListView.ItemTemplate>
      <DataTemplate>
        <ViewCell>
          <StackLayout Margin="10">
            <Grid HorizontalOptions="Start" VerticalOptions="Start">
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="150" />
                <ColumnDefinition Width="auto" />
                <ColumnDefinition Width="*" />
              </Grid.ColumnDefinitions>
              <Grid.RowDefinitions>
                <RowDefinition Height="auto" />
              </Grid.RowDefinitions>
              <StackLayout Orientation="Vertical" Grid.Column="0">
                <Label Text="{Binding User.Name}" HorizontalTextAlignment="Center" />
                <Image Source="{Binding User.Avatar.Uri}" />
                <Label Margin="10, 0, 10, 0" HorizontalTextAlignment="Center" 
                       Text="{Binding User.Score, StringFormat='Score: {0:F0}'}" />
              </StackLayout>
              <Label Grid.Column="1" Margin="10,0,10,0" VerticalTextAlignment="Start" 
                     Text="{Binding Score}" />
              <StackLayout Grid.Column="2">
                <Image Source="{Binding Attached_Image.Url}" 
                       IsVisible="{Binding HasAttachedImage}" />
                <Label Text="{Binding Text}" />
              </StackLayout>
            </Grid>
          </StackLayout>
        </ViewCell>
      </DataTemplate>
    </ListView.ItemTemplate>
  </ListView>
</ContentPage>

Navigation et Commandes avec Xamarin

Si la plupart des concepts de la librairie Xamarin embrassent UWP, la navigation entre les pages et la gestion de certains événements divergent. Au travers de cette section, on s'attardera sur ces deux éléments de conception.

Navigation

La navigation au sein d'une application se représente sous la forme de pile. A chaque navigation, Xamarin empile les pages dans la mémoire. A l'appui sur le bouton "Retour", la dernière page est dépilée.

Figure 1.13. Gestion de la pile de navigation (Source: developer.xamarin.com)

Gestion de la pile de navigation (Source: developer.xamarin.com)
Gestion de la pile de navigation (Source: developer.xamarin.com)

L'accès à la navigation est possible grâce à la propriété Navigation d'un objet Page. C'est par ce biais qu'il sera possible d'accéder à l'historique de navigation au sein de l'application, ainsi que naviguer en ajoutant des pages dans cette pile.

var stack = this.Navigation.NavigationStack; // Accès à l'historique
Navigation.PushAsync(new Page2()); // Navigation vers la page 2
Navigation.PopAsync(); // Retour vers la page précédente

Dans le cas d'une navigation vers une autre page (PushAsync ou PopAsync), un deuxième paramètre optionel permet d'animer la navigation ou non. Par défaut, l'animation est jouée.

Pour passer des paramètres à la page suivante (PushAsync), il suffira de les spécifier en paramètre du constructeur.

class Page1 : ContentPage
{
  private void Button_Clicked(object sender, EventArgs e)
  {
    Navigation.PushAsync(new Page2("Paramètre"));
  }
}
class Page2 : ContentPage
{
  public Page2(string parametre)
  {
    Debug.WriteLine(parametre); // Contient "Paramètre"
  }
}
[Warning]

Pour permettre à la page principale de naviguer, il est nécessaire de le spécifier dans le fichier App.xaml.cs de la matière suivante :

public App ()
{
  InitializeComponent();
  MainPage = new NavigationPage(new MainPage());
}

Commandes

Si les commandes ne sont pas une spécificité de Xamarin à proprement parler car elles existent également dans la bibliothèque .NET, leur utilisation en est renforcée, et il sera parfois nécessaire de les utiliser. On peut citer comme exemple tout déclenchement d'événement nécessitant un paramètre non-accessible par le code-behind.

L'interface ICommand se représente comme suit :

public interface ICommand
{
  void Execute(object arg);
  bool CanExecute(object arg)
  event EventHandler CanExecuteChanged; 
}

Explications :

  • Execute : Définit la méthode à appeler au déclenchement de la commande.

  • CanExecute : Définit les conditions d'exécution de la méthode. Si cet attribut est faux, alors la commande ne sera pas appelée.

  • CanExecuteChanged : Déclencheur activé quand la valeur de CanExecute change pour notifier les autres parties du code.

On trouvera ci-dessous un exemple d'instance de classe Command :

public Command CommandeTest {
  get {
    return new Command(Execution, PeutExecuter);
  }
}

private void Execution(object data) 
{
  Debug.WriteLine("La commande s'est bien exécutée !");
}

private void PeutExecuter(object data) {
  if(data == null)
  {
    Debug.WriteLine("data est null, on n'exécute pas la commande");
    return false;
  }
  else
  {
    Debug.WriteLine("data n'est pas null, on exécute la commande");
    return true;
  }
}

Une commande s'implémente sur les contrôles Xamarin suivants :

  • Button

  • MenuItem

  • ToolbarItem

  • SearchBar

  • TextCell

  • ImageCell

  • ListView

Pour implémenter une commande sur un de ces contrôles, il faudra spécifier l'attribut Command correspondant au nom de la propriété de type Command, et l'attribut CommandParameter contenant le paramètre à passer à l'objet Command. Ce paramètre peut être un élément de Binding ou une constante.

<Button Command="{Binding CommandeTest}" CommandParameter="Mon paramètre" />

Exercice : Affichage du site DevRant - Partie 2

On s'attelera dans cet exercice à une meilleure navigation dans la liste affichée via la prise en charge d'une pagination, ainsi qu'à l'ajout de deux pages :

  • ProfilePage, affichant le profil de l'utilisateur ayant posté une diatribe

  • ViewRantPage, affichant la diatribe avec ses commentaires

Exercice corrigé : Affichage du site DevRant - Partie 2

1.1.

Implémentez l'interface INotifyPropertyChanged sur la classe MainPageViewModel, puis ajoutez les propriétés suivantes :

  • PageIterator : De type int et ayant la valeur par défaut de 1, cette propriété représentera la page que l'utilisateur visite actuellement

  • CanGoPrevious : De type bool, cette propriété retournera vrai quand PageIterator est supérieur à 1.

Dans le mutateur de PageIterator (set), si la nouvelle valeur est différente de l'ancienne, appliquez la nouvelle valeur, appelez la méthode FillRants() et notifiez le changement de valeur pour les propriétés PageIterator et CanGoPrevious.

Dans le corps de la méthode FillRants, passez en paramètre de GetLatestRants la propriété PageIterator.

public class MainPageViewModel : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
  private int _pageIterator = 1;
  public int PageIterator
  {
    get { return _pageIterator; }
    set
    {
      if (_pageIterator != value)
      {
        _pageIterator = value;
        FillRants();
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("PageIterator"));
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("CanGoPrevious"));
      }
    }
  }
  public bool CanGoPrevious { get { return PageIterator > 1; } }
  private void FillRants()
  {
    Rants.Clear();
    var data = DevRantReader.GetInstance().GetLatestRants(PageIterator);
    // Reste de la méthode omise
  }
}

1.2.

Dans la page MainPage, entourez le contrôle ListView d'une grille pour laquelle la première ligne aura la hauteur * et pour la seconde la hauteur auto. Définissez le contrôle ListView comme s'intégrant au sein de la première ligne.

Dans la seconde ligne, ajoutez un contrôle Grid ayant trois colonnes, toutes ayant pour largeur *.

  • Dans la première colonne, ajoutez un bouton "Previous". Ce bouton aura l'attribut IsVisible lié à la propriété CanGoPrevious déclarée précédemment.

  • Dans la seconde colonne, ajoutez un label ayant pour contenu le texte "Page: " suivi de la valeur de PageIterator.

  • Dans la troisième colonne, ajoutez un bouton "Next".

Les deux boutons devront respectivement décrémenter et incrémenter PageIterator. Par ce simple mécanisme, le changement d'affichage est désormais fonctionnel.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
      <RowDefinition Height="auto" />
    </Grid.RowDefinitions>
    <ListView Grid.Row="0" HasUnevenRows="True" ItemsSource="{Binding Rants}">
      <!-- Contenu de ListView omis -->
    </ListView>
    <Grid Grid.Row="1">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
      </Grid.RowDefinitions>
      <Button Grid.Column="0" Text="Previous" Clicked="Previous_Clicked" 
              IsVisible="{Binding CanGoPrevious}" />
      <Label Grid.Column="1" Text="{Binding PageIterator, StringFormat='Page : {0:F0}'}" 
             HorizontalTextAlignment="Center"/>
      <Button Grid.Column="2" Text="Next" Clicked="Next_Clicked"/>
    </Grid>
  </Grid>
</ContentPage>
class MainPage : ContentPage
{
  // Reste de la classe omise
  public void Next_Clicked(object sender, EventArgs e)
  {
    ViewModel.PageIterator++;
  }

  public void Previous_Clicked(object sender, EventArgs e)
  {
    ViewModel.PageIterator--;
  }
}

1.3.

Créez une nouvelle page via le modèle "Forms Blank Content Page Xaml" appelée "ProfilePage".

Ajoutez à la classe MainPageViewModel une propriété Page de type Page. A l'affectation du ViewModel dans le constructeur de MainPage, affectez à cette propriété Page la page courante.

Ajoutez à la classe MainPageViewModel une propriété de type Command appelée ProfileCommand. Cet objet ne prendra qu'un paramètre, la méthode DoProfileCommand. Le second paramètre n'étant pas précisé, cette commande pourra toujours s'exécuter.

Créez dans MainPageViewModel une méthode DoProfileCommand qui ajoutera à la pile de navigation la page ProfilePage. Considérez que son paramètre sera de type User et passez-le en paramètre du constructeur de ProfilePage.

Modifiez le constructeur de la page ProfilePage pour récupérer cet objet User, et affectez-le à la propriété BindingContext de cette même page.

Ajoutez sous le score de l'utilisateur un bouton ayant :

  • Pour contenu le texte "See profile"

  • Pour commande l'objet ProfileCommand accessible de cette manière :

    Command="{Binding Source={x:Reference MainContentPage}, 
                      Path=BindingContext.ProfileCommand}"
  • Pour paramètre de commande l'utilisateur courant

class MainPageViewModel : INotifyPropertyChanged
{
  // Reste de la classe omise
  public ContentPage Page { get; set; }
  
  public Command ProfileCommand
  {
    get
    {
      return new Command(DoProfileCommand);
    }
  }
  
  private void DoProfileCommand(object user)
  {
    Page.Navigation.PushAsync(new ProfilePage(user as User));
  }
}
// Classe MainPage
public MainPage()
{
  InitializeComponent();
  ViewModel = new MainPageViewModel()
  {
    Page = this
  };
  this.BindingContext = ViewModel;
}
<!-- Bouton permettant l'affichage du profil -->
<Button Text="See profile" Command="{Binding Source={x:Reference MainContentPage}, 
                                             Path=BindingContext.ProfileCommand}" 
        CommandParameter="{Binding User}"/>
// Classe ProfilePage
public partial class ProfilePage : ContentPage
{
  public ProfilePage(User user)
  {
    InitializeComponent();
    this.BindingContext = user;
    MainPage.ViewModel.ProfileCalled = false;
  }
}

1.4.

Modifiez le code XAML de la page ProfilePage pour reproduire l'affichage suivant en vous inspirant du code XAML de la page MainPage :

Figure 1.14. Rendu visuel attendu de la page ProfilePage

Rendu visuel attendu de la page ProfilePage

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       x:Class="DevRantApp.ProfilePage"
       Title="{Binding Name, StringFormat='Browse {0:F0} rants'}">
  <Grid Margin="10,0,10,0">
    <Grid.RowDefinitions>
      <RowDefinition Height="40" />
      <RowDefinition Height="auto" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="auto" />
    </Grid.ColumnDefinitions>
    <StackLayout Grid.Row="0" Grid.Column="0" Orientation="Horizontal">
      <Image Source="{Binding Avatar.Uri}" />
      <Label Text="{Binding Name}" VerticalTextAlignment="Center" />
    </StackLayout>
    <Label Text="{Binding Score}" HorizontalTextAlignment="End" 
              VerticalTextAlignment="Center" />
    <Grid Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0">
      <Grid.RowDefinitions>
        <RowDefinition Height="auto" />
        <RowDefinition Height="auto" />
        <RowDefinition Height="auto" />
      </Grid.RowDefinitions>
      <Label Grid.Row="0" Text="{Binding ExtraInfo.Profile.About}" />
      <Label Grid.Row="1" Text="{Binding ExtraInfo.Profile.Skills}" />
      <Grid Grid.Row="2">
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="*" />
          <ColumnDefinition Width="*" />
          <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Label Text="{Binding ExtraInfo.Profile.Location, StringFormat='Location: {0:F0}'}"
               Grid.Column="0" />
        <Label Text="{Binding ExtraInfo.Profile.Website, StringFormat='Website: {0:F0}'}" 
               Grid.Column="1" />
        <Label Text="{Binding ExtraInfo.Profile.GitHub, StringFormat='GitHub: {0:F0}'}" 
               Grid.Column="2" />
      </Grid>
    </Grid>
    <ListView HasUnevenRows="True" Grid.Row="2" Grid.ColumnSpan="2" Grid.Column="0" 
              ItemsSource="{Binding ExtraInfo.Rants}">
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <Grid Margin="10" HorizontalOptions="Start" VerticalOptions="Start">
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto" />
                <ColumnDefinition Width="*" />
              </Grid.ColumnDefinitions>
              <Grid.RowDefinitions>
                <RowDefinition Height="auto" />
              </Grid.RowDefinitions>
              <Label Grid.Column="0" Margin="10,0,10,0" VerticalTextAlignment="Center" 
                     Text="{Binding Score}" />
              <StackLayout Grid.Column="1">
                <Image Source="{Binding Attached_Image.Url}" 
                       IsVisible="{Binding HasAttachedImage}" />
                <Label Text="{Binding Text}" />
              </StackLayout>
            </Grid>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </Grid>
</ContentPage>

1.5.

Créez une nouvelle page via le modèle "Forms Blank Content Page Xaml" appelée ViewRantPage.

Ajoutez à la classe MainPageViewModel une propriété RantCommand de type Command. A l'exécution, cette commande devra exécuter la méthode DoRantCommand et nécessitera l'approbation de la méthode CanRantCommandExecute.

La méthode DoRantCommand devra ajouter à la pile de navigation la page ViewRantPage nouvellement créée. Cette page prendra en paramètre un objet de type Rant, tout comme ProfilePage. La méthode CanRantCommandExecute retournera pour le moment toujours true.

Ajoutez un événement au clic sur un élément du contrôle ListView. Dans cet événement, assurez-vous que la propriété SelectedItem de l'événement n'est pas null. Si elle est définie, ajoutez à la pile de navigation cette page ViewRantPage. Passez en paramètre du constructeur l'objet Rant contenu dans SelectedItem.

Modifiez le constructeur de la page ViewRantPage pour récupérer cet objet Rant. Affectez à la propriété BindingContext le retour de la méthode GetRant de la classe DevRantReader, cette méthode permettant de retourner des informations supplémentaires sur l'objet telles que les commentaires.

public class MainPageViewModel : INotifyPropertyChanged
{
  // Reste de la classe omise
  public Command RantCommand
  {
    get
    {
      return new Command(DoRantCommand, CanRantCommandExecute);
    }
  }
  
  private void DoRantCommand(object rant)
  {
    Page.Navigation.PushAsync(new ViewRantPage(rant as Rant));
  }
  private bool CanRantCommandExecute(object o)
  {
    return true;
  }
}
<!-- Page MainPage.xaml -->
<ListView HasUnevenRows="True" ItemsSource="{Binding Rants}" 
          ItemSelected="Rants_ItemSelected">
  <!-- ... -->
</ListView>
// Class MainPage
public void Rants_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
  if (e.SelectedItem == null) return;
  if(ViewModel.RantCommand.CanExecute(null))
  {
    Rant rant = (Rant)e.SelectedItem;
    ViewModel.RantCommand.Execute(rant);
  }
}
// Classe ViewRantPage
public partial class ViewRantPage : ContentPage
{
  public ViewRantPage (Rant rant)
  {
    InitializeComponent ();
    this.BindingContext = DevRantReader.GetInstance().GetRant(rant.Id);
  }
}

1.6.

Ajoutez une nouvelle propriété à la classe MainPageViewModel appelée ProfileCalled et de type bool. Cette propriété permettra d'éviter le double déclenchement d'événement de navigation. En effet, au clic sur un élément de MainPage une navigation a lieu. Or, le bouton d'affichage du profil est contenu dans ces éléments.

A la navigation vers le profil, définissez ProfileCalled à true. A la fin du constructeur de ProfilePage, définissez ProfileCalled à false. Dans le mutateur de ProfileCalled, déclenchez l'événement PropertyChanged.

Enfin, modifiez la méthode CanRantCommandExecute pour retourner true uniquement si ProfileCalled est à false.

public class MainPageViewModel : INotifyPropertyChanged
{
  // Reste de la classe omise
  private bool _profileCalled;
  public bool ProfileCalled
  {
    get { return _profileCalled; }
    set
    {
      if (_profileCalled != value)
      {
        _profileCalled = value;
        PropertyChanged?.Invoke(this, 
          new PropertyChangedEventArgs("ProfileCalled"));
      }
    }
  }
  
  // ...
  
  private void DoProfileCommand(object user)
  {
    this.ProfileCalled = true;
    Page.Navigation.PushAsync(new ProfilePage(user as User));
  }
  
  // ...
  
  private bool CanRantCommandExecute(object o)
  {
    return !ProfileCalled;
  }
}
// Classe ProfilePage
public ProfilePage(User user)
{
  InitializeComponent();
  this.BindingContext = user;
  MainPage.ViewModel.ProfileCalled = false;
}

1.7.

Modifiez le code XAML de la page ViewRantPage pour reproduire l'affichage suivant en vous inspirant du code XAML des pages ProfilePage et MainPage.

Figure 1.15. Rendu visuel attendu de la page ViewRantPage

Rendu visuel attendu de la page ViewRantPage

Implémentez le bouton See Profile en définissant une propriété ProfileCommand au sein de ViewRantPage.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       x:Class="DevRantPage.ViewRantPage"
       x:Name="MainContentPage"
       Title="View another Rant">
  <Grid Margin="10,0,10,0">
    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <ScrollView Grid.Row="0">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="100" />
          <ColumnDefinition Width="auto" />
          <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <StackLayout Orientation="Vertical">
          <Label Text="{Binding User.Name}" HorizontalTextAlignment="Center" />
          <Image Source="{Binding User.Avatar.Uri}" />
          <Label Margin="10, 0, 10, 0" HorizontalTextAlignment="Center" 
                 Text="{Binding User.Score, StringFormat='Score: {0:F0}'}" />
          <Button Command="{Binding Path=ProfileCommand, 
                                       Source={x:Reference MainContentPage}}" 
                  Text="See profile" CommandParameter="{Binding User}"/>
        </StackLayout>
        <Label Grid.Column="1" Grid.Row="0" Margin="10" VerticalTextAlignment="Center" 
               Text="{Binding Score}" />
        <StackLayout Grid.Column="2" Grid.Row="0">
          <Image Source="{Binding Attached_Image.Url}" />
          <Label Text="{Binding Text}" />
        </StackLayout>
      </Grid>
    </ScrollView>
    <StackLayout Grid.Row="1">
      <Label Text="Comments" />
      <ListView ItemsSource="{Binding Comments}" HasUnevenRows="True">
        <ListView.ItemTemplate>
          <DataTemplate>
            <ViewCell>
              <Grid Margin="10" HorizontalOptions="Start" VerticalOptions="Start">
                <Grid.ColumnDefinitions>
                  <ColumnDefinition Width="auto" />
                  <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                  <RowDefinition Height="auto" />
                </Grid.RowDefinitions>
                <Label Grid.Column="0" Margin="10,0,10,0" VerticalTextAlignment="Center" 
                       Text="{Binding Score}" />
                <StackLayout Grid.Column="1">
                  <Image Source="{Binding Attached_Image.Url}" />
                  <Label Text="{Binding Body}" />
                </StackLayout>
              </Grid>
            </ViewCell>
          </DataTemplate>
        </ListView.ItemTemplate>
      </ListView>
    </StackLayout>
  </Grid>
</ContentPage>
// Classe ViewRantPage
public partial class ViewRantPage : ContentPage
{
  public Command ProfileCommand
  {
    get
    {
      return new Command(user => {
        Navigation.PushAsync(new ProfilePage(user as User));
      });
    }
  }
  public ViewRantPage (Rant rant)
  {
    InitializeComponent ();
    this.BindingContext = DevRantReader.GetInstance().GetRant(rant.Id);
  }
}

Xamarin.iOS, Xamarin.Android

Pour clore ce chapitre sur Xamarin, on détaillera ci-après les possibilités offertes par Xamarin.iOS et Xamarin.Android. En effet, nous avons vu précédemment Xamarin.Forms, une librairie permettant de développer via du code partagé des applications sur tous les terminaux.

Il faut rappeler que cette possibilité est contrainte au dénominateur commun de chaque plateforme et de fait ne peut proposer une fonctionnalité spécifique à un système d'exploitation en particulier. Les équipes de Xamarin estiment d'ailleurs que pour une application professionnelle, le taux de code partagé se situe entre 50% et 70%. Pour le reste, on préférera ou devra utiliser des solutions spécifiques à chaque système.

Ces deux librairies suivent ce but. Toujours en offrant un développement C#, elles reprennent les concepts et les possibilités de chaque système. Il devient alors possible de créer pour iOS des instances d'UITableView ou d'UIButton, comme pour Android des instances de ListView ou de Button. Xamarin revendique pour ces deux librairies 100% de "code coverage", signifiant que toute opération réalisée en Swift ou Java peut l'être via Xamarin.

[Note]

Le but n'est pas ici de détailler le développement iOS ou Android. Pour plus d'informations sur ces sujets, n'hésitez pas à vous rendre sur notre cours 3APL dédié au développement iOS, ou notre cours 3AND dédié au développement Android.

Conséquemment, pour appréhender plus aisément les exemples suivants, une connaissance minimum du développement sur ces plateformes est nécessaire.

Pour illustrer ces deux librairies, nous utiliserons ici un exercice d'initiation aux interactions utilisateurs présent dans le cours 3AND, mettant en scène une application listant des citations. Elle dispose également d'un champ texte et d'un bouton pour ajouter une nouvelle citation à la liste.

La classe représentant les citations est constituée comme suit :

public class Quote {
  public string StrQuote { get; set; }
  public int Rating { get; set; }
  public DateTime CreationDate { get; set; }
  // Utilisée uniquement dans Xamarin.Android
  public override string ToString()
  {
    return StrQuote;
  }
}

Xamarin.iOS

Figure 1.16. Application GeekQuote réalisée avec Xamarin.iOS

Application GeekQuote réalisée avec Xamarin.iOS

Visual Studio 2015 pour Mac offre comme Xcode les fichiers Storyboard avec l'éditeur graphique permettant de positionner chaque élément avec des glisser-déposer. Cependant, l'ensemble de l'application est également réalisable de manière programmatique via le fichier ViewController.cs, comme le montre l'exemple suivant.

public partial class ViewController : UIViewController
{
  // Liste de citations
  private static ObservableCollection<Quote> _quotes = 
        new ObservableCollection<Quote>();
  public static ObservableCollection<Quote> Quotes { 
    get { return _quotes; } set { _quotes = value; } 
  }
  
  // Méthode simple d'ajout de citations dans la liste
  private void AddQuote(string strQuote) 
  {
    Quotes.Add(new Quote()
    {
      StrQuote = strQuote,
      Rating = 1,
      CreationDate = DateTime.Now
    });
  }
  
  // Méthode d'aide à la récupération des chaînes de 
  // caractères contenues dans le fichier localized.strings
  private string __(string key)
  {
    return NSBundle.MainBundle.LocalizedString(key, key);
  }
  
  // Construction de la vue
  public override void ViewDidLoad()
  {
    base.ViewDidLoad();
    
    // Initialisation des données
    CGRect ScreenBounds = UIScreen.MainScreen.Bounds;
    AddQuote(__("Quote1"));
    AddQuote(__("Quote2"));

    // Champ de saisie d'une nouvelle citation
    UITextField textField = new UITextField()
    {
      Placeholder = __("TypeQuote")
    };
    double textFieldWidth = ScreenBounds.Width * .75;
    textField.Frame = new CGRect(0, 0, textFieldWidth, 50);

    // Bouton d'ajout d'une nouvelle citation
    UIButton button = UIButton.FromType(UIButtonType.System);
    button.Frame = new CGRect(textFieldWidth, 0, ScreenBounds.Width - textFieldWidth, 50);
    button.SetTitle(__("AddQuote"), UIControlState.Normal);
    button.TouchUpInside += (sender, ea) =>
    {
      string title = __("Error");
      string subtitle = __("ErrorLabel");
      string text = textField.Text.Trim();
      if(text != string.Empty) 
      {
        AddQuote(text);
        title = __("Success");
        subtitle = text;
        textField.Text = string.Empty;
      }
      
      // Affichage d'une pop-up pour confirmer ou infirmer l'ajout
      var alert = UIAlertController.Create(title, subtitle, UIAlertControllerStyle.Alert);
      alert.AddAction(UIAlertAction.Create("Ok", UIAlertActionStyle.Cancel, null));
      PresentViewController(alert, animated: true);
    };

    // Création de la liste de citations
    UITableView table = new UITableView()
    {
      Frame = new CGRect(0, 50, ScreenBounds.Width, ScreenBounds.Height - 50)
    };
    table.Source = new QuoteTableSource();
    
    // A l'ajout d'une citation dans CollectionChanged,
    // on recharge les données de notre liste de citations
    Quotes.CollectionChanged += (sender, e) => table.ReloadData();
    
    // Ajout des trois éléments visuels à la page courante
    Add(textField);
    Add(button);
    Add(table);
  }

  protected ViewController(IntPtr handle) : base(handle) {}
  
  public override void DidReceiveMemoryWarning()
  {
    base.DidReceiveMemoryWarning();
  }
}
// Classe QuoteTableSource
// Celle-ci fournit les règles de génération du composant UITableView
public class QuoteTableSource : UITableViewSource
{
  string CellIdentifier = "QuoteTableCell";
  
  public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
  {
    UITableViewCell cell = tableView.DequeueReusableCell(CellIdentifier);
    Quote item = ViewController.Quotes[indexPath.Row];

    if (cell == null)
    { 
      cell = new UITableViewCell(UITableViewCellStyle.Default, CellIdentifier); 
    }

    cell.TextLabel.Text = item.StrQuote;
    return cell;
  }

  public override nint RowsInSection(UITableView tableview, nint section)
  {
    return ViewController.Quotes.Count;
  }
}
/* Fichier Base.lproj/Localizable.strings */
"AddQuote" = "Ajouter";
"TypeQuote" = "Entrez votre citation...";
"Quote1" = "Première citation";
"Quote2" = "Deuxième citation";
"Success" = "Citation ajoutée";
"Error" = "Erreur";
"ErrorLabel" = "La citation est vide !";

Xamarin.Android

Figure 1.17. Application GeekQuote réalisée avec Xamarin.Android

Application GeekQuote réalisée avec Xamarin.Android

Quel que soit le système, il est possible de réaliser une application Android. On aura d'ailleurs accès à un dossier "resources", stockant les images dans leurs dossiers respectifs, mais également les dossiers "layouts" pour la construction des pages, et le dossier "values" pour la gestion des différentes valeurs utilisées par l'application. La classe R (rebaptisée Resource.Designer.cs) et le manifeste de l'application font également partie d'un projet Xamarin.Android.

<!-- Resources/layouts/QuoteList.axml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:id="@+id/quotesList"
  android:orientation="vertical" >

  <LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:focusable="true"
    android:focusableInTouchMode="true">

  <EditText
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    android:layout_weight="80"
    android:id="@+id/quotefield"
    android:hint="@string/quotefield" />
  <Button
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    android:layout_weight="20"
    android:id="@+id/quotebutton"
    android:onClick="Add"
    android:hint="@string/quotebutton" />
  </LinearLayout>

  <ListView
    android:id="@+id/quotesview"
    android:layout_width="match_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" />
</LinearLayout>
// Classe QuoteActivity.cs
[Activity (Label = "Xamarin.GeekQuote", MainLauncher = true, Icon = "@drawable/icon")]
public class QuoteListActivity : Activity
{
  // Liste de citations
  private ObservableCollection<Quote> _quotes = new ObservableCollection<Quote>();
  public ObservableCollection<Quote> Quotes
  {
    get { return _quotes; } set { _quotes = value; }
  }
  
  // Méthode simple d'ajout de citations dans la liste
  private void AddQuote(string strQuote) {
    Quotes.Add(new Quote()
    {
      StrQuote = strQuote,
      Rating = 1,
      CreationDate = DateTime.Now
    });
  }

  protected override void OnCreate(Bundle bundle)
  {
    base.OnCreate(bundle);
    SetContentView(Resource.Layout.QuoteList);

    // Récupération du contrôle ListView
    ListView lv = FindViewById<ListView>(Resource.Id.quotesview);
    
    // Rafraîchissement de la propriété Adapter au changement de la collection
    Quotes.CollectionChanged += (sender, ea) => {
      lv.Adapter = new ArrayAdapter<Quote>(this, Android.Resource.Layout.SimpleListItem1, Quotes);
    };

    // Récupération des citations par défaut dans le fichier String.xml
    foreach (string s in this.Resources.GetStringArray(Resource.Array.quotes))
    {
      AddQuote(s);
    }
  }

  // Méthode appelée au clic sur le bouton
  [Export("Add")]
  public void Add_onClick(View v)
  {
    EditText editText = FindViewById<EditText>(Resource.Id.quotefield);
    if (editText == null) return;
    string text = editText.Text.Trim();
    if (text.Length > 0)
    {
      AddQuote(text);
      editText.Text = string.Empty;
      Toast.MakeText(this,Resources.GetString(Resource.String.success),ToastLength.Short).Show();
    }
    else
    {
      Toast.MakeText(this,Resources.GetString(Resource.String.error),ToastLength.Long).Show();
    }
  }
}
<!-- Resources/values/Strings.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="app_name">Xamarin.GeekQuote</string>
  <string name="quotefield">Ecrivez votre citation...</string>
  <string name="quotebutton">Ajouter</string>
  <string name="success">Citation ajoutée.</string>
  <string name="error">Champ vide !</string>
  <string-array name="quotes">
    <item>Une première citation</item>
    <item>Une seconde citation</item>
  </string-array>
</resources>
A propos de SUPINFO | Contacts & adresses | Enseigner à SUPINFO | Presse | Conditions d'utilisation & Copyright | Respect de la vie privée | Investir
Logo de la société Cisco, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société IBM, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sun-Oracle, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Apple, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sybase, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Novell, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Intel, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Accenture, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société SAP, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Prometric, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo du IT Academy Program par Microsoft, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management

SUPINFO International University
Ecole d'Informatique - IT School
École Supérieure d'Informatique de Paris, leader en France
La Grande Ecole de l'informatique, du numérique et du management
Fondée en 1965, reconnue par l'État. Titre Bac+5 certifié au niveau I.
SUPINFO International University is globally operated by EDUCINVEST Belgium - Avenue Louise, 534 - 1050 Brussels