Partage via


Partie 5, Mettre à jour les pages générées dans une application ASP.NET Core

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Avertissement

Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la Stratégie de prise en charge de .NET et .NET Core. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

L’application de gestion des films générée est un bon début, mais la présentation n’est pas idéale. ReleaseDate doit être écrit en deux mots, Release Date.

Application de gestion des films ouverte dans Chrome

Mettre le modèle à niveau

Mettez à jour Models/Movie.cs avec le code mis en évidence suivant :

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string Genre { get; set; } = string.Empty;

    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}

Dans le code précédent :

  • L’annotation de données [Column(TypeName = "decimal(18, 2)")] permet à Entity Framework Core de mapper correctement Price en devise dans la base de données. Pour plus d’informations, consultez Types de données.
  • L’attribut [Display] spécifie le nom complet d’un champ. Dans le code précédent, Release Date au lieu de ReleaseDate.
  • l’attribut [DataType] spécifie le type de données (Date). Les informations de temps stockées dans le champ ne s’affichent pas.

DataAnnotations est abordé dans le tutoriel suivant.

Accédez à Pages/Movies, puis placez le curseur sur un lien Modifier pour afficher l’URL cible.

Fenêtre de navigateur avec la souris sur le lien Edit et l’URL de lien https://localhost:1234/Movies/Edit/5 affichée

Les liens Edit, Details, et Delete sont générés par le Tag Helper d’ancre dans le fichier Pages/Movies/Index.cshtml.

@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

Les Tag Helpers permettent au code côté serveur de participer à la création et au rendu des éléments HTML dans les fichiers Razor.

Dans le code précédent, le Tag Helper d’ancre génère dynamiquement la valeur d’attribut HTML href dans la page Razor (l’itinéraire est relatif), asp-page et l’ID de l’itinéraire (asp-route-id). Pour plus d’informations, consultez Génération d’URL pour Pages.

Utilisez Afficher la Source dans un navigateur pour examiner le balisage généré. Une partie du code HTML généré est affichée ci-dessous :

<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>

Les liens générés dynamiquement transmettent l’ID de film avec une chaîne de requête. Par exemple, ?id=1 dans https://localhost:5001/Movies/Details?id=1.

Ajouter un modèle d’itinéraire

Mettez à jour les Pages Razor Edit, Details et Delete pour utiliser le modèle de route {id:int}. Remplacez la directive de chacune de ces pages (@page "{id:int}") par @page. Exécuter l’application, puis affichez le code source.

Le code HTML généré ajoute l’ID à la partie de chemin de l’URL :

<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>

Une requête à la page avec le modèle d’itinéraire {id:int} qui n’inclut pas l’entier retourne une erreur HTTP 404 (introuvable). Par exemple, https://localhost:5001/Movies/Details retourne une erreur 404. Pour que l’ID soit facultatif, ajoutez ? à la contrainte d’itinéraire :

@page "{id:int?}"

Testez le comportement de @page "{id:int?}" :

  1. Définissez la directive de page dans Pages/Movies/Details.cshtml sur @page "{id:int?}".
  2. Définissez un point d’arrêt dans public async Task<IActionResult> OnGetAsync(int? id), dans Pages/Movies/Details.cshtml.cs.
  3. Accédez à https://localhost:5001/Movies/Details/.

Avec la directive @page "{id:int}", le point d’arrêt n’est jamais atteint. Le moteur de routage retourne l’erreur HTTP 404. Avec @page "{id:int?}", la méthode OnGetAsync retourne NotFound (HTTP 404) :

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}

Passer en revue la gestion des exceptions d’accès concurrentiel

Passez en revue la méthode OnPostAsync dans le fichier Pages/Movies/Edit.cshtml.cs :

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Attach(Movie).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie.Id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

private bool MovieExists(int id)
{
  return _context.Movie.Any(e => e.Id == id);
}

Le code précédent détecte les exceptions d’accès concurrentiel quand un client supprime le film et que l’autre client envoie les modifications apportées au film.

Pour tester le bloc catch :

  1. Définissez un point d’arrêt sur catch (DbUpdateConcurrencyException).
  2. Sélectionnez Edit (Modifier) pour un film, apportez des modifications, mais ne cliquez pas sur Save (Enregistrer).
  3. Dans une autre fenêtre de navigateur, sélectionnez le lien Delete du même film, puis supprimez le film.
  4. Dans la fenêtre de navigateur précédente, postez les modifications apportées au film.

Dans le code destiné à la production, il est nécessaire de détecter les conflits d’accès concurrentiel. Pour plus d’informations, consultez Gérer les conflits d’accès concurrentiel.

Validation de la publication et de la liaison

Examinez le fichier Pages/Movies/Edit.cshtml.cs :

public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; } = default!;

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null || _context.Movie == null)
        {
            return NotFound();
        }

        var movie =  await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
        if (movie == null)
        {
            return NotFound();
        }
        Movie = movie;
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Movie).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool MovieExists(int id)
    {
      return _context.Movie.Any(e => e.Id == id);
    }

Quand une requête HTTP GET est effectuée sur la page Movies/Edit, par exemple, https://localhost:5001/Movies/Edit/3 :

  • La méthode OnGetAsync extrait le film de la base de données et retourne la méthode Page.
  • La méthode Page restitue la page Pages/Movies/Edit.cshtmlRazor. Le fichier Pages/Movies/Edit.cshtml contient la directive de modèle @model RazorPagesMovie.Pages.Movies.EditModel, ce qui rend le modèle Movie disponible sur la page.
  • Le formulaire d’édition s’affiche avec les valeurs relatives au film.

Quand la page Movies/Edit est postée :

  • Les valeurs de formulaire affichées dans la page sont liées à la propriété Movie. L’attribut [BindProperty] active la liaison de données.

    [BindProperty]
    public Movie Movie { get; set; }
    
  • S’il y a des erreurs dans l’état du modèle, par exemple, si ReleaseDate ne peut pas être converti en date, le formulaire est à nouveau affiché avec les valeurs soumises.

  • S’il n’y a aucune erreur de modèle, le film est enregistré.

Les méthodes HTTP GET dans les pages Razor Index, Create et Delete suivent un modèle similaire. La méthode HTTP POST OnPostAsync dans la page Razor Create suit un modèle semblable à la méthode OnPostAsync dans la page Razor Edit.

Étapes suivantes

L’application de gestion des films générée est un bon début, mais la présentation n’est pas idéale. ReleaseDate doit être écrit en deux mots, Release Date.

Application de gestion des films ouverte dans Chrome

Mettre le modèle à niveau

Mettez à jour Models/Movie.cs avec le code mis en évidence suivant :

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string Genre { get; set; } = string.Empty;

    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}

Dans le code précédent :

  • L’annotation de données [Column(TypeName = "decimal(18, 2)")] permet à Entity Framework Core de mapper correctement Price en devise dans la base de données. Pour plus d’informations, consultez Types de données.
  • L’attribut [Display] spécifie le nom complet d’un champ. Dans le code précédent, Release Date au lieu de ReleaseDate.
  • l’attribut [DataType] spécifie le type de données (Date). Les informations de temps stockées dans le champ ne s’affichent pas.

DataAnnotations est abordé dans le tutoriel suivant.

Accédez à Pages/Movies, puis placez le curseur sur un lien Modifier pour afficher l’URL cible.

Fenêtre de navigateur avec la souris sur le lien Edit et l’URL de lien https://localhost:1234/Movies/Edit/5 affichée

Les liens Edit, Details, et Delete sont générés par le Tag Helper d’ancre dans le fichier Pages/Movies/Index.cshtml.

@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

Les Tag Helpers permettent au code côté serveur de participer à la création et au rendu des éléments HTML dans les fichiers Razor.

Dans le code précédent, le Tag Helper d’ancre génère dynamiquement la valeur d’attribut HTML href dans la page Razor (l’itinéraire est relatif), asp-page et l’ID de l’itinéraire (asp-route-id). Pour plus d’informations, consultez Génération d’URL pour Pages.

Utilisez Afficher la Source dans un navigateur pour examiner le balisage généré. Une partie du code HTML généré est affichée ci-dessous :

<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>

Les liens générés dynamiquement transmettent l’ID de film avec une chaîne de requête. Par exemple, ?id=1 dans https://localhost:5001/Movies/Details?id=1.

Ajouter un modèle d’itinéraire

Mettez à jour les Pages Razor Edit, Details et Delete pour utiliser le modèle de route {id:int}. Remplacez la directive de chacune de ces pages (@page "{id:int}") par @page. Exécuter l’application, puis affichez le code source.

Le code HTML généré ajoute l’ID à la partie de chemin de l’URL :

<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>

Une requête à la page avec le modèle d’itinéraire {id:int} qui n’inclut pas l’entier retourne une erreur HTTP 404 (introuvable). Par exemple, https://localhost:5001/Movies/Details retourne une erreur 404. Pour que l’ID soit facultatif, ajoutez ? à la contrainte d’itinéraire :

@page "{id:int?}"

Testez le comportement de @page "{id:int?}" :

  1. Définissez la directive de page dans Pages/Movies/Details.cshtml sur @page "{id:int?}".
  2. Définissez un point d’arrêt dans public async Task<IActionResult> OnGetAsync(int? id), dans Pages/Movies/Details.cshtml.cs.
  3. Accédez à https://localhost:5001/Movies/Details/.

Avec la directive @page "{id:int}", le point d’arrêt n’est jamais atteint. Le moteur de routage retourne l’erreur HTTP 404. Avec @page "{id:int?}", la méthode OnGetAsync retourne NotFound (HTTP 404) :

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}

Passer en revue la gestion des exceptions d’accès concurrentiel

Passez en revue la méthode OnPostAsync dans le fichier Pages/Movies/Edit.cshtml.cs :

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Attach(Movie).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie.Id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

private bool MovieExists(int id)
{
  return _context.Movie.Any(e => e.Id == id);
}

Le code précédent détecte les exceptions d’accès concurrentiel quand un client supprime le film et que l’autre client envoie les modifications apportées au film.

Pour tester le bloc catch :

  1. Définissez un point d’arrêt sur catch (DbUpdateConcurrencyException).
  2. Sélectionnez Edit (Modifier) pour un film, apportez des modifications, mais ne cliquez pas sur Save (Enregistrer).
  3. Dans une autre fenêtre de navigateur, sélectionnez le lien Delete du même film, puis supprimez le film.
  4. Dans la fenêtre de navigateur précédente, postez les modifications apportées au film.

Dans le code destiné à la production, il est nécessaire de détecter les conflits d’accès concurrentiel. Pour plus d’informations, consultez Gérer les conflits d’accès concurrentiel.

Validation de la publication et de la liaison

Examinez le fichier Pages/Movies/Edit.cshtml.cs :

public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; } = default!;

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null || _context.Movie == null)
        {
            return NotFound();
        }

        var movie =  await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
        if (movie == null)
        {
            return NotFound();
        }
        Movie = movie;
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Movie).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool MovieExists(int id)
    {
      return _context.Movie.Any(e => e.Id == id);
    }

Quand une requête HTTP GET est effectuée sur la page Movies/Edit, par exemple, https://localhost:5001/Movies/Edit/3 :

  • La méthode OnGetAsync extrait le film de la base de données et retourne la méthode Page.
  • La méthode Page restitue la page Pages/Movies/Edit.cshtmlRazor. Le fichier Pages/Movies/Edit.cshtml contient la directive de modèle @model RazorPagesMovie.Pages.Movies.EditModel, ce qui rend le modèle Movie disponible sur la page.
  • Le formulaire d’édition s’affiche avec les valeurs relatives au film.

Quand la page Movies/Edit est postée :

  • Les valeurs de formulaire affichées dans la page sont liées à la propriété Movie. L’attribut [BindProperty] active la liaison de données.

    [BindProperty]
    public Movie Movie { get; set; }
    
  • S’il y a des erreurs dans l’état du modèle, par exemple, si ReleaseDate ne peut pas être converti en date, le formulaire est à nouveau affiché avec les valeurs soumises.

  • S’il n’y a aucune erreur de modèle, le film est enregistré.

Les méthodes HTTP GET dans les pages Razor Index, Create et Delete suivent un modèle similaire. La méthode HTTP POST OnPostAsync dans la page Razor Create suit un modèle semblable à la méthode OnPostAsync dans la page Razor Edit.

Étapes suivantes

L’application de gestion des films générée est un bon début, mais la présentation n’est pas idéale. ReleaseDate doit être écrit en deux mots, Release Date.

Application de gestion des films ouverte dans Chrome

Mettre le modèle à niveau

Mettez à jour Models/Movie.cs avec le code mis en évidence suivant :

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string Genre { get; set; } = string.Empty;

    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}

Dans le code précédent :

  • L’annotation de données [Column(TypeName = "decimal(18, 2)")] permet à Entity Framework Core de mapper correctement Price en devise dans la base de données. Pour plus d’informations, consultez Types de données.
  • L’attribut [Display] spécifie le nom complet d’un champ. Dans le code précédent, Release Date au lieu de ReleaseDate.
  • l’attribut [DataType] spécifie le type de données (Date). Les informations de temps stockées dans le champ ne s’affichent pas.

DataAnnotations est abordé dans le tutoriel suivant.

Accédez à Pages/Movies, puis placez le curseur sur un lien Modifier pour afficher l’URL cible.

Fenêtre de navigateur avec la souris sur le lien Edit et l’URL de lien https://localhost:1234/Movies/Edit/5 affichée

Les liens Edit, Details, et Delete sont générés par le Tag Helper d’ancre dans le fichier Pages/Movies/Index.cshtml.

@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

Les Tag Helpers permettent au code côté serveur de participer à la création et au rendu des éléments HTML dans les fichiers Razor.

Dans le code précédent, le Tag Helper d’ancre génère dynamiquement la valeur d’attribut HTML href dans la page Razor (l’itinéraire est relatif), asp-page et l’ID de l’itinéraire (asp-route-id). Pour plus d’informations, consultez Génération d’URL pour Pages.

Utilisez Afficher la Source dans un navigateur pour examiner le balisage généré. Une partie du code HTML généré est affichée ci-dessous :

<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>

Les liens générés dynamiquement transmettent l’ID de film avec une chaîne de requête. Par exemple, ?id=1 dans https://localhost:5001/Movies/Details?id=1.

Ajouter un modèle d’itinéraire

Mettez à jour les Pages Razor Edit, Details et Delete pour utiliser le modèle de route {id:int}. Remplacez la directive de chacune de ces pages (@page "{id:int}") par @page. Exécuter l’application, puis affichez le code source.

Le code HTML généré ajoute l’ID à la partie de chemin de l’URL :

<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>

Une requête à la page avec le modèle d’itinéraire {id:int} qui n’inclut pas l’entier retourne une erreur HTTP 404 (introuvable). Par exemple, https://localhost:5001/Movies/Details retourne une erreur 404. Pour que l’ID soit facultatif, ajoutez ? à la contrainte d’itinéraire :

@page "{id:int?}"

Testez le comportement de @page "{id:int?}" :

  1. Définissez la directive de page dans Pages/Movies/Details.cshtml sur @page "{id:int?}".
  2. Définissez un point d’arrêt dans public async Task<IActionResult> OnGetAsync(int? id), dans Pages/Movies/Details.cshtml.cs.
  3. Accédez à https://localhost:5001/Movies/Details/.

Avec la directive @page "{id:int}", le point d’arrêt n’est jamais atteint. Le moteur de routage retourne l’erreur HTTP 404. Avec @page "{id:int?}", la méthode OnGetAsync retourne NotFound (HTTP 404) :

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}

Passer en revue la gestion des exceptions d’accès concurrentiel

Passez en revue la méthode OnPostAsync dans le fichier Pages/Movies/Edit.cshtml.cs :

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Attach(Movie).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie.Id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

private bool MovieExists(int id)
{
  return _context.Movie.Any(e => e.Id == id);
}

Le code précédent détecte les exceptions d’accès concurrentiel quand un client supprime le film et que l’autre client envoie les modifications apportées au film.

Pour tester le bloc catch :

  1. Définissez un point d’arrêt sur catch (DbUpdateConcurrencyException).
  2. Sélectionnez Edit (Modifier) pour un film, apportez des modifications, mais ne cliquez pas sur Save (Enregistrer).
  3. Dans une autre fenêtre de navigateur, sélectionnez le lien Delete du même film, puis supprimez le film.
  4. Dans la fenêtre de navigateur précédente, postez les modifications apportées au film.

Dans le code destiné à la production, il est nécessaire de détecter les conflits d’accès concurrentiel. Pour plus d’informations, consultez Gérer les conflits d’accès concurrentiel.

Validation de la publication et de la liaison

Examinez le fichier Pages/Movies/Edit.cshtml.cs :

public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; } = default!;

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null || _context.Movie == null)
        {
            return NotFound();
        }

        var movie =  await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
        if (movie == null)
        {
            return NotFound();
        }
        Movie = movie;
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Movie).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool MovieExists(int id)
    {
      return _context.Movie.Any(e => e.Id == id);
    }

Quand une requête HTTP GET est effectuée sur la page Movies/Edit, par exemple, https://localhost:5001/Movies/Edit/3 :

  • La méthode OnGetAsync extrait le film de la base de données et retourne la méthode Page.
  • La méthode Page restitue la page Pages/Movies/Edit.cshtmlRazor. Le fichier Pages/Movies/Edit.cshtml contient la directive de modèle @model RazorPagesMovie.Pages.Movies.EditModel, ce qui rend le modèle Movie disponible sur la page.
  • Le formulaire d’édition s’affiche avec les valeurs relatives au film.

Quand la page Movies/Edit est postée :

  • Les valeurs de formulaire affichées dans la page sont liées à la propriété Movie. L’attribut [BindProperty] active la liaison de données.

    [BindProperty]
    public Movie Movie { get; set; }
    
  • S’il y a des erreurs dans l’état du modèle, par exemple, si ReleaseDate ne peut pas être converti en date, le formulaire est à nouveau affiché avec les valeurs soumises.

  • S’il n’y a aucune erreur de modèle, le film est enregistré.

Les méthodes HTTP GET dans les pages Razor Index, Create et Delete suivent un modèle similaire. La méthode HTTP POST OnPostAsync dans la page Razor Create suit un modèle semblable à la méthode OnPostAsync dans la page Razor Edit.

Étapes suivantes

L’application de gestion des films générée est un bon début, mais la présentation n’est pas idéale. ReleaseDate doit être écrit en deux mots, Release Date.

Application de gestion des films ouverte dans Chrome

Mettre à jour le code généré

Mettez à jour Models/Movie.cs avec le code mis en évidence suivant :

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; } = string.Empty;

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; } = string.Empty;

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
    }
}

Dans le code précédent :

  • L’annotation de données [Column(TypeName = "decimal(18, 2)")] permet à Entity Framework Core de mapper correctement Price en devise dans la base de données. Pour plus d’informations, consultez Types de données.
  • L’attribut [Display] spécifie le nom complet d’un champ. Dans le code précédent, « Release Date » au lieu de « ReleaseDate ».
  • l’attribut [DataType] spécifie le type de données (Date). Les informations de temps stockées dans le champ ne s’affichent pas.

DataAnnotations est abordé dans le tutoriel suivant.

Accédez à Pages/Movies, puis placez le curseur sur un lien Modifier pour afficher l’URL cible.

Fenêtre de navigateur avec la souris sur le lien Edit et l’URL de lien https://localhost:1234/Movies/Edit/5 affichée

Les liens Edit, Details, et Delete sont générés par le Tag Helper d’ancre dans le fichier Pages/Movies/Index.cshtml.

@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

Les Tag Helpers permettent au code côté serveur de participer à la création et au rendu des éléments HTML dans les fichiers Razor.

Dans le code précédent, le Tag Helper d’ancre génère dynamiquement la valeur d’attribut HTML href dans la page Razor (l’itinéraire est relatif), asp-page et l’ID de l’itinéraire (asp-route-id). Pour plus d’informations, consultez Génération d’URL pour Pages.

Utilisez Afficher la Source dans un navigateur pour examiner le balisage généré. Une partie du code HTML généré est affichée ci-dessous :

<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>

Les liens générés dynamiquement transmettent l’ID de film avec une chaîne de requête. Par exemple, ?id=1 dans https://localhost:5001/Movies/Details?id=1.

Ajouter un modèle d’itinéraire

Mettez à jour les Pages Razor Edit, Details et Delete pour utiliser le modèle de route {id:int}. Remplacez la directive de chacune de ces pages (@page "{id:int}") par @page. Exécuter l’application, puis affichez le code source.

Le code HTML généré ajoute l’ID à la partie de chemin de l’URL :

<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>

Une requête à la page avec le modèle d’itinéraire {id:int} qui n’inclut pas l’entier retourne une erreur HTTP 404 (introuvable). Par exemple, https://localhost:5001/Movies/Details retourne une erreur 404. Pour que l’ID soit facultatif, ajoutez ? à la contrainte d’itinéraire :

@page "{id:int?}"

Testez le comportement de @page "{id:int?}" :

  1. Définissez la directive de page dans Pages/Movies/Details.cshtml sur @page "{id:int?}".
  2. Définissez un point d’arrêt dans public async Task<IActionResult> OnGetAsync(int? id), dans Pages/Movies/Details.cshtml.cs.
  3. Accédez à https://localhost:5001/Movies/Details/.

Avec la directive @page "{id:int}", le point d’arrêt n’est jamais atteint. Le moteur de routage retourne l’erreur HTTP 404. Avec @page "{id:int?}", la méthode OnGetAsync retourne NotFound (HTTP 404) :

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}

Passer en revue la gestion des exceptions d’accès concurrentiel

Passez en revue la méthode OnPostAsync dans le fichier Pages/Movies/Edit.cshtml.cs :

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Attach(Movie).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie.ID))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

private bool MovieExists(int id)
{
  return (_context.Movie?.Any(e => e.ID == id)).GetValueOrDefault();
}

Le code précédent détecte les exceptions d’accès concurrentiel quand un client supprime le film et que l’autre client envoie les modifications apportées au film. Le code précédent ne détecte pas les conflits qui se produisent quand deux clients ou plus modifient simultanément le même film. Dans ce cas, les modifications effectuées par plusieurs clients sont appliquées dans l’ordre d’appel de SaveChanges, et les modifications appliquées ultérieurement peuvent remplacer les modifications antérieures par des valeurs obsolètes.

Pour tester le bloc catch :

  1. Définissez un point d’arrêt sur catch (DbUpdateConcurrencyException).
  2. Sélectionnez Edit (Modifier) pour un film, apportez des modifications, mais ne cliquez pas sur Save (Enregistrer).
  3. Dans une autre fenêtre de navigateur, sélectionnez le lien Delete du même film, puis supprimez le film.
  4. Dans la fenêtre de navigateur précédente, postez les modifications apportées au film.

Le code de production peut souhaiter détecter d’autres conflits d’accès concurrentiel, comme plusieurs clients qui modifient une entité en même temps. Pour plus d’informations, consultez Gérer les conflits d’accès concurrentiel.

Validation de la publication et de la liaison

Examinez le fichier Pages/Movies/Edit.cshtml.cs :

public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; } = default!;

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null || _context.Movie == null)
        {
            return NotFound();
        }

        var movie =  await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);
        if (movie == null)
        {
            return NotFound();
        }
        Movie = movie;
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Movie).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool MovieExists(int id)
    {
      return (_context.Movie?.Any(e => e.ID == id)).GetValueOrDefault();
    }

Quand une requête HTTP GET est effectuée sur la page Movies/Edit, par exemple, https://localhost:5001/Movies/Edit/3 :

  • La méthode OnGetAsync extrait le film de la base de données et retourne la méthode Page.
  • La méthode Page restitue la page Pages/Movies/Edit.cshtmlRazor. Le fichier Pages/Movies/Edit.cshtml contient la directive de modèle @model RazorPagesMovie.Pages.Movies.EditModel, ce qui rend le modèle Movie disponible sur la page.
  • Le formulaire d’édition s’affiche avec les valeurs relatives au film.

Quand la page Movies/Edit est postée :

  • Les valeurs de formulaire affichées dans la page sont liées à la propriété Movie. L’attribut [BindProperty] active la liaison de données.

    [BindProperty]
    public Movie Movie { get; set; }
    
  • S’il y a des erreurs dans l’état du modèle, par exemple, si ReleaseDate ne peut pas être converti en date, le formulaire est à nouveau affiché avec les valeurs soumises.

  • S’il n’y a aucune erreur de modèle, le film est enregistré.

Les méthodes HTTP GET dans les pages Razor Index, Create et Delete suivent un modèle similaire. La méthode HTTP POST OnPostAsync dans la page Razor Create suit un modèle semblable à la méthode OnPostAsync dans la page Razor Edit.

Étapes suivantes

L’application de gestion des films générée est un bon début, mais la présentation n’est pas idéale. ReleaseDate doit être écrit en deux mots, Release Date.

Application de gestion des films ouverte dans Chrome

Mettre à jour le code généré

Ouvrez le fichier Models/Movie.cs, puis ajoutez les lignes affichées en évidence dans le code suivant :

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
    }
}

Dans le code précédent :

  • L’annotation de données [Column(TypeName = "decimal(18, 2)")] permet à Entity Framework Core de mapper correctement Price en devise dans la base de données. Pour plus d’informations, consultez Types de données.
  • L’attribut [Display] spécifie le nom complet d’un champ. Dans le code précédent, « Release Date » au lieu de « ReleaseDate ».
  • l’attribut [DataType] spécifie le type de données (Date). Les informations de temps stockées dans le champ ne s’affichent pas.

DataAnnotations est abordé dans le tutoriel suivant.

Accédez à Pages/Movies, puis placez le curseur sur un lien Modifier pour afficher l’URL cible.

Fenêtre de navigateur avec la souris sur le lien Edit et l’URL de lien https://localhost:1234/Movies/Edit/5 affichée

Les liens Edit, Details, et Delete sont générés par le Tag Helper d’ancre dans le fichier Pages/Movies/Index.cshtml.

@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

Les Tag Helpers permettent au code côté serveur de participer à la création et au rendu des éléments HTML dans les fichiers Razor.

Dans le code précédent, le Tag Helper d’ancre génère dynamiquement la valeur d’attribut HTML href dans la page Razor (l’itinéraire est relatif), asp-page et l’ID de l’itinéraire (asp-route-id). Pour plus d’informations, consultez Génération d’URL pour Pages.

Utilisez Afficher la Source dans un navigateur pour examiner le balisage généré. Une partie du code HTML généré est affichée ci-dessous :

<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>

Les liens générés dynamiquement transmettent l’ID de film avec une chaîne de requête. Par exemple, ?id=1 dans https://localhost:5001/Movies/Details?id=1.

Ajouter un modèle d’itinéraire

Mettez à jour les Pages Razor Edit, Details et Delete pour utiliser le modèle de route {id:int}. Remplacez la directive de chacune de ces pages (@page "{id:int}") par @page. Exécuter l’application, puis affichez le code source.

Le code HTML généré ajoute l’ID à la partie de chemin de l’URL :

<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>

Une requête à la page avec le modèle d’itinéraire {id:int} qui n’inclut pas l’entier retourne une erreur HTTP 404 (introuvable). Par exemple, https://localhost:5001/Movies/Details retourne une erreur 404. Pour que l’ID soit facultatif, ajoutez ? à la contrainte d’itinéraire :

@page "{id:int?}"

Testez le comportement de @page "{id:int?}" :

  1. Définissez la directive de page dans Pages/Movies/Details.cshtml sur @page "{id:int?}".
  2. Définissez un point d’arrêt dans public async Task<IActionResult> OnGetAsync(int? id), dans Pages/Movies/Details.cshtml.cs.
  3. Accédez à https://localhost:5001/Movies/Details/.

Avec la directive @page "{id:int}", le point d’arrêt n’est jamais atteint. Le moteur de routage retourne l’erreur HTTP 404. Avec @page "{id:int?}", la méthode OnGetAsync retourne NotFound (HTTP 404) :

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}

Passer en revue la gestion des exceptions d’accès concurrentiel

Passez en revue la méthode OnPostAsync dans le fichier Pages/Movies/Edit.cshtml.cs :

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Attach(Movie).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie.ID))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

private bool MovieExists(int id)
{
    return _context.Movie.Any(e => e.ID == id);
}

Le code précédent détecte les exceptions d’accès concurrentiel quand un client supprime le film et que l’autre client envoie les modifications apportées au film.

Pour tester le bloc catch :

  1. Définissez un point d’arrêt sur catch (DbUpdateConcurrencyException).
  2. Sélectionnez Edit (Modifier) pour un film, apportez des modifications, mais ne cliquez pas sur Save (Enregistrer).
  3. Dans une autre fenêtre de navigateur, sélectionnez le lien Delete du même film, puis supprimez le film.
  4. Dans la fenêtre de navigateur précédente, postez les modifications apportées au film.

Dans le code destiné à la production, il est nécessaire de détecter les conflits d’accès concurrentiel. Pour plus d’informations, consultez Gérer les conflits d’accès concurrentiel.

Validation de la publication et de la liaison

Examinez le fichier Pages/Movies/Edit.cshtml.cs :

public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; }

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

        if (Movie == null)
        {
            return NotFound();
        }
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Movie).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool MovieExists(int id)
    {
        return _context.Movie.Any(e => e.ID == id);
    }

Quand une requête HTTP GET est effectuée sur la page Movies/Edit, par exemple, https://localhost:5001/Movies/Edit/3 :

  • La méthode OnGetAsync extrait le film de la base de données et retourne la méthode Page.
  • La méthode Page restitue la page Pages/Movies/Edit.cshtmlRazor. Le fichier Pages/Movies/Edit.cshtml contient la directive de modèle @model RazorPagesMovie.Pages.Movies.EditModel, ce qui rend le modèle Movie disponible sur la page.
  • Le formulaire d’édition s’affiche avec les valeurs relatives au film.

Quand la page Movies/Edit est postée :

  • Les valeurs de formulaire affichées dans la page sont liées à la propriété Movie. L’attribut [BindProperty] active la liaison de données.

    [BindProperty]
    public Movie Movie { get; set; }
    
  • S’il y a des erreurs dans l’état du modèle, par exemple, si ReleaseDate ne peut pas être converti en date, le formulaire est à nouveau affiché avec les valeurs soumises.

  • S’il n’y a aucune erreur de modèle, le film est enregistré.

Les méthodes HTTP GET dans les pages Razor Index, Create et Delete suivent un modèle similaire. La méthode HTTP POST OnPostAsync dans la page Razor Create suit un modèle semblable à la méthode OnPostAsync dans la page Razor Edit.

Étapes suivantes