Appliquer des styles CSS aux vues Angular

Cet article fait partie de la série d’articles Angular from Scratch.

@_stfeyes

Angular permet d’appliquer un style CSS sur les éléments des vues des composants. Ce style CSS peut être défini de façon globale pour toute l’application, au niveau d’un composant ou plus spécifiquement à un élément.

Le but de cet article est d’expliciter différentes méthodes pour appliquer un style CSS aux éléments d’un composant. Il est possible d’appliquer un style en utilisant des fichiers Sass, less ou stylus avec les extensions respectivement .scss, .less ou .styl toutefois ces formats de feuille de style ne seront pas traités dans cet article.

Dans un premier temps, on va indiquer comment appliquer un style ou une classe CSS aux éléments d’une application Angular de façon globale, puis à l’échelle d’un composant.

Pour comprendre les différents exemples, on va créer une application Angular en utilisant le CLI Angular:

ng new css-test --skip-tests --no-routing --style css

Cette ligne permet de créer une application nommée css-test:

  • en évitant la génération des fichiers de tests (avec --skip-tests),
  • en évitant d’installer un module de routing (avec --no-routing) et
  • en précisant l’extension des fichiers de style CSS (avec --style css)

On supprime tout ce qui se trouve dans le template du composant principal (cf. csstest/src/app/app.component.html).

Où définir les styles CSS ?

Appliquer un style globalement

Pour définir et appliquer des styles globalement à l’échelle de l’application, il faut indiquer le fichier CSS contenant ces styles dans les paramètres du projet Angular dans angular.json au niveau du paramètre styles. Ce paramètre permet d’indiquer le chemin des fichiers de style utilisables dans l’application:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "csstest": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/csstest",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "aot": true,
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.css"
            ],
            "scripts": []
          }
        }
      }
    }
  }
}

Par défaut le fichier src/styles.css existe et permet de définir des styles, toutefois on peut rajouter d’autres fichiers avec le paramètre styles dans angular.json.

Pour illustrer dans l’application d’exemple, on rajoute un composant nommé Parent en exécutant:

ng g c Parent

Avec l’implémentation suivante:

<h1>Composant parent</h1> 
<p>Contenu du composant parent</p> 

On modifie le composant principal:

<h1>Composant principal</h1> 
<div style="display: block; border: 1px solid black; padding: 10px; background-color: white;"> 
  <app-parent></app-parent> 
</div> 

<app-parent></app-parent> correspond au paramètre selector du composant parent. Pour plus de précisions sur ce paramètre, voir l’article Les vues des composants Angular.

On modifie le fichier css/src/styles.css pour préciser un style pour l’élément <h1>:

h1 { 
  color: blue; 
  font-family: arial,sans-serif; 
} 

.componenttitle { 
  text-transform: uppercase; 
} 

A l’exécution, on peut voir que le style défini pour <h1> est appliqué à l’élément dans le composant principal et dans le composant Parent, les éléments <h1> sont en bleu:

On peut aussi appliquer une classe définie dans un fichier global au niveau d’un composant. Par exemple si on modifie le template du composant Parent pour appliquer la classe componenttitle:

<h1 class="componenttitle">Composant parent</h1> 

La classe est prise en compte, à l’exécution, l’élément <h1> du composant Parent est en majuscules:

Si on affiche les outils développeurs du browser dans l’onglet “Editeur de styles” sous Firefox ou “Eléments” et “Styles” dans Chrome, on peut voir que la feuille de style styles.css contient les styles qui ont été définis pour toute l’application:

Affichage des outils de développement dans un browser

Pour afficher l’éditeur de styles:

  • Sous Firefox: on peut utiliser la raccourci [Maj] + [F7] ou en allant dans le menu “Outils” ⇒ “Développement web” ⇒ “Editeur de style”.
  • Sous Chrome: utiliser le raccourci [F12] (sous MacOS: [⌥] + [⌘] + [C], sous Linux: [Ctrl] + [Maj] + [I]) puis cliquer sur l’onglet “Elements”. A partir du menu, il faut aller dans “Afficher” ⇒ “Options pour les développeurs” ⇒ “Examiner les éléments”.

Appliquer un style sur les éléments d’un composant

Au lieu de définir des styles pour toute l’application, on peut les définir à l’échelle d’un composant seulement pour les éléments se trouvant dans ce composant.

Plusieurs méthodes sont possibles pour définir des styles dans un composant. Cette partie va détailler ces différentes méthodes.

Encapsulation des styles d’un composant

Par défaut, les styles définis dans un composant n’affecte que le composant dans lequel ils sont définis. Les styles des composants sont ainsi isolés et n’affectent pas les autres.

Pour illustrer cette partie, on va créer un 2e composant nommé other que l’on va placer sous le composant Parent en exécutant:

ng g c other

Et on modifie le template du composant principale (cf. src/app/app.component.html):

<h1>Composant principal</h1> 
<div style="display: block; border: 1px solid black; padding: 10px; margin-bottom: 10px; background-color: white;"> 
  <app-parent></app-parent> 
</div> 
<div style="display: block; border: 1px solid black; padding: 10px; margin-bottom: 10px; background-color: white;"> 
  <app-other></app-other> 
</div> 

L’implémentation du composant other est similaire à celle du composant Parent:

<h1>Composant other</h1> 
<p>Contenu du composant other</p> 

Si on modifie le contenu du fichier de style du composant Parent (cf. src/app/parent/parent.component.css):

p { 
  background-color: yellow; 
} 

On peut voir que seul le composant Parent est affecté par cette modification. Le contenu du composant other reste inchangé:

Ce comportement s’explique par le fait que le style des composants est isolé. Si on affiche l’onglet “Elements” des outils développeurs, on peut voir un code similaire au code suivant:

<app-root _nghost-thd-c19 ng-version="11.2.4"> 
  <h1 _ngcontent-thd-c19>Composant principal</h1> 
  <div _ngcontent-thd-c19 style="display: block; border: 1px solid black; padding: 10px; margin-bottom: 10px; background-color: white;"> 
    <app-parent _ngcontent-thd-c19 _nghost-thd-c16> 
      <h1 _ngcontent-thd-c16 class="componenttitle">Composant parent</h1> 
      <p _ngcontent-thd-c16>Contenu du composant parent</p> 
    </app-parent> 
  </div> 
  <div _ngcontent-thd-c19 style="display: block; border: 1px solid black; padding: 10px; margin-bottom: 10px; background-color: white;"> 
    <app-other _ngcontent-thd-c19 _nghost-thd-c18> 
      <h1 _ngcontent-thd-c18 >Composant other</h1> 
      <p _ngcontent-thd-c18>Contenu du composant other</p> 
    </app-other> 
  </div> 
</app-root> 

Ainsi des identifiants ont été rajoutés (parties en gras). Si on affiche le style de l’élément <p> en jaune du composant Parent, on peut voir:

p[_ngcontent-thd-c16] { 
  background-color: yellow; 
} 

Le style n’est pas défini de façon globale mais il est limité aux éléments contenant un attribut _ngcontent-thd-c16. Le seul élément <p> avec un attribut _ngcontent-thd-c16 est celui du composant Parent et non celui du composant other (qui est _ngcontent-thd-c18).

Ainsi, c’est en utilisant des attributs _nghost et _ngcontent spécifiques à un composant et en limitant la définition des styles aux éléments ayant certains attributs qu’Angular isole les styles pour le composant dans lequel ils ont été définis.

Comportement avec les composants enfants

L’encapsulation du style des vues implique que, par défaut, le style est strictement limité à la vue d’un composant. Le style défini dans un composant n’aura pas de conséquence sur des composants enfant et sur du contenu projeté. Seuls les éléments se trouvant dans le composant où le style est défini seront impactés.

Par exemple, si on considère 2 composants Parent et Child tels que Child est un composant enfant de Parent:

  • Composant Child:
    Template
    (child.component.html)
    <h1>Composant Child</h1>
    <p>Contenu composant Child</p> 
    Classe
    (child.component.ts)
    @Component({
      selector: 'app-child', 
      template: './child.component.html'
    }) 
    export class ChildComponent { 
    } 
  • Composant Parent:
    Template
    (parent.component.html)
    <h1>Composant Parent</h1>
    <p>Contenu composant Parent</p> 
    <app-child></app-child>
    Feuille CSS
    (parent.component.css)
    p {
      background-color: cadetblue; 
    } 
    Classe
    (parent.component.ts)
    @Component({
      selector: 'app-parent', 
      template: './parent.component.html', 
      styleUrls: [ './parent.component.css' ] 
    }) 
    export class ParentComponent { 
    } 

On peut voir que seul l’élément se trouvant dans le composant Parent est impacté par le style défini dans la feuille de style parent.component.css:

Si on modifie le code des composants de cette façon:

  • Composant Child:
    Template
    (child.component.html)
    <h1>Composant Child</h1>
    <p>Contenu composant Child</p> 
    Classe
    (child.component.ts)
    @Component({
      selector: '[app-child]',
      template: './child.component.html'
    }) 
    export class ChildComponent { 
    } 
  • Composant Parent:
    Template
    (parent.component.html)
    <h1>Composant Parent</h1>
    <p>Contenu composant Parent</p> 
    <p app-child></p> 
    Feuille CSS
    (parent.component.css)
    p {
      background-color: cadetblue; 
    } 
    Classe
    (parent.component.ts)
    @Component({
      selector: 'app-parent', 
      template: './parent.component.html', 
      styleUrls: [ './parent.component.css' ] 
    }) 
    export class ParentComponent { 
    } 

Le paramètre selector du composant Child est [app-child], ainsi pour afficher le composant Child dans la vue du composant Parent, il faut indiquer le selector sous forme d’attribut:

<p app-child></p> 

On peut voir que le style s’applique aussi à l’élément <p app-child></p>. On pourrait penser que le style défini dans le composant Parent s’applique au composant imbriqué Child toutefois ce n’est pas tout à fait le cas. En réalité, le style s’applique à l’élément hôte <p> du composant enfant qui se trouve dans le composant Parent:

Pour appliquer un style à des composants enfant, il faut utiliser le sélecteur :host ::ng-deep.

Définir les styles CSS

Paramètre styleUrls de @Component()

Par défaut, les styles CSS d’un composant sont définis dans un fichier séparé du template et de la classe du composant. Le chemin de ce fichier est indiqué en utilisant le paramètre styleUrls dans le décorateur @Component() du composant:

@Component({  
  selector: 'app-example',  
  templateUrl: './example.component.html',  
  styleUrls: ['./example.component.css']
})  
export class ExampleComponent {  
  constructor() { }  
} 

styleUrls est un tableau et permet d’indiquer plusieurs fichiers:

@Component({  
  selector: 'app-example',  
  templateUrl: './example.component.html',  
  styleUrls: ['./file1.css', './file2.css', './file3.css' ]
})  
export class ExampleComponent {  
  constructor() { }  
} 

Paramètre styles de @Component()

Utiliser un fichier séparé pour définir les styles CSS n’est pas obligatoire, on peut définir des styles directement dans la classe du composant en utilisant le paramètre styles du décorateur @Component(), par exemple:

@Component({  
  selector: 'app-example',  
  templateUrl: './example.component.html',  
  styles: [ 
    'div { display: block; border: 1px solid black; padding: 10px; margin-bottom: 10px; background-color: cadetblue; color: white }', 
    'h1 { text-transform: uppercase; color: blue }' 
  ]
})  
export class ExampleComponent {  
  constructor() { }  
} 

Le paramètre styles permet d’indiquer un tableau de façon à pouvoir définir plusieurs styles.

Avec le CLI Angular, on peut créer un composant directement avec le paramètre styles plutôt que le paramètre par défaut styleUrls en uilisant l’option --inline-style:

ng g c <nom du composant> --inline-style

Avec cette option, un fichier séparé contenant les styles ne sera pas créé.

Importer des fichiers de style

Comme pour les fichiers CSS classiques, il est possible d’importer des fichiers CSS dans les fichiers de styles des composants. L’intérêt est de profiter de l’encapsulation des vues de composant sans dupliquer le code des feuilles de style.

Pour importer un fichier CSS, il suffit d’indiquer le chemin relatif du fichier à importer avec la syntaxe suivante:

@import '<chemin relatif du fichier CSS à importer>'; 

Ou

@import url('<chemin relatif du fichier CSS à importer>'); 

Par exemple, pour importer un fichier CSS dans src/app/commonStyle.css dans le composant src/app/parent/parent.component.ts, il faut indiquer dans le fichier src/app/parent/parent.component.css:

@import url('../commonStyle.css'); 

L’intérêt est de pouvoir partager ce fichier pour plusieurs composants sans modifier le paramètre ViewEncapsulation et sans devoir dupliquer les styles.

Si le fichier à importer se trouve dans le même répertoire, il faut faire précéder le nom du fichier par './', par exemple:

@import './commonStyle.css'; 

Supprimer l’isolation des styles aux composants

On peut paramétrer un comportement différent concernant l’isolation des éléments de style définis dans un composant. Ce paramétrage se fait au niveau du paramètre encapsulation dans le décorateur @Component():

import { Component, ViewEncapsulation } from '@angular/core'; 

@Component({ 
  ... 
  encapsulation: ViewEncapsulation.Emulated 
}) 
export class CustomComponent { 
  ... 
} 

Les valeurs possibles de ViewEncapsulation sont:

  • ViewEncapsulation.Emulated (valeur par défaut): cette valeur isole les styles dans les composants par émulation. Angular effectue cette émulation en ajoutant des attributs _nghost et _ngcontent sur les éléments et des sélecteurs d’attributs sur les styles définis dans les composants. C’est le comportement décrit plus haut.
  • ViewEncapsulation.ShadowDom: avec ce paramètre les styles définis dans les composants sont aussi isolés dans le composant (comme pour ViewEncapsulation.Emulated). La différence est que ce n’est pas Angular qui va émuler ce comportement en rajoutant des attributs. Il va se servir de la fonctionnalité Shadow DOM du browser pour effectuer cette isolation.

    Ainsi pour utiliser la fonctionnalité Shadow DOM, Angular crée un Shadowing tree pour chaque composant de façon à séparer la structure et le style de ces composants du DOM.

    Si on reprend l’exemple précédent en affectant le paramètre ViewEncapsulation.ShadowDom au composant Parent, on peut voir qu’il n’y a plus de sélecteur de style pour l’élément <p> toutefois le style est toujours limité au composant Parent:

    p { 
      background-color: yellow; 
    } 
    
  • ViewEncapsulation.None: ce paramétrage consiste à ne pas isoler les styles au composant dans lesquels ils sont définis. En affectant ce paramètre sur un composant, le style sera défini de façon globale à toute l’application.

    Par exemple, si on considère 2 composants First et Second que l’on place directement dans le composant principal. Si on applique le paramètre ViewEncapsulation.None pour le composant First:

    • Template du composant principal (cf. src/app/app.component.html):
      <h1>Composant principal</h1> 
      <div style="display: block; border: 1px solid black; padding: 10px; margin-bottom: 10px; background-color: white;"> 
        <app-first></app-first> 
        <app-second></app-second> 
      </div>  
      
    • Composant First:
      Template
      (first.component.html)
      <h1>Composant First</h1>
      <p>Contenu composant Child</p> 
      Feuille CSS
      (first.component.css)
      p {
        background-color: yellow; 
      } 
      Classe
      (first.component.ts)
      @Component({
        selector: 'app-first', 
        template: './first.component.html', 
        styleUrls: [ './first.component.css' ], 
        encapsulation: ViewEncapsulation.None
      }) 
      export class FirstComponent { 
      } 
    • Composant Second:
      Template
      (second.component.html)
      <h1>Composant Second</h1>
      <p>Contenu composant Second</p>
      Classe
      (second.component.ts)
      @Component({
        selector: 'app-second', 
        template: './second.component.html'
      }) 
      export class SecondComponent { 
      } 

    On peut voir que les éléments <p> des composant First et Second sont affectés par le style (la propriété background-color des éléments <p> est yellow):

Si la vue d’un composant avec le paramètre encapsulation: ViewEncapsulation.None n’est pas affichée, le style défini dans ce composant ne sera pas appliqué de façon globale.

Créer directement des composants en précisant le paramètre encapsulation

Avec le CLI Angular, on peut créer des composants directement en précisant la valeur du paramètre encapsulation par exemple en exécutant la commande:

ng g c <nom du composant> --view-encapsulation None

Les valeurs possibles sont Emulated, None ou ShadowDom.

Comment appliquer des styles sur les éléments d’un composant ?

Plusieurs méthodes sont possibles pour définir et appliquer un style ou une classe CSS à un élément d’un composant:

  • Statiquement en définissant les styles ou en appliquant les classes CSS de façon inline directement dans l’élément en utilisant les attributs style et class.
  • Par attribute binding avec un attribut de type [attr.style] pour définir un style ou [attr.class] pour appliquer une classe CSS.
  • Par property binding avec un attribut de type [style] pour définir un style ou [class] pour appliquer une classe CSS.
  • En utilisant les objets ngStyle ou ngClass.

Appliquer un style ou une classe statiquement

Appliquer un style ou une classe sur un élément revient à utiliser les attributs style ou class directement comme pour une page HMTL classique, par exemple:

<div style="background-color: yellow"> 
  ... 
</div> 

Ou

<div class="cssClassName"> 
  ... 
</div> 

Si on prend l’exemple d’un composant dont l’implémentation est:

Template
(example.component.html)
<h1>Composant Example</h1>
<p>Contenu du composant example</p> 
Feuille CSS
(example.component.css)
.bluebackground {
  background-color: cadetblue; 
} 
 
.whitetext { 
  color: white; 
} 
Classe
(example.component.ts)
import { Component } from '@angular/core';

@Component({ 
  selector: 'app-example',  
  templateUrl: './example.component.html', 
  styleUrls: [ './example.component.css' ] 
}) 
export class ExampleComponent {} 

Appliquer un style est immédiat:

<h1>Composant Example</h1> 
<p style="background-color: cadetblue; color: white">Contenu du composant example</p> 

De même pour appliquer des classes:

<h1>Composant Example</h1> 
<p class="bluebackground whitetext">Contenu du composant example</p> 

Attribute binding

Pour appliquer des styles sur un élément par attribute binding, on peut utiliser directement l’attribut [attr.style] avec une chaîne de caractères contenant les styles:

<div [attr.style]="definedStyles"> 
  ... 
</div> 

definedStyles contient les styles sous forme de chaîne de caractères, par exemple:

background-color: cadetblue; color: white; 

De même pour appliquer des classes CSS, on peut utiliser l’attribut [attr.class] avec une chaîne de caractères contenant les classes à appliquer:

<div [attr.class]="definedClasses"> 
  ... 
</div> 

definedClasses contient les classes sous forme de chaîne de caractères, par exemple:

bluebackground whitetext 

Par exemple, une implémentation complête dans un composant pour appliquer un style par attribute binding serait du type:

Template
(example.component.html)
<h1>Composant Example</h1>
<div [attr.style]="definedStyles"> 
  Contenu du composant example 
</div> 

<button (click)="changeStyle()">Change style</button> 
Classe
(example.component.ts)
import { Component } from '@angular/core';

@Component({ 
  selector: 'app-example',  
  templateUrl: './example.component.html'
}) 
export class ExampleComponent { 
  definedStyles = ''; 

  changeStyle(): void { 
    if (this.definedStyles === '') { 
      this.definedStyles = 'background-color: cadetblue; color: white'; 
    } 
    else { 
      this.definedStyles = ''; 
    } 
  } 
} 

En cliquant sur le bouton, on peut voir que le style est ajouté dynamiquement sur l’élément <div>.

De même pour une classe CSS, on peut appliquer une classe par attribute binding en utilisant attr.class:

Template
(example.component.html)
<h1>Composant Example</h1>
<div [attr.class]="definedCssClasses"> 
  Contenu du composant example 
</div> 
 
<button (click)="changeCssClasses()">Change CSS classes</button> 
Feuille CSS
(example.component.css)
.bluebackground {
  background-color: cadetblue; 
} 

.whitetext { 
  color: white; 
}
Classe
(example.component.ts)
import { Component } from '@angular/core';

@Component({ 
  selector: 'app-example',  
  templateUrl: './example.component.html', 
  styleUrls: [ './example.component.css' ] 
}) 
export class ExampleComponent { 
  definedCssClasses = ''; 

  changeCssClasses(): void { 
    if (this.definedCssClasses === '') { 
      this.definedCssClasses = 'bluebackground whitetext'; 
    } 
    else { 
      this.definedCssClasses = ''; 
    } 
  } 
} 

Property binding

On peut appliquer des styles ou des classes CSS sur des éléments du template en utilisant des property bindings. A la différence de l’attribute binding, le propery binding passe par l’intermédiaire d’une propriété Angular pour effectuer les changements dans le DOM (l’attribute binding intervient plus directement sur l’attribut d’un élément HTML).

Le property binding permet plus de flexibilité en permettant d’affecter des styles ou des classes CSS sous forme d’une chaîne de caractères, d’un tableau de chaînes de caractères, d’une expression ou d’object literal.

Utiliser des chaines de caractères

Pour affecter des styles sous forme de chaînes de caractères, la syntaxe est du type:

<div [style]="definedStyles"> 
  ... 
</div> 

definedStyles contient les styles sous forme de chaîne de caractères, par exemple:

background-color: cadetblue; color: white; 

De même pour appliquer des classes CSS, on peut utiliser l’attribut [class] avec une chaîne de caractères contenant les classes à appliquer:

<div [class]="definedClasses"> 
  ... 
</div> 

definedClasses contient les classes sous forme de chaîne de caractères, par exemple:

bluebackground whitetext 

Utiliser une expression

On peut utiliser directement une expression pour la valeur des attributs [style] et [class] pour affecter respectivement des styles ou une classe CSS:

<div [style]="<expression>"> 
  ... 
</div> 

Par exemple:

Template
(example.component.html)
<h1>Composant Example</h1>
<div [style]="backgroundColorStyle + ';' + whiteTextStyle"> 
  Contenu du composant example 
</div> 
Classe
(example.component.ts)
import { Component } from '@angular/core';

@Component({ 
  selector: 'app-example',  
  templateUrl: './example.component.html' 
}) 
export class ExampleComponent { 
  backgroundColorStyle = 'background-color: cadetblue'; 
  whiteTextStyle = 'color: white'; 
} 

A l’exécution, on peut voir que le style défini sous forme d’une expression "backgroundColorStyle + ';' + whiteTextStyle" est appliqué sur l’élément <div>.

Utiliser un object literal

On peut affecter les styles ou les classes CSS en utilisant des object literals. L’intérêt de ce type d’objet est de permettre de les paramétrer plus facilement dans la classe du composant.

L’object literal doit comporter:

  • des propriétés dont les noms correspondent aux propriétés du style et
  • des valeurs correspondant aux valeurs des styles à appliquer.
{ 
  <propriété de style 1>: '<valeur 1>', 
  <propriété de style 2>: '<valeur 2>', 
  ... 
  <propriété de style N>: '<valeur N>' 
} 

De même pour les classes CSS, l’object literal doit comporter:

  • des propriétés dont le nom correspond au nom de la classe CSS et
  • des valeurs sont forme de booléen. La valeur True indiquant que la classe doit s’appliquer.
{ 
  <nom de la classe CSS 1>: '<true ou false>', 
  <nom de la classe CSS 2>: '<true ou false>', 
  ... 
  <nom de la classe CSS N>: '<true ou false>' 
} 

Exemple pour affecter un style
Par exemple, le composant suivant permet d’affecter un style sous la forme d’un object literal à un élément <div>:

Template
(example.component.html)
<h1>Composant Example</h1>
<div [style]="styleAsObjectLiteral"> 
  Contenu du composant example 
</div> 
 
<button (click)="changeStyle()">Change style</button> 
Classe
(example.component.ts)
import { Component } from '@angular/core';

@Component({ 
  selector: 'app-example',  
  templateUrl: './example.component.html' 
}) 
export class ExampleComponent { 
  styleAsObjectLiteral = ExampleComponent.getDivStyle('', ''); 

  static getDivStyle(backgroundColorValue: string, textColorValue: string): { 
    backgroundColor: string, 
    color: string 
  } { 
    return { 
      backgroundColor: backgroundColorValue, 
      color: textColorValue 
    }; 
  } 

  changeStyle(): void { 
    if (this.styleAsObjectLiteral.backgroundColor === '') { 
      this.styleAsObjectLiteral = ExampleComponent.getDivStyle('cadetblue', 'white'); 
    } 
    else { 
      this.styleAsObjectLiteral = ExampleComponent.getDivStyle('', ''); 
    } 
  } 
} 

Pour que la détection de changement fonctionne et que le changement de valeur de l’object literal soit répercuté dans la vue gràce au property binding, il faut affecter l’object literal entièrement et non modifier seulement la valeur des propriétés.

Le changement de style ne sera pas répercuté dans la vue si on implémente la méthode changeStyle() de cette façon:

changeStyle(): void { 
    if (this.styleAsObjectLiteral.backgroundColor === '') {
      this.styleAsObjectLiteral.backgroundColor = 'cadetblue'; 
      this.styleAsObjectLiteral.color = 'white'; 
    } 
    else { 
      this.styleAsObjectLiteral.backgroundColor = ''; 
      this.styleAsObjectLiteral.color = ''; 
    }
  } 

Il faut utiliser le Camel case pour définir les propriétés de l’object literal

Dans cet exemple, l’object literal permet de définir la propriété background-color et color de l’élément <div>:

{ 
  backgroundColor: 'cadetblue', 
  color: 'white' 
} 

Cet exemple utilise le Camel case pour nommer les propriétés de l’object literal de façon à paramétrer les propriétés de l’élément HTML qui sont en Kebab case:

  • backgroundColor en Camel case permet de paramétrer le style background-color en Kebab case,
  • color permet de paramétrer le style color.

Exemple pour affecter une classe CSS
L’exemple affecte une classe CSS à un élément <div> en utilisant un object literal:

Template
(example.component.html)
<h1>Composant Example</h1>
<div [class]="cssClassesAsObjectLiteral"> 
  Contenu du composant example 
</div> 
 
<button (click)="changeCssClass()">Change class CSS</button> 
Feuille CSS
(example.component.css)
.bluebackground {
  background-color: cadetblue; 
} 

.whitetext { 
  color: white;  
} 
Classe
(example.component.ts)
import { Component } from '@angular/core';

@Component({ 
  selector: 'app-example',  
  templateUrl: './example.component.html',
  styleUrls: [ './example.component.css' ] 
}) 
export class ExampleComponent { 
  applyBlueBackgroundClass = false; 
  applyWhiteText = false; 
  cssClassesAsObjectLiteral = { 
  bluebackground: this.applyBlueBackgroundClass; 
  whitetext: this.applyWhiteText; 

  changeCssClass(): void { 
    this.applyBlueBackgroundClass = !this.applyBlueBackgroundClass; 
    this.applyWhiteText = !this.applyWhiteText; 

    this.cssClassesAsObjectLiteral = { 
      bluebackground: this.applyBlueBackgroundClass; 
      whitetext: this.applyWhiteText; 
    }; 
  } 
} 

Property binding sur une seule propriété du style

Au lieu d’effecter le style complètement, on peut modifier une propriété par property binding. Par exemple, pour appliquer des property bindings seulement sur les propriétés background-color et color d’un élément <div>:

<div [style.background-color]="<membre de type string dans la classe du composant>" 
     [style.color]="<membre de type string dans la classe du composant>"> 
  ... 
</div>  
Camel case vs Kebab case

Les propriétés de style peuvent être nommées de 2 façons en Camel case et en Kebab case:

  • En Kebab case: pour désigner la propriété de style background-color, on peut effectuer le property binding avec la propriété du même nom de l’objet style, par exemple
    <div [style.background-color]="<membre de type string dans la classe>"> 
      ... 
    </div> 
    
  • Camel case: pour désigner la propriété de style background-color, on peut effectuer le property binding avec la propriété backgroundColor de l’objet style, par exemple:
    <div [style.backgroundColor]="<membre de type string dans la classe>"> 
      ... 
    </div> 
    

L’exemple complet est:

Template
(example.component.html)
<div [style.background-color]="backgroundColorStyle" [style.color]="textStyle" >
  Contenu composant Example 
</div> 

<button (click)="changeStyle()">Change style</button> 
Classe
(example.component.ts)
import { Component } from '@angular/core';

@Component({ 
  selector: 'app-example',  
  templateUrl: './example.component.html' 
}) 
export class ExampleComponent { 
  backgroundColorStyle = ''; 
  textStyle = ''; 

  changeStyle(): void { 
    if (this.backgroundColorStyle === '') { 
      this.backgroundColorStyle = 'cadetblue'; 
    } 
    else { 
      this.backgroundColorStyle = ''; 
    } 

    if (this.textStyle === '') { 
      this.textStyle = 'white'; 
    } 
    else { 
      this.textStyle = ''; 
    } 
  } 
} 

Property binding sur une seule classe CSS

Au lieu d’affecter toutes les classes CSS en utilisant un object literal, on peut effectuer un property binding pour une seule classe CSS. Par exemple, pour indiquer avec un booléen si on veut appliquer les classes CSS bluebackground et whitetext sur un élément <div>, on peut effectuer des property bindings séparément:

<div [class.bluebackground]="<membre de type booléen dans la classe du composant>" 
     [class.whitetext]="<membre de type booléen dans la classe du composant>"> 
  ... 
</div>  

L’exemple complet est:

Template
(example.component.html)
<div [class.bluebackground]="applyBlueBackgroundClass" [class.whitetext]="applyWhiteTextClass" >
  Contenu composant Example 
</div> 

<button (click)="changeCssClasses()">Change CSS classes</button> 
Feuille CSS
(example.component.css)
.bluebackground {
  background-color: cadetblue; 
} 

.whitetext { 
  color: white;  
} 
Classe
(example.component.ts)
import { Component } from '@angular/core';

@Component({ 
  selector: 'app-example',  
  templateUrl: './example.component.html',
  styleUrls: [ './example.component.css' ]  
}) 

export class ExampleComponent { 
  applyBlueBackgroundClass = true; 
  applyWhiteTextClass = true; 
 
  changeCssClasses(): void { 
    this.applyBlueBackgroundClass = !this.applyBlueBackgroundClass; 
    this.applyWhiteTextClass =!this.applyWhiteTextClass;  
  } 
} 
[ngStyle] et [ngClass]

Quand on applique des property bindings avec des tableaux ou des object literals, on peut utiliser [ngStyle] et [ngClass] à place de, respectivement, [style] et [class] pour appliquer des styles ou une classe.

Sélecteurs Angular

Il existe des sélecteurs de style Angular qui permettent d’affiner les conditions d’application des styles et des classes. Après compilation, ces styles n’apparaissent plus dans le code de l’application.

Sélecteur :host

Le sélecteur :host peut être utilisé dans la déclaration des styles pour désigner l’élément hôte de la vue d’un composant.

Par exemple si on considère 2 composants appelés Parent et Child tels que Child est un composant enfant de Parent:

L’implémentation est:

  • Composant Child:
    Template
    (child.component.html)
    <h1>Composant Child</h1>
    <p>Contenu composant Child</p> 
    Classe
    (child.component.ts)
    @Component({
      selector: 'app-child', 
      template: './child.component.html', 
      styleUrls: [ './child.component.css' ] 
    }) 
    export class ChildComponent { 
    } 
  • Composant Parent:
    Template
    (parent.component.html)
    <h1>Composant Parent</h1>
    <p>Contenu composant Parent</p> 
    <app-child></app-child>
    Classe
    (parent.component.ts)
    @Component({
      selector: 'app-parent', 
      template: './parent.component.html'
    }) 
    export class ParentComponent { 
    } 

Si on souhaite appliquer un style seulement au composant Child, on pourrait être tenté de définir le style CSS suivant dans le fichier child.component.css:

/* Ce code ne fonctionne pas */  
app-child { 
  display: block; 
  background-color: cadetblue; 
  border: 1px black solid; 
  font-family: Arial, sans-serif; 
} 

Cette définition ne fonctionne pas, pour appliquer un style sur le composant, il faut utiliser le sélecteur :host à la place de app-child:

:host { 
  display: block; 
  background-color: cadetblue; 
  border: 1px black solid; 
  font-family: Arial, sans-serif; 
  padding: 10px;
} 

Le résultat est:

Dans l’exemple précédent, le composant Child était directement l’élément affiché dans le composant Parent, le code HTML était du type:

<app-parent> 
  <h1>Composant Parent</h1> 
  <p>Contenu composant Parent</p> 
  <app-child> 
    <h1>Composant Child</h1> 
    <p>Contenu composant Child</p> 
  </app-child>
</app-parent> 

Au lieu d’être directement l’élément affiché, le composant enfant peut se trouver dans un élément hôte, par exemple si on définit le composant Child de cette façon:

Template
(child.component.html)
<h1>Composant Child</h1>
<p>Contenu composant Child</p> 
Classe
(child.component.ts)
@Component({
  selector: '[app-child]',
  template: './child.component.html', 
  styleUrls: [ './child.component.css' ] 
}) 
export class ChildComponent { 
} 

Pour l’afficher dans le composant Parent, on modifie le template de cette façon:

Composant Parent:

<h1>Composant Parent</h1> 
<p>Contenu composant Parent</p> 
<p app-child></p>

L’élément hôte de la vue du composant Child est désormais un élément <p>:

<app-parent> 
  <h1>Composant Parent</h1> 
  <p>Contenu composant Parent</p> 
  <p app-child> 
    <h1>Composant Child</h1> 
    <p>Contenu composant Child</p> 
  </p>
</app-parent> 

Le sélecteur :host s’applique à l’hôte du composant quel qu’il soit. Ainsi même si l’élément hôte a changé, le style est toujours appliqué. Le résultat est le même que précédemment.

Appliquer un style en appliquant une condition sur l’hôte

On peut restreindre l’application du style en appliquant une condition sur l’hôte du composant.

Par exemple si on veut appliquer le style si l’hôte est un élément <p>:

:host(p) { 
  ... 
} 

Si on veut appliquer le style si la classe redtext est appliquée sur l’hôte:

.host(.redtext) { 
  ... 
} 

On peut restreindre davantage les conditions en rajoutant d’autres conditions après :host(). Par exemple, si on veut appliquer un style:

  • Si l’hôte du composant est un élément <p>
  • Seulement sur les éléments <h1> se trouvant dans le composant

On définit le style de cette façon:

:host(p) h1 { 
  ... 
} 

Si considère les composants Parent et Child tels que Child est un composant enfant de Parent:

  • Composant Child:
    Template
    (child.component.html)
    <h1>Composant Child</h1>
    <p>Contenu composant Child</p> 
    Classe
    (child.component.ts)
    @Component({
      selector: '[app-child]', 
      template: './child.component.html', 
      styleUrls: [ './child.component.css' ] 
    }) 
    export class ChildComponent { 
    } 
  • Composant Parent:
    Template
    (parent.component.html)
    <h1>Composant Parent</h1>
    <p>Contenu composant Parent</p> 
    <p app-child></p> 
    <span app-child></span> 
    Classe
    (parent.component.ts)
    @Component({
      selector: 'app-parent', 
      template: './parent.component.html'
    }) 
    export class ParentComponent { 
    } 

Pour appliquer un style sur l’hôte du composant Child seulement si l’hôte est un élément <p>, on ajoute dans le fichier child.component.css:

:host(p) { 
  display: block; 
  background-color: cadetblue; 
  border: 1px black solid; 
  font-family: Arial, sans-serif; 
  padding: 10px;
} 

Dans cet exemple, on voit que le style n’est pas appliqué pour la ligne <span app-child></span>:

Sélecteur :host-context

Le sélecteur :host-context permet de désigner un antécédent du composant courant. On peut ainsi appliquer un style si un antécédent respecte une condition particulière.

Par exemple si on veut appliquer un style si un antécédent du composant est un élément <p>, on peut définir le style de cette façon:

:host-context(p) { 
  ... 
} 

Si on veut appliquer un style si la classe redtext est appliquée sur un antécédent du composant, on peut définir le style de cette façon:

:host-context(.redtext) { 
  ... 
} 

On peut restreindre l’application du style en rajoutant d’autres conditions après :host-context(). Par exemple, si on veut appliquer un style:

  • Si un antécédent du composant est un élément <p>
  • Seulement sur les éléments <h1> du composant.

On peut définir le style de cette façon:

:host-context(p) h1 { 
  ... 
} 

Si on considère 3 composants Parent, Middle et Child tels que:

  • Middle est un composant enfant de Parent et
  • Child est un composant enfant de Middle.

L’implémentation est:

  • Composant Child:
    Template
    (child.component.html)
    <h1>Composant Child</h1>
    <p>Contenu composant Child</p> 
    Classe
    (child.component.ts)
    @Component({
      selector: '[app-child]', 
      template: './child.component.html', 
      styleUrls: [ './child.component.css' ] 
    })
    export class ChildComponent { 
    } 
  • Composant Middle:
    Template
    (middle.component.html)
    <h1>Composant Middle</h1>
    <p>Contenu composant Middle</p> 
    <span app-child></span>
    Classe
    (middle.component.ts)
    @Component({
      selector: '[app-middle]', 
      template: './middle.component.html', 
      styleUrls: [ './middle.component.css' ] 
    }) 
    export class MiddleComponent { 
    } 
  • Composant Parent:
    Template
    (parent.component.html)
    <h1>Composant Parent</h1>
    <p>Contenu composant Parent</p> 
    <p app-middle></p>
    Classe
    (parent.component.ts)
    @Component({
      selector: 'app-parent', 
      template: './parent.component.html', 
      styleUrls: [ './parent.component.css' ] 
    })
    export class ParentComponent { 
    } 

Pour appliquer un style sur l’hôte du composant Child seulement si un antécédent de Child est un élément <p>, on ajoute dans le fichier child.component.css:

:host-context(p) { 
  display: block; 
  background-color: cadetblue; 
  border: 1px black solid; 
  font-family: Arial, sans-serif; 
  padding: 10px;
} 

Le résultat est:

Dans cet exemple, même si l’hôte du composant Child est un élément span, le style est appliqué car l’hôte du composant Middle dans lequel se trouve Child est un élément <p>.

Pour appliquer un style seulement sur les éléments <h1> du composant Child si l’hôte d’un antécédent de ce composant est un élément <p> (dans child.component.css):

:host-context(p) h1 { 
  color: greenyellow; 
} 

Le résultat est:

Sélecteur ::ng-deep

La documentation indique que ce sélecteur doit être décomissioné dans les futures versions d’Angular toutefois dans la version 11, il est toujours supporté.

Il sert à appliquer un style ou une classe globalement et en particulier à des composants enfant. L’intérêt de ce sélecteur est d’appliquer un style sur des composants enfant pour lesquels on ne peut pas modifier le code.

Concernant la syntaxe, il faut placer ::ng-deep devant les autres sélecteurs de style nécessitant une application globale:

::ng-deep <autres sélecteurs de style> 

Par exemple:

  • ::ng-deep p {} permet d’appliquer un style sur tous les éléments <p> de l’application.
  • ::ng-deep .bluebackground {} permet d’appliquer un style à tous les éléments pour lesquels la classe CSS bluebackground est utilisée.
  • ::ng-deep p.bluebackground {} applique un style sur tous les éléments <p> utilisant la classe CSS bluebackground.
  • span ::ng-deep p {} applique le même style sur les éléments <span> du composant et sur tous les éléments <p> de l’application. Il faut placer ::ng-deep devant les sélecteurs pour lesquels on veut une application globale.

Les sélecteurs /deep/ et >>> ont les mêmes fonctions que ::ng-deep toutefois il est déconseillé de les utiliser. La syntaxe est la même:

/deep/ <autres sélecteurs de style> 

Ou

>>> <autres sélecteurs de style> 
::ng-deep doit être utilisé avec ViewEncapsulation.Emulated

Il faut utiliser le sélecteur ::ng-deep avec le paramètre ViewEncapsulation.Emulated. Cette valeur étant la valeur par défaut, il n’est pas obligatoire de la préciser.

Appliquer un style globalement

Si on utilise ::ng-deep seul, la définition d’un style ou d’une classe devient globale.

Par exemple, si considère les 3 composants First, Second et Parent tels que First et Second sont des composants enfant de Parent:

  • Composant First:
    Template
    (first.component.html)
    <h1>Composant First</h1>
    <p>Contenu composant First</p> 
    Classe
    (first.component.ts)
    @Component({
      selector: 'app-first', 
      template: './first.component.html', 
      styleUrls: [ './first.component.css' ] 
    }) 
    export class FirstComponent { 
    } 
  • Composant Second:
    Template
    (second.component.html)
    <h1>Composant Second</h1>
    <p>Contenu composant Second</p> 
    Classe
    (second.component.ts)
    @Component({
      selector: 'app-second', 
      template: './second.component.html', 
      styleUrls: [ './second.component.css' ] 
    }) 
    export class SecondComponent { 
    } 
  • Composant Parent:
    Template
    (parent.component.html)
    <h1>Composant Parent</h1>
    <p>Contenu composant Parent</p> 
    <app-first></app-first> 
    <app-second></app-second> 
    Classe
    (parent.component.ts)
    @Component({
      selector: 'app-parent', 
      template: './parent.component.html', 
      styleUrls: [ './parent.component.css' ] 
    }) 
    export class ParentComponent { 
    } 

Si on définit le style suivant dans le fichier CSS du composant First (first.composant.css):

::ng-deep p { 
  background-color: cadetblue; 
} 

On peut voir que le style sera appliqué à tous les éléments <p>:

Appliquer un style à des composants enfant

Pour appliquer un style à des composants sans l’appliquer globalement, il faut que ::ng-deep soit précédé de :host:

:host ::ng-deep <autres sélecteurs de style> 

Par exemple, si on reprend l’exemple des composants Child, Middle et Parent tels que:

  • Child est un composant enfant de Middle et
  • Middle est un composant enfant de Parent.

L’implémentation est:

  • Composant Child:
    Template
    (child.component.html)
    <h1>Composant Child</h1>
    <p>Contenu composant Child</p> 
    Classe
    (child.component.ys)
    @Component({
      selector: 'app-child', 
      template: './child.component.html', 
      styleUrls: [ './child.component.css' ] 
    }) 
    export class ChildComponent { 
    } 
  • Composant Middle:
    Template
    (middle.component.html)
    <h1>Composant Middle</h1>
    <p>Contenu composant Middle</p> 
    <app-child></app-child> 
    Classe
    (middle.component.ts)
    @Component({
      selector: 'app-middle', 
      template: './middle.component.html', 
      styleUrls: [ './middle.component.css' ] 
    }) 
    export class MiddleComponent { 
    } 
  • Composant Parent:
    Template
    (parent.component.html)
    <h1>Composant Parent</h1>
    <p>Contenu composant Parent</p> 
    <app-middle></app-middle> 
    Classe
    (parent.component.ts)
    @Component({
      selector: 'app-parent', 
      template: './parent.component.html', 
      styleUrls: [ './parent.component.css' ] 
    }) 
    export class ParentComponent { 
    } 

Si on définit le style suivant dans le fichier CSS du composant Middle (cf. Middle.composant.css):

:host ::ng-deep p { 
  background-color: cadetblue; 
} 

On peut voir que le style sera appliqué aux élément <p> de Middle et Child et non celui de Parent:

:host ::ng-deep est particulièrement utile si on veut personnaliser des éléments situés sur des composants pour lesquels on ne maitrise pas l’implémentation, par exemple des composants se trouvant dans @angular/material.

Appliquer un style dans la classe du composant

Jusqu’içi, on a indiqué des méthodes pour appliquer des styles à partir du template ou d’une feuille de style d’un composant. Il existe d’autres méthodes permettant d’appliquer un style par programmation dans le classe du composant en utilisant, par exemple, les décorateurs @HostBinding() ou @ViewChild().

Avec @HostBinding()

Le décorateur @HostBinding() permet d’effectuer un binding entre un attribut dans la classe d’un composant et une propriété de l’objet du DOM qui est l’hôte du composant. Par exemple, utiliser ce décorateur permet d’accéder directement à des propriétés de style ou de classe.

Par exemple, si on considère les composants Child et Parent tels Child est un composant enfant de Parent. L’implémentation est:

  • Composant Child:
    Template
    (child.component.html)
    <h1>Composant Child</h1>
    <p>Contenu composant Child</p> 
    Classe
    (child.component.ts)
    import { Component, HostBinding } from '@angular/ core';
    
    @Component({  
      selector: '[app-child]', 
      template: './child.component.html'
    })  
    export class ChildComponent {  
      @HostBinding('style.background-color') backgroundColor: string = 'cadetblue';
    }  
  • Composant Parent:
    Template
    (parent.component.html)
    <h1>Composant Parent</h1>
    <p>Contenu composant Parent</p> 
    <p app-child></p> 
    Classe
    (parent.component.ts)
    @Component({
      selector: 'app-parent', 
      template: './parent.component.html'
    }) 
    export class ParentComponent { 
    } 

En utilisant @HostBinding() dans le composant Child, on peut modifier la propriété style.background-color pour appliquer à l’élément hôte du composant la couleur d’arrière plan. La vue du composant Child est affichée dans le composant Parent grâce à <p app-child></p>. Ainsi l’élément hôte de Child est <p>, par suite @HostBinding() s’applique à cet élément <p>:

L’implémentation est similaire pour appliquer une classe CSS. Il faut au préalable définir la classe dans la feuille CSS du composant Parent car l’encapsulation des styles limite la portée de la classe à ce composant seulement:

  • Composant Child:
    Template
    (child.component.html)
    <h1>Composant Child</h1>
    <p>Contenu composant Child</p> 
    Classe
    (child.component.ts)
    import { Component, HostBinding } from '@angular/ core';
     
    @Component({  
      selector: '[app-child]', 
      template: './child.component.html'
    })  
    export class ChildComponent {  
      @HostBinding('class.bluebackground') applyBlueBackground: boolean = true; 
    }  
  • Composant Parent:
    Template
    (parent.component.html)
    <h1>Composant Parent</h1>
    <p>Contenu composant Parent</p> 
    <p app-child></p> 
    Feuille CSS
    (parent.component.css)
    .bluebackground {
      background-color: cadetblue; 
    } 
    Classe
    (parent.component.ts)
    @Component({
      selector: 'app-parent', 
      template: './parent.component.html', 
      styleUrls: [ './parent.component.css' ] 
    }) 
    export class ParentComponent { 
    } 

Le résultat est similaire à l’exemple précédent.

Avec ElementRef.nativeElement

On peut appliquer des styles ou des classes CSS en utilisant des objets de type ElementRef et Renderer2. Ces objets sont:

  • ElementRef: objet Angular permettant d’accéder à l’objet du DOM correspondant à un élément HTML grâce à la propriété ElementRef.nativeElement.
  • Renderer2: il s’agit d’un objet Angular permettant d’effectuer des modifications dans les objets du DOM.

La combinaison de ces 2 objets permet d’appliquer des éléments de style ou une classe CSS.

Par exemple, si on considère le composant suivant:

Template
(example.component.html)
<h1>Composant Example</h1>
<p #content>Contenu composant Example</p> 
Feuille CSS
(example.component.css)
.redtext {
  color: red; 
} 
Classe
(example.component.ts)
import { Component, OnInit, ViewChild, Renderer2 } from '@angular/core';

@Component({ 
  selector: 'app-example', 
  template: './example.component.html', 
  styleUrls: [ './example.component.css' ] 
}) 
export class ExampleComponent implements OnInit { 
  @ViewChild('content', { static: true }) contentElementRef !: ElementRef; 

  constructor(private renderer: Renderer2) {} 

  ngOnInit(): void { 
    this.renderer.setStyle(this.contentElementRef.nativeElement, 
      'background-color', 'cadetblue'); 

    this.renderer.setAttribute(this.contentElementRef.nativeElement, 
      'class', 'redtext'); 
  } 

}

Dans cet exemple, on utilise les éléments d’implémentation suivant:

  • On définit un élément <p> dans le template du composant et on l’identifie avec la variable référence #content. Cette variable référence va permettre d’atteindre cet élément dans la classe du composant.
  • Dans la classe du composant, on utilise le décorateur @ViewChild() pour requêter la vue et accéder à un élément qui y est définit. On identifie l’élément en utlisant la variable référence #content. L’option { static: true } permet d’indiquer qu’il faut effectuer la requête sur le contenu statique de la vue.

    @ViewChild() permet d’instancier un objet de type ElementRef correspondant à un objet wrappant l’objet du DOM correspondant à l’élément HTML <p> se trouvant dans la vue.

    Pour plus de détails sur @ViewChild() voir l’article Requêter les éléments d’une vue d’un composant Angular.

  • L’objet Renderer2 peut être initialisé pour le composant par injection de dépendances.

Grâce aux objets Renderer2 et ElementRef, on va:

  • Appliquer la couleur cadetblue à l’arrière plan de l’élément <p>:
    this.renderer.setStyle(this.contentElementRef.nativeElement, 'background-color', 'cadetblue');
    
  • Appliquer la classe CSS redtext au contenu de l’élément <p> (cette classe est définie dans la feuille de style du composant):
    this.renderer.setAttribute(this.contentElementRef.nativeElement, 'class', 'redtext');
    

Priorités d’application des styles et classes CSS

L’application des styles et des classes CSS se fait suivant un ordre particulier. Cet ordre s’applique de plusieurs façons:

  • Pour un élément à l’intérieur d’un composant: si plusieurs styles sont appliqués, la priorité s’exerce en considérant d’abord une définition spécifique:
    1. Les property bindings s’appliquant sur une propriété spécifique:
      [style.background-color]='backgroundColor'
      

      Et:

      [class.bluebackground]='canApplyBlueBackground'
      
    2. Les property bindings s’appliquant avec [style] ou [class]:
      [style]='appliedStyles'
      

      Et:

      [class]='appliedCssClasses'
      
    3. Les styles et classes appliqués statiquement:
      style='background-color: cadetblue'
      

      Et:

      class='redtext' 
      
  • Si des styles ou classes sont appliqués sur l’hôte d’un composant, la priorité s’exerce à partant du template du composant vers ce qui est définit à l’extérieur du composant:
    1. Bindings effectués sur l’hôte du composant
    2. Les host bindings effectués par une directive
    3. Les host bindings effectués par le composant

Priorités d’application des styles sur un élément

Si on applique des styles sur un élément de plusieurs façons, la priorité d’application des styles s’effectuera en priorité sur les styles définis par:

  1. Property binding sur une propriété spécifique,
  2. Property binding en utilisant [style],
  3. Les styles appliqués statiquement.

Par exemple, si on considère le composant suivant:

Template
(example.component.html)
<h1>Composant Example</h1>
<p [style.background-color]='greenBackgroundColor' 
   [style]='appliedStyles' 
   style='background-color: cadetblue'>
        1. Property binding sur une propriété spécifique
</p> 
<p [style]='appliedStyles' 
   style='background-color: cadetblue'>
        2. Property binding sur [style]
</p> 
<p style='background-color: cadetblue'>
        3. Style statique
</p>
Classe
(example.component.ts)
@Component({
  selector: 'app-example', 
  template: './example.component.html' 
}) 
export class ExampleComponent implements OnInit { 

  appliedStyles = 'background-color: yellow'; 
  greenBackgroundColor = 'lime'; 
} 

On peut voir la priorité d’application des styles:

Priorités d’application des styles sur l’hôte d’un composant

Si on applique des styles sur l’hôte d’un composant, la priorité d’application des styles s’effectuera en priorité sur les styles définis par:

  1. Bindings effectués sur l’hôte du composant
  2. Les host bindings effectués par une directive
  3. Les host bindings effectués par le composant

Par exemple, si on considère 2 composants Child et Parent tels que Child est un composant enfant de Parent ainsi que la directive applyGreenBackground. L’implémentation de ces éléments est:

  • La directive applyGreenBackground:
    import { Directive, HostBinding } from '@angular/core'; 
    
    @Directive({ 
      selector: '[applyGreenBackground]' 
    }) 
    export class ApplyGreenBackgroundDirective { 
      @HostBinding('style.background-color') backgroundColor: string = 'lime'; 
    } 
    
  • Composant Child:
    Template
    (child.component.html)
    <h1>Composant Child</h1>
    <ng-content></ng-content> 
    Classe
    (child.component.ts)
    import { Component, HostBinding } from '@angular/core';
    
    @Component({  
      selector: '[app-child]', 
      template: './child.component.html'  
    })  
    export class ChildComponent {  
      @HostBinding('style.background-color') backgroundColor: string = 'cadetblue'; 
    }  
  • Composant Parent:
    Template
    (parent.component.html)
    <h1>Composant Parent</h1>
    <p app-child applyGreenBackground [style]='appliedStyles'>
      1. Binding effectué dans le template
    </p> 
    <p app-child applyGreenBackground>
      2. Host binding effectué par la directive
    </p> 
    <p app-child>
      3. Host binding effectué par le composant
    </p> 
    Classe
    (parent.component.ts)
    @Component({
      selector: 'app-parent', 
      template: './parent.component.html'
    }) 
    export class ParentComponent { 
      appliedStyles = 'background-color: yellow'; 
    } 

Cet exemple montre 3 applications de styles par bindings sur le composant Child. Le template du composant Child contient <ng-content></ng-content> pour effectuer une projection de contenu (voir content projection pour plus de détails).

Les 3 exemples d’applications de styles sont:

  • Un binding effectué dans le template du composant Parent:
    <p app-child applyGreenBackground [style]='appliedStyles'>1. Binding effectué dans le template</p>
    

    Cet exemple montre que ce binding est le plus prioritaire.

  • Un host binding effectué par une directive:
    <p app-child applyGreenBackground>2. Host binding effectué par la directive</p> 
    

    La directive ApplyGreenBackgroundDirective modifie l’arrière-plan de son élément hôte avec le décorateur @HostBinding().

  • Un host binding efectué par le composant Child:
    <p app-child>3. Host binding effectué par le composant</p> 
    

    Le composant Child modifie l’arrière-plan de son élément hôte avec le décorateur @HostBinding().

On peut voir la priorité d’application des styles:

Pour résumer…

Dans une application Angular, on peut définir des styles ou des classes CSS de plusieurs façons:

  • Globalement pour toute l’application, au niveau d’un ou plusieurs fichiers indiqué dans angular.json au niveau du paramètre styles:
    
    {
      "projects": {
        "<nom du projet>": {
          "architect": {
            "build": {
              "options": {
                "styles": [
                  "src/styles.css"
                ]
              }
            }
          }
        }
      }
    }
    
  • Plus spécifiquement aux éléments affichés dans la vue d’un composant en définissant les styles ou les classes:
    • Dans des fichiers de style .css référencés au niveau du paramètre styleUrls du décorateur @Component() du composant:
      @Component({
          styleUrls: [ './<nom du composant>.component.css' ] 
      })
      
    • Directement dans la classe du composant en utilisant le paramètre style du décorateur @Component():
      @Component({
        styles: [ 
          '<styles CSS>' 
        ]
      })
      

Dans un fichier .css, il est possibe d’importer un autre fichier CSS en indiquant directement son chemin relatif:

@import url('<chemin relatif du fichier CSS à importer>'); 

Par défaut, les styles définis dans un composant sont limités aux éléments de la vue de ce composant. On peut supprimer cette isolation en utilisant le paramètre encapsulation du décorateur @Component():

@Component({ 
  ... 
  encapsulation: ViewEncapsulation.Emulated 
}) 

Les différentes valeurs possibles de ce paramètre sont:

  • ViewEncapsulation.Emulated (valeur par défaut): le style est isolé au composant. Angular effectue cette isolation en rajoutant des attributs _nghost et _ngcontent aux éléments HTML au moment de la compilation.
  • ViewEncapsulation.ShadowDom: le style est isolé au composant en utilisant la fonctionnalité Shadow DOM du browser.
  • ViewEncapsulation.None: le style n’est pas isolé au composant. Il sera appliqué à tous les éléments affichés à condition que la vue du composant soit affichée.

Appliquer un style ou une classe sur un élément d’un composant

Plusieurs méthodes sont possibles:

  • Statiquement en utilisant les attributs style et class sur l’élément du template:
    <div style="background-color: yellow"> 
      ... 
    </div>
    

    Ou

    <div class="cssClassName"> 
      ... 
    </div> 
    
  • Par attribute binding:
    <div [attr.style]="definedStyles"> 
      ... 
    </div>
    

    definedStyles est un membre de la classe contenant la chaîne de caractères avec le style.
    Ou

    <div [attr.class]="definedClasses"> 
      ... 
    </div>
    

    definedClasses contient les classes sous forme de chaînes de caractères séparées par des espaces.

  • Par Property binding:
    <div [style]="definedStyles"> 
      ... 
    </div> 
    

    definedStyles peut être:

    • Une chaîne de caractères contenant les styles,
    • Un tableau de chaînes de caractères contenant les styles,
    • Une expression,
    • Un object literal dont le nom des propriétés correspond au nom du style à appliquer. Le nom de la propriété doit être en Camel case.

    Ou

    <div [class]="definedClasses"> 
      ... 
    </div>
    

    definedClasses peut être:

    • Une chaîne de caractères contenant les noms de classe séparés par un espace,
    • Un tableau de chaînes de caractères contenant les noms de classe,
    • Un object literal dont le nom des propriétés correspond au nom de la classe, la valeur étant un booléan (True indique que la classe doit être appliquée, False indique que la classe ne doit pas être appliquée).

    On peut appliquer une seule propriété de style en utilisant les syntaxes:

    • En Kebab case:
      <div [style.background-color]="<membre de type string dans la classe du composant>"> 
        ... 
      </div>
      
    • En Camel case:
      <div [style.backgroundColor]="<membre de type string dans la classe du composant>"> 
        ... 
      </div>
      

    De même on peut appliquer une seule classe avec la syntaxe:

    <div [class.bluebackground]="<membre de type booléen dans la classe du composant>" 
      ... 
    </div>  
    

Sélecteurs Angular

Angular permet d’utiliser les sélecteurs spécifiques pour affiner l’application des styles:

  • :host applique un style à l’élément hôte de la vue d’un composant:
    :host { 
      /* Styles CSS */
    } 
    

    Pour appliquer le style si le type de l’hôte est <p>:

    :host(p) { 
      /* Styles CSS */
    } 
    

    Pour appliquer le style si la classe redtest est appliquée à l’hôte:

    .host(.redtext) { 
      /* Styles CSS */
    } 
    

    Pour appliquer le style si l’hôte est <p> sur les éléments <h1> du composant:

    :host(p) h1 { 
      /* Styles CSS */
    }
    
  • :host-context permet d’appliquer un style suivant une condition sur un antécédent du composant:
    :host-context(p) { 
      /* Styles CSS */
    }
    

    Ce style est appliqué si un antécédent de la vue du composant est un élément <p>.

    Pour appliquer un style si la classe redtest est appliquée à un antécédent du composant:

    :host-context(.redtext) { 
      /* Styles CSS */
    } 
    

    Pour appliquer un style si un antécédent est un élément <p> en limitant aux éléments <h1> de la vue du composant:

    :host-context(p) h1 { 
      /* Styles CSS */
    } 
    
  • ::ng-deep permet d’appliquer un style ou une classe globalement. Le sélecteur ::ng-deep doit être utilisé avec le paramètre ViewEncapsulation.Emulated. Il faut placer ::ng-deep devant les sélecteurs pour lesquels on veut une application globale:
    ::ng-deep p {
      /* Styles CSS */
    }
    

    Cette définition applique le style sur tous les éléments <p>.

    Pour appliquer un style si la classe bluebackground est appliquée sur un élément:

    ::ng-deep .bluebackground {
      /* Styles CSS */
    }
    

    Pour appliquer un style sur les éléments <p> sur lesquels la classe bluebackground est appliquée:

    ::ng-deep p.bluebackground {
      /* Styles CSS */
    }
    

    Dans l’exemple suivant, le style est appliquée spécifiquement aux éléments <span> du composant et globalement à tous les éléments <p>:

    span ::ng-deep p {
      /* Styles CSS */
    }
    
  • :host ::ng-deep permet d’appliquer un style aux composants enfant:
    :host ::ng-deep p {
      /* Styles CSS */
    }
    

    Le style sera appliqué aux éléments <p> du composant et de ses enfants.

Appliquer un style dans la classe d’un composant

  • @HostBinding(): ce décorateur effectue un binding entre un membre de la classe et une propriété de l’objet du DOM qui est l’hôte du composant.
    export class ExampleComponent {  
      @HostBinding('style.background-color') backgroundColor: string = 'cadetblue';
    } 
    

    Cette implémentation permet d’appliquer la couleur cadetblue à la propriété background-color de l’élément hôte du composant Example.

    export class ExampleComponent {  
      @HostBinding('class.bluebackground') applyBlueBackground: boolean = true; 
    } 
    

    Cet exemple permet d’appliquer le classe bluebackground à l’élément hôte du composant Example.

  • ElementRef.nativeElement: permet de s’interfacer avec l’objet du DOM d’un élément affiché sur la vue d’un composant. On peut appliquer un style ou une classe en passant par cet objet, par exemple:
    export class ExampleComponent implements OnInit { 
      @ViewChild('content', { static: true }) contentElementRef !: ElementRef; 
    
      constructor(private renderer: Renderer2) {} 
    
      ngOnInit(): void { 
        this.renderer.setStyle(this.contentElementRef.nativeElement, 
          'background-color', 'cadetblue'); 
    
        this.renderer.setAttribute(this.contentElementRef.nativeElement, 
          'class', 'redtext'); 
      } 
    }
    

    Dans cet exemple:

    • On effectue une requête sur la vue du composant pour récupérer un objet identifié par une variable référence #content.
    • On affecte la couleur cadetblue à la propriété de style background-color de l’élément requêté.
    • On applique la classe redtext à cet élément requêté.

Priorités d’application des styles sur un élément

Si des styles sont appliqués de façon concurrente sur un élément, la priorité d’application de ces styles se fait dans cet ordre:

  1. Property binding sur une propriété spécifique,
  2. Property binding en utilisant [style],
  3. Les styles appliqués statiquement.

Priorités d’application des styles sur l’hôte d’un composant

Si des styles sont appliqués de façon concurrente sur l’hôte d’un composant, la prorité d’application de ces styles se fait dans cet ordre:

  1. Bindings effectués sur l’hôte du composant
  2. Les host bindings effectués par une directive
  3. Les host bindings effectués par le composant

Les vues des composants Angular

Cet article fait partie de la série d’articles Angular from Scratch.

@amyb99

Les composants Angular sont des objets permettant d’afficher une vue. Il s’agit d’une unité d’implémentation correspondant à une partie de l’affichage d’une application Angular. Cette unité comporte de nombreux éléments permettant de faciliter l’implémentation de façon à, dans un 1er temps, initialiser les objets statiques de la vue et dans un 2e temps, à interagir avec le DOM pour modifier dynamiquement des éléments d’affichage.

Pour ordonner cette unité d’implémentation, un composant est formé de différents éléments:

  • La vue du composant appelée template: cette partie comporte du code HTML correspondant aux objets statiques. Ce squelette statique est enrichi par du code interprété par Angular pour permettre des interactions dynamiques entre les objets statiques et du code métier se trouvant dans le reste du composant.
    Cet article détaille quelques éléments de cette partie du composant.
  • La classe du composant: c’est une classe Typescript dans laquelle on peut définir:
    • des membres contenant les données affichées par la vue,
    • des méthodes et fonctions pour exécuter des traitements déclenchés par des actions sur la vue ou par des évènements divers.
  • Des métadonnées: il s’agit d’informations supplémentaires d’implémentation qui permettront à Angular d’interfacer le template avec la classe du composant.

Ce schéma provenant de la documentation Angular permet de résumer les différents éléments du composant:

Source: angular.io/guide/architecture-components

Le but de cet article est de passer en revue les éléments les plus importants pour implémenter un template:

  • Indiquer comment on peut implémenter le template d’un composant.
  • Détailler l’utilité du paramètre selector qui permettra de placer la vue dans le reste de l’application Angular.
  • Présenter les différents types de bindings pour permettre les interactions entre le template et la classe du composant.
  • Indiquer comment on peut nommer des éléments sur le template avec une variable référence.
  • Détailler quelques directives usuelles facilitant l’implémentation du template.

Implémenter le template

Pour créer un composant Angular, il faut exécuter la commande suivante en utilisant le CLI Angular:

ng g c <nom composant>

Par exemple, pour créer le composant Example:

ng g c example

Cette instruction va créer 4 fichiers:

  • src/app/example/example.component.ts: il s’agit de la classe du composant. C’est le seul fichier obligatoire.
  • src/app/example/example.component.html: ce fichier est le template du composant.
  • src/app/example/example.component.css: ce fichier permet d’implémenter, le cas échéant, les styles ou classes CSS relatifs à la vue (le détail de ce fichier ne sera pas traité dans cet article et fera l’objet d’un article ultérieur).
  • src/app/example/example.component.spec.ts: ce fichier permet d’implémenter des tests liés à la classe du composant (le détail de ce fichier ne sera pas traité dans cet article).

Le contenu du template et de la classe du composant est:

Template
(example.component.html)
<p>example works!</p> 
Classe du composant
(example.component.ts)
import { Component, OnInit } from '@angular/core'; 

@Component({ 
  selector: 'app-example', 
  templateUrl: './example.component.html', 
  styleUrls: ['./example.component.css'] 
}) 
export class ExampleComponent implements OnInit { 

  constructor() { } 

  ngOnInit(): void { 
  } 
} 

Dans la classe du composant dans le décorateur @Component(), quelques paramètres permettent d’enrichir les métadonnées du composant:

  • selector: ce paramètre permet d’indiquer où la vue correspondant au composant sera affichée dans l’application Angular. Ce paramètre sera détaillé dans Paramètre selector.
  • templateUrl: ce paramètre indique le chemin du fichier template.
  • styleUrls: ce paramètre indique les chemins des fichiers CSS contenant les styles utilisés par la vue du composant.

Il est possible de ne pas utiliser un fichier séparé pour l’implémentation du template. Avec le paramètre template dans le décorateur @Component(), on peut implémenter le template directement dans la classe du composant, par exemple:

import { Component, OnInit } from '@angular/core'; 

@Component({ 
  selector: 'app-example', 
  template: `<p>example works!</p>`,
  styleUrls: ['./example.component.css'] 
}) 
export class ExampleComponent implements OnInit { 
  
  constructor() { } 

  ngOnInit(): void { 
  } 
} 

La valeur du paramètre template doit être indiquée entre les caractères `...` (accessible avec les touches [AltGr] + [7]) et non de simples quotes '...'.

Paramètre selector

Le paramètre selector peut être utilisé au niveau du décorateur @Component() d’un composant et plus généralement d’une directive. Il permet d’indiquer où le rendu de la vue du composant sera effectué. Ce paramètre n’est pas obligatoire, si on utilise un router Angular il peut être omis.

Pour comprendre l’intérêt de ce paramètre, on prend l’exemple du composant Example créé précédemment:

Template
<p>example works!</p>
Classe du composant
import { Component } from '@angular/core'; 

@Component({ 
  selector: 'app-example',
  templateUrl: './example.component.html' 
}) 
export class ExampleComponent  { 
} 

La valeur du paramètre selector de ce composant est:

app-example 

Cela signifie que le rendu sera effectué sur la vue d’un composant si le template de ce composant contient:

<app-example></app-example> 

Ainsi si:

  1. On supprime tout le contenu de la vue du composant principal dans le fichier src/app/app.component.html.
  2. On importe le composant ExampleComponent dans la classe du composant principal (dans src/app/app.component.ts) en ajoutant:
    import { ExampleComponent } from './example/example.component';
    
  3. Dans le template du composant principal (dans src/app/app.component.html), on indique l’appel au selector:
    <app-example></app-example>
    

L’implémentation du composant principal devient:

Template
<app-example></app-example> 
Classe du composant
import { Component } from '@angular/core'; 
import { ExampleComponent } from './example/example.component'; 

@Component({ 
  selector: 'app-root', 
  templateUrl: './app.component.html', 
  styleUrls: ['./app.component.css'] 
}) 

export class AppComponent { 
} 

Après avoir exécuté les commandes suivantes:

npm install 
ng serve 

L’affichage est:

Ainsi le rendu de la vue du composant ExampleComponent (dans src/app/example/example.component.html) a été effectué dans la vue du composant principal AppComponent (dans src/app/app.component.html).

Le paramètre selector peut servir dans le cadre de composant enfant c’est-à-dire si on imbrique un composant dans un autre (pour plus de détails voir les composants enfants).

Enfin le paramètre selector peut être utilisé sous des formes différentes de celle présentée dans cet exemple:

  • Attribut d’un élément HTML:
    Si le paramètre selector est défini de cette façon dans le composant:

    selector: '[custom-directive]' 
    

    La vue du composant sera affichée si le selector est utilisé sous forme d’attribut d’un élément HTML, par exemple:

    <span custom-directive></span> 
    

    L’attribut peut comporter une valeur:

    <span custom-directive="attribute value"></span> 
    

    La directive est reconnue aussi dans le cas d’un property binding (cette notion est définie plus bas):

    <span [custom-directive]="bindedProperty"></span> 
    

    Toutefois dans ce dernier cas, la directive devra comporter un paramètre d’entrée avec le même nom ou le même alias que le paramètre selector.

  • Classe CSS:
    Si le paramètre selector est défini de cette façon dans le composant:

    selector: '.custom-directive'  
    

    Dans ce cas, la directive doit être utilisée dans une classe CSS d’un élément HTML, par exemple:

    <span class="custom-directive"></span>
    

D’autres types de conditions sont possibles pour afficher le composant (voir les directives pour plus détails sur ces conditions).

Binding

Les interactions entre la vue et la classe du composant sont possibles avec différents types de bindings. L’implémentation de ces bindings permet d’enrichir la vue avec des données ou de déclencher l’exécution de code dans la classe du composant.

Le binding permet l’interfaçage entre le template et la classe du composant en:

  • Faisant passer des données de la classe du composant vers le template.
  • Déclanchant l’exécution de méthodes dans la classe du composant à la suite du déclenchement d’évènements dans le template.

Le binding permet de faciliter l’implémentation de tous ces mécanismes en rendant, par exemple, automatique:

  • La mise à jour d’un élément graphique quand la valeur d’un membre a changé dans la classe du composant,
  • Déclenchant l’exécution d’une méthode quand un évènement est survenu dans un élément HTML de la vue.

Ces mécanismes sont possibles grâce à la détection de changements effectuée par Angular (pour plus de détails, voir le Fonctionnement de la détection de changements).

Il existe plusieurs méthodes pour effectuer le binding:

  • L’interpolation permettant d’exécuter dans le template une expression Javascript pouvant contenir des attributs provenant de la classe du composant.
  • Le property binding qui permet d’effectuer un binding d’un membre de la classe du composant vers le template.
  • L’event binding permettant de déclencher un évènement dans la classe du composant à partir d’un évènement déclenché dans un objet du DOM.
  • L’attribute binding permettant d’effectuer un binding entre l’attribut d’un élément HTML et un attribut de la classe du composant.
  • Le two-way binding permettant à la fois l’échange de données entre la classe du composant et le template (1er sens) et le déclenchement d’évènements dans la classe du composant (2e sens).

On va détailler chacune de ces méthodes.

Interpolation

C’est le binding le plus simple qui permet l’échange de données dans un sens: de la classe du composant vers le template. Il permet d’exécuter une expression et d’utiliser directement des membres ou des fonctions publiques de la classe du composant dans le template avec la syntaxe {{ ... }}.

Par exemple, si on considère le composant suivant:

Template
<h1>{{title}}</h1>
Classe du composant
@Component({ 
  selector: 'app-example', 
  templateUrl: './example.component.html' 
}) 
export class ExampleComponent { 
     title = 'Page title';
} 

L’instruction {{title}} permet d’utiliser la valeur de le membre title de la classe dans le template.

Il est possible d’exécuter une fonction et d’utiliser sa valeur de retour, par exemple:

Template
<h1>{{getPageTitle()}}</h1> 
Classe du composant
@Component({ 
  selector: 'app-example', 
  templateUrl: './example.component.html' 
}) 
export class ExampleComponent { 
     private title = 'Page title'; 

     getPageTitle: string { 
          return this.title; 
     } 
} 

On exécute la fonction getPageTitle() pour afficher sa valeur de retour.

Dans la partie entre les crochets, on peut exécuter une expression, par exemple:

Template
{{getPageTitle().toUpperCase()}} 
Classe du composant
@Component({ 
  selector: 'app-example', 
  templateUrl: './example.component.html' 
}) 
export class ExampleComponent { 
  private title = 'Page title'; 

  getPageTitle: string { 
    return this.title; 
  } 
} 

getPageTittle().toUpperCase() permet de transformer les lettres du titre en majuscules.

Pour que le code de l’expression soit valide, il ne doit pas provoquer la création d’un nouvel objet ou le changement de valeur d’un objet existant. L’instanciation d’un objet ou les affectations ne sont, par exemple, pas possibles.

Property binding

Le property binding permet d’échanger des données de la classe du composant vers le template. Il permet de renseigner une propriété d’un objet dans le DOM avec une valeur provenant de le membre de la classe du composant. Indirectement la propriété dans le DOM se reflète sur son équivalent au niveau de la vue.

Par exemple, pour renseigner la propriété alt d’un élément HTML img avec un membre imgAltText du composant, le code est:

Template
<p>Example Componant</p> 
<img [alt]='imgAltText' /> 
Classe du composant
@Component({ 
  selector: 'app-example', 
  templateUrl: './example.component.html' 
}) 
export class ExampleComponent { 
  imgAltText = 'image to display';
} 

Le code [alt]='imgAltText' permet de renseigner la propriété alt de l’élément HTML avec la valeur du membre imgAltText de la classe du composant.

L’affichage est:

Ainsi:

  • [ ] est le propriété de l’élément cible du DOM
  • ' ' est la source du binding dans la classe du composant.

Avec l’interpolation, on pourrait avoir un résultat similaire en utilisant l’implémentation suivante dans le template:

<img alt='{{imgAltText}}' /> 

Si dans cet exemple on omet les crochets [ ] autour de la propriété de l’élément HTML il n’y a pas de binding et le texte 'imgAltText' est affecté statiquement à l’attribut alt de img, par exemple:

Si la propriété de l’élément HTML n’existe pas, une erreur se produit, par exemple:

<img [unknown]='imgAltText' /> 

L’erreur est du type:

Can't bind to 'unknown' since it isn't a known property of 'img'.

Le property binding permet d’effectuer un binding avec une propriété d’un élément HTML mais il permet aussi d’effectuer un binding dans des cas plus complexes comme:

  • Effectuer une binding avec un paramètre d’entrée d’un composant enfant: voir les composants enfant pour plus de détails.
  • Plus généralement effectuer un binding avec le paramètre d’une directive: voir les directives pour plus de détails.
Ne pas confondre propriété d’un objet du DOM et attribut d’un élément HTML

Le property binding permet d’interagir avec une propriété d’un objet du DOM et non directement avec un attribut d’un élément HTML. Une propriété du DOM et un attribut HTML ne correspondent pas à la même chose même s’ils possèdent le même nom, par exemple:

  • Des attributs HTML peuvent ne pas avoir de propriétés correspondantes comme colspan.
  • Des propriétés du DOM peuvent ne pas avoir d’équivalent au niveau des attributs HTML comme textContent.
  • Des attributs HTML et des propriétés DOM n’ont pas une correspondance directe comme value. Dans le cas du DOM c’est la valeur actuelle alors que dans le cas de l’attribut c’est la valeur initiale.

Si on prend l’exemple de l’attribut colspan de l’élément HTML td, il faut utiliser la propriété DOM colSpan. Ainsi, pour effectuer un property binding entre la propriété colSpan de l’objet du DOM et un attribut nommé spanValue dans la classe du composant, on écrira:

<td [colSpan]='spanValue'></div>

Le code suivant produit une erreur car la propriété DOM colspan n’existe pas:

<td [colspan]='spanValue'></div>

Il n’est pas évident de connaître les équivalents entre attribut HTML et propriété du DOM, on peut s’aider de ce tableau dans le code source d’Angular: dom_element_schema_registry.ts.

Attribute binding

L’attribute binding est similaire au property binding sauf qu’on utilise la syntaxe attr.<nom de l'attribut> pour désigner l’attribut de l’élément HTML.

Ainsi pour effectuer un binding entre le membre imgAltText de la classe du composant et l’attribut alt d’un élément img, la syntaxe est:

Template
<p>Example Componant</p> 
<img [attr.alt]='imgAltText' />
Classe du composant
@Component({ 
  selector: 'app-example', 
  templateUrl: './example.component.html' 
}) 
export class ExampleComponent { 
  imgAltText = 'image to display'; 
} 
Différence entre l’attribute binding et le property binding

La différence entre l’attribute binding et le property binding est que l’attribute binding intervient sur l’attribut de l’élément HTML pour affecter dynamiquement une valeur. Le property binding passe par l’intermédiaire d’une propriété dans l’objet dans le DOM correspondant à l’élément HTML.
L’attribute binding fait toujours référence à un attribut existant alors que le property binding peut faire référence à une propriété Angular qui n’a pas forcément d’équivalent dans l’objet dans le DOM.

Par exemple, si on considère l’exemple suivant:

<div [ngStyle]="{'color': yellow}"></div>

Ce code permet d’affecter un style à l’élément HTML div en passant par la directive Angular ngStyle. ngStyle ne correspond pas à un attribut HTML. On ne peut pas écrire:

<div ngStyle="{'color': yellow}"></div>

De même, l’affectation de la propriété colspan de l’élément HTML td peut prêter à confusion:

  • colspan fait référence à l’attribut de l’élément HTML td.
  • colSpan fait référence à la propriété de l’objet dans le DOM qu’Angular pourra modifier.

Ainsi, pour effectuer un attribute binding entre l’attribut colspan de l’élément HTML td et un attribut nommé spanValue dans la classe du composant, on écrira:

<td [attr.colspan]='spanValue'></div>

Pour effectuer un property binding entre la propriété colSpan de l’objet du DOM et un attribut nommé spanValue dans la classe du composant, on écrira:

<td [colSpan]='spanValue'></div>

Event binding

L’event binding permet d’exécuter du code dans la classe du composant à partir d’évènements déclenchés sur un élément du DOM.

Par exemple, pour exécuter la méthode displayText() dans la classe du composant à partir d’un clique sur un bouton, l’implémentation est:

Template
<p>{{textToDisplay}}</p> 
<p><button (click)='displayText()'>Click Me</button></p> 
Classe du composant
@Component({ 
  selector: 'app-example', 
  templateUrl: './example.component.html' 
}) 
export class ExampleComponent { 
    TextToDisplay = ''; 

    displayText(): void { 
        this.textToDisplay = 'My Text'; 
    } 
} 

Ainsi l’évènementclick dans le code HTML déclenche l’exécution de la méthode displayText(). Par binding, la valeur de la propriété textToDisplay est mise à jour directement dans le DOM.

Cet exemple permet de montrer l’exemple d’event binding entre un élément HTML et une méthode dans la classe du composant toutefois il existe des cas d’utilisations plus complexes dans le cadre:

Two-way binding

Le two-way binding permet d’effectuer un binding dans plusieurs directions:

  • Un property binding pour injecter la valeur d’un membre d’une classe de composant dans une propriété d’un composant.
  • Un event binding permettant d’exécuter une méthode après le déclenchement d’un évènement.

On indiquera 2 cas d’application d’un two-way binding:

  • Dans le cas d’un composant enfant et
  • Dans le cas d’un formulaire avec l’objet Angular ngModel.

Composant enfant

L’implémentation du two-way binding avec un composant enfant est possible en implémentant:

  • un paramètre d’entrée avec le décorateur @Input().
  • Un évènement auquel on peut s’abonner à l’extérieur du composant avec le décorateur @Output(). Pour que le two-way binding fonctionne, l’évènement doit s’appeler <nom du paramètre d'entrée>Change.

Par exemple, on crée le composant counter en exécutant:

ng g c counter

On modifie l’implémentation pour que ce composant contienne un compteur qui sera incrémenté en cliquant sur un bouton. La valeur initiale de ce compteur sera injectée avec un paramètre d’entrée. Chaque évènement de click sur le bouton déclenchera un évènement à l’extérieur du composant. L’implémentation est:

Template
<p>Counter: {{count}}</p> 
<button (click)='incrementValue()'>Increment</button> 
Classe du composant
import { Component, Input, Output, EventEmitter } from '@angular/core'; 

@Component({ 
  selector: 'counter', 
  templateUrl: './counter.component.html' 
}) 
export class CounterComponent { 

  @Input() count: number; 
  @Output() countChange: EventEmitter<number>= new EventEmitter<number>();

  incrementValue(): void { 
    this.count++;
    this.countChange.emit(this.count); 
  } 
} 

On peut remarquer que le paramètre d’entrée s’appelle count. Pour permettre le two-way binding l’évènement doit s’appeler countChange.

On utilise ce composant à l’intérieur d’un autre composant en tant que composant enfant.

On crée un composant parent qui contiendra le composant counter en exécutant:

ng g c parent

L’implémentation du composant parent est:

Template
<p>Parent Componant</p> 
<counter [(count)]="bindedCount"></counter> 
<p>Two-way binded counter: {{bindedCount}}</p> 
Classe du composant
import { Component} from '@angular/core'; 

@Component({ 
  selector: 'app-parent', 
  templateUrl: './parent.component.html' 
}) 
export class ParentComponent { 
  bindedCount = 10;
} 

De point de vue de l’implémentation:

  • Le template du composant parent contient le code pour afficher le composant enfant counter.
  • L’attribut [(count)]='bindedCount' permet d’implémenter un two-way binding avec la notation [(...)]:
    • Un property binding permet d’injecter dans la propriété count du composant counter la valeur du membre bindedCount provenant de la classe du composant parent (1er sens du binding).
    • Un event binding permet de mettre la valeur bindedCount dans la classe du composant parent à chaque fois que la propriété count est mise à jour dans le composant counter (2e sens du binding).

Ainsi, à l’exécution, on peut voir que:

  • La paramètre d’entrée count est bien paramétrer avec la valeur d’initialisation 10 provenant du membre bindedCount du composant parent.
  • A chaque click sur le bouton, le membre count dans le composant counter est incrémenté et le membre bindedCount dans le composant parent est aussi mis à jour.

L’interface se présente de cette façon:

ngModel

Une autre implémentation permet d’effectuer un two-way binding dans le cas de formulaire en utilisant ngModel.

Par exemple, si on utilise l’élément HTML input:

Template
<p><input [(ngModel)]='textToDisplay' /></p> 
<p><button (click)='eraseInputValue()'>Erase</button></p> 
<p>{{textToDisplay}}</p> 
Classe du composant
@Component({ 
  selector: 'app-example', 
  templateUrl: './example.component.html'  
}) 
export class ExampleComponent { 
    TextToDisplay = ''; 

    eraseInputValue(): void { 
        this.textToDisplay = 'My Text'; 
    } 
} 

Si on exécute ce code directement, un erreur se produira:

Can't bind to 'ngModel' since it isn't a known property of 'input'.

Pour corriger le problème, il faut importer le module FormsModule dans le module du composant. Dans notre exemple, le module du composant est le module root. Ainsi il faut rajouter dans le fichier src/app/app.module.ts, l’import de FormsModule:

import { FormsModule } from '@angular/forms'; 

@NgModule({ 
  imports: [ CommonModule, FormsModule ] 
}) 
export class AppModule {} 

Dans cet exemple, si on ecrit dans la partie input, la propriété textToDisplay sera directement mise à jour et l’interpolation {{textToDisplay}} affichera directement la nouvelle valeur. Ainsi l’échange se fait du template vers le membre de la classe du composant.

Si on clique sur le bouton, la valeur de la propriété textToDisplay est effacée par la méthode eraseInputValue() et la nouvelle valeur est directement répercutée dans la vue. L’échange se fait dans l’autre sens c’est-à-dire du membre de la classe du composant vers la vue.

Si on n’utilisait pas ngModel, il aurait fallu écrire le code coté template de cette façon:

<input [value]='textToDisplay' (input)='textToDisplay = $event.target.value' /> 

Cette implémentation associe plusieurs comportements:

  • Un property binding entre la propriété value de l’élément HTML input et le membre textToDisplay de la classe du composant. Ce binding permet l’échange de valeur du composant vers la vue.
  • Un event binding sur l’évènement input qui permet d’exécuter le code: textToDisplay = $event.target.value permettant d’affecter une nouvelle valeur au membre textToDisplay du composant à partir du contenu de l’élément input.

Utiliser ngModel simplifie la syntaxe en permettant d’effectuer un binding entre les membres de la classe du composant avec des éléments HTML de type input, select ou textarea. Par exemple cette directive s’utilise de cette façon pour un élément de type input:

<input [ngModel]='textToDisplay' (ngModelChange)='textToDisplay = $event' /> 

Avec cette syntaxe, le property binding et l’event binding apparaissent plus explicitement. L’event binding (ngModelChange) notifie quand un changement est détecté dans la vue, il extrait la cible de l’évènement déclenché à partir de event au lieu d’utliser event.target.value comme dans la syntaxe précédente.

Variable référence dans le template (#variable)

On peut définir des variables dans le template pour référencer un élément du DOM. La portée de cette variable sera le template c’est-à-dire qu’elle sera accessible et qu’elle doit être unique dans le cadre du template.

Par défaut, la variable contient une référence vers un élément du DOM et non un objet Angular. Toutefois il est possible de modifier le comportement pour que la variable contienne un objet Angular utilisé dans le template comme ngModel ou ngForm.

Pour déclarer une variable référence, il faut utiliser la syntaxe #<nom de la variable> ou ref-<nom de la variable> (cette dernière syntaxe n’est pas courante).

Par exemple:

<p> 
  <input #inputElement type='text' value='initialValue' /> 
  <button #clickButton>Click Me</button> 
</p> 

#inputElement et #clickButton permettent d’accéder à l’objet dans le DOM de, respectivement, l’élément input et button dans le reste du code du template. Ces variables ne sont toutefois pas directement accessible dans la classe du composant.

On peut, ainsi, utiliser les variables référence pour accéder aux objets en utilisant le nom de la variable sans le #:

<p>Input element value: {{inputElement.value}}</p> 
<p>Button textContent: {{clickButton.textContent}}</p> 

Le résultat est:

Il est important de comprendre que ces variables font référence aux objets du DOM, c’est la raison pour laquelle elles ne sont pas directement accessibles dans la classe du composant. Elles donnent la possibilité d’accéder aux 3 types d’objets dans le template:

  • Les éléments HTML: si on exécute le code suivant, on peut accéder à l’attribut value de l’élément HTML input. La valeur de cet attribut ne changera pas si on change la valeur de l’objet DOM correspondant.
  • Les objets du DOM et
  • Certains objets Angular comme ngModel ou ngForm.

Variable référence contenant une propriété du DOM

Pour comprendre les différences, on peut considérer l’exemple suivant;

<p><input #inputElement type='text' value='initialValue' /></p> 
<p>Input element: {{inputElement.getAttribute('value')}}</p> 

On peut accéder à l’attribut value de l’élément HTML input. La valeur de cet attribut ne changera pas même si on change la valeur de l’objet DOM correspondant. Le résultat de inputElement.getAttribute('value') est 'initial value'.

Si on modifie le code en effectuant un binding dans les 2 directions (i.e. two-way binding) avec ngModel, on peut modifier le contenu de value de l’élément input sans modifier la valeur de l’attribut HTML value:

<p><input #inputElement type='text' value='initialValue' [(ngModel)]='inputValue' /></p> 
<p>Input element: {{inputElement.getAttribute('value')}}</p> 
<p>Input element: {{inputElement.value}}</p> 

Il faut rajouter la propriété inputValue dans la classe du composant:

@Component({ ... }) 
export class ExampleComponent { 
  inputValue: string; 
} 

Le résultat est:

Dans cet exemple, l’attribut HTML initial n’est pas modifié toutefois la propriété de l’objet DOM contenant la valeur est modifié.

On peut voir le détail de l’objet dans le DOM en rajoutant la méthode suivante dans la classe du composant:

log(elementToLog) { 
  console.log(elementToLog); 
} 

Et en modifiant le template:

<p><input #inputElement type='text' value='initialValue' [(ngModel)]='inputValue' /></p> 
<p>Input element: {{inputElement.getAttribute('value')}}</p> 
<p>Input element: {{inputElement.value}}</p> 
<p>{{log(inputElement)}}</p>

Dans la console, on peut voir la structure de l’objet dans le DOM:

Variable référence contenant ngModel

On peut modifier le comportement pour que la variable référence inputElement contienne un autre objet que l’objet du DOM. Par exemple, en utilisant l’exemple précédent on peut affecter l’objet Angular ngModel dans la variable référence inputElement:

<p><input #inputElement='ngModel' type='text' [(ngModel)]='inputValue' /></p> 
<p>Input element: {{inputElement.value}}</p> 

En ajoutant la ligne:

{{log(inputElement)}} 

On peut voir qu’à la différence de l’exemple précédent, la variable référence inputElement ne contient plus l’objet du DOM mais l’objet Angular de type ngModel:

Accéder à une variable référence dans la classe du composant

Il est possible d’accéder dans la classe du composant à un objet identifié avec une variable référence dans le template. L’accès à cet objet n’est pas direct et doit se faire en effectuant une requête sur les éléments de la vue. Le décorateur @ViewChild() permet d’effectuer cette requête en utilisant le nom de la variable référence.

Par exemple, si on considère le composant suivant:

Template
<div #divElement>Texte à afficher</div> 
Classe du composant
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';

@Component({ ... }) 
export class ExampleComponent implements OnInit { 
    @ViewChild('divElement', {static: true}) htmlDivElement: ElementRef;

    ngOnInit(): void { 
        console.Log(this.htmlDivElement.nativeElement.innerText);
    } 
} 

Dans cet exemple, le template contient un élément HTML div identifié avec une variable référence divElement. Dans la classe du composant, on peut requêter la vue en utilisant le décorateur @ViewChild() et obtenir un objet permettant d’accéder à l’élément div du template.
Les paramètres du décorateur @ViewChild() sont:

  • 'divElement': ce paramètre correspond à la variable référence utilisée dans le template.
  • { static: true }: ce paramètre est optionnel et permet d’indiquer que l’élément à requêter fait partie du contenu statique de la vue en opposition au contenu dynamique. L’intérêt de ce paramètre est qu’il autorise à effectuer la requête au début du cycle de vie du composant de façon à ce que l’objet requêté soit accessible lors de l’exécution de ngOnInit().

Si on ne précise pas { static: true }, la valeur par défaut est { static: false }. Cela signifie que la requête sera exécutée plus tard lors du cycle de vie du composant. L’objet ne sera pas disponible à l’exécution de ngOnInit(), à ce moment la variable htmlDivElement est indéfinie (i.e. undefined). La variable htmlDivElement sera renseignée à l’exécution de ngAfterViewInit():

import { Component, OnInit, AfterViewInit, ViewChild, ElementRef } from '@angular/core';

@Component({ ... }) 
export class ExampleComponent implements OnInit, AfterViewInit { 
    @ViewChild('divElement') htmlDivElement: ElementRef;

    ngOnInit(): void { 
        // ERREUR undefined
        // console.Log(this.htmlDivElement.nativeElement.innerText);
    } 

    ngAfterViewInit(): void { 
        // OK
        console.Log(this.htmlDivElement.nativeElement.innerText);
    } 
} 

Pour plus de détails sur le paramètre static, sur le cycle de vie d’un composant et sur le requêtage d’une vue, voir Requêter les éléments d’une vue d’un composant Angular.

Directives structurelles usuelles

Les directives sont des objets Angular permettant de modifier ou d’enrichir un élément du DOM en rajoutant ou en modifiant une propriété par programmation. L’intérêt des directives est de pouvoir les utiliser dans un composant en les implémentant dans le template.

Fonctionnellement les directives peuvent paraître semblables aux composants enfant toutefois la grande différence entre les directives et les composants est que la directive n’a pas de vue. Dans la documentation Angular, les directives sont découpées en 3 catégories:

  • Les composants: ce sont des directives avec une vue implémentée dans un fichier template.
  • Les autres types de directives ne disposent pas de vue mais elles permettent de modifier le DOM en ajoutant ou en supprimant des éléments du DOM:
    • Les directives attribut (i.e. attribute directives): ces directives peuvent modifier l’apparence et le comportement des éléments, composants et d’autres directives.
    • Les directives structurelles (i.e. structural directives): elles se distinguent des directives attribut car elles utilisent un modèle (i.e. template) pour modifier le DOM. Il ne faut pas confondre ce modèle avec le template d’un composant.

Il est possible d’implémenter complêtement des directives toutefois Angular propose des directives usuelles qu’on peut directement utiliser dans le fichier template. Le but de cette partie est d’expliciter certaines directives les plus usuelles comme:

  • ngIf pour implémenter une instruction conditionnelle if...then...else.
  • ngFor pour implémenter l’équivalent d’une boucle for.
  • ngSwitch pour implémenter un switch...case.

L’article sur les directives permet d’indiquer davantage de détails pour les implémenter: cdiese.fr/angular-directives.

ngIf

ngIf permet d’afficher des éléments après avoir évalué si une condition est vraie. Cette directive transpose l’instruction conditionnelle if...then...else pour qu’elle soit utilisable dans le template d’un composant.

La syntaxe sous sa forme la plus simple si on l’utilise dans un élément HTML div, est:

<div *ngIf="<condition>">Contenu affiché si la condition est vraie</div> 

Ainsi si <condition> est égale à true, l’élément HTML sera affiché de cette façon:

<div>Contenu affiché si la condition est vraie</div> 

Si <condition> == false, rien ne sera affiché.

La syntaxe découle directement du fonctionnement des directives:

  • La directive est implémentée sous la forme d’un attribut d’un élément HTML car le paramètre selector de la directive est indiqué sous la forme selector: '[ngIf]' (voir paramètre selector pour plus de détails).
  • La caractère * devant ngIf indique qu’il s’agit d’une directive structurelle.

La syntaxe indiquée plus haut est une forme compacte dont l’équivalent dans sa forme plus étendue est:

<ng-template [ngIf]="<condition>"> 
  <div>Contenu affiché si <condition> == true</div> 
</ng-template> 

<ng-template> est un objet complexe permettant d’implémenter un modèle utilisé pour créer des vues intégrées (i.e. embedded view). Ces vues seront directement ajoutées au DOM suivant l’implémentation de modèle.

Par exemple, si on considère le composant suivant:

Template
<div *ngIf="condition">condition==true</div> 
<div *ngIf="!condition">condition==false</div> 

<p>Value of 'condition': {{condition}}</p> 
Classe du composant
@Component({ ... }) 
export class NgifExampleComponent { 
    condition = true; 
} 

Dans le fichier .css du composant, on ajoute le style:

div {  
    border: 1px solid black;   
    display: block;  
    background-color: lightgreen;  
} 

La résultat sera du type:

Ainsi comme condition == true alors seul le 1er élément div est affiché.

La forme étendue de la syntaxe indiquée plus haut est:

<ng-template [ngIf]="condition"> 
    <div>condition=true</div> 
</ng-template> 
<ng-template [ngIf]="!condition"> 
    <div>condition=false</div> 
</ng-template> 

Syntaxe avec else

On peut utiliser une syntaxe correspondant à une clause else, toutefois il faut passer par une variable référence. Cette variable référence contiendra la vue intégrée à afficher si la condition est fausse.

Avec else, la syntaxe équivalent à l’exemple précédent est:

<div *ngIf="condition else whenFalse"> 
    condition=true 
</div> 
<ng-template #whenFalse><div>condition=false</div></ng-template> 

Dans cet exemple:

  • "condition else whenFalse" est la configuration de la directive ngIf sous la forme d’une microsyntaxe (voir Configurer une directive structurelle par microsyntaxe pour plus de détails).
  • #whenFalse est la variable référence utilisée pour indiquer la vue intégrée à afficher quand la condition est fausse.

La forme étendue de la syntaxe avec else est:

<ng-template [ngIf]="condition" [ngIfElse]="whenFalse"> 
    <div>condition=true</div> 
</ng-template> 
<ng-template #whenFalse><div>condition=false</div></ng-template> 

Syntaxe avec then et else

On peut utiliser une syntaxe indiquant explicitement une clause then:

<div *ngIf="condition then whenTrue else whenFalse"></div> 
<ng-template #whenTrue><div>condition=true</div></ng-template> 
<ng-template #whenFalse><div>condition=false</div></ng-template> 

Cette syntaxe utilise 2 variables références whenTrue et whenFalse correspondant aux vues intégrées à afficher suivant la valeur de la condition.

La forme étendue équivalente de la syntaxe est:

<ng-template [ngIf]="condition" [ngIfThen]="whenTrue" [ngIfElse]="whenFalse"> 
</ng-template> 
<ng-template #whenTrue><div>condition=true</div></ng-template> 
<ng-template #whenFalse><div>condition=false</div></ng-template> 

ngFor

ngFor permet de parcourir une liste d’objets. Cette directive transpose la boucle for pour qu’elle soit utilisable dans le template d’un composant.

La syntaxe sous sa forme la plus simple si on l’utilise dans un élément HTML div, est:

<div *ngFor="let <nom variable locale d'un élément> of <liste d'éléments>"> 
  {{<nom variable locale d'un élément>}} 
</div> 

L’intérêt est de pouvoir répéter l’affichage d’un élément HTML. Par exemple si on considère l’exemple suivant:

<div *ngFor="let item of items">{{item}}</div> 

Si la liste d’éléments items contient les entiers 0, 1, 2, 3, 4 le résultat de l’exécution sera:

<div>0</div> 
<div>1</div> 
<div>2</div> 
<div>3</div> 
<div>4</div> 

Le détail de la syntaxe utilisée est:

  • ngFor correspond à une directive implémentée sous la forme d’un attribut d’un élément HTML car le paramètre selector de la directive est indiqué sous la forme selector: '[ngFor]' (voir paramètre selector pour plus de détails).
  • La caractère * devant ngFor indique qu’il s’agit d’une directive structurelle.
  • Le code "let item of items" correspond à la configuration de la directive en utilisant une microsyntaxe (voir Configurer une directive structurelle par microsyntaxe pour plus de détails). Cette configuration comporte 2 expressions:
    • let item permettant de définir une variable locale nommée item. Cette variable est affectée de façon implicite (cf. Propriété implicit) par la directive lors de l’exécution de la boucle.
    • of items: cette expression permet d’affecter le contenu de la variable items au paramètre d’entrée (cf. Paramètre inputs) nommé ngForOf de la directive. items contient la liste des éléments qui seront répétés par la boucle.
Les caractères ';', ':' et le retour à la ligne sont facultatifs dans les expressions par microsyntaxe

Dans la suite d’expressions par microsyntaxe utilisées pour configurer la directive, les caractères ';', ':' et le retour à la ligne n’ont pas d’incidence. Si on considère l’exemple:

<div *ngFor="let item of items">{{item}}</div> 

Les expressions suivantes sont équivalentes malgré la présence des caractères ';', ':' et du retour à la ligne:

<div *ngFor="let item; of items">{{item}}</div> 
<div *ngFor="let item; of: items">{{item}}</div> 

Ou

<div *ngFor="let item;  
of items;">{{item}}</div> 

La syntaxe indiquée plus haut est une forme compacte dont l’équivalent dans sa forme plus étendue est:

<ng-template [ngFor] let-item="$implicit" [ngForOf]="items"> 
  <div>{{item}}</div> 
</ng-template> 

Voir <ng-template> pour plus de détails.

Par exemple si on considère le composant suivant:

Template
<p>Car types are:</p> 
<table> 
    <tr> 
        <th>Id</th> 
        <th>Name</th> 
    </tr> 
    <tr *ngFor="let carType of carTypes">
        <td>{{carType.id}}</td> 
        <td>{{carType.name}}</td> 
    </tr> 
</table> 
Classe du composant
@Component({ ... }) 
export class NgForOfExampleComponent { 
  carTypes = [ 
      { id: "A", name: "Pick-Up"}, 
      { id: "B", name: "Van"}, 
      { id: "C", name: "Truck"}, 
      { id: "D", name: "Sedan"}, 
      { id: "E", name: "Cabriolet"}, 
    ]; 
} 

Dans le fichier .css du composant, on ajoute le style:

table, td, th {
    border: 1px solid black;   
} 

La résultat sera du type:

Dans cet exemple, le tableau carTypes est parcouru de façon à répéter l’affichage d’une ligne pour chaque élément du tableau.

La forme étendue de la syntaxe indiquée plus haut est:

<ng-template [ngFor] let-carType="$implicit"[ngForOf]="carTypes"> 
<tr> 
<td>{{carType.id}}</td> 
<td>{{carType.name}}</td> 
</tr> 
</ng-template> 

index, count, first, last, odd et even

D’autres propriétés dans ngFor peuvent être utilisées pour indiquer des informations supplémentaires:

  • index indique l’index de l’élément courant dans la liste.
  • count contient le nombre d’éléments de la liste.
  • first est un booléen contenant true si l’élément courant est le premier élément de la liste.
  • last est un booléen contenant true si l’élément courant est le dernier élément de la liste.
  • odd contient true si l’index de l’élément courant est impair.
  • even contient true si l’index de l’élément courant est pair.

Si on modifie l’exemple précédent en utilisant ces propriétés dans le template:

<p>Car types are:</p> 
<table> 
    <tr> 
        <th>Index</th> 
        <th>Id</th> 
        <th>Car type</th> 
        <th>Is first type?</th> 
        <th>Is last type?</th> 
        <th>Is odd index?</th> 
        <th>Is even index?</th> 
        <th>Type count</th> 
    </tr> 
    <tr *ngFor="let carType of carTypes  
            index as typeIndex  
            first as isFirstType  
            last as isLastType 
            odd as isOddIndex 
            even as isEvenIndex 
            count as typeCount"> 
        <td>{{typeIndex}}</td> 
        <td>{{carType.id}}</td> 
        <td>{{carType.name}}</td> 
        <td>{{isFirstType}}</td> 
        <td>{{isLastType}}</td> 
        <td>{{isOddIndex}}</td> 
        <td>{{isEvenIndex}}</td> 
        <td>{{typeCount}}</td> 
    </tr> 
</table> 

Dans les différentes expressions en microsyntaxe, la forme index as typeIndex permet d’affecter la valeur de la propriété index à une variable locale nommée typeIndex.

Le résultat de cette implémentation est:

ngSwitch

ngSwitch est une directive permettant d’afficher un élément HTML si une expression est vraie. Cette directive transpose switch...case pour qu’il soit utilisable dans le template d’un composant.

La syntaxe sous sa forme la plus simple si on l’utilise dans un élément HTML div, est:

<div [ngSwitch]="<expression utilisée pour la comparaison>"> 
<div *ngSwitchCase="<expression comparée 1>">...</div> 
<div *ngSwitchCase="<expression comparée 2>">...</div> 
... 
<div *ngSwitchCase="<expression comparée N>">...</div> 
<div *ngSwitchDefault>...</div> 
</div> 

Ainsi:

  • L’attribut ngSwitch permet d’indiquer l’expression qui sera utilisée pour effectuer la comparaison.
  • ngSwitchCase va permettre d’indiquer l’expression pour laquelle l’égalité sera vérifiée.
  • ngSwitchDefault correspond au cas par défaut si l’égalité n’a été trouvée pour aucune expression.

Par exemple si on considère le composant suivant:

Template
<p>Vehicle size is:</p> 
<div [ngSwitch]="vehicleSize"> 
    <div *ngSwitchCase="'big'"><b>Big</b></div> 
    <div *ngSwitchCase="'medium'">Medium</div> 
    <div *ngSwitchCase="'little'"><i>Little</i></div> 
    <div *ngSwitchDefault>(unknown)</div> 
</div> 
Classe du composant
@Component({ ... }) 
export class NgSwitchExampleComponent { 
    vehicleSize='big' 
} 

La résultat sera du type:

Dans cet exemple, le membre vehicleSize dans la classe du composant contient la valeur 'big'. Ainsi seul l’élément HTML <div><b>Big</b></div> sera affiché.

Il est possible d’utiliser une expression pour l’attribut ngSwitch mais aussi pour l’attribut ngSwitchCase. Par exemple si on considère le composant suivant:

Template
<table> 
        <tr> 
            <th>Car type</th> 
            <th>Size</th> 
        </tr> 
        <tr *ngFor="let carType of carTypes" [ngSwitch]="getVehicleSizeRange(carType.size)"> 
            <td>{{carType.name}}</td> 
            <td *ngSwitchCase="bigSize" class='big'>Big</td> 
            <td *ngSwitchCase="mediumSize" class='medium'>Medium</td> 
            <td *ngSwitchCase="littleSize" class='little'>Little</td> 
            <td *ngSwitchDefault>(unknown)</td> 
        </tr> 
</table> 
Classe du composant
@Component({ ... }) 
export class NgSwitchExampleComponent { 
    vehicleSize='big'; 
    carTypes = [ 
      { id: 'A', name: 'Pick-Up', size:4}, 
      { id: 'B', name: 'Van', size:6}, 
      { id: 'C', name: 'Truck', size:7}, 
      { id: 'D', name: 'Sedan', size:4}, 
      { id: 'E', name: 'Cabriolet', size:3}, 
    ]; 
  
    bigSize='big'; 
    mediumSize='medium'; 
    littleSize='little'; 

  getVehicleSizeRange(size: number): string { 
    if (size < 4) return this.littleSize; 
    else if (size >= 4 && size < 6) return this.mediumSize; 
    else return this.bigSize; 
  } 
} 

On ajoute les styles suivants dans le fichier .css:

table, td, th {
    border: 1px solid black;   
  }

.big { 
    background-color: red;   
} 

.medium { 
    background-color: orange;   
} 

.little { 
    background-color: green;   
} 

Le résultat est du type:

Cet exemple est plus élaboré et permet de montrer qu’une expression est utilisée à la fois pour ngSwitch et les différentes clauses ngSwitchCase.

ngPlural

ngPlural permet d’apporter une solution pour la gestion des pluriels. Suivant le nombre d’éléments d’une liste, il va permettre d’afficher une forme au singulier ou au pluriel, par exemple:

  • "Un élément"
  • "Des éléments"
  • "Aucun élément"
  • Etc..

ngPlural donne la possibilité d’indiquer toutes les formes possibles en utilisant l’attribut ngPluralCase:

<p [ngPlural]="<nom d'éléments>"> 
  <ng-template ngPluralCase="0">Aucun élément</ng-template> 
  <ng-template ngPluralCase="1">Un élément</ng-template> 
  <ng-template ngPluralCase="other">Des éléments</ng-template> 
</p> 

Ainsi:

  • ngPlural est utilisé pour indiquer le nombre d’éléments.
  • ngPluralCase permet d’indiquer les différents cas de figures.

On ne peut pas utiliser n’importe quelle valeur avec ngPluralCase. La liste des valeurs acceptées est:

  • Directement des valeurs numériques: "0", "1", "2" et "other" pour indiquer le cas par défaut.
  • Des expressions comportant une valeur précédée de =. "other" peut être utilisé pour indiquer le cas par défaut.

    Par exemple:

    <p [ngPlural]="itemCount"> 
      <ng-template ngPluralCase="=0">Aucun élément</ng-template> 
      <ng-template ngPluralCase="=1">Un élément</ng-template> 
      <ng-template ngPluralCase="other">Des éléments</ng-template> 
    </p> 
    
  • Des valeurs correspondants aux CDLR (i.e. Common Locale Data Repository): ces valeurs sont:
    • "zero" pour indiquer aucun élément.
    • "one" pour indiquer exactement un élément.
    • "two" pour indiquer exactement 2 éléments.
    • "few" pour indiquer entre 3 et 10 éléments.
    • "many" pour indiquer entre 11 et 99 éléments.
    • "other" pour les autres cas de figure.

    Les valeurs prisent en compte dépendent du paramètre de langue. Par exemple pour le français ou l’anglais, il n’existe qu’une forme singulière ou une forme plurielle. Donc seules les valeurs "one" ou "other" sont utilisées. Les valeurs "zero", "two", "few" et "many" ne sont pas prises en comptes.

Dans le cas où aucune valeur ne peut satisfaire le nombre d’élément évalué, une erreur du type suivant peut se produire:

Error: No plural message found for value "..."

Par exemple, si on considère l’exemple du composant suivant:

Template
La liste contient <span [ngPlural]="items.length">  
   <ng-template ngPluralCase="=0">aucun élément</ng-template>  
   <ng-template ngPluralCase="=1">un élément</ng-template>  
   <ng-template ngPluralCase="=2">deux éléments</ng-template>  
   <ng-template ngPluralCase="other">des éléments</ng-template>
</span>. 
<p><button (click)="addItem()">Add item</button></p> 
<p>{{ items.length }}</p> 
Classe du composant
import { Component, OnInit, Inject } from '@angular/core'; 

@Component({ 
  ... 
}) 
export class NgPluralExampleComponent implements OnInit { 
  items: number[]; 
  
  ngOnInit(): void { 
    this.items = []; 
  } 

  addItem(): void { 
    this.items.push(Math.random()); 
  } 
} 

Ce composant permet de montrer le comportement si on augmente le nombre d’éléments dans la liste en cliquant sur Add item.

Dans le cas d’une utilisation explicite des valeurs "=0", "=1", "=2" et "other", les 4 formes seront affichées successivement:

  • 0: La liste contient aucun élément.
  • 1: La liste contient un élément.
  • 2: La liste contient deux éléments.
  • Au delà: La liste contient des éléments.

Le comportement est le même si on indique les valeurs "0", "1", "2" et "other":

<ng-template ngPluralCase="0">aucun élément</ng-template>  
<ng-template ngPluralCase="1">un élément</ng-template>  
<ng-template ngPluralCase="2">deux éléments</ng-template>  
<ng-template ngPluralCase="other">des éléments</ng-template>   

En revanche si on utilise les valeurs "zero", "one", "two", "few", "many" et "other", seules les valeurs "one" et "other" sont prises en compte:

<ng-template ngPluralCase="zero">aucun élément</ng-template>  
<ng-template ngPluralCase="one">un élément</ng-template>  
<ng-template ngPluralCase="two">deux éléments</ng-template>  
<ng-template ngPluralCase="few">un peu d'éléments</ng-template>  
<ng-template ngPluralCase="many">quelques éléments</ng-template>  
<ng-template ngPluralCase="other">des éléments</ng-template>  

L’affichage est:

  • 0: La liste contient des éléments.
  • 1: La liste contient un élément.
  • Au delà: La liste contient des éléments.

En anglais ou en français, seules les formes singulières et plurielles sont prises en compte. Ce n’est pas le cas de toutes les langues. Dans le cas de l’arabe les autres formes peuvent être prises en compte.

Par exemple si on change les paramètres locaux de langues en effectuant les étapes suivantes:

  1. Dans le module du composant (par exemple app.module.ts), si on modifie la langue pour choisir "ar-AE" correspondant à l’arabe des Emirats Arabes Unis:
    import { BrowserModule } from '@angular/platform-browser'; 
    import { NgModule } from '@angular/core'; 
    ... 
    import { LOCALE_ID } from '@angular/core';
    import { registerLocaleData } from '@angular/common'; 
    import localeAr from '@angular/common/locales/ar-AE';
    
    registerLocaleData(localeAr);
    
    @NgModule({ 
      declarations: [ 
        AppComponent, 
      ], 
      imports: [ 
        BrowserModule, 
        AppRoutingModule 
      ], 
      providers: [ 
        { provide: LOCALE_ID, useValue: 'ar-AE'}
      ], 
      bootstrap: [AppComponent] 
    }) 
    
    export class AppModule { } 
    
  2. On modifie le composant pour afficher le paramètre de langue:
    import { Component, OnInit, Inject } from '@angular/core'; 
    import { LOCALE_ID } from '@angular/core';
    
    @Component({ 
        ... 
    }) 
    export class NgPluralExampleComponent implements OnInit { 
      items: number[]; 
    
      constructor(@Inject(LOCALE_ID) localeId) {
        console.log(localeId); 
      } 
    
      ngOnInit(): void { 
        this.items = []; 
      } 
    
      addItem(): void { 
        this.items.push(Math.random()); 
      } 
    } 
    

A l’exécution, on peut voir que l’affichage se fait de cette façon:

  • 0: La liste contient aucun élément.
  • 1: La liste contient un élément.
  • 2: La liste contient deux éléments.
  • Si n % 100 = 3..10: La liste contient un peu d'éléments.
  • Si n % 100 = 11..99: La liste contient quelques éléments.

Pour résumer…

Architecture d’un composant

Un composant est composé de:

  • Une vue appelée template dans laquelle on implémente les éléments visibles.
  • Une classe du composant implémentée un Typescript.
  • Des métadonnées permettant d’apporter à Angular des informations supplémentaires concernant le composant.

La vue d’un composant correspond au template. Ce template peut être implémenté en utilisant du code HTML enrichi de façon à rendre la vue dynamique. L’implémentation de cet aspect dynamique de la vue est possible grâce à différents mécanismes spécifiques à Angular:

  • Le binding qui permet la mise à jour de données et l’exécution de fonctions à partir du déclenchement d’évènements dans la vue.
  • Des directives qui facilitent l’implémentation de comportements dans la vue.

A la création d’un composant:

  • La classe du composant se trouve dans un fichier .ts: cette classe doit comporter le décorateur @Component():
    import { Component } from '@angular/core'; 
    
    @Component({ 
      
    })
    export class ExampleComponent { 
    }
    
  • Le template se trouve généralement dans un fichier séparé .html: le paramètre templateUrl dans le décorateur @Component() indique le chemin de ce fichier:
    @Component({ 
      templateUrl: './example.component.html'
    })
    export class ExampleComponent { 
    }
    

    Le template peut aussi être implémenté directement dans le fichier de la classe du composant en utilisant le paramètre template:

    @Component({ 
      template: `<p>Contenu du template</p>`
    })
    export class ExampleComponent { 
    }
    
  • Les métadonnées sont renseignées dans des paramètres dans le décorateur @Component(). Les paramètres les plus courants sont:
    • selector indiquant où la vue sera affiché dans l’application
    • styleUrls qui est un tableau indiquant les chemins des fichiers CSS permettant de définir des styles ou des classes CSS utilisés dans la vue du composant.

    Ces paramètres sont facultatifs:

    @Component({ 
      selector: 'app-example', 
      templateUrl: './example.component.html', 
      styleUrls: ['./example.component.css'] 
    })
    export class ExampleComponent { 
    }
    
Paramètre selector

Le paramètre selector peut être utilisé si l’affichage du composant n’est pas géré par un router Angular. La valeur de ce paramètre peut être utilisée pour indiquer où la vue du composant sera rendue dans l’application.
Par exemple, si la valeur du paramètre selector est 'app-example' alors la vue du composant sera rendue si un autre composant contient dans son template:

<app-example></app-example>

D’autres conditions sont possibles pour afficher le composant:

  • Si le paramètre selector est selector: '[app-example]' alors le composant sera affiché dans l’élément HTML contenant l’attribut app-example, par exemple:
    <span app-example></span> 
    
  • Si le paramètre selector est selector: '.app-example' alors le composant sera affiché dans l’élément HTML dont la classe CSS est app-example, par exemple:
    <span class="app-example"></span>
    
Binding

Il existe différent type de bindings correspondant à des interactions différentes entre le template et la classe du composant:
Interpolation
Exécution d’une expression Typescript contenant des membres ou des fonctions publics dans la classe du composant:

Template
<div>{{textToDisplay}}</div>
Classe du composant
@Component({ ... }) 
export class ExampleComponent { 
     textToDisplay = 'Texte à afficher';
}

Property binding
Permet de mettre à jour le contenu d’une propriété DOM d’un élément affiché avec la valeur d’un membre dans la classe du composant.

Template
<div [innerText]="textToDisplay"></div>
Classe du composant
@Component({ ... }) 
export class ExampleComponent { 
     textToDisplay = 'Texte à afficher';
}

Attribute binding
Permet de mettre à jour le contenu d’un attribut d’un élément HTML avec la valeur d’un membre dans la classe du composant:

Template
<input type="text" [attr.value]="textToDisplay" />

ou

<input type="text" attr.value="{{textToDisplay}}" />
Classe du composant
@Component({ ... }) 
export class ExampleComponent { 
     textToDisplay = 'Texte à afficher';
}

Event binding
Déclenche l’exécution d’une fonction dans la classe du composant à partir du déclenchement d’un évènement dans un objet du DOM:

Template
<p>{{valueToDisplay}}</p>
<button (click)="incrementValue()">Increment</button>
Classe du composant
@Component({ ... }) 
export class ExampleComponent { 
    valueToDisplay = 0;

    incrementValue(): void {
      this.valueToDisplay++;
   }
}

Two-way binding
Permet de renseigner la valeur d’une propriété d’un composant enfant et d’être notifié des changements de valeur de cette propriété de façon à mettre à jour le membre d’un composant parent:

  • Composant enfant:
    Template
    <button (click)='incrementValue()'>Increment</button> 
    Classe du composant
    @Component({ 
      selector: 'counter', 
      templateUrl: './counter.component.html' 
    }) 
    export class CounterComponent { 
    
      @Input() count: number; 
      @Output() countChange: EventEmitter<number>= 
          new EventEmitter<number>();
    
      incrementValue(): void { 
        this.count++;
        this.countChange.emit(this.count); 
      } 
    } 
    
  • Composant parent:
    Template
    <counter [(count)]="bindedCount"></counter> 
    <p>Count: {{bindedCount}}</p> 
    Classe du composant
    @Component({ 
      selector: 'app-parent', 
      templateUrl: './parent.component.html' 
    }) 
    export class ParentComponent { 
      bindedCount = 0;
    } 
    
Variable référence

Une variable référence permet de nommer un élément HTML dans le template de façon à utiliser des propriétés de cet élément dans d’autres parties du template:

  • Référencer un élément HTML (contenu statique):
    Template
    <p><input #inputElement type='text' value="Valeur initial"/></p>
    <p>Contenu de input: {{inputElement.value}}</p> 
    Classe du composant
    @Component({ ... }) 
    export class ExampleComponent { 
    }
    
  • Accéder à une variable référence à partir de la classe du composant:
    Template
    <div #divElement>Texte à afficher</div>
    Classe du composant
    @Component({ ... }) 
    export class ExampleComponent implements OnInit { 
        @ViewChild('divElement', {static: true}) htmlDivElement: ElementRef;
    
        ngOnInit(): void { 
            console.Log(this.htmlDivElement.nativeElement.innerText);
        } 
    }
    

    Ou

    @Component({ ... }) 
    export class ExampleComponent implements AfterViewInit { 
        @ViewChild('divElement', {static: false}) htmlDivElement: ElementRef;
    
        ngAfterViewInit(): void { 
            console.Log(this.htmlDivElement.nativeElement.innerText);
        } 
    }
    
Directives usuelles

Ces directives permettent d’implémenter des comportements dans le template:

ngIf
Permet d’implémenter l’équivalent d’un if...then...else:

  • Equivalent de if...then:
    Template
    <div *ngIf="condition">Afficher si vrai</div> 
    Classe du composant
    @Component({ ... }) 
    export class ExampleComponent { 
        condition = true; 
    }
    
  • Equivalent de if...then...else:
    Template
    <div *ngIf="condition else whenFalse">Afficher si vrai</div> 
    <ng-template #whenFalse><div>Afficher si faux</div></ng-template> 
    Classe du composant
    @Component({ ... }) 
    export class ExampleComponent { 
        condition = true; 
    }
    

ngFor
Permet d’implémenter l’équivalent d’une boucle for:

  • Template
    <div *ngFor="let item of items">{{item}}</div> 
    Classe du composant
    @Component({ ... }) 
    export class ExampleComponent { 
      items = [ 0, 1, 2, 3, 4 ]; 
    }
    
  • Avec une propriété index contenant l’index de l’élément courant:
    Template
    <div *ngFor="let item of items index as itemIndex">
        Index {{itemIndex}}: {{item}}
    </div>
    Classe du composant
    @Component({ ... }) 
    export class ExampleComponent { 
      items = [ 0, 1, 2, 3, 4 ]; 
    }
    

Les autres propriétés disponibles sont:

  • count contient le nombre d’éléments de la liste.
  • first est un booléen contenant true si l’élément courant est le premier élément de la liste.
  • last est un booléen contenant true si l’élément courant est le dernier élément de la liste.
  • odd contient true si l’index de l’élément courant est impair.
  • even contient true si l’index de l’élément courant est pair.

ngSwitch
Permet d’implémenter l’équivalent de switch...case:

Template
<p>La taille est:</p> 
<div [ngSwitch]="size"> 
    <div *ngSwitchCase="'little'">Petite</div> 
    <div *ngSwitchCase="'medium'">Moyenne</div> 
    <div *ngSwitchCase="'great'">Grande</div> 
    <div *ngSwitchDefault>(inconnue)</div> 
</div>
Classe du composant
@Component({ ... }) 
export class ExampleComponent { 
    size = 'big' 
} 
Références

Fonctionnalités C# 8.0


Le but de cet article est de résumer et d’expliquer les fonctionnalités de C# 8.0. Dans un premier temps, on explicitera le contexte de C# 8 par rapport aux différents frameworks qui permettent de l’utiliser. Ensuite, on rentrera dans le détail des fonctionnalités.
Les fonctionnalités les plus rapides à expliquer se trouvent dans cet article. Les autres fonctionnalités nécessitant davantage d’explications se trouvent dans des articles séparés.

Précisions sur les versions de C#

Depuis C# 7, l’environnement .NET s’est étauffé avec .NET Core. Du code C# peut, ainsi, être compilé à partir de plusieurs frameworks. A partir de C# 8.0, l’environnement historique du framework .NET commence à être remplacé par .NET Core. Ainsi, certaines fonctionnalités de C# 8.0 ne sont pas disponibles dans le framework .NET mais seulement dans .NET Core. Le but de cette partie est d’expliciter les versions des composants .NET en relation avec C# 8.0.

Chronologie des releases

Ce tableau permet de résumer les dates de sorties de C# 8.0, de Visual Studio, du compilateur Roslyn, des versions du framework .NET et de .NET Core.

Date Version C# Version Visual Studio Compilateur Version Framework .NET Version .NET Core
Mai 2018 C# 7.3 VS 2017 (15.7) Roslyn 2.7/2.8 .NET 4.7.2
(NET Standard 1.0⇒2.0)
.NET Core 2.1
(NET Standard 1.0⇒2.0)
Aout 2018 VS 2017 (15.8) Roslyn 2.9
Novembre 2018 VS 2017 (15.9) Roslyn 2.10 .NET Core 2.2
(NET Standard 1.0⇒2.0)
Avril 2019 VS 2019 (16.0) Roslyn 3.0 .NET 4.8
(NET Standard 1.0⇒2.0)
Mai 2019 VS 2019 (16.1) Roslyn 3.1
Aout 2019 VS 2019 (16.2) Roslyn 3.2
Septembre 2019 C# 8.0 VS2019 (16.3) .NET Core 3.0
(NET Standard 1.0⇒2.1)
Novembre 2019 VS2019 (16.4)
Décembre 2019 .NET Core 3.1
(NET Standard 1.0⇒2.1)
Mars 2020 VS2019 (16.5)
Mai 2020 VS2019 (16.6) Roslyn 3.7
Juillet 2020 VS2019 (16.7)
Novembre 2020 C# 9.0 VS2019 (16.8) Roslyn 3.8 .NET 5.0
(NET Standard 1.0⇒2.1)

Lien entre la version C# et le compilateur

Le tableau précédent permet d’indiquer la version de C# dans le contexte des frameworks de façon à avoir une idée des sorties des autres éléments de l’environnement .NET. Toutefois, la version de C# est liée à la version du compilateur C#. Le compilateur est ensuite livré avec Visual Studio (depuis Visual Studio 2017 15.3) ou avec le SDK .NET Core.

Le chemin du compilateur est lié au composant avec lequel il est livré:

  • Avec Visual Studio: par exemple pour Visual Studio 2019 Professional: C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\Roslyn\csc.exe
  • Avec les Build tools: par exemple pour les Build Tools for Visual Studio 2019: C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\Roslyn\csc.exe
  • Avec le SDK .NET Core:
    • Sur Linux: /usr/share/dotnet/sdk/<version>/Roslyn/bincore/csc.dll
    • Sur Windows: C:\Program Files\dotnet\sdk\<version>\Roslyn\bincore\csc.dll

On peut connaître la version du compilateur en tapant:

csc.exe -help

On peut savoir quelles sont les versions de C# que le compilateur peut gérer en exécutant:

csc -langversion:? 

Limiter la version C# à compiler

Par défaut, les versions C# traitées par le compilateur sont:

  • Framework .NET: C# 7.3
  • .NET Core 3.x: C# 8.0
  • .NET Core 2.x: C# 7.3
  • .NET Standard 2.1: C# 8.0
  • .NET Standard 2.0: C# 7.3
  • .NET Standard 1.x: C# 7.3

On peut volontairement limiter la versions C# que le compilateur va traiter.

  • Dans Visual Studio:
    dans les propriétés du projet ⇒ Onglet Build ⇒ Advanced ⇒ Paramètre Language version.
  • En éditant directement le fichier csproj du projet et en indiquant la version avec le paramètre LangVersion:
    <Project Sdk="Microsoft.NET.Sdk"> 
        <PropertyGroup> 
            <OutputType>Exe</OutputType> 
            <TargetFramework>netcoreapp2.0</TargetFramework> 
            <LangVersion>8.0</LangVersion> 
        </PropertyGroup> 
    </Project> 
    

Fonctionnalités C# 8.0

Les fonctionnalités les plus basiques de C# 8.0 sont présentées dans cet article. Les autres fonctionnalités nécessitant davantage d’explications sont présentées dans d’autres articles:

C# 8.0 n’est pas supporté par le framework .NET

Officiellement C# 8.0 est supporté par les frameworks satisfaisant .NET Standard 2.1 c’est-à-dire .NET Core 3.0 et .NET Core 3.1. Ainsi comme le framework .NET satisfait au maximum avec .NET Standard 2.0, il ne permet pas officiellement de compiler du code C# 8.0.

Microsoft ne fait plus évoluer les fonctionnalités du CLR du framework .NET ce qui exclut les fonctionnalités nécessitant une modification du CLR. Pour les autres fonctionnalités qui ne concernent que des éléments de syntaxe, il est possible de les utiliser parfois avec quelques aménagements.

Les fonctionnalités directement compatibles avec .NET Standard 2.0 sont:

Ces fonctionnalités sont directement utilisables à condition de compiler du code C# 8.0. Par exemple, si on cible le .NET Standard 2.0 avec le SDK .NET Core en indiquant netstandard20 dans la cible du fichier .csproj:

<Project Sdk="Microsoft.NET.Sdk"> 
    <PropertyGroup> 
        <OutputType>Exe</OutputType> 
        <TargetFramework>netstandard20</TargetFramework>
    </PropertyGroup> 
</Project> 

On obtiendra une erreur de compilation indiquant que la fonctionnalité n’est pas disponible en C# 7.3 (car, par défaut, pour générer une assembly .NET Standard 2.0 le compilateur compile du code C# 7.3):

error CS8370: Feature 'unmanaged constructed types' is not available in C# 7.3.
  Please use language version 8.0 or greater.

Si on précise explicitement la version C#, l’erreur n’est plus générée à la compilation:

<Project Sdk="Microsoft.NET.Sdk"> 
    <PropertyGroup> 
        <OutputType>Exe</OutputType> 
        <TargetFramework>netstandard20</TargetFramework>
        <LangVersion>8.0</LangVersion>
    </PropertyGroup> 
</Project> 

D’autres fonctionnalités ne sont pas supportées toutefois il est possible de les utiliser en implémentant les types manquants:

La fonctionnalité “Méthode d’interface par défaut” (i.e. default interface members) n’est pas compatible car elle nécessite une modification du CLR.

Fonction locale statique

C# 7.0 a permis de déclarer une fonction à l’intérieur d’une autre fonction. Cette fonction locale permet d’accéder aux variables et arguments de la fonction parente:

IEnumerable<int> GetPositiveNumber(IEnumerable<int> numbers, bool strictComparison)
{
  return numbers.Where(n => isPositive(n));

  bool isPositive(int number)
  {
    return strictComparison ? number > 0 : number >= 0;
  }
}

A partir de C# 8.0, la fonction locale peut être statique pour ne pas avoir accès au contexte de la fonction parente:

IEnumerable<int> GetPositiveNumber(IEnumerable<int> numbers, bool strictComparison)
{
  return numbers.Where(n => isPositive(n, strictComparison));

  static bool isPositive(int number, bool isStrict)
  {
    return isStrict ? number > 0 ; number >= 0;
  }
}

Utilisation de using sans bloc de code

Avant C# 8.0, using devait obligatoirement être suivi d’un bloc de code:

using (<objet satisfaisant IDisposable>) 
{ 
  // Bloc de code
  // ... 
} 

C# 8.0 permet d’utiliser using sans bloc de code. La portée de l’objet concerné par using correspond au bloc de code dans lequel se trouve using. La méthode Dispose() sera exécutée à la sortie de ce bloc de code.

Par exemple, dans le cas d’une méthode:

public void UseDisposableObject() 
{ 
  using var disposableObject = new DisposableObject(); 

  // Utilisation de disposableObject 
  // ... 

  // disposableObject.Dispose() est exécuté juste avant la sortie de la méthode 
} 

Dans le cas d’un bloc de code explicite:

public void UseDisposableObject() 
{ 
  {
    using var disposableObject = new DisposableObject(); 
    // Utilisation de disposableObject 
    // ...

    // disposableObject.Dispose() est exécuté juste avant la sortie du bloc 
  } 

  // A ce niveau disposableObject est hors de portée 
}

Méthode d’interface par défaut

Il est désormais possible, à partir de C# 8.0, de fournir une implémentation par défaut d’une méthode au niveau d’une interface, par exemple:

public interface IQuadrangle
{
  int Length { get; }

  int Width { get; }

  int GetArea()
  {
    return this.Length * this.Width;
  }
}

L’implémentation de la méthode GetArea() se trouve directement au niveau de l’interface IQuadrangle. Dans cette méthode, il est possible d’accéder à des propriétés déclarées dans l’interface comme Length et Width.

Pour rendre la lecture plus facile, on peut utiliser la version réduite de la syntaxe d’une méthode (disponible à partir de C# 6.0):

public interface IQuadrangle
{
  // ...
  int GetArea() => this.Length * this.Width;
}

Les règles liées à l’implémentation d’une méthode dans une interface sont différentes de celles appliquées dans le cas d’un héritage: la méthode n’est accessible que pour les variables dont le type est celui de l’interface. Cela signifie que:

  • La méthode n’est pas accessible si une variable est d’un type différent de l’interface.
  • Il n’y a pas de règles liées à l’héritage en utilisant new ou override.

Par exemple, si on considère la classe suivante satisfaisant IQuadrangle:

public class Rectangle : IQuadrangle
{
  public Rectangle(int length, int width)
  {
    this.Length = length;
    this.Width = width;
  }

  public int Length { get; }
  public int Width { get; }
}

Accessible avec le type de l’interface

On peut utiliser la méthode si la variable est du type de l’interface:

IQuadrangle rect = new Rectangle(2, 3);
int area = rect.GetArea(); // OK

Par contre si on considère le type de la classe, la méthode n’est pas accessible:

Rectangle rect = new Rectangle(2, 3);
int area = rect.GetArea(); // ERREUR: 'Rectangle' does not contain a definition for 'GetArea'.

Modifier l’implémentation dans la classe

On peut réimplémenter la méthode dans la classe. L’implémentation dans la classe sera utilisée en priorité:

public class Rectangle : IQuadrangle
{
  // ...

  public int GetArea()
  {
    Console.WriteLine("From Rectangle");
    return this.Length * this.Width;
  }
}

Quelque soit le type de la variable, l’implémentation utilisée sera celle de la classe:

IQuadrangle quad = new Rectangle(2, 3);
int area = quad.GetArea(); // From Rectangle

Rectangle rect = new Rectangle(2, 3);
int area = rect.GetArea(); // From Rectangle

Accéder à la méthode dans la classe

Accéder à la méthode dans la classe n’est pas direct car la méthode n’est accessible que si on considère le type de l’interface, par exemple:

public class Rectangle : IQuadrangle
{
  // ...

  public int AddToRectangleArea(int otherArea)
  {
    return otherArea + GetArea(); // ERREUR: GetArea() n'est pas accessible dans la classe si elle n'est pas réimplémentée dans la classe
  }
}

Pour accéder à la méthode, il faut considérer l’interface:

public class Rectangle : IQuadrangle
{
  // ...

  public int AddToRectangleArea(int otherArea)
  {
    return otherArea + ((IQuadrangle)this).GetArea(); // OK
  }
}

Implémenter une méthode statique dans l’interface

Les méthodes statiques sont supportées par cette fonctionnalité:

public interface IQuadrangle
{
  int Length { get; }

  int Width { get; }

  static int GetArea(IQuadrangle quadrangle)
  {
    return quadrangle.Length * quadrangle.Width;
  }
}

La méthode étant statique, ne permet pas d’accéder aux propriétés instanciées de l’interface.

Les mêmes règles s’appliquent quant à l’accès de la méthode statique à l’extérieur ou dans une classe satisfaisant l’interface: la méthode n’est accessible que si on considère le type de l’interface. Cependant comme la méthode est statique, l’accès à la méthode se faire en considérant le type de l’interface:

  • A l’extérieur:
    Rectangle rect = new Rectangle(2, 3);
    int area = IQuadrangle.GetArea(rect);
    
  • A l’intérieur de la classe:
    public class Rectangle : IQuadrangle
    {
      // ...
    
      public int GetRectangleArea()
      {
        Console.WriteLine("From Rectangle");
        return IQuadrangle.GetArea(this);
      }
    }
    

Index et plage d’une liste

A partir de C# 8.0, 2 nouveaux types sont supportés par les structures de données de type liste comme System.Array ou les listes génériques:

Le support de ces types par les listes permet de gérer davantage de cas de figure.

System.Index

Cette structure permet de stocker l’index d’une liste à partir du début ou de la fin de la liste en commençant par 0, par exemple:

  • Index à partir du début d’une structure:
    Index index = new Index(2);
    // ou
    Index index = new Index(2, false);
    
  • Index à partir de la fin d’une structure:
    Index index = new Index(2, true); // 2e valeur en partant de la fin
    Index index = new Index(0, true); // ERREUR: le premier index en partant de la fin est 1
    

    Une autre notation possible:

    Index index = ^1; // Dernière valeur de la liste
    Index index = ^2; // 2e valeur en partant de la fin
    Index index = ^0; // ERREUR
    

L’index s’utilise avec une liste:

var array = new int[]{ 0, 1, 2, 3, 4, 5 };
var index = ^1;
int value = array[index];
// ou plus directement
value = array[^1];

System.Range

Cette nouvelle structure est une plage d’index pouvant être utilisée avec une liste. Cette plage comprend un index de début et un index de fin. Utilisée avec une liste, la plage permet d’obtenir une autre liste dont les valeurs correspondent à la plage d’index.

L’index de fin de la plage est exclusif

L’index de fin est exclusif, cela signifie qu’il ne fait pas partie de la plage d’index.
Si on considère une liste d’entiers et la plage d’index suivantes:

var values = new char[] { 'A', 'B', 'C', 'D', 'E', 'F' };
Range range = new Range(0, 3); // Plage de la 1ère à la 3e valeur

values[range] contient les valeurs 'A', 'B' et 'C'. 'D' ne fait pas partie des valeurs de la plage car la plage est définie avec:

  • 0 en tant qu’index de début et
  • 3 en tant qu’index exclusif de fin.

Plusieurs syntaxes sont possibles pour instancier un objet Range:

  • En utilisant la syntaxe courte des index:
    Range range = new Range(2, ^1); // Plage de la 3e à l'avant dernière valeur 
    range = new Range(2, ^0); // Plage de la 3e à la dernière valeur 
    
  • Avec un index ou plusieurs objets de type Index:
    Index startIndex = new Index(0);
    Index endIndex = new Index(2);
    Range range = new Range(0, endIndex);
    range = new Range(startIndex, 2);
    range = new Range(startIndex, endIndex);
    
  • Les plages peuvent utilisées une syntaxe courte:
    Range range = 0..2;
    range = 0..endIndex;
    range= startIndex..endIndex;
    range = 2..^0; // Plage de la 3e à la dernière valeur
    range = 2..^1; // Plage de la 3e à l'avant dernière valeur
    

Les objets Range s’utilise directement avec les listes:

var values = new char[] { 'A', 'B', 'C', 'D', 'E', 'F' };
Range range = 2..^0;
char[] subSet = values[range];

subSet contient les valeurs de values de la 3e à la dernière valeur: 'C', 'D', 'E' et 'F'.

Autre exemple:

range = 2..^1;
subSet = values[range];

subSet contient les valeurs de values de la 3e à l’avant dernière valeur: 'C', 'D' et 'E'.

Une exception System.ArgumentOutOfRangeException est lancée si la plage est en dehors de valeurs disponibles dans la liste:

Range range = 2..7;
char[] subSet = values[range]; // ERREUR: la liste values contient 6 valeurs, le 7e index n'existe pas. 

Si la liste contient les index de la plage:

Range range = 2..6;
char[] subSet = values[range]; // OK

subSet contient les valeurs du 2e index au 5e index (l’index de fin est exclusif): 'C', 'D', 'E' et 'F'.

Si la plage ne permet pas de renvoyer des valeurs alors une exception est lancée:

Range range = 7..^1; // Plage de la 8e à l'avant dernière valeur
char[] subSet = values[range]; // ERREUR: values ne contient que 6 valeurs, l'index 7 n'existe pas.

Amélioration des chaines de caractères textuelles interpolées

Cette fonctionnalité permet de déclarer des chaînes de caractères textuelles interpolées avec $@"..." et @$"...". Avant C# 8.0, seule la syntaxe $@"..." était possible.

Ainsi:

int fileCount = 2;
string interpolatedString = $@"C:\MyRepo contient {fileCount} fichiers.";

est équivalent à:

string interpolatedString = @$"C:\MyRepo contient {fileCount} fichiers.";

Pour rappel, une chaîne de caractères textuelle interpolée correspond à 2 fonctionnalités:

  • Une chaîne de caractères textuelle (i.e. verbatim string literal): déclarée en la préfixant avec @"...". Ce type de chaîne permet d’indiquer un contenu dans lequel il n’est pas nécessaire d’échapper certains caractères spéciaux comme \ (i.e. antislash), retour chariot \r (i.e. carriage return) ou saut de ligne \n (i.e. line feed). Ces caractères sont interprétés directement, par exemple:
    • Avec le caractère \: pour déclarer une chaîne de caractères contenant C:\CustomFolder\InnerFolder\, on peut utiliser la syntaxe "C:\\CustomFolder\\InnerFolder\\" ou @"C:\CustomFolder\InnerFolder\".
    • Avec les caractères \r (i.e. carriage return) et \n (i.e. line feed): pour déclarer une chaîne contenant:
      Retour
      à
      la
      ligne
      

      On peut utiliser la syntaxe: Retour\r\nà\r\nla\r\nligne ou plus directement avec une chaîne textuelle:

      @"Retour
      à
      la
      ligne"
      

    Avec une chaîne de caractères textuelles, le caractère " peut être échappé avec "" (dans le cas d’une chaîne normale, il faut utiliser \").

  • Une chaîne de caractères interpolée: permet de déclarer une chaîne en évaluant une expression entre les caractères {...} par exemple $"La date du jour est: {DateTime.Now}".

    Cette syntaxe permet d’autres raccourcis comme:

    1. Permettre d’aligner des chaînes en indiquant un nombre minimum de caractères avec la syntaxe {<expression>,<nombre de caractères>}:
      • Si le nombre de caractères d’alignement > 0 ⇒ des espaces sont rajoutés à gauche, par exemple:
        int result = 2;
        Console.WriteLine($"Le résultat est: '{result,5}'.");
        

        L’affichage est:

        Le résultat est: '    2'.
        
      • Si le nombre de caractères d’alignement < 0 ⇒ des espaces sont rajoutés à droite, par exemple:
        int result = 2;
        Console.WriteLine($"Le résultat est: '{result,-5}'.");
        

        L’affichage est:

        Le résultat est: '2    '.
        
    2. Formatter une chaîne en utilisant la syntaxe {<expression>:<formattage de la chaîne>}, par exemple:
      DateTime now = DateTime.Now;
      string syntax1 = $"La date du jour est: {now.ToString("dd/MM/yyyy")}.";
      string syntax2 = $"La date du jour est: {now:dd/MM/yyyy}.";
      

      Le contenu de syntax1 et syntax2 est le même:

      La date du jour est: 05/02/2021.
      

      Une liste exhautive des possibilités de formattage d’une chaîne se trouve sur: docs.microsoft.com/fr-fr/dotnet/standard/base-types/composite-formatting.

    Pour échapper les caractères { et } dans une chaîne interpolée, il faut utiliser {{ et }}.

Autres fonctionnalités

Les autres fonctionnalités sont traitées dans d’autres articles:

Références

Unmanaged constructed types (C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.

Le titre de cette fonctionnalité a été gardée en anglais car la traduction française de “constructed type” n’est pas vraiment utilisée.

Cette fonctionnalité permet d’étendre les types pouvant être utilisés lors d’appels à du code non managés, les arguments de ces appels devant être non managés.

Avant de commencer…

Quelques définitions:

Generic type vs constructed type

Les types génériques (i.e. generic type) sont des types comportant des arguments de type avec < >. Il peut n’y avoir aucun argument ou plusieurs arguments, par exemple:

  • int est un type non générique car il ne comporte pas de <>.
  • List<> est un type générique même s’il n’y a aucun argument de type. Il n’est pas possible d’instancier une classe de type List<> toutefois le type List<> existe.
  • List<T> est un type générique avec un argument de type non défini.
  • List<int> est un type générique avec un argument de type défini.
  • Dictionary<int, string> est un type générique avec des arguments définis.

Un constructed type est un sous-ensemble des types génériques, il s’agit d’un type générique comportant au moins un argument de type défini, par exemple:

  • List<> n’est pas un constructed type car il n’y a pas d’argument de type.
  • List<T> n’est pas un constructed type car l’argument de type n’est pas défini.
  • List<int> est un constructed type car l’argument de type est défini.
  • Dictionary<int, T> est un constructed type car il existe au moins un argument de type défini.

Type non managé

Un type non managé (i.e. unmanaged type) est un type d’objet qui peut ne pas être géré par le garbage collector. Les objets de types non managés peuvent, par exemple, être stockés sur la pile et non obligatoirement dans le tas managé. A l’opposé, les types managés ne peuvent pas être stockés sur la pile et sont exclusivement stockés dans le tas managé.

L’ensemble des types non managés est proche de celui des types blittables. Les types blittables qualifient les types dont la représentation est similaire en mémoire entre du code managé et du code natif. Ces types permettent d’effectuer des appels à du code natif en utilisant, par exemple, Platform Invoke. Les types non managés sont des types blittables toutefois l’inverse n’est pas forcément vrai (par exemple des objets complexes comme les classes peuvent devenir blittables suivant certaines conditions).

A l’opposé des types blittables, les types non managés peuvent être utilisés en dehors d’appels à du code natif.

Les types non managés sont:

  • Les types primitifs comme bool, byte, short, int, long, char, double, décimal et leurs équivalents non signés.
  • Les types enum
  • Le type pointeur
  • Les structures non managées c’est-à-dire les structures ne comportant que des membres non managés.

Par exemple, si on considère le code suivant:

int simpleInt = 5;
unsafe
{
  int* ptr = &simpleInt;
  Console.WriteLine(new IntPtr(ptr));
}

Ce code permet d’afficher la valeur d’un pointeur d’un entier stocké sur la pile. L’objet simpleInt de type int est stocké sur la pile et comme int est un type non managé, le compilateur autorise à utiliser l’opérateur & pour obtenir un pointeur vers cet objet. Ainsi en dehors de tableau et du type string, si le compilateur autorise & alors le type est non managé.

Obtenir un pointeur vers un objet de type référence avec fixed

A partir de C# 7.3, il est possible de manipuler un pointeur vers n’importe quel objet de type référence stocké dans le tas managé avec le mot-clé fixed (pour plus de détails, voir Amélioration de fixed en C# 7).

Structure managée vs structure non managée

Si on considère la structure suivante:

public struct SimpleStruct
{
  public int innerInt; // Type non managé
}

Cette structure est non managée car elle ne contient qu’un membre qui est non managé, on peut donc écrire:

SimpleStruct simpleStruct = new SimpleStruct() { innerInt = 5 };
unsafe
{
  SimpleStruct* ptr = &simpleStruct; // OK
  // ...
}

Pas d’erreur, on peut obtenir un pointeur avec &.

Si on modifie la structure en ajoutant un membre de type référence:

public struct SimpleStruct
{
  public int innerInt; // Type non managé
  public List<int> intList; // Type managé
}

On ne peut plus extraire le pointeur car la structure n’est plus un type non managé. Le membre intList est un objet de type référence stocké dans le tas managé donc la structure est stockée dans le tas managé. Par suite elle devient un type managé:

SimpleStruct simpleStruct = new SimpleStruct() {
  innerInt = 5,
  intList = new List<int>()
};
unsafe
{
  SimpleStruct* ptr = &simpleStruct; // ERREUR
  // ...
}

Le compilateur n’autorise plus l’utilisation de &:

Error CS0208: Cannot take the address of, get the size of, or declare a pointer to a managed type ...

Enfin une structure non managée peut aussi être stockée dans le tas managé si elle est le membre d’un objet stocké dans le tas managé.

Les tableaux

Les objets de type référence sont managés toutefois les tableaux qui sont des objets de type référence peuvent être non managés. Par exemple, un tableau peut être alloué sur la pile en utilisant stackalloc.

Constructed types non managés

C# 8.0

Avant C# 8.0, tous les objets avec un constructed type étaient managés. A partir de C# 8.0, les contructed types peuvent être non managés si les paramètres de type sont non managés.

Par exemple, si on considère la structure:

public struct GenericStruct<T1, T2>
{
  public T1 innerVar1;
  public T2 innerVar2;
}

Cette structure est générique avec des arguments de type non définis. Si on l’utilise avec des type non managés alors cette structure sera non managées:

var genericStruct = new GenericStruct<int, float>{
  innerVar1 = 1,
  innerVar2 = 1f;
};

unsafe
{
  GenericStruct<int, float>* ptr = &genericStruct; // OK la structure est non managée
  Console.WriteLine(ptr->innerVar1);
  Console.WriteLine(ptr->innerVar2);
}

En revanche, si un argument de type ne permet pas de créer des objets non managés, la structure ne pourra pas être non managées:

var genericStruct = new GenericStruct<int, string>{
  innerVar1 = 1,
  innerVar2 = "";
};

unsafe
{
  GenericStruct<int, string>* ptr = &genericStruct; // ERREUR genericStruct est managée
  // ...
}

string est un type référence donc la structure ne permet pas de créer des objets non managés.

Contrainte unmanaged

C# 7.3

A partir de C# 7.3, on peut utiliser la contrainte unmanaged pour indiquer qu’un argument de type doit être non managé:

public struct StructName<T> where T: unmanaged {}

Par exemple, si on prend l’exemple précédent, on peut définir la structure suivante:

public struct GenericStruct<T1, T2>
  where T1: unmanaged
  where T2: unmanaged
{
  public T1 innerVar1;
  public T2 innerVar2;
}

Il devient alors impossible d’instancier des objets avec des arguments de type qui ne sont pas non managés:

var genericStruct = new GenericStruct<int, float>(); // OK
var genericStruct = new GenericStruct<int, string>(); // ERREUR

Enumérer de façon asynchrone (C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.

C# 8.0 apporte un cas d’utilisation supplémentaire au pattern async/await en permettant d’énumérer de façon asynchrone (i.e. asynchronous streams).

Rappels concernant yield

Pour rappel, le mot-clé yield a été introduit en C# 2.0 de façon à créer un énumérateur à la volée et de contrôler l’énumération avec:

  • yield return pour renvoyer un objet à la volée lors de l’énumération.
  • yield break pour arrêter l’énumération.

Par exemple, si on considère la fonction suivante:

public int[] GetRandomNumbers(int numberCount)  
{  
  Random random = new Random();  
  var numbers = new int[numberCount];  
  for (int i = 0; i < numberCount; i++)  
  {  
    int number = random.Next(0, 100);  
    Console.WriteLine($"Generating {number}");  

    numbers[i] = number;  
  }  

  return numbers;  
}   

A l’exécution, tout le tableau de nombre devra être complêté pour sortir de la fonction et pour commencer l’énumération dans la boucle foreach:

foreach (var randomNumber in GetRandomNumbers(10000))  
{  
  Console.WriteLine(randomNumber);  
  if (randomNumber == 3)  
  {  
    Console.WriteLine("3 found");  
    break;  
  }  
}  

Tant que la fonction GetRandomNumbers() n’a pas terminé son exécution, l’énumération ne peut pas commencer. Ainsi si randomNumber est égal à 3 à la 1ère itération, on aura généré 9999 nombres inutilement puisqu’ils ne serviront pas pour le reste de l’exécution:

Generating 3
Generating 32   
Generating 78  
...  
Generating 53  
3 found  

Si on utilise yield, l’implémentation devient:

public IEnumerable<int> GetRandomNumbers(int numberCount)  
{  
  Random random = new Random();  
  for (int i = 0; i < numberCount; i++)  
  {  
    int number = random.Next(0, 100);  
    Console.WriteLine($"Generating {number}");  
    yield return number;  
  }  
}  

A l’exécution de la boucle foreach, l’énumération commence sans exécuter GetRandomNumbers() complêtement. Chaque itération va exécuter le contenu de GetRandomNumbers(), si randomNumber est égal à 3 à la 1ère itération, le contenu de GetRandomNumbers() ne sera exécuté qu’une seule fois:

Generating 3
3 found  

Enumérer de façon asynchrone

C# 8.0 permet d’effectuer une énumération avec yield de façon asynchrone. Dans la fonction dans laquelle se trouve yield, la syntaxe doit être du type:

async IAsyncEnumerable EnumerationFunction(...)  
{  
  // ...  

  await ... // Code exécuté de façon asynchrone  
  yield return ... // Valeur renvoyée lors de l'énumération
}  

Syntaxe de l’énumération

Avec await foreach

Pour effectuer l’énumération asynchrone, on peut utiliser une boucle await foreach:

await foreach (var item in EnumerationFunction())  
{  
  // ...  
}  

Par exemple, si on reprend l’exemple précédent:

public async IAsyncEnumerable<int> GetRandomNumbers(int numberCount)  
{  
  Random random = new Random();  
  for (int i = 0; i < numberCount; i++)  
  {  
    int number = await Task.Run(() => random.Next(0, 100)); // Code exécuté de façon asynchrone
    Console.WriteLine($"Generating {number}");  
    yield return number; // Valeur renvoyée lors de l'énumération
  }  
}  

L’énumeration avec await foreach peut être effectuée de cette façon:

await foreach (var randomNumber in GetRandomNumbers(10000))  
{  
  Console.WriteLine(randomNumber);  
  if (randomNumber == 3)  
  {  
    Console.WriteLine("3 found");  
    break;  
  }  
}  

Avec une itération manuelle

Dans le cas où on itère de façon manuelle c’est-à-dire sans utiliser foreach, on peut utiliser:

Dans le cas de l’exemple précédent, le code deviendrait:

await using (IAsyncEnumerator<int> enumerator = GetRandomNumbers(10000).GetAsyncEnumerator())  
{  
  while (await enumerator.MoveNextAsync())  
  {  
    if (enumerator.Current == 3)  
    {  
      Console.WriteLine("3 found");  
      break;  
    }  
  }  
}  

Cette syntaxe dispose l’objet IAsyncEnumerator<T> de façon asynchrone avec await using (voir “Disposer des objets de façon asynchrone” pour plus de détails).

Implémentation avec ConfigureAwait(false)

Il est possible de rajouter ConfigureAwait(false) pour optimiser le code en évitant d’utiliser le contexte d’exécution d’origine lors de l’exécution de la continuation avec async/await.

Par exemple:

  • Dans une boucle await foreach:
    await foreach (var randomNumber in GetRandomNumbers(10000).ConfigureAwait(false))  
    {  
      // ...  
    } 
    
  • Dans le cas d’une itération manuelle:
    IAsyncEnumerator<int> enumerator = GetRandomNumbers(10000).GetAsyncEnumerator();  
    await using (var _ = enumerator.ConfigureAwait(false))  
    {  
      // ...  
    }  
    

    Ou

    IAsyncEnumerator<int> enumerator = GetRandomNumbers(10000).GetAsyncEnumerator();  
    await using var _ = enumerator.ConfigureAwait(false);  
    
    // ...  
    

Utiliser un CancellationToken

On peut introduire un CancellationToken pour interrompre l’exécution du code asynchrone.

Par exemple:

  • Dans une boucle await foreach:
    var tokenSource = new CancellationTokenSource();  
    await foreach (var randomNumber in GetRandomNumbers(10000).WithCancellation(tokenSource.Token))  
    {  
      // ...  
    }  
    
  • Dans le cas d’une itération manuelle:
    var tokenSource = new CancellationTokenSource();  
    var token = tokenSource.Token; 
    await using (var enumerator = GetRandomNumbers(10000).WithCancellation(token).GetAsyncEnumerator())  
    {  
      // ...  
    }  
    

    Ou

    await using var enumerator = GetRandomNumbers(10000).WithCancellation(token).GetAsyncEnumerator();   
    // ...  
    

Disposer des objets de façon asynchrone (C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.

C# 8.0 apporte un cas d’utilisation supplémentaire au pattern async/await en permettant de disposer des objets de façon asynchrone avec using (i.e. asynchronous disposable).

Rappels sur async/await

Pour rappel async/await est apparu avec C# 5 de façon à faciliter l’implémentation de traitements asynchrones. Brièvement la syntaxe de cette implémentation doit être utilisée de cette façon:

  • async est placé dans la signature de la fonction comportement un traitement pouvait être exécuté de façon asynchrone.
  • await est placé devant l’instruction générant une tâche dont l’exécution est lancée et dont il faudra attendre la fin de l’exécution. L’instruction doit générer un objet dont le type est “awaitable” comme Task, Task<T>, ValueTask ou ValueTask<T> (ValueTask et ValueTask<T> apparus à partir de C# 7.0 sont des objets de type valeur équivalent à Task et Task<T>).
  • Dans une méthode asynchrone, tout ce qui se trouve après l’instruction await sera exécutée sous la forme d’une continuation.
  • Une méthode dont le corps contient une ou plusieurs instructions avec await doit comporter async dans sa signature.
  • Si une méthode contient async, il n’est pas obligatoire que le corps de la méthode contienne await. Si une méthode async ne contient aucune instruction avec await, l’exécution sera effectuée de façon synchrone.

Par exemple, si on considère la fonction suivante:

public async Task ExecuteAsynchronously() 
{ 
  await Task.Delay(10000); // On simule l'exécution d'un traitement dans une Task 
} 

On peut appeler cette méthode de cette façon:

// La méthode appelante doit comporter async si on utilise await
public async Task CallingMethod()  
{ 
  await ExecuteAsynchronously(); 
} 

Ou

// async n'est pas nécessaire si on n'utilise pas await 
public void CallingMethod()  
{ 
  // On ne fait qu'attendre le résultat d'une Task 
  ExecuteAsynchronously().Wait();  
  // ou 
  ExecuteAsynchronously().Result; 
} 

Le code dans ExecuteAsynchronously() est exécuté de façon asynchrone par rapport au code se trouvant à l’extérieur de ExecuteAsynchronously(). Cela signifie que:

  • L’appel await ExecuteAsynchronously() rendra la main immédiatement au code appelant de façon à ne pas bloquer son exécution.
  • Le code dans ExecuteAsynchronously() sera exécuté dans une Task différente du code appelant. Concrêtement et suivant le contexte d’exécution, cette Task pourra être exécutée dans un thread différent du thread appelant mais ce n’est pas obligatoire.
  • Dans l’appel, await signifie que le code appelant va attendre la fin de l’exécution de la Task dans ExecuteAsynchronously() pour continuer l’exécution.

Exécution asynchrone ne signifie pas forcément que l’exécution s’effectue en parallèle:

  • Exécution asynchrone signifie que les exécutions de tâches sont effectuées sans qu’elles ne soient synchronisées. Une exécution de tâches asynchrones peut être effectuée par un seul thread de façon séquentielle.
  • Exécution en parallèle signifie que des tâches peuvent être exécutées éventuellement dans des threads différents et tirer parti d’un processeur multithreadé ou d’une machine multiprocesseurs.

Si on souhaite exécuter du code de façon concurrente avec le code dans ExecuteAsynchronously(), on peut utiliser la syntaxe suivante:

Task asyncTask = ExecuteAsynchronously(); 
// Code exécuté de façon concurrente 
// ... 

await asyncTask; 
// Code exécuté sous forme de continuation 
// ... 

On peut se rendre de l’aspect asynchrone de l’exécution de code avec await dans le cas d’une implémentation d’une interface graphique avec une boucle de messages comme en WPF ou en WinForms:

  • Si on lance des traitements de façon synchrone dans le thread graphique, la boucle de message de l’interface est bloquée et les éléments graphiques sont inaccessibles ou bloqués pendant le traitement.
  • Si on lance des traitements de façon asynchrone avec await dans le thread graphique, la boucle de message est toujours exécutée et les éléments graphiques restent accessibles pendant le traitement.

Disposer des objets de façon asynchrone

Le garbage collector (GC) permet de gérer la durée de vie des objets managés d’un processus .NET: lorsqu’un objet n’apparait plus dans le graphe des objets indiquant les dépendances entre les objets, il est “supprimé”. Cette suppression se fait en récupérant l’espace mémoire occupé lors de l’étape Collect.

Finalize() et Dispose()

Avant l’étape Collect, il peut être nécessaire d’exécuter du code pour libérer des dépendances de l’objet à collecter:

  • Méthode Finalize(): cette méthode n’est pas directement exposée au développeur. Le garbage collector l’exécute avant l’étape Collect. Une partie du code de cette méthode peut être personnalisé en implémentant un destructeur, par exemple pour libérer des dépendances non managées. En effet, le garbage collector n’intervient que sur les objets managés du processus, si le processus manipule des objets non managés, il faut les libérer explicitement.
  • Méthode Dispose(): cette méthode est implémentée dans les objets satisfaisant l’interface IDisposable:
    public class DisposableObject : IDisposable  
    {  
      public void Dispose()  
      {
        // ...
      }
    }
    

Le garbage collector n’exécute pas la méthode Dispose(), elle doit être appelée explicitement par du code de façon à libérer des dépendances managées ou non managées d’un objet. L’appel à Dispose() peut se faire en appelant directement cette méthode ou en utilisant using:

using (<objet satisfaisant IDisposable>)  
{  
  // ...
}  

A la fin du bloc de code using, la méthode Dispose() de l’objet satisfaisant IDisposable sera exécutée.

Par exemple:

var disposableObject = new DisposableObject(); 
using (disposableObject) 
{ 
  // ... 
} 
// ATTENTION: à ce stade disposableObject ne doit pas être utilisé car Dispose() a été exécuté 

Ou plus directement:

using (var disposableObject = new DisposableObject()) 
{ 
  // ... 
} 

L’exécution de la méthode Dispose() ne dispense pas le garbage collector d’exécuter la méthode Finalize() lorsque l’objet est libéré. Pour éviter au garbage collector d’exécuter inutilement Finalize(), on peut rajouter GC.SuppressFinalize() dans la méthode Dispose() de façon à indiquer au GC qu’il n’est pas nécessaire d’exécuter Finalize().

Dans le cas où il est nécessaire de libérer des dépendances d’un objet, une bonne implémentation doit:

  1. Satisfaire IDisposable
  2. L’implémentation de la méthode Dispose() doit indiquer au garbage collector de ne pas exécuter Finalize() avec GC.SuppressFinalize(),
  3. Eventuellement libérer des dépendances managées et non managées dans la méthode Dispose().
  4. Comporter un destructeur si des dépendances non managées doivent être libérées. L’implémentation du destructeur peut être nécessaire si la méthode Dispose() n’est pas appelée explicitement dans tous les cas de figure.
  5. Le destructeur peut libérer les dépendances non managées et supprimer les liens entre l’objet et d’autres objets managés en affectant null aux données membres. Supprimer ce lien permet au garbage collector de supprimer les instances inutiles dans le graphe des objets.
  6. L’appel à Dispose() ne doit être fait qu’une seule fois: l’implémentation doit être idempotente c’est-à-dire qu’elle doit permettre d’être exécutée plusieurs fois mais les objets ne doivent être libérés qu’une seule fois. Ainsi avant d’exécuter Dispose(), il est nécessaire de vérifier que l’exécution n’a pas été déjà effectuée (c’est l’intérêt du membre disposed dans l’implémentation de DisposableObject plus bas).

Dans la documentation, on peut trouver un exemple d’implémentation prenant en compte tous ces éléments. Si on prend l’exemple d’une dépendance de type SqlConnection à libérer (System.Data.SqlClient.SqlConnection dérive de System.Data.Common.DbConnection qui satisfait IDisposable):

class DisposableObject : IDisposable // (1) 
{  
  bool disposed = false; 
  private SqlConnection dependency = new SqlConnection("Test connection");  

  public void Dispose()  
  {  
    this.Dispose(true); // (3)

    GC.SuppressFinalize(this); // (2) 
  }  
  
  private void Dispose(bool disposing)  
  {  
    if (this.disposed) // (6) 
      return;  

    if (disposing) 
    {  
      // Libération de dépendances managées  
      if (this.dependency != null) 
        this.dependency.Dispose(); 
    }  

    // On supprime le lien entre DisposableObject et l’instance de SqlConnection. 
    this.dependency = null; // (5)

    // Libération de dépendances non managées  
    // ... 

    this.disposed = true;  
  }   

  // Personnalisation de Finalize() si nécessaire 
  ~DisposableObject() // (4)
  {  
    this.Dispose(false); // (5) 
  }  
}  

Avec la syntaxe using, l’exécution de Dispose() est synchrone.

IAsyncDisposable

C# 8.0

A partir de C# 8.0, il est possible d’exécuter du code permettant la libération de ressources de façon asynchrone en utilisant le pattern async/await. L’implémentation est similaire à celle avec IDisposable:

  • Il faut satisfaire IAsyncDisposable
  • Implémenter une méthode dont la signature est:
    public ValueTask DisposeAsync();  
    

    Par exemple:

    public class DisposableObject : IAsyncDisposable  
    {  
      public async ValueTask DisposeAsync()  
      {  
        // ...
      }  
    }  
    

    La méthode DisposeAsync() doit retourner un objet de type ValueTask qui est un objet de type valeur équivalent à l’objet Task.

  • Utiliser la syntaxe await using pour que l’exécution de la méthode DisposeAsync() soit asynchrone:
    await using (<objet satisfaisant IAsyncDisposable>)  
    {  
      // ...  
    }  
    

    Par exemple:

    var disposableObject = new DisposableObject(); 
    await using (disposableObject) 
    { 
      // ... 
    } 
    // ATTENTION: à ce stade disposableObject ne doit pas être utilisé car DisposeAsync() a été exécuté 
    

    Ou plus directement:

    await using (var disposableObject = new DisposableObject()) 
    { 
      // ... 
    } 
    

Une autre syntaxe utilisant ConfigureAwait(false) permet d’optimiser le code en évitant d’utiliser le contexte d’exécution d’origine lors de l’exécution de la continuation avec async/await:

await using (<objet satisfaisant IAsyncDisposable>.ConfigureAwait(false))  
{  
  // ...  
}  

Par exemple:

var disposableObject = new DisposableObject(); 
await using (disposableObject.ConfigureAwait(false)) 
{ 
  // ... 
} 

Ou

var disposableObject = new DisposableObject(); 
await using (System.Runtime.CompilerServices.ConfiguredAsyncDisposable configuredDisposable = 
  disposableObject.ConfigureAwait(false)) 
{ 
  // ... 
} 
// ATTENTION: à ce stade disposableObject ne doit pas être utilisé car DisposeAsync() a été exécuté 

Comme pour IDisposable, l’implémentation doit respecter quelques recommandations:

  1. Indiquer au garbage collector de ne pas exécuter Finalize() avec GC.SuppressFinalize(),
  2. De libérer des dépendances managées de façon asynchrone dans la méthode DisposeAsync().
  3. L’implémentation doit être idempotente.

Dans la documentation, on peut ainsi trouver un exemple d’implémentation. Si on prend l’exemple d’une dépendance de type SqlConnection à libérer (System.Data.SqlClient.SqlConnection dérive de System.Data.Common.DbConnection qui satisfait IAsyncDisposable):

class DisposableAsyncObject : IAsyncDisposable  
{  
  private SqlConnection dependency = new SqlConnection("Test connection");  
   
  public async void DisposeAsync()  
  {  
    await this.DisposeAsyncCore(); // (b)

    GC.SuppressFinalize(this); // (a) 
  }  

  private async ValueTask DisposeAsyncCore() 
  { 
    if (this.dependency != null) 
      await this.dependency.DisposeAsync(); 

    this.dependency = null; 
  } 
}  

Dans le cas d’une implémentation satisfaisant IDisposable et IAsyncDisposable:

class DisposableAsyncObject : IAsyncDisposable, IDisposable 
{  
  bool disposed = false;  
  private SqlConnection dependency = new SqlConnection("Test connection");  

  public async void DisposeAsync()  
  {  
    await this.DisposeAsyncCore(); // (b)
  
    this.Dispose(false); // false pour ne pas disposer les dépendances de façon synchrone. 
    GC.SuppressFinalize(this); // (a)  

  }  

  public void Dispose()  
  {  
    this.Dispose(true); // (3)

    GC.SuppressFinalize(this); // (2) 
  }

  private async ValueTask DisposeAsyncCore() 
  { 
    if (this.dependency != null) 
      await this.dependency.DisposeAsync(); 

    this.dependency = null; 
  }

  private void Dispose(bool disposing)  
  {  
    if (this.disposed) // (c) 
      return;  

    if (disposing) 
    {  
      // Libération de dépendances managées de façon synchrone 
      if (this.dependency != null) 
        this.dependency.Dispose(); 
    }  

    // On supprime le lien entre DisposableObject et l’instance de SqlConnection. 
    this.dependency = null; // (5)

    // Libération de dépendances non managées  
    // ...   

    this.disposed = true;  

  }  

  // Personnalisation de Finalize() si nécessaire 
  ~DisposableAsyncObject() // (4)
  {  
    this.Dispose(false); // (5)
  }  
}  

Quelques remarques supplémentaires concernant l’implémentation:

  • L’implémentation asynchrone avec IAsyncDisposable ne remplace pas l’implémentation avec IDisposable. La première méthode convient pour libérer des dépendances de façon asynchrone quand cela est possible. Si ce n’est pas possible, la meilleure implémentation est d’utiliser la version synchrone.
  • Si la classe implémente IAsyncDisposable, il n’est pas obligatoire d’implémenter IDisposable.
  • Si on libère des dépendances de façon synchrone dans DisposeAsync(), ces exécutions seront synchrones et il n’y aura pas de libération asynchrone des dépendances.

Utilisation de using sans {…}

C# 8.0

Avant C# 8.0, using devait obligatoirement être suivi d’un bloc de code:

using (<objet satisfaisant IDisposable>) 
{ 
  // ... 
} 

C# 8.0 permet d’utiliser using sans bloc de code. La portée de l’objet concerné par using correspond au bloc de code dans lequel se trouve using. La méthode Dispose() sera exécutée à la sortie de ce bloc de code.

Par exemple, dans le cas d’une méthode:

public void UseDisposableObject() 
{ 
  using var disposableObject = new DisposableObject(); 

  // Utilisation de disposableObject 
  // ... 

  // disposableObject.Dispose() est exécuté juste avant la sortie de la méthode 
} 

Dans le cas d’un bloc de code:

public void UseDisposableObject() 
{ 
  {
    using var disposableObject = new DisposableObject(); 
    // Utilisation de disposableObject 
    // ...

    // disposableObject.Dispose() est exécuté juste avant la sortie du bloc 
  } 

  // A ce niveau disposableObject est hors de portée 
} 

Avec await using

await using est compatible avec cette syntaxe:

public void UseDisposableObject() 
{ 
  await using var disposableAsyncObject = new DisposableAsyncObject(); 

  // Utilisation de disposableAsyncObject 
  // ... 

  // disposableAsyncObject.DisposeAsync() est exécuté juste avant la sortie de la méthode 
} 

Avec ConfigureAwait()

Avec ConfigureAwait(), on peut utiliser la syntaxe:

var disposableObject = new DisposableObject(); 
await using System.Runtime.CompilerServices.ConfiguredAsyncDisposable useless = 
  disposableObject.ConfigureAwait(false); 

Ou plus simplement:

await using var _ = disposableObject.ConfigureAwait(false); 
DisposeAsync() peut ne pas être exécuté en cas d’exception avec ConfigureAwait(false)

Suivant le pattern utilisé dans le cas où on utilise 2 objets satisfaisant IAsyncDisposable, DisposeAsync() peut ne pas être exécuté.

Par exemple, si on considère la classe suivante:

public class DisposableAsyncObject: IAsyncDisposable 
{ 
  public DisposableAsyncObject(int id) 
  { 
    this.Id = id; 
  } 

  public int Id { get; } 

  public async ValueTask DisposeAsync() 
  { 
    Console.WriteLine($"Instance {this.Id} of DisposableAsyncObject disposed"); 
  } 
} 

Si on exécute le code suivant dans lequel une exception survient:

var instanceOne = new DisposeAsyncObject(1); 
var instanceTwo = new DisposeAsyncObject(2); 

// On lance volontairement une exception 
if (instanceTwo.Id == 2) 
  throw new InvalidOperationException(); 

await using var uselessVarOne = instanceOne.ConfigureAwait(false); 
await using var uselessVarTwo = instanceTwo.ConfigureAwait(false); 

Console.WriteLine(instanceOne.Id); 
Console.WriteLine(instanceTwo.Id); 

A l’exécution, on peut se rendre compte qu’aucune des 2 instances n’est disposée car les lignes await ne sont pas atteintes au moment de l’exception:

Unhandled exception. SystemOperationException: ...

En utilisant await using juste après l’instanciation de l’objet instanceOne, DisposeAsync() sera exécuté même si l’exception survient:

var instanceOne = new DisposeAsyncObject(1); 
await using var uselessVarOne = instanceOne.ConfigureAwait(false); 

var instanceTwo = new DisposeAsyncObject(2); 

// On lance volontairement une exception 
if (instanceTwo.Id == 2) 
  throw new InvalidOperationException();  

await using var uselessVarTwo = instanceTwo.ConfigureAwait(false); 

Console.WriteLine(instanceOne.Id); 
Console.WriteLine(instanceTwo.Id); 

instanceOne.DisposeAsync() est exécutée:

Instance 1 of DisposableAsyncObject disposed
Unhandled exception. SystemOperationException: ... 

Imbriquer les blocs await using permet d’exécuter DisposeAsync() dans le cas des 2 instances:

var instanceOne = new DisposeAsyncObject(1); 
await using (var uselessVarOne = instanceOne.ConfigureAwait(false)) 
{ 
  var instanceTwo = new DisposeAsyncObject(2); 
  await using (var uselessVarTwo = instanceTwo.ConfigureAwait(false)) 
  { 
    // On lance volontairement une exception 
    if (instanceTwo.Id == 2) 
      throw new InvalidOperationException(); 

    Console.WriteLine(instanceOne.Id); 
    Console.WriteLine(instanceTwo.Id); 
  } 
} 

DisposeAsync() est exécuté pour les 2 instances:

Instance 1 of DisposableAsyncObject disposed
Instance 2 of DisposableAsyncObject disposed 
Unhandled exception. SystemOperationException: ... 

Références nullables (C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.

Cette fonctionnalité fait partie des fonctionnalités les plus importantes de C# 8.0. Elle vise à éviter que la référence d’un objet soit nulle par inadvertance.

En C#, les objets de type référence sont manipulés en utilisant une référence permettant d’atteindre l’objet stocké dans le tas managé. Les opérations les plus courantes sur une référence sont:

  • L’initialisation d’une nouvelle référence faite par copie d’une autre référence (la référence est copiée mais pas l’objet en mémoire) ou en utilisant l’opérateur new pour instancier un nouvel objet:
    Circle circle = new Circle();
    

    La classe Circle est:

    public class Circle
    {
      public int Radius;
    
      public void UpdateRadius(int newRadius)
      {
        this.radius = newRadius;
      }
    }
    
  • Le passage d’une référence en argument d’une fonction: lors de l’appel de la fonction, une copie de la référence est effectuée si elle est passée en argument (sauf si on utilise le mot-clé ref).
  • Le déférencement: cette opération permet de déférencer une référence pour utiliser l’objet dont elle fait référence en mémoire. Le déférencement permet, par exemple, d’appeler une méthode de l’objet ou d’accéder à une propriété:
    Circle circle = new Circle(); 
    circle.UpdateRadius(3); // déférencement pour appeler une fonction
    circle.Radius = 5; // déférencement pour accéder à une propriété
    

En C#, l’initialisation, l’affectation, le passage de référence sont des opérations réalisables avec une référence nulle. Le déférencement ne l’est pas, la référence nulle ne pointant sur aucun objet en mémoire. C’est ce dernier cas qui provoque le plus d’erreurs d’inadvertance puisqu’elles provoquent des erreurs de type NullReferenceException. Tony Hoare, qui est le scientifique lauréat du prix Turing à l’origine des références nulles avait qualifié son invention d’erreur à un milliard de dollars à cause de toutes les erreurs que les références nulles ont pu provoquer.

Les références nullables est une fonctionnalité de C# 8.0 visant à empêcher l’utilisation de références nulles en générant des warnings à la compilation lorsque des références potentiellement nulles sont détectées dans le code. Une référence est potentiellement nulle si elle n’est pas initialisée avec une valeur différente de null ou si elle est affectée par la valeur null.

Transformer les warnings en erreurs de compilation

Les warnings générés par le compilateur à la suite d’utilisation de référence nulles ne sont pas bloquants. Il est possible de les rendre bloquant en transformant ces warnings en erreurs en activant l’option TreatWarningsAsErrors. Pour activer cette option:

  • Dans un projet .NET Core: il faut éditer le fichier .csproj du projet et ajouter le nœud <TreatWarningsAsErrors>:
    <Project Sdk="Microsoft.NET.Sdk">  
      <PropertyGroup>  
        <OutputType>Exe</OutputType>  
        <TargetFramework>netcoreapp3.1</TargetFramework>  
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
      </PropertyGroup>  
    </Project> 
    
  • Dans Visual Studio, il faut éditer les options du projet:
    Dans les propriétés du projet ⇒ Onglet “Build” ⇒ dans la partie “TreatWarningsAsErrors” ⇒ sélectionner “All”.

Cette option permettra de générer des erreurs bloquantes à la compilation au lieu des warnings.

Référence nullable vs référence non-nullable

Par défaut, le comportement du compilateur est le même que pour les versions précédents C# 8.0 c’est-à-dire:

  • Le compilateur n’affiche pas de warnings à la compilation dans le cas où une référence est assignée avec null ou si un déférencement est effectué pour une référence potentiellement nulle.
  • Les références nullables ne sont pas possibles: les objets de type valeur nullables (i.e. nullable value types) sont apparus en C# 2.0. Ils sont notés avec la syntaxe <type>? par exemple int? pour un entier nullable. Cette fonctionnalité n’était valable que pour les objets de type valeur et non les objets de type référence (puisqu’une référence peut être nulle).
C# 8.0

A partir de C# 8.0, il est possible de créer des références nullables de la même façon que les objets de type valeur avec la notation <type>?, par exemple:

Circle? Circle = null; 

Le compilateur considère ainsi 2 types de références:

  • Les références non-nullables: ce sont les références normales, elles sont appelées “non-nullables” toutefois, par défaut sans modification de la configuration, elles peuvent être assignées avec la valeur nulle.
  • Les références nullables: elles sont déclarées avec la notation <type>? et elles sont nullables comme les références non-nullables toutefois, par défaut, elles génèrent des warnings si l’option Nullable n’est pas activée:
    Warning CS8632: The annotation for nullable reference types should 
      only be used in code within a '#nullable' annotations context.
    

Ces 2 types de référence prennent leur sens si on active l’option Nullable.

Activer l’option de configuration Nullable

Cette option permet de changer le comportement du compilateur vis-à-vis des références nullables et non-nullables. Il existe plusieurs niveaux de d’activation de cette option. Pour l’activer, il faut ajouter enable, warnings ou annotations dans un nœud Nullable dans le fichier .csproj du projet:

<Project Sdk="Microsoft.NET.Sdk">  
  <PropertyGroup>  
    <OutputType>Exe</OutputType>  
    <TargetFramework>netcoreapp3.1</TargetFramework>  
    <Nullable>enable</Nullable>
  </PropertyGroup>  
</Project>  

Les différents niveaux d’activation de cette option peuvent être résumés de cette façon:

Niveau Comportement général Référence nullable Référence non-nullable
enable Les références nullables sont utilisables et
des warnings sont générés si des références sont potentiellement nulles.
  • L’utilisation de références nullables ne génère pas de warnings.
  • Le déférencement d’une référence nullable potientiellement nulle génère un warning (CS8602).
  • L’affectation de null à une référence non-nullable génère un warning (CS8600).
  • Le déférencement d’une référence non-nullable potentiellement nulle génère un warning (CS8602).
warnings Les références nullables génèrent des warnings et
des warnings sont générés si des références sont potentiellement nulles.
  • L’utilisation de références nullables génère des warnings (CS8632).
  • Le déférencement d’une référence nullable potientiellement nulle génère un warning (CS8602).
annotations Les références nullables sont utilisables et
les warnings ne sont pas générés si des références sont potentiellement nulles
Pas de warnings Pas de warnings
disable
(valeur par défaut)
Les références nullables génèrent un warning et
des warnings ne sont pas générés si des références sont potentiellement nulles.
  • L’utilisation de références nullables génère des warnings (CS8632).
  • Le déférencement d’une référence nullable potientiellement nulle ne génère pas de warning.

Contexte nullable

Le contexte nullable correspond au contexte dans lequel le code est compilé. Ce contexte permet au compilateur de savoir:

  • quelles sont les règles à appliquer pour vérifier la syntaxe,
  • si des références sont nullables et
  • si des références peuvent contenir une valeur nulle et sont déférencées.

Le contexte nullable comporte 2 contextes sous-jacents:

  • Le contexte d’annotation nullable dans lequel l’utilisation des références nullables est possible et ne provoque pas de warnings.
  • Le contexte des warnings pour les références nulles dans lequel des warnings sont générés dans le cas où:
    • Une référence non-nullable est affectée avec la valeur null.
    • Une référence non-nullable est potentiellement nulle et est déférencée.

Il existe 2 méthodes pour indiquer ces contextes:

  • Par configuration: en indiquant le paramètre Nullable au niveau du fichier .csproj. De cette façon, on indique le contexte nullable dans tout le projet.

    Si on reprend le tableau plus haut, on obtient:

    Configuration Contexte d’annotation nullable Contexte des warnings pour les références nulles Référence nullable Référence non-nullable
    enable Activé Activé OK
    Warning en cas de déférencement d’une valeur nulle
    Warnings si nulle
    warnings Désactivé Non autorisé (provoque un warning)
    annotations Activé Désactivé OK
    Pas de warnings en cas de déférencement d’une valeur nulle
    Pas de warnings si nulle
    disable
    (valeur par défaut)
    Désactivé Non autorisé (provoque un warning)
  • Par code: la configuration permet d’indiquer un paramétrage pour tout le projet, ensuite il est possible d’affiner au niveau du code pour indiquer un contexte sur une portion de code. Dans le code, on peut indiquer un contexte sur une portion avec les annotations suivantes:
    Annotation Contexte d’annotation nullable Contexte des warnings pour les références nulles
    #nullable enable Activé Activé
    #nullable disable Désactivé Désactivé
    #nullable restore Les contextes sont ceux indiqués dans la configuration du projet
    #nullable enable warnings Pas de changement Activé
    #nullable disable warnings Désactivé
    #nullable restore warnings Contexte indiqué dans la configuration du projet
    #nullable enable annotations Activé Pas de changement
    #nullable disable annotations Désactivé
    #nullable restore annotations Contexte indiqué dans la configuration du projet

Se prémunir contre les valeurs nulles

La syntaxe C# prévoit quelques opérateurs pour se prémunir contre les valeurs nulles.

Opérateur !. (null-forgiving)

C# 8.0

Cet opérateur utilisable à partir de C# 8.0, est autorisé dans un contexte d’annotation nullable (c’est-à-dire quand il est possible d’utiliser des références nullables <nom variable>?). Il vise à éviter d’avoir le warning correspondant au déférencement d’une référence nullable potentiellement nulle et quand le compilateur ne peut pas déterminer si la référence est nulle ou non. Il n’a pas d’incidence sur l’exécution du code.

Par exemple, si on exécute le code suivant avec les warnings pour les références nulles activés:

int intValue = 5; 
Circle? nullableRef = null; 
if (intValue > 4) 
  nullableRef = new Circle{ Radius = 3 }; 

Console.WriteLine(nullableRef.Radius); // Warning 

A la compilation, ce code produit le warning suivant indiquant un déférencement de la référence nullableRef qui pourrait être nulle:

warning CS8602: Deference of a possible null reference. 

Pour éviter ce warning, on peut:

  • Désactiver les warnings pour les références nulles en modifiant le code de cette façon:
    #nullable disable warnings 
    
    Console.WriteLine(nullableRef.Radius); // Pas de warning
    
    #nullable enable warnings 
    
  • Utiliser l’opérateur null-forgiving:
    Console.WriteLine(nullableRef!.Radius);
    

Comme indiqué plus haut l’opérateur null-forgiving ne protège pas d’erreurs en cas de déférencement d’une référence nullable effectivement nulle:

Circle? nullableRef = null; 
Console.WriteLine(nullableRef!.Radius); // NullReferenceException 

D’autres opérateurs permettent d’éviter des erreurs provenant de références nulles.

Autres opérateurs contre les NullReferenceException

Pour se prémunir des NullReferenceException, on peut utiliser les opérateurs (la plupart de ces opérateurs existent avant C# 8.0):

Opérateur ?. (null-conditional)

L’opérateur null-conditional ?. (à partir de C# 6) s’utilise avec la syntaxe circle?.Radius.

Le comportement est:

  • si circle contient null alors la valeur de circle?.Radius est null.
  • si circle est différent de null alors circle?.Radius correspond à la valeur de Radius.

Opérateur ?[] (null-conditional)

L’opérateur null-conditional ?[] (à partir de C# 6) s’utilise avec des objets contenant des index.

Par exemple dans le cas d’un tableau:

Circle[] circles = new Circle[] { new Circle(), null }; 
Console.WriteLine(circles?[1].Radius); 

Le comportement est:

  • si circles[1] contient null alors la valeur de circles?[1].Radius est null.
  • si circles[1] est différent de null alors circles?[1].Radius correspond à la valeur de Radius.

Opérateur ?? (null-coalescing)

L’opérateur ?? permet d’évaluer si une variable est nulle.

Cet opérateur s’utilise de cette façon:

<variable à évaluer> ?? <expression si la variable est nulle> 

Ainsi:

  • Si <variable à évaluer> contient null alors l’expression retournée est <expression si la variable est nulle>.
  • Si <variable à évaluer> est différent de null alors l’expression retournée est la valeur de <variable à évaluer>.

Par exemple:

Circle firstCircle = new Circle{ Radius = 1}; 
Circle secondCircle = null; 

var radius = (secondCircle ?? firstCircle).Radius; 
Console.WriteLine(radius); // 1 c'est-à-dire la valeur de firstCircle.Radius

secondCircle = new Circle{ Radius = 2}; 

radius = (secondCircle ?? firstCircle).Radius; 
Console.WriteLine(radius); // 2 c'est-à-dire la valeur de secondCircle.Radius

A partir de C# 8.0, pour utiliser l’opérateur ?? la variable à évaluer ne peut pas être un objet de type valeur non nullable.

Opérateur ??=

C# 8.0

L’opérateur ??= permet d’affecter le résultat d’une expression à une variable si cette variable contient null (à partir de C# 8.0).

La syntaxe est du type <variable à évaluer> ??= <expression à affecter si variable null>.

Par exemple:

Circle firstCircle = new Circle{ Radius = 1 }; 
Circle? secondCircle = null; 

secondCircle ??= firstCircle; 
Console.WriteLine(secondCircle.Radius); // 1 

Dans ce code secondCircle ??= firstCircle est équivalent à:

if (secondCircle == null) 
  secondCircle = firstCircle;

Membre d’une structure en lecture seule avec readonly (C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.

Cette fonctionnalité permet d’indiquer que des membres d’une structure ne modifient aucune données membres de cette structure. On peut ne pas comprendre au premier abord l’utilité de cette fonctionnalité car d’autres fonctionnalités déjà existantes (comme readonly struct apparue en C# 7) permettent déjà de rendre une structure immutable. Pour comprendre son intérêt, il faut avoir en tête quelques éléments:

  • Une structure struct est un objet de type valeur stocké le plus souvent sur la pile toutefois elle peut être stockée dans le tas managé si elle satisfait une interface ou si elle est le membre d’un objet de type référence.
  • Une structure ref struct est un objet de type valeur toujours stocké sur la pile.
  • Les affectations ou les passages en argument de méthode d’une structure entraînent une copie par valeur de l’objet. Cette copie peut avoir un impact sur les performances durant l’exécution dans le cas où certaines opérations sont effectuées fréquemment et si la structure contient beaucoup de membres.
  • L’utilisation des mot-clés in ou ref (apparus en C# 7) permettent de manipuler des structures par référence et ainsi éviter des copies lors des affectations ou des passages en argument si la structure est immutable. Dans le cas où la structure n’est pas immutable, le runtime peut effectuer des defensive copies (on explique par la suite ce qu’est une defensive copy) dégrandant elles-aussi les performances.
  • Pour qu’une structure soit immutable par syntaxe, on peut utiliser les mots-clés readonly struct (ou readonly ref struct dans le cas d’une ref struct).

Le gros inconvénient de readonly struct et readonly ref struct est qu’ils rendent la structure complètement immutable et qu’il n’y a pas d’autre granularité possible. C# 8.0 permet d’utiliser readonly à un niveau plus fin en autorisant à l’appliquer sur une méthode membre, des propriétés ou des index.

readonly ne s’applique qu’aux objets struct et ref struct

On peut utiliser le mot-clé readonly sur des méthodes, sur des propriétés ou sur des index d’une structure ou d’un objet de type ref struct pour indiquer au compilateur que l’opération ne modifie pas la structure. Il n’est pas possible d’appliquer ce mot-clé dans le cas d’une classe.

En effet, readonly permet de se prémunir des defensive copies qui peuvent se produire dans le cas d’objet de type valeur comme les structures. Les classes sont des objets de type référence stockés dans le tas managé et manipulés avec des références. Elles ne sont pas concernées par les defensive copies.

A l’opposé, readonly au niveau d’une donnée membre peut s’appliquer dans le cas d’une structure et d’une classe.

Utilisation de readonly sur les membres d’une structure

readonly sur des méthodes membres

readonly peut être appliquer sur des méthodes membres d’une structure.

Par exemple, si on considère la structure suivante:

public struct Circle
{
  public int radius;

  public Circle(int radius)
  {
    this.radius = radius;
  }

  // Modifie une donnée membre
  public void UpdateRadius(int newRadius)
  {
    this.radius = newRadius;
  }

  // Ne modifie pas la structure
  public int AddToRadius(int number)
  {
    return this.radius + number;
  }
}

On souhaite pouvoir utiliser cette structure de façon à ce qu’elle soit immutable et en évitant dans certains cas les defensive copies:

  • Si on appelle seulement AddToRadius(), la structure reste immutable toutefois il peut y avoir quand même des defensive copies car le compilateur ne sait pas si AddToRadius() réellement ou non la structure.
  • Si on rend la structure immutable en la déclarant avec readonly:
    public readonly struct Circle
    { ... }
    

    Il y aura une erreur de compilation car l’affectation dans UpdateRadius() n’est plus possible.

Permettre de rajouter readonly au niveau des fonctions membres, des propriétés ou des index permet d’indiquer l’aspect immutable d’une opération sur une structure à un niveau plus fin pour éviter que toute la structure soit immutable.

Dans le cas de l’exemple précédent, si on modifie le code de cette façon:

public struct Circle
{
  public int radius;

  public Circle(int radius)
  {
    this.radius = radius;
  }

  public void UpdateRadius(int newRadius)
  {
    this.radius = newRadius;
  }

  public readonly int AddToRadius(int number)
  {
    return this.radius + number;
  }
}

Si on exécute seulement la fonction AddToRadius(), la structure est immutable et il n’y a pas de defensive copies. On peut, toutefois, effectuer des opérations rendant la structure mutable avec UpdateRadius().

readonly sur des propriétés

On peut appliquer readonly sur des propriétés de façon à indiquer que l’utilisation de la propriété ne modifie pas la structure.

Par exemple, si on considère l’exemple précédent de la structure Circle, on peut appliquer readonly au niveau d’un accesseur:

  • En lecture seulement:
    public struct Circle
    {
      private int radius;
    
      public int Radius
      {
        readonly get => this.radius;
        set => this.radius = value;
      }
    
      // ...
    }
    
  • En écriture seulement:
    public struct Circle
    {
      private int radius;
    
      public int Radius
      {
        get => this.radius;
        readonly set => Console.WriteLine(value);
      }
    
      // ...
    }
    

    Une erreur survient à la compilation si on applique une opération en écriture avec readonly set => ....

  • En lecture et en écriture en mettant readonly au niveau de la propriété plutôt que des accesseurs:
    public struct Circle
    {
      private int radius;
    
      public readonly int Radius
      {
        get => this.radius;
        set => Console.WriteLine(value);
      }
    
      // ...
    }
    

readonly au niveau d’un index

readonly peut être appliqué au niveau d’un index de la même façon que pour les propriétés. Si on considère la structure suivante:

public struct Numbers
{
  private int[] numbers;

  private Numbers(int count)
  {
    this.numbers = new int[count];
  }

  public int this[int i]
  {
    get => this.numbers[i];
    set => this.numbers[i] = value;
  }
}

On peut indiquer que l’utilisation de l’index ne modifie pas la structure:

  • En lecture seulement:
    public int this[int i]
    {
      readonly get => this.numbers[i];
      set => this.numbers[i] = value;
    }
    
  • En écriture seulement:
    public int this[int i]
    {
      get => this.numbers[i];
      readonly set => this.numbers[i] = value;
    }
    
  • En lecture et en écriture:
    public readonly int this[int i]
    {
      get => this.numbers[i];
      set => this.numbers[i] = value;
    }
    
Ne pas confondre readonly et ref readonly

Même si le mot-clé readonly est utilisé dans les 2 cas, readonly utilisé pour indiquer qu’une opération ne modifie pas une structure et ref readonly sont 2 notions différentes:

  • readonly sur les membres d’une structure permet d’éviter les defensive copies lors d’opérations appliquées à la structure.
  • ref readonly permet d’indiquer qu’un objet de type valeur est manipulé par référence et non par valeur.

Par exemple, si on considère la structure suivante:

public struct IntRefWrapper
{
  private int[] numbers;

  public IntRefWrapper(int count)
  {
    this.numbers = new int[count];
  }

  public readonly ref readonly int GetIntByRef(int index)
  {
    return ref this.numbers[index];
  }
}

Dans la fonction GetIntByRef(), on utilise les 2 notions:

  • public readonly permet d’indiquer que la méthode ne modifie pas la structure.
  • ref readonly int indique le type de retour de la fonction est un objet de type int retourné par référence.

Précisions sur les defensive copies

Pour se rendre compte des defensive copies, on peut considérer l’exemple de la structure suivante:

public struct Circle
{
  public int radius;

  public Circle(int radius)
  {
    this.radius = radius;
  }

  public void UpdateRadius(int newRadius)
  {
    this.radius = newRadius;
  }
}

Cette structure est mutable à cause de la méthode UpdateRadius() qui permet de modifier la donnée membre radius.

Si on considère la méthode suivante:

public static void ChangeRadius(int newRadius, in Circle circle)
{
  circle.UpdateRadius(newRadius);
  Console.WriteLine(circle.radius);
}

Cette méthode utilise le paramètre Circle circle avec le mot-clé in de façon à ce que ce soit une référence du paramètre en lecture seule qui soit utilisée et éviter une copie par valeur de l’objet (pour plus de détails sur in voir Manipuler des objets de type valeur par référence). Le gros inconvénient de in est qu’il entraîne un defensive copy, on peut s’en rendre compte si on exécute le code suivant:

var circle = new Circle(4);
ChangeRadius(3, circle); // 4
Console.WriteLine(circle.radius); // 4

radius contient toujours 4 car in impose que circle dans ChangeRadius() soit en lecture seule. Le compilateur effectue une defensive copy pour assurer que circle n’est effectivement pas modifié dans le corps de ChangeRadius(). On modifie le code de la structure Circle pour afficher l’adresse de l’objet:

public struct Circle
{
  // ...

  public void UpdateRadius(int newRadius)
  {
    // On commente volontairement cette ligne de façon à rendre la structure immutable
    //this.radius = newRadius;

    // Permet d’afficher l’adresse mémoire de l’instance
    unsafe
    {
      fixed (Circle* ptr = &this)
      {
        Console.WriteLine(new IntPtr(ptr));
      }
    }
  }
}

Si on exécute le même code, on s’aperçoit que l’adresse est différente à cause de la defensive copy:

var circle = new Circle(4);

// Permet d’afficher l’adresse mémoire de circle
unsafe
{
  fixed (Circle* ptr = &circle)
  {
    Console.WriteLine(new IntPtr(ptr));
  }
}

ChangeRadius(3, circle);
Console.WriteLine(circle.radius);

Le résultat est:

347086513304
347086512744
4
4

L’adresse est différente à cause de la copie même si la structure est maintenant immutable. Pour empêcher cette copie, on peut indiquer au compilateur que la structure est immutable en modifiant sa déclaration en readonly struct:

public readonly struct Circle
{
  // ...
}

Si on re-éxécute le code, les adresses sont maintenant identiques car il n’y a plus de defensive copy:

950267405944
950267405944
4
4

Comme on l’a indiqué plus haut, on peut éviter de rendre toute la structure immutable avec readonly struct. On peut se contenter d’indiquer au compilateur que l’appel à UpdateRadius() ne modifie pas la structure en rajoutant readonly au niveau de la fonction uniquement. L’implémentation de la structure devient:

public struct Circle
{
  public int radius;

  public Circle(int radius)
  {
    this.radius = radius;
  }

  public readonly void UpdateRadius(int newRadius)
  {
    //this.radius = newRadius;

    // Permet d’afficher l’adresse mémoire de l’instance
    unsafe
    {
      fixed (Circle* ptr = &this)
      {
        Console.WriteLine(new IntPtr(ptr));
      }
    }
  }
}

Si on exécute le code suivant:

ChangeRadius(3, circle);
Console.WriteLine(circle.radius);

Le résultat est:

950267448258
950267448258
4
4

Les adresses mémoire de la structure sont les mêmes avant et après exécution de la méthode UpdateRadius(). Dans ce cas là, il n’y a pas non plus de defensive copy. L’utilisation de readonly au niveau de la méthode permet d’éviter de rendre toute la structure immutable. On peut, ainsi, implémenter dans la même structure:

  • des méthodes qui ne modifient pas de données membres qui seront réservées aux endroits critiques où il ne faut pas que le runtime effectue des defensive copies.
  • d’autres méthodes modifiant éventuellement des données membres dans la structure.

Associer ces 2 types de méthodes n’est pas possible avec une readonly struct.

readonly protège seulement des affectations

Utiliser readonly au niveau des membres d’une structure permet d’empêcher les nouvelles affectations de données membres dans la structure. Si on tente d’effectuer ce type d’affectation, une erreur de compilation surviendra. En revanche, si on tente de modifier une donnée membre sans effectuer d’affectation, il n’y aura pas d’erreur de compilation.

Si on considère le code suivant proche de l’exemple précédent:

public struct Numbers
{
  private int id; // Objet de type valeur
  private List<int> numbers; // Référence vers un objet de type référence

  public Numbers(int id, IEnumerable<int> numbers)
  {
    this.id = id;
    this.numbers = new List<int>(numbers);
  }

  public readonly int ID => this.id;
  public readonly IList<int> numbers => this.numbers;

  public readonly void AddNumber(int newNumber)
  {
    this.numbers.Add(newNumber);
  }

  public void UpdateID(int newId)
  {
    this.id = newId,
  }
}

// ...
public static void ChangeIDAndAddNumber(in Numbers numbers, int newId, int newNumber)
{
  numbers.UpdateID(newId);
  numbers.AddNumber(newNumber);
  Console.WriteLine($" ID: {numbers.ID}; item count: {numbers.Items.Count()}");
}

Dans l’implémentation de la structure, on peut constater que la signature public readonly void AddNumber() n’empêche pas de modifier le membre numbers avec this.numbers.Add(newNumber). Il n’y a pas d’erreur de compilation.

Si on exécute ce code:

var numbers = new Numbers(1, new int[] { 1, 2, 3});
Console.WriteLine($" ID: {numbers.ID}; item count: {numbers.Items.Count()}");
ChangeIDAndAddNumber(numbers, 2, 4);

Le résultat est:

ID: 1; item count: 3
ID: 1; item count: 4

Le résultat est similaire à l’exemple de la partie précédente. La defensive copy qui est effectuée dans la méthode ChangeIDAndAddNumber() entraîne que l’objet d’origine n’est pas modifié, la valeur de ID est toujours la même. En revanche le nombre d’éléments dans la liste numbers est différent car cette liste est modifiée.

Ainsi, readonly ne sert à se prémunir que des defensive copies qui peuvent se produire dans le cas d’objets de type valeur. Le membre numbers dans la structure Numbers est une référence vers une liste qui est un objet de type référence. La liste est stockée dans le tas managée mais la référence numbers (la référence est un objet de type valeur) est stockée dans la pile comme la structure Number. La liste peut être modifiée dans le tas managée toutefois la référence dans la structure n’est pas modifiée. C’est la raison pour laquelle malgré la defensive copy, le nombre d’éléments de la liste change.

Pour résumer, readonly au niveau des membres d’une structure permet d’empêcher de modifier les membres de la structure par affectation toutefois, les membres de type référence peuvent être modifiés.