Training
Module
Create a web UI with ASP.NET Core - Training
Learn how to create web pages using Razor with ASP.NET Core.
This browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Note
This isn't the latest version of this article. For the current release, see the .NET 9 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 9 version of this article.
Important
This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.
For the current release, see the .NET 9 version of this article.
The scaffolded movie app has a good start, but the presentation isn't ideal. ReleaseDate should be two words, Release Date.
Update Models/Movie.cs
with the following highlighted code:
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; }
}
In the previous code:
[Column(TypeName = "decimal(18, 2)")]
data annotation enables Entity Framework Core to correctly map Price
to currency in the database. For more information, see Data Types.Release Date
instead of ReleaseDate
.Date
). The time information stored in the field isn't displayed.DataAnnotations is covered in the next tutorial.
Browse to Pages/Movies and hover over an Edit link to see the target URL.
The Edit, Details, and Delete links are generated by the Anchor Tag Helper in the Pages/Movies/Index.cshtml
file.
@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>
Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files.
In the preceding code, the Anchor Tag Helper dynamically generates the HTML href
attribute value from the Razor Page (the route is relative), the asp-page
, and the route identifier (asp-route-id
). For more information, see URL generation for Pages.
Use View Source from a browser to examine the generated markup. A portion of the generated HTML is shown below:
<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>
The dynamically generated links pass the movie ID with a query string. For example, the ?id=1
in https://localhost:5001/Movies/Details?id=1
.
Update the Edit, Details, and Delete Razor Pages to use the {id:int}
route template. Change the page directive for each of these pages from @page
to @page "{id:int}"
. Run the app and then view source.
The generated HTML adds the ID to the path portion of the URL:
<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>
A request to the page with the {id:int}
route template that does not include the integer returns an HTTP 404 (not found) error. For example, https://localhost:5001/Movies/Details
returns a 404 error. To make the ID optional, append ?
to the route constraint:
@page "{id:int?}"
Test the behavior of @page "{id:int?}"
:
Pages/Movies/Details.cshtml
to @page "{id:int?}"
.public async Task<IActionResult> OnGetAsync(int? id)
, in Pages/Movies/Details.cshtml.cs
.https://localhost:5001/Movies/Details/
.With the @page "{id:int}"
directive, the break point is never hit. The routing engine returns HTTP 404. Using @page "{id:int?}"
, the OnGetAsync
method returns NotFound
(HTTP 404):
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
var movie = await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
if (movie == null)
{
return NotFound();
}
else
{
Movie = movie;
}
return Page();
}
Review the OnPostAsync
method in the Pages/Movies/Edit.cshtml.cs
file:
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);
}
The previous code detects concurrency exceptions when one client deletes the movie and the other client posts changes to the movie.
To test the catch
block:
catch (DbUpdateConcurrencyException)
.Production code may want to detect concurrency conflicts. See Handle concurrency conflicts for more information.
Examine the Pages/Movies/Edit.cshtml.cs
file:
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)
{
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);
}
When an HTTP GET request is made to the Movies/Edit page, for example, https://localhost:5001/Movies/Edit/3
:
OnGetAsync
method fetches the movie from the database and returns the Page
method.Page
method renders the Pages/Movies/Edit.cshtml
Razor Page. The Pages/Movies/Edit.cshtml
file contains the model directive @model RazorPagesMovie.Pages.Movies.EditModel
, which makes the movie model available on the page.When the Movies/Edit page is posted:
The form values on the page are bound to the Movie
property. The [BindProperty]
attribute enables Model binding.
[BindProperty]
public Movie Movie { get; set; }
If there are errors in the model state, for example, ReleaseDate
cannot be converted to a date, the form is redisplayed with the submitted values.
If there are no model errors, the movie is saved.
The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST OnPostAsync
method in the Create Razor Page follows a similar pattern to the OnPostAsync
method in the Edit Razor Page.
The scaffolded movie app has a good start, but the presentation isn't ideal. ReleaseDate should be two words, Release Date.
Update Models/Movie.cs
with the following highlighted code:
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; }
}
In the previous code:
[Column(TypeName = "decimal(18, 2)")]
data annotation enables Entity Framework Core to correctly map Price
to currency in the database. For more information, see Data Types.Release Date
instead of ReleaseDate
.Date
). The time information stored in the field isn't displayed.DataAnnotations is covered in the next tutorial.
Browse to Pages/Movies and hover over an Edit link to see the target URL.
The Edit, Details, and Delete links are generated by the Anchor Tag Helper in the Pages/Movies/Index.cshtml
file.
@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>
Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files.
In the preceding code, the Anchor Tag Helper dynamically generates the HTML href
attribute value from the Razor Page (the route is relative), the asp-page
, and the route identifier (asp-route-id
). For more information, see URL generation for Pages.
Use View Source from a browser to examine the generated markup. A portion of the generated HTML is shown below:
<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>
The dynamically generated links pass the movie ID with a query string. For example, the ?id=1
in https://localhost:5001/Movies/Details?id=1
.
Update the Edit, Details, and Delete Razor Pages to use the {id:int}
route template. Change the page directive for each of these pages from @page
to @page "{id:int}"
. Run the app and then view source.
The generated HTML adds the ID to the path portion of the URL:
<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>
A request to the page with the {id:int}
route template that does not include the integer returns an HTTP 404 (not found) error. For example, https://localhost:5001/Movies/Details
returns a 404 error. To make the ID optional, append ?
to the route constraint:
@page "{id:int?}"
Test the behavior of @page "{id:int?}"
:
Pages/Movies/Details.cshtml
to @page "{id:int?}"
.public async Task<IActionResult> OnGetAsync(int? id)
, in Pages/Movies/Details.cshtml.cs
.https://localhost:5001/Movies/Details/
.With the @page "{id:int}"
directive, the break point is never hit. The routing engine returns HTTP 404. Using @page "{id:int?}"
, the OnGetAsync
method returns 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();
}
Review the OnPostAsync
method in the Pages/Movies/Edit.cshtml.cs
file:
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);
}
The previous code detects concurrency exceptions when one client deletes the movie and the other client posts changes to the movie.
To test the catch
block:
catch (DbUpdateConcurrencyException)
.Production code may want to detect concurrency conflicts. See Handle concurrency conflicts for more information.
Examine the Pages/Movies/Edit.cshtml.cs
file:
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);
}
When an HTTP GET request is made to the Movies/Edit page, for example, https://localhost:5001/Movies/Edit/3
:
OnGetAsync
method fetches the movie from the database and returns the Page
method.Page
method renders the Pages/Movies/Edit.cshtml
Razor Page. The Pages/Movies/Edit.cshtml
file contains the model directive @model RazorPagesMovie.Pages.Movies.EditModel
, which makes the movie model available on the page.When the Movies/Edit page is posted:
The form values on the page are bound to the Movie
property. The [BindProperty]
attribute enables Model binding.
[BindProperty]
public Movie Movie { get; set; }
If there are errors in the model state, for example, ReleaseDate
cannot be converted to a date, the form is redisplayed with the submitted values.
If there are no model errors, the movie is saved.
The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST OnPostAsync
method in the Create Razor Page follows a similar pattern to the OnPostAsync
method in the Edit Razor Page.
The scaffolded movie app has a good start, but the presentation isn't ideal. ReleaseDate should be two words, Release Date.
Update Models/Movie.cs
with the following highlighted code:
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; }
}
In the previous code:
[Column(TypeName = "decimal(18, 2)")]
data annotation enables Entity Framework Core to correctly map Price
to currency in the database. For more information, see Data Types.Release Date
instead of ReleaseDate
.Date
). The time information stored in the field isn't displayed.DataAnnotations is covered in the next tutorial.
Browse to Pages/Movies and hover over an Edit link to see the target URL.
The Edit, Details, and Delete links are generated by the Anchor Tag Helper in the Pages/Movies/Index.cshtml
file.
@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>
Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files.
In the preceding code, the Anchor Tag Helper dynamically generates the HTML href
attribute value from the Razor Page (the route is relative), the asp-page
, and the route identifier (asp-route-id
). For more information, see URL generation for Pages.
Use View Source from a browser to examine the generated markup. A portion of the generated HTML is shown below:
<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>
The dynamically generated links pass the movie ID with a query string. For example, the ?id=1
in https://localhost:5001/Movies/Details?id=1
.
Update the Edit, Details, and Delete Razor Pages to use the {id:int}
route template. Change the page directive for each of these pages from @page
to @page "{id:int}"
. Run the app and then view source.
The generated HTML adds the ID to the path portion of the URL:
<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>
A request to the page with the {id:int}
route template that does not include the integer returns an HTTP 404 (not found) error. For example, https://localhost:5001/Movies/Details
returns a 404 error. To make the ID optional, append ?
to the route constraint:
@page "{id:int?}"
Test the behavior of @page "{id:int?}"
:
Pages/Movies/Details.cshtml
to @page "{id:int?}"
.public async Task<IActionResult> OnGetAsync(int? id)
, in Pages/Movies/Details.cshtml.cs
.https://localhost:5001/Movies/Details/
.With the @page "{id:int}"
directive, the break point is never hit. The routing engine returns HTTP 404. Using @page "{id:int?}"
, the OnGetAsync
method returns 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();
}
Review the OnPostAsync
method in the Pages/Movies/Edit.cshtml.cs
file:
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);
}
The previous code detects concurrency exceptions when one client deletes the movie and the other client posts changes to the movie.
To test the catch
block:
catch (DbUpdateConcurrencyException)
.Production code may want to detect concurrency conflicts. See Handle concurrency conflicts for more information.
Examine the Pages/Movies/Edit.cshtml.cs
file:
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);
}
When an HTTP GET request is made to the Movies/Edit page, for example, https://localhost:5001/Movies/Edit/3
:
OnGetAsync
method fetches the movie from the database and returns the Page
method.Page
method renders the Pages/Movies/Edit.cshtml
Razor Page. The Pages/Movies/Edit.cshtml
file contains the model directive @model RazorPagesMovie.Pages.Movies.EditModel
, which makes the movie model available on the page.When the Movies/Edit page is posted:
The form values on the page are bound to the Movie
property. The [BindProperty]
attribute enables Model binding.
[BindProperty]
public Movie Movie { get; set; }
If there are errors in the model state, for example, ReleaseDate
cannot be converted to a date, the form is redisplayed with the submitted values.
If there are no model errors, the movie is saved.
The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST OnPostAsync
method in the Create Razor Page follows a similar pattern to the OnPostAsync
method in the Edit Razor Page.
The scaffolded movie app has a good start, but the presentation isn't ideal. ReleaseDate should be two words, Release Date.
Update Models/Movie.cs
with the following highlighted code:
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; }
}
}
In the previous code:
[Column(TypeName = "decimal(18, 2)")]
data annotation enables Entity Framework Core to correctly map Price
to currency in the database. For more information, see Data Types.Date
). The time information stored in the field isn't displayed.DataAnnotations is covered in the next tutorial.
Browse to Pages/Movies and hover over an Edit link to see the target URL.
The Edit, Details, and Delete links are generated by the Anchor Tag Helper in the Pages/Movies/Index.cshtml
file.
@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>
Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files.
In the preceding code, the Anchor Tag Helper dynamically generates the HTML href
attribute value from the Razor Page (the route is relative), the asp-page
, and the route identifier (asp-route-id
). For more information, see URL generation for Pages.
Use View Source from a browser to examine the generated markup. A portion of the generated HTML is shown below:
<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>
The dynamically generated links pass the movie ID with a query string. For example, the ?id=1
in https://localhost:5001/Movies/Details?id=1
.
Update the Edit, Details, and Delete Razor Pages to use the {id:int}
route template. Change the page directive for each of these pages from @page
to @page "{id:int}"
. Run the app and then view source.
The generated HTML adds the ID to the path portion of the URL:
<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>
A request to the page with the {id:int}
route template that does not include the integer will return an HTTP 404 (not found) error. For example, https://localhost:5001/Movies/Details
will return a 404 error. To make the ID optional, append ?
to the route constraint:
@page "{id:int?}"
Test the behavior of @page "{id:int?}"
:
Pages/Movies/Details.cshtml
to @page "{id:int?}"
.public async Task<IActionResult> OnGetAsync(int? id)
, in Pages/Movies/Details.cshtml.cs
.https://localhost:5001/Movies/Details/
.With the @page "{id:int}"
directive, the break point is never hit. The routing engine returns HTTP 404. Using @page "{id:int?}"
, the OnGetAsync
method returns 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();
}
Review the OnPostAsync
method in the Pages/Movies/Edit.cshtml.cs
file:
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();
}
The previous code detects concurrency exceptions when one client deletes the movie and the other client posts changes to the movie. The previous code does not detect conflicts that occur because of two or more clients editing the same movie concurrently. In this case edits by multiple clients are applied in the order that SaveChanges
is called and edits that are applied later may overwrite earlier edits with stale values.
To test the catch
block:
catch (DbUpdateConcurrencyException)
.Production code may want to detect additional concurrency conflicts such as multiple clients editing an entity at the same time. See Handle concurrency conflicts for more information.
Examine the Pages/Movies/Edit.cshtml.cs
file:
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();
}
When an HTTP GET request is made to the Movies/Edit page, for example, https://localhost:5001/Movies/Edit/3
:
OnGetAsync
method fetches the movie from the database and returns the Page
method.Page
method renders the Pages/Movies/Edit.cshtml
Razor Page. The Pages/Movies/Edit.cshtml
file contains the model directive @model RazorPagesMovie.Pages.Movies.EditModel
, which makes the movie model available on the page.When the Movies/Edit page is posted:
The form values on the page are bound to the Movie
property. The [BindProperty]
attribute enables Model binding.
[BindProperty]
public Movie Movie { get; set; }
If there are errors in the model state, for example, ReleaseDate
cannot be converted to a date, the form is redisplayed with the submitted values.
If there are no model errors, the movie is saved.
The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST OnPostAsync
method in the Create Razor Page follows a similar pattern to the OnPostAsync
method in the Edit Razor Page.
The scaffolded movie app has a good start, but the presentation isn't ideal. ReleaseDate should be two words, Release Date.
Open the Models/Movie.cs
file and add the highlighted lines shown in the following code:
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; }
}
}
In the previous code:
[Column(TypeName = "decimal(18, 2)")]
data annotation enables Entity Framework Core to correctly map Price
to currency in the database. For more information, see Data Types.Date
). The time information stored in the field isn't displayed.DataAnnotations is covered in the next tutorial.
Browse to Pages/Movies and hover over an Edit link to see the target URL.
The Edit, Details, and Delete links are generated by the Anchor Tag Helper in the Pages/Movies/Index.cshtml
file.
@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>
Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files.
In the preceding code, the Anchor Tag Helper dynamically generates the HTML href
attribute value from the Razor Page (the route is relative), the asp-page
, and the route identifier (asp-route-id
). For more information, see URL generation for Pages.
Use View Source from a browser to examine the generated markup. A portion of the generated HTML is shown below:
<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>
The dynamically generated links pass the movie ID with a query string. For example, the ?id=1
in https://localhost:5001/Movies/Details?id=1
.
Update the Edit, Details, and Delete Razor Pages to use the {id:int}
route template. Change the page directive for each of these pages from @page
to @page "{id:int}"
. Run the app and then view source.
The generated HTML adds the ID to the path portion of the URL:
<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>
A request to the page with the {id:int}
route template that does not include the integer will return an HTTP 404 (not found) error. For example, https://localhost:5001/Movies/Details
will return a 404 error. To make the ID optional, append ?
to the route constraint:
@page "{id:int?}"
Test the behavior of @page "{id:int?}"
:
Pages/Movies/Details.cshtml
to @page "{id:int?}"
.public async Task<IActionResult> OnGetAsync(int? id)
, in Pages/Movies/Details.cshtml.cs
.https://localhost:5001/Movies/Details/
.With the @page "{id:int}"
directive, the break point is never hit. The routing engine returns HTTP 404. Using @page "{id:int?}"
, the OnGetAsync
method returns 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();
}
Review the OnPostAsync
method in the Pages/Movies/Edit.cshtml.cs
file:
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);
}
The previous code detects concurrency exceptions when one client deletes the movie and the other client posts changes to the movie.
To test the catch
block:
catch (DbUpdateConcurrencyException)
.Production code may want to detect concurrency conflicts. See Handle concurrency conflicts for more information.
Examine the Pages/Movies/Edit.cshtml.cs
file:
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);
}
When an HTTP GET request is made to the Movies/Edit page, for example, https://localhost:5001/Movies/Edit/3
:
OnGetAsync
method fetches the movie from the database and returns the Page
method.Page
method renders the Pages/Movies/Edit.cshtml
Razor Page. The Pages/Movies/Edit.cshtml
file contains the model directive @model RazorPagesMovie.Pages.Movies.EditModel
, which makes the movie model available on the page.When the Movies/Edit page is posted:
The form values on the page are bound to the Movie
property. The [BindProperty]
attribute enables Model binding.
[BindProperty]
public Movie Movie { get; set; }
If there are errors in the model state, for example, ReleaseDate
cannot be converted to a date, the form is redisplayed with the submitted values.
If there are no model errors, the movie is saved.
The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST OnPostAsync
method in the Create Razor Page follows a similar pattern to the OnPostAsync
method in the Edit Razor Page.
ASP.NET Core feedback
ASP.NET Core is an open source project. Select a link to provide feedback:
Training
Module
Create a web UI with ASP.NET Core - Training
Learn how to create web pages using Razor with ASP.NET Core.
Events
Apr 22, 3 PM - Apr 23, 7 PM
Focus on Modernization and learn how to unlock the benefits of modern app development
Register now