Razor Pages and MVC views
Page models group a URL's handlers with its markup, and tag helpers remove the stringly-typed parts of a view.
Page models and handlers
// Pages/Orders/Edit.cshtml.cs
public sealed class EditModel : PageModel
{
private readonly AppDbContext _db;
public EditModel(AppDbContext db) => _db = db;
[BindProperty]
public OrderInput Input { get; set; } = new();
[TempData] public string? StatusMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int id, CancellationToken ct)
{
var order = await _db.Orders.FindAsync(new object[] { id }, ct);
if (order is null) return NotFound();
Input = new OrderInput(order.Id, order.Note);
return Page();
}
public async Task<IActionResult> OnPostAsync(int id, CancellationToken ct)
{
if (!ModelState.IsValid) return Page();
var order = await _db.Orders.FindAsync(new object[] { id }, ct);
if (order is null) return NotFound();
order.Note = Input.Note;
await _db.SaveChangesAsync(ct);
StatusMessage = "Saved.";
// Post, redirect, get: refresh does not resubmit the form
return RedirectToPage("./Edit", new { id });
}
}<form method="post">
<!-- The anti-forgery token is added by the form tag helper -->
<input asp-for="Input.Note" class="form-control" />
<span asp-validation-for="Input.Note" class="text-danger"></span>
<button type="submit" asp-page-handler="Save">Save</button>
<a asp-page="/Orders/Index" asp-route-id="@Model.Input.Id">Cancel</a>
</form>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}- Handlers are named
OnGet,OnPostand named variants likeOnPostSaveAsyncselected byasp-page-handler. - Rendering a page directly from a POST without redirecting means a refresh re-submits. Redirect after every successful state change.
asp-forgenerates the name, the value and the validation attributes from the model, which is why a renamed property updates the view.- The
@section Scriptsblock requires the layout to render it; a partial that is never called is a silent source of missing client-side validation.
Layouts, partials and view components
<!-- Views/Shared/_Layout.cshtml -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>@ViewData["Title"] - Example</title>
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
<header><partial name="_Nav" model="Model.Nav" /></header>
<main>
@RenderBody()
</main>
<footer><partial name="_Footer" /></footer>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>// A view component is a partial with logic and its own DI
public sealed class OrderSummaryViewComponent : ViewComponent
{
private readonly AppDbContext _db;
public OrderSummaryViewComponent(AppDbContext db) => _db = db;
public async Task<IViewComponentResult> InvokeAsync(int customerId,
CancellationToken ct)
{
var summary = await _db.Orders
.Where(o => o.CustomerId == customerId)
.GroupBy(o => o.Currency)
.Select(g => new CurrencyTotal(g.Key, g.Sum(o => o.Total)))
.ToListAsync(ct);
return View(summary);
}
}
// Used from a page or a view
// @await Component.InvokeAsync("OrderSummary", new { customerId = 7 })💡
Choose Razor Pages when the URL maps to one page with its own handlers; choose MVC controllers when one URL shape serves several related views; choose Blazor when the interaction is genuinely stateful on the client. Mixing all three in one application is possible and usually reflects a migration in progress.
FAQ
Why is my validation message not showing?
Usually because the validation script partial is missing, or because client-side validation is disabled and the server-side message is not rendered by a asp-validation-for tag.
Should TempData or a query string carry the status message?
TempData for a redirect after a POST, because it survives exactly one redirect and is then cleared. A query string leaks into logs and into bookmarks.
Related
Interactive UI with Blazor Controllers, dependency injection and middleware
Last refreshed 2026-09-18.