Update the generated pages in an ASP.NET Core app

By Rick Anderson

The scaffolded movie app has a good start, but the presentation isn't ideal. ReleaseDate should be Release Date (two words).

Movie application open in Chrome

Update the generated code

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; }
    }
}

The [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.

DataAnnotations is covered in the next tutorial. The Display attribute specifies what to display for the name of a field (in this case "Release Date" instead of "ReleaseDate"). The DataType attribute specifies the type of the data (Date), so the time information stored in the field isn't displayed.

Browse to Pages/Movies and hover over an Edit link to see the target URL.

Browser window with mouse over the Edit link and a link Url of http://localhost:1234/Movies/Edit/5 is shown

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 AnchorTagHelper dynamically generates the HTML href attribute value from the Razor Page (the route is relative), the asp-page, and the route id (asp-route-id). See URL generation for Pages for more information.

Use View Source from your favorite 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, http://localhost:5000/Movies/Details will return a 404 error. To make the ID optional, append ? to the route constraint:

@page "{id:int?}"

To test the behavior or @page "{id:int?}":

  • Set the page directive in Pages/Movies/Details.cshtml to @page "{id:int?}"
  • Set a break point in public async Task<IActionResult> OnGetAsync(int? id) (in Pages/Movies/Details.cshtml.cs).
  • Navigate to https://localhost:5001/Movies/Details/

With the @page "{id:int}" directive, the break point is never hit. The routing engine return HTTP 404. Using @page "{id:int?}", the OnGetAsync method returns NotFound (HTTP 404).

Although not recommended, you could write the the delete method as:

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        Movie = await _context.Movie.FirstOrDefaultAsync();
    }
    else
    {
        Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);
    }

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

Test the preceding code:

  • Select a delete link.
  • Remove the ID from the URL. For example, change https://localhost:5001/Movies/Delete/8 to https://localhost:5001/Movies/Delete
  • Step through the code in the debugger.

Review concurrency exception handling

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 the one client deletes the movie and the other client posts changes to the movie.

To test the catch block:

  • Set a breakpoint on catch (DbUpdateConcurrencyException)
  • Select Edit for a movie, make changes, but don't enter Save.
  • In another browser window, select the Delete link for the same movie, and then delete the movie.
  • In the previous browser window, post changes to the movie.

Production code may want to detect concurrency conflicts. See Handle concurrency conflicts for more information.

Posting and binding review

Examine the Pages/Movies/Edit.cshtml.cs file:

public class EditModel : PageModel
{
    private readonly RazorPagesMovieContext _context;

    public EditModel(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.SingleOrDefaultAsync(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 (!_context.Movie.Any(e => e.ID == Movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }
}

When an HTTP GET request is made to the Movies/Edit page (for example, http://localhost:5000/Movies/Edit/2):

  • The OnGetAsync method fetches the movie from the database and returns the Page method.
  • The 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.
  • The Edit form is displayed with the values from the movie.

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 posted again 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.

Search is added in the next tutorial.