.NET Razor Pages
Introduction
Razor Pages is a page-based programming model introduced in ASP.NET Core 2.0 that makes building web UI simpler and more productive. Unlike the MVC (Model-View-Controller) pattern, which separates an application into three main components, Razor Pages combines the view (UI) and controller logic into a single file, making it more intuitive for many developers, especially beginners.
Razor Pages is built on top of the ASP.NET Core MVC framework, so it includes all the features of MVC such as dependency injection, routing, and model binding, but with a simplified structure that focuses on pages rather than controllers and views.
Getting Started with Razor Pages
Prerequisites
To get started with Razor Pages, you need:
- .NET SDK (version 6.0 or later recommended)
- A code editor (like Visual Studio, Visual Studio Code, or JetBrains Rider)
Creating a New Razor Pages Project
You can create a new Razor Pages project using the .NET CLI:
dotnet new webapp -n MyFirstRazorApp
cd MyFirstRazorApp
dotnet run
This creates a new Razor Pages project named "MyFirstRazorApp", moves into the project directory, and runs the application. You can access your running application by navigating to https://localhost:5001
in your web browser.
Understanding the Project Structure
A typical Razor Pages project has the following structure:
MyFirstRazorApp/
├── Pages/ # Contains all Razor Pages
│ ├── Shared/ # Contains layout files and partial views
│ ├── _ViewImports.cshtml # Imports common namespaces
│ ├── _ViewStart.cshtml # Sets up common layout
│ ├── Index.cshtml # The homepage
│ └── Privacy.cshtml # A sample privacy page
├── wwwroot/ # Static files (CSS, JS, images)
├── appsettings.json # Configuration settings
└── Program.cs # Application startup and configuration
Anatomy of a Razor Page
A Razor Page consists of two parts:
- The Page File (.cshtml): Contains the HTML markup with embedded C# code
- The Page Model File (.cshtml.cs): Contains the C# code that handles page events and data
Page File (.cshtml)
Here's a simple example of a Razor Page file:
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Welcome to Razor Pages</h1>
<p>The current time is: @DateTime.Now</p>
<form method="post">
<button type="submit" class="btn btn-primary">Click Me</button>
</form>
@if (Model.IsButtonClicked)
{
<p class="mt-3 alert alert-success">You clicked the button at @Model.ClickedTime</p>
}
</div>
Page Model File (.cshtml.cs)
The corresponding Page Model file:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MyFirstRazorApp.Pages
{
public class IndexModel : PageModel
{
public bool IsButtonClicked { get; set; }
public DateTime ClickedTime { get; set; }
public void OnGet()
{
IsButtonClicked = false;
}
public void OnPost()
{
IsButtonClicked = true;
ClickedTime = DateTime.Now;
}
}
}
Key Concepts in Razor Pages
Page Directive
The @page
directive at the top of a .cshtml file indicates that the file is a Razor Page. It can include route parameters:
@page "{id:int}"
This specifies that the page expects an integer parameter named "id" in the URL.
Page Model
The PageModel class is where you write the C# code that handles requests. It typically includes:
- Properties: Data that will be displayed on the page
- Handler methods: Methods that respond to HTTP requests
- OnGet(): Handles GET requests
- OnPost(): Handles POST requests
- OnPostAsync(): Async version of OnPost
Data Binding
Razor Pages makes it easy to bind form data to properties in your PageModel:
<form method="post">
<div class="form-group">
<label for="Name">Name:</label>
<input asp-for="Customer.Name" class="form-control" />
<span asp-validation-for="Customer.Name" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
In the PageModel:
[BindProperty]
public Customer Customer { get; set; } = new();
public void OnPost()
{
if (!ModelState.IsValid)
{
return;
}
// Process the customer data
}
The [BindProperty]
attribute tells Razor Pages to bind form values to the Customer property on POST requests.
Practical Example: Creating a Simple Todo Application
Let's create a simple Todo application to demonstrate Razor Pages in action.
Step 1: Create a Todo Model
First, create a Models
folder in your project and add a Todo.cs
file:
namespace MyFirstRazorApp.Models
{
public class Todo
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public bool IsDone { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.Now;
}
}
Step 2: Create a Service to Manage Todos
Create a Services
folder and add an interface and implementation:
// ITodoService.cs
using MyFirstRazorApp.Models;
namespace MyFirstRazorApp.Services
{
public interface ITodoService
{
List<Todo> GetAllTodos();
Todo? GetTodoById(int id);
void AddTodo(Todo todo);
void UpdateTodo(Todo todo);
void DeleteTodo(int id);
}
}
// TodoService.cs
using MyFirstRazorApp.Models;
namespace MyFirstRazorApp.Services
{
public class TodoService : ITodoService
{
private readonly List<Todo> _todos = new();
private int _nextId = 1;
public TodoService()
{
// Add some sample todos
AddTodo(new Todo { Title = "Learn Razor Pages" });
AddTodo(new Todo { Title = "Build a Todo App" });
}
public List<Todo> GetAllTodos() => _todos;
public Todo? GetTodoById(int id) => _todos.FirstOrDefault(t => t.Id == id);
public void AddTodo(Todo todo)
{
todo.Id = _nextId++;
_todos.Add(todo);
}
public void UpdateTodo(Todo todo)
{
var existingTodo = GetTodoById(todo.Id);
if (existingTodo != null)
{
existingTodo.Title = todo.Title;
existingTodo.IsDone = todo.IsDone;
}
}
public void DeleteTodo(int id)
{
var todo = GetTodoById(id);
if (todo != null)
{
_todos.Remove(todo);
}
}
}
}
Step 3: Register the Service in Program.cs
Add the following to your Program.cs
file:
// Add this after builder.Services.AddRazorPages();
builder.Services.AddSingleton<ITodoService, TodoService>();
Step 4: Create the Todos Page
Create a new file at Pages/Todos/Index.cshtml
:
@page
@model MyFirstRazorApp.Pages.Todos.IndexModel
@{
ViewData["Title"] = "Todo List";
}
<h1>Todo List</h1>
<div class="row mb-4">
<div class="col-md-6">
<form method="post">
<div class="input-group">
<input asp-for="NewTodo.Title" class="form-control" placeholder="Add a new todo..." />
<button type="submit" class="btn btn-primary">Add</button>
</div>
<span asp-validation-for="NewTodo.Title" class="text-danger"></span>
</form>
</div>
</div>
<div class="row">
<div class="col-md-8">
@if (Model.Todos.Any())
{
<ul class="list-group">
@foreach (var todo in Model.Todos)
{
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<form method="post" asp-page-handler="ToggleStatus" asp-route-id="@todo.Id" class="d-inline">
<input type="checkbox" onchange="this.form.submit()" checked="@todo.IsDone" />
</form>
<span class="@(todo.IsDone ? "text-decoration-line-through" : "")">@todo.Title</span>
</div>
<form method="post" asp-page-handler="Delete" asp-route-id="@todo.Id" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
</li>
}
</ul>
}
else
{
<p>No todos yet. Add your first one above!</p>
}
</div>
</div>
And its corresponding PageModel at Pages/Todos/Index.cshtml.cs
:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyFirstRazorApp.Models;
using MyFirstRazorApp.Services;
using System.ComponentModel.DataAnnotations;
namespace MyFirstRazorApp.Pages.Todos
{
public class IndexModel : PageModel
{
private readonly ITodoService _todoService;
public IndexModel(ITodoService todoService)
{
_todoService = todoService;
}
public List<Todo> Todos { get; set; } = new();
[BindProperty]
public TodoInputModel NewTodo { get; set; } = new();
public void OnGet()
{
Todos = _todoService.GetAllTodos();
}
public IActionResult OnPost()
{
if (!ModelState.IsValid)
{
Todos = _todoService.GetAllTodos();
return Page();
}
_todoService.AddTodo(new Todo
{
Title = NewTodo.Title
});
return RedirectToPage();
}
public IActionResult OnPostToggleStatus(int id)
{
var todo = _todoService.GetTodoById(id);
if (todo != null)
{
todo.IsDone = !todo.IsDone;
_todoService.UpdateTodo(todo);
}
return RedirectToPage();
}
public IActionResult OnPostDelete(int id)
{
_todoService.DeleteTodo(id);
return RedirectToPage();
}
}
public class TodoInputModel
{
[Required(ErrorMessage = "Please enter a title for your todo")]
[StringLength(100, ErrorMessage = "Title must be between {2} and {1} characters", MinimumLength = 2)]
public string Title { get; set; } = string.Empty;
}
}
Step 5: Add a Navigation Link
Edit Pages/Shared/_Layout.cshtml
to add a navigation link to your todo page:
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Todos/Index">Todos</a>
</li>
Running the Todo Application
Now run the application with dotnet run
and navigate to the Todos page. You should be able to:
- View the list of todos
- Add new todos
- Toggle todo completion status
- Delete todos
This simple application demonstrates the key features of Razor Pages:
- Page models with handler methods
- Form submission and model binding
- Dependency injection
- Page handlers for different actions
Advanced Razor Pages Concepts
Handler Methods with Parameters
You can define specific handler methods for different actions:
public IActionResult OnPostDelete(int id)
{
// Delete the item with the specified id
return RedirectToPage();
}
Then in your HTML, you can specify which handler to use:
<form method="post" asp-page-handler="Delete" asp-route-id="@item.Id">
<button type="submit">Delete</button>
</form>
Page Models with Dependency Injection
Razor Pages supports dependency injection, making it easy to use services in your page models:
public class IndexModel : PageModel
{
private readonly IDataService _dataService;
public IndexModel(IDataService dataService)
{
_dataService = dataService;
}
public void OnGet()
{
Items = _dataService.GetItems();
}
}
Page Routing
You can customize routes using the @page
directive:
@page "{category?}"
This makes the category parameter optional. You can also add constraints:
@page "{id:int}"
This ensures the id parameter must be an integer.
Using Partial Views
You can break down complex pages into partial views:
<div class="sidebar">
<partial name="_SidebarPartial" model="Model.SidebarData" />
</div>
Summary
Razor Pages is a powerful and beginner-friendly approach to building web applications in ASP.NET Core. Its page-based model simplifies the development process while still allowing for complex applications to be built. Key benefits include:
- Simplified structure combining view and controller logic
- Built-in support for common web development tasks
- Strong integration with ASP.NET Core features like dependency injection
- Clean separation of concerns with the Page/PageModel pattern
- Easy data binding and form handling
By understanding the fundamentals of Razor Pages, you now have the knowledge to build a wide range of web applications, from simple brochure sites to complex data-driven applications.
Additional Resources
Practice Exercises
- Blog Application: Create a simple blog with posts and comments using Razor Pages.
- Contact Form: Build a contact form with validation that sends emails.
- Product Catalog: Create a product listing page with filtering and pagination.
- User Management: Implement a user registration and profile system using Identity.
- Dashboard: Create a dashboard with charts and statistics using a library like Chart.js.
By working through these exercises, you'll gain practical experience with Razor Pages and solidify your understanding of ASP.NET Core web development.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)