Problem
How to use Razor Pages in ASP.NET Core 2.0.
Solution
Create an empty project and amend the Startup.cs file to add services and middleware for MVC.
public void ConfigureServices(
IServiceCollection services)
{
services.AddSingleton<IMovieService, MovieService>();
services.AddMvc();
}
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env)
{
app.UseMvc();
}
Add a service and domain model (implementation of IMovieService is just an in-memory list in sample source code).
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public int ReleaseYear { get; set; }
public string Summary { get; set; }
}
public interface IMovieService
{
List<Movie> GetMovies();
Movie GetMovie(int id);
void AddMovie(Movie item);
void UpdateMovie(Movie item);
void DeleteMovie(int id);
bool MovieExists(int id);
}
Add input and output models (to receive and send data via property bindings).
public class MovieInputModel
{
public int Id { get; set; }
[Required]
public string Title { get; set; }
public int ReleaseYear { get; set; }
public string Summary { get; set; }
}
public class MovieOutputModel
{
public int Id { get; set; }
public string Title { get; set; }
public int ReleaseYear { get; set; }
public string Summary { get; set; }
public DateTime LastReadAt { get; set; }
}
Add a folder called Pages and add Index, _Layout, _ViewImports, and _ViewStart pages to it. These pages are no different than MVC. Also, add a folder Movies for your CRUD pages.
Add 4 new RazorPage items to the Movies folder - called Index, Create, Edit, and Delete. These will add .cshtml and .cshtml.cs files.
Each of these pages will have our IMovieService injected via constructor injection, e.g.
private readonly IMovieService service;
public IndexModel(IMovieService service)
{
this.service = service;
}
Modify the Index. cshtml
@page
@model IndexModel
<strong>Movie Listing</strong>
<div>
<a asp-page="/Index">Home</a> |
<a asp-page="./Create">Add New</a>
</div>
<table>
<thead>
<tr>
<th>Title</th>
<th>Year</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movies)
{
<tr>
<td>@item.Title</td>
<td>@item.ReleaseYear</td>
<td><a asp-page="./Edit" asp-route-id="@item.Id">Edit</a></td>
<td><a asp-page="./Delete" asp-route-id="@item.Id">Delete</a></td>
</tr>
}
</tbody>
</table>
Modify the Index. cshtml.cs
public List<MovieOutputModel> Movies { get; set; }
public void OnGet()
{
this.Movies = this.service.GetMovies()
.Select(item => new MovieOutputModel
{
Id = item.Id,
Title = item.Title,
ReleaseYear = item.ReleaseYear,
Summary = item.Summary,
LastReadAt = DateTime.Now
})
.ToList();
}
Modify the Create.cshtml
@page
@model CreateModel
<strong>New Movie</strong>
<form method="post">
<div asp-validation-summary="All"></div>
<label asp-for="Movie.Id"></label>
<input asp-for="Movie.Id" /><br />
<label asp-for="Movie.Title"></label>
<input asp-for="Movie.Title" />
<span asp-validation-for="Movie.Title"></span><br />
<label asp-for="Movie.ReleaseYear"></label>
<input asp-for="Movie.ReleaseYear" /><br />
<label asp-for="Movie.Summary"></label>
<textarea asp-for="Movie.Summary"></textarea><br />
<button type="submit">Save</button>
<a asp-page="./Index">Cancel</a>
</form>
Modify the Create.cshtml.cs
[BindProperty]
public MovieInputModel Movie { get; set; }
public void OnGet()
{
this.Movie = new MovieInputModel();
}
public IActionResult OnPost()
{
if (!ModelState.IsValid)
return Page();
var model = new Movie
{
Id = this.Movie.Id,
Title = this.Movie.Title,
ReleaseYear = this.Movie.ReleaseYear,
Summary = this.Movie.Summary
};
service.AddMovie(model);
return RedirectToPage("./Index");
}
Modify the Edit. cshtml
@page "{id:int}"
@model EditModel
@{
}
<strong>Edit Movie - @Model.Movie.Title</strong>
<form method="post">
<div asp-validation-summary="All"></div>
<input type="hidden" asp-for="Movie.Id" />
<label asp-for="Movie.Title"></label>
<input asp-for="Movie.Title" />
<span asp-validation-for="Movie.Title"></span><br />
<label asp-for="Movie.ReleaseYear"></label>
<input asp-for="Movie.ReleaseYear" /><br />
<label asp-for="Movie.Summary"></label>
<textarea asp-for="Movie.Summary"></textarea><br />
<button type="submit">Save</button>
<a asp-page="./Index">Cancel</a>
</form>
Modify the Edit. cshtml.cs
[BindProperty]
public MovieInputModel Movie { get; set; }
public IActionResult OnGet(int id)
{
var model = this.service.GetMovie(id);
if (model == null)
return RedirectToPage("./Index");
this.Movie = new MovieInputModel
{
Id = model.Id,
Title = model.Title,
ReleaseYear = model.ReleaseYear,
Summary = model.Summary
};
return Page();
}
public IActionResult OnPost()
{
if (!ModelState.IsValid)
return Page();
var model = new Movie
{
Id = this.Movie.Id,
Title = this.Movie.Title,
ReleaseYear = this.Movie.ReleaseYear,
Summary = this.Movie.Summary
};
service.UpdateMovie(model);
return RedirectToPage("./Index");
}
Modify the Delete. cshtml
@page "{id:int}"
@model DeleteModel
<strong>Delete Movie</strong>
<p>Are you sure you want to delete <strong>@Model.Title</strong>?</p>
<form method="post">
<input type="hidden" asp-for="Id" />
<button type="submit">Yes</button>
<a asp-page="./Index">No</a>
</form>
Modify the Delete. cshtml.cs
[BindProperty]
public int Id { get; set; }
public string Title { get; set; }
public IActionResult OnGet(int id)
{
var model = this.service.GetMovie(id);
if (model == null)
return RedirectToPage("./Index");
this.Id = model.Id;
this.Title = model.Title;
return Page();
}
public IActionResult OnPost()
{
if (!service.MovieExists(this.Id))
return RedirectToPage("./Index");
service.DeleteMovie(this.Id);
return RedirectToPage("./Index");
}
Run and browse to /Movies.
On clicking, first, Edit (notice the URL would be /Movies/Edit/1).
On clicking Delete (notice the URL would be /Movies/Delete/3).
Note. You could download the source code to play with it.
Discussion
Razor Pages are introduced in ASP.NET Core 2.0 to make building simple web applications quicker and are a good way to play with various ASP.NET Core concepts like Razor, Layout Pages and Tag Helpers etc.
Razor Pages use ASP.NET Core MVC under the hood however the programming model is not the same. Unlike MVC where Controllers, Models, and Views are distinct and separate components of the architecture, in Razor Pages, these concepts are brought together under one roof, Page Model.
Page Model
I like to think of Page Model as a combination of Controller and Models. They're like controller because they receive the HTTP requests and like a model because they hold the data/properties for views.
For a .cshtml file to act as Page Model, it must contain as its first line the @page directive. The .cshtml.cs (code-behind) class inherits from PageModel abstract class. By convention, the code-behind class has a Model appended to the page’s name e.g. Index page’s code-behind is IndexModel.
Routing
Routing to pages depends on their location in your project directory structure, under the Pages folder (by default). If a page is not specified in the URL, the default Index is used.
In our sample, we navigated to URL /Movies to view the page located at /Pages/Movies/Index in our solution. Similarly, the URL /Movies/Edit maps to /Pages/Movies/Edit page.
ASP.NET Core 2.0 has introduced new constructs used for generating URLs.
- Page() method
- asp-page Tag Helper
- RedirectToPage() method on PageModel base class
Note that URLs starting with / are absolute paths and point to the Pages folder. We can also use relative URLs starting with ./ or ../ or by simply omitting the /. To understand better, here is what happens when navigating to various URLs from Page/Movies/Delete,
We can specify routing constraints as part of the @page directive to indicate to runtime to expect route parameters or throw 404 (Not Found) if missing. In our Edit page, we used the constraint like.
@page "{id:int}"
If you prefer to use a different name than Pages for your root folder, you could do so by configuring page options.
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.RootDirectory = "/MyPages";
});
Handlers
As mentioned earlier, the page receives HTTP requests (i.e. acts as an Action in MVC world) and these are handled by the handler methods. These handlers return IActionResult and named using the convention of On[verb]. The most commonly used are OnGet() and OnPost(). For asynchronous you could append Async to the name, but this is optional.
The PageModel base class has the RedirectToPage() method (that returns RedirectToPageResult) to navigate to other pages and the Page() method (that returns PageResult) to return the current page. Note that if the return type of the handler method is void, runtime returns a PageResult.
In order to have multiple handler methods for HTTP verbs, we can use named handler methods using the asp-page-handler attribute. The name specified here should have a method in the page class using the convention On[verb][handler]. Let’s add a link on our movies list to delete the movie,
<td>
<a asp-page="./Index"
asp-page-handler="delete"
asp-route-id="@item.Id">Delete</a>
</td>
Add a method in the page model class to handle this request (note its name and parameter).
public IActionResult OnGetDelete(int id)
{
if (!service.MovieExists(id))
return RedirectToPage("./Index");
service.DeleteMovie(id);
return RedirectToPage("./Index");
}
Move your mouse over the Delete link and you’ll notice the URL like /Movies?id=1&handler=delete. If you prefer to replace query parameters with URL segments then add the following route constraints to the @page directive and the generated URL will be /Movies/delete/1.
@page "{handler?}/{id:int?}"
Binding
@model directive on pages points to the page model class because as mentioned earlier, this class acts as the model for Razor Pages. This works for reading the properties however in order to populate them when posting data (i.e. when using verbs other than GET) we need to use an attribute [BindProperty] to mark properties to use Model Binding.
Source Code: GitHub