Razor View Development Patterns
View Structure
Project Layout
Views/ ├── Shared/ │ ├── _Layout.cshtml # Main layout template │ ├── _LayoutEmpty.cshtml # Minimal layout (no header/footer) │ ├── _Header.cshtml # Header partial │ ├── _Footer.cshtml # Footer partial │ ├── _Navigation.cshtml # Navigation partial │ ├── _Pagination.cshtml # Reusable pagination │ └── Components/ # View components │ └── SearchBox/ │ └── Default.cshtml ├── Home/ │ ├── Index.cshtml │ └── About.cshtml ├── Products/ │ ├── Index.cshtml │ ├── Details.cshtml │ └── _ProductCard.cshtml # Page-specific partial ├── _ViewImports.cshtml # Shared imports and tag helpers └── _ViewStart.cshtml # Default layout assignment
_ViewImports.cshtml
@using MyApp.Web @using MyApp.Web.Models @using MyApp.Core.Models @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, MyApp.Web
_ViewStart.cshtml
@{ Layout = "_Layout"; }
Layout Templates
Main Layout
@* Views/Shared/_Layout.cshtml *@ <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - My Application</title>
@* Head section for page-specific styles *@
@await RenderSectionAsync("Styles", required: false)
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head> <body class="@ViewData["BodyClass"]"> <header> @await Html.PartialAsync("_Header") @await Html.PartialAsync("_Navigation") </header>
<main class="container">
@RenderBody()
</main>
<footer>
@await Html.PartialAsync("_Footer")
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@* Scripts section for page-specific JavaScript *@
@await RenderSectionAsync("Scripts", required: false)
</body> </html>
Page Using Layout
@model ProductListViewModel @{ ViewData["Title"] = "Products"; ViewData["BodyClass"] = "products-page"; }
@section Styles { <link rel="stylesheet" href="~/css/products.css" asp-append-version="true" /> }
<h1>@ViewData["Title"]</h1>
<div class="product-grid"> @foreach (var product in Model.Products) { @await Html.PartialAsync("_ProductCard", product) } </div>
@section Scripts { <script src="~/js/products.js" asp-append-version="true"></script> }
Nested Layouts
@* Views/Shared/_LayoutAdmin.cshtml *@ @{ Layout = "_Layout"; }
<div class="admin-container"> <aside class="admin-sidebar"> @await Html.PartialAsync("_AdminNav") </aside> <div class="admin-content"> @RenderBody() </div> </div>
@section Scripts { <script src="~/js/admin.js" asp-append-version="true"></script> @await RenderSectionAsync("AdminScripts", required: false) }
Partial Views
Rendering Partials
@* Async (recommended) *@ @await Html.PartialAsync("_ProductCard", product)
@* With explicit view path *@ @await Html.PartialAsync("~/Views/Shared/_Header.cshtml")
@* Synchronous (avoid for I/O operations) *@ @Html.Partial("_Sidebar")
@* As tag helper *@ <partial name="_ProductCard" model="product" />
@* With view-data *@ <partial name="_Pagination" model="Model.Pagination" view-data="ViewData" />
Partial View Example
@* Views/Products/_ProductCard.cshtml *@ @model Product
<article class="product-card"> <a asp-action="Details" asp-route-id="@Model.Id" class="product-card__link"> <img src="@Model.ImageUrl" alt="@Model.Name" class="product-card__image" /> <div class="product-card__content"> <h3 class="product-card__title">@Model.Name</h3> <p class="product-card__price">@Model.Price.ToString("C")</p> @if (Model.IsOnSale) { <span class="product-card__badge">Sale</span> } </div> </a> </article>
View Components
Component Class
// ViewComponents/RecentArticlesViewComponent.cs public class RecentArticlesViewComponent : ViewComponent { private readonly IArticleService _articleService;
public RecentArticlesViewComponent(IArticleService articleService)
{
_articleService = articleService;
}
public async Task<IViewComponentResult> InvokeAsync(int count = 5)
{
var articles = await _articleService.GetRecentAsync(count);
return View(articles);
}
}
Component View
@* Views/Shared/Components/RecentArticles/Default.cshtml *@ @model IEnumerable<Article>
<section class="recent-articles"> <h3>Recent Articles</h3> <ul> @foreach (var article in Model) { <li> <a asp-controller="Articles" asp-action="Details" asp-route-slug="@article.Slug"> @article.Title </a> <time datetime="@article.PublishedDate.ToString("yyyy-MM-dd")"> @article.PublishedDate.ToString("MMM dd, yyyy") </time> </li> } </ul> </section>
Invoking Components
@* Tag helper syntax (preferred) *@ <vc:recent-articles count="5" />
@* Async syntax *@ @await Component.InvokeAsync("RecentArticles", new { count = 5 })
@* With cache *@ <cache expires-after="@TimeSpan.FromMinutes(10)"> <vc:recent-articles count="5" /> </cache>
Tag Helpers
Built-in Tag Helpers
@* Anchor tag helper *@ <a asp-controller="Products" asp-action="Details" asp-route-id="@product.Id" asp-route-category="@product.Category"> View Details </a>
@* Form tag helpers *@ <form asp-controller="Contact" asp-action="Submit" method="post"> <div class="form-group"> <label asp-for="Name"></label> <input asp-for="Name" class="form-control" /> <span asp-validation-for="Name" class="text-danger"></span> </div>
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Category"></label>
<select asp-for="Category" asp-items="Model.Categories" class="form-control">
<option value="">Select a category</option>
</select>
<span asp-validation-for="Category" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
@* Image with cache busting *@ <img src="~/images/logo.png" asp-append-version="true" alt="Logo" />
@* Environment-specific rendering *@
<environment include="Development">
<link rel="stylesheet" href="/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="/css/site.min.css" asp-append-version="true" />
</environment>
Custom Tag Helper
// TagHelpers/EmailTagHelper.cs [HtmlTargetElement("email")] public class EmailTagHelper : TagHelper { public string Address { get; set; } public string Subject { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
var href = $"mailto:{Address}";
if (!string.IsNullOrEmpty(Subject))
{
href += $"?subject={Uri.EscapeDataString(Subject)}";
}
output.Attributes.SetAttribute("href", href);
output.Content.SetContent(Address);
}
}
@* Usage *@ <email address="support@example.com" subject="Help Request" />
Model Binding
Strongly-Typed Views
@model ContactFormViewModel
<form asp-action="Submit" method="post"> @Html.AntiForgeryToken()
<div class="form-group">
<label asp-for="Name" class="form-label"></label>
<input asp-for="Name" class="form-control" placeholder="Your name" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" type="email" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Message" class="form-label"></label>
<textarea asp-for="Message" class="form-control" rows="5"></textarea>
<span asp-validation-for="Message" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Send Message</button>
</form>
@section Scripts { <partial name="_ValidationScriptsPartial" /> }
ViewModel with Validation
public class ContactFormViewModel { [Required(ErrorMessage = "Name is required")] [StringLength(100, ErrorMessage = "Name cannot exceed 100 characters")] [Display(Name = "Full Name")] public string Name { get; set; }
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Please enter a valid email address")]
public string Email { get; set; }
[Required(ErrorMessage = "Message is required")]
[StringLength(2000, MinimumLength = 10,
ErrorMessage = "Message must be between 10 and 2000 characters")]
public string Message { get; set; }
}
Conditional Rendering
If/Else Statements
@if (Model.Products.Any()) { <div class="product-grid"> @foreach (var product in Model.Products) { <partial name="_ProductCard" model="product" /> } </div> } else { <div class="empty-state"> <p>No products found.</p> <a asp-action="Index" asp-controller="Home" class="btn btn-link"> Return to Home </a> </div> }
Switch Statements
@switch (Model.Status) { case OrderStatus.Pending: <span class="badge badge-warning">Pending</span> break; case OrderStatus.Processing: <span class="badge badge-info">Processing</span> break; case OrderStatus.Shipped: <span class="badge badge-primary">Shipped</span> break; case OrderStatus.Delivered: <span class="badge badge-success">Delivered</span> break; default: <span class="badge badge-secondary">Unknown</span> break; }
Null Checking
@* Null conditional *@ <p>Author: @Model.Author?.Name ?? "Unknown"</p>
@* With fallback *@ @if (Model.ImageUrl != null) { <img src="@Model.ImageUrl" alt="@Model.Title" /> } else { <img src="~/images/placeholder.jpg" alt="No image available" /> }
@* Ternary operator *@ <div class="@(Model.IsActive ? "active" : "inactive")"> @Model.Title </div>
Loops and Collections
For Loop
@for (int i = 0; i < Model.Items.Count; i++) { <div class="item @(i % 2 == 0 ? "even" : "odd")"> <span class="item-number">@(i + 1)</span> <span class="item-name">@Model.Items[i].Name</span> </div> }
Foreach with Index
@{ var index = 0; } @foreach (var item in Model.Items) { <div class="item" data-index="@index"> @item.Name </div> index++; }
Rendering Lists
<ul class="breadcrumb"> @foreach (var crumb in Model.Breadcrumbs) { var isLast = crumb == Model.Breadcrumbs.Last(); <li class="breadcrumb-item @(isLast ? "active" : "")"> @if (isLast) { @crumb.Title } else { <a href="@crumb.Url">@crumb.Title</a> } </li> } </ul>
HTML Helpers vs Tag Helpers
Prefer Tag Helpers
@* HTML Helper (older approach) *@ @Html.ActionLink("Details", "Details", "Products", new { id = product.Id }, new { @class = "btn btn-link" })
@* Tag Helper (modern, preferred) *@ <a asp-controller="Products" asp-action="Details" asp-route-id="@product.Id" class="btn btn-link"> Details </a>
@* HTML Helper for form *@ @Html.TextBoxFor(m => m.Name, new { @class = "form-control", placeholder = "Enter name" })
@* Tag Helper (cleaner) *@ <input asp-for="Name" class="form-control" placeholder="Enter name" />
When to Use HTML Helpers
@* Complex dynamic attributes *@ @Html.TextBoxFor(m => m.Name, Model.GetInputAttributes())
@* Raw HTML content *@ @Html.Raw(Model.HtmlContent)
@* Display templates *@ @Html.DisplayFor(m => m.CreatedDate)
@* Editor templates *@ @Html.EditorFor(m => m.Address)
ViewData, ViewBag, and TempData
ViewData (Dictionary)
@* In Controller *@ ViewData["Title"] = "Product Details"; ViewData["ShowSidebar"] = true;
@* In View *@ <h1>@ViewData["Title"]</h1> @if ((bool?)ViewData["ShowSidebar"] == true) { <partial name="_Sidebar" /> }
ViewBag (Dynamic)
@* In Controller *@ ViewBag.Categories = await _categoryService.GetAllAsync();
@* In View *@ <select asp-items="@(new SelectList(ViewBag.Categories, "Id", "Name"))"> <option value="">All Categories</option> </select>
TempData (Survives Redirect)
// In Controller TempData["SuccessMessage"] = "Product saved successfully!"; return RedirectToAction("Index");
@* In View *@ @if (TempData["SuccessMessage"] != null) { <div class="alert alert-success alert-dismissible"> @TempData["SuccessMessage"] <button type="button" class="close" data-dismiss="alert">×</button> </div> }
AJAX and Partial Rendering
AJAX Form Submission
<form id="contact-form" asp-action="SubmitAjax" method="post"> @Html.AntiForgeryToken()
<div class="form-group">
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<div id="result"></div>
@section Scripts { <script> $('#contact-form').on('submit', function(e) { e.preventDefault();
var $form = $(this);
var $button = $form.find('button[type="submit"]');
$button.prop('disabled', true).text('Sending...');
$.ajax({
url: $form.attr('action'),
type: 'POST',
data: $form.serialize(),
success: function(response) {
$('#result').html(response);
$form[0].reset();
},
error: function(xhr) {
$('#result').html('<div class="alert alert-danger">An error occurred.</div>');
},
complete: function() {
$button.prop('disabled', false).text('Submit');
}
});
});
</script>
}
Returning Partial Views from Controller
[HttpGet] public async Task<IActionResult> LoadMore(int page) { var products = await _productService.GetPagedAsync(page, 10); return PartialView("_ProductGrid", products); }
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> SubmitAjax(ContactFormViewModel model) { if (!ModelState.IsValid) { return PartialView("_ContactFormErrors", model); }
await _contactService.ProcessAsync(model);
return PartialView("_ContactFormSuccess");
}
Accessibility Considerations
@* Semantic HTML with ARIA *@ <nav aria-label="Main navigation"> <ul class="nav" role="menubar"> @foreach (var item in Model.NavItems) { <li role="none"> <a href="@item.Url" role="menuitem" aria-current="@(item.IsActive ? "page" : null)"> @item.Title </a> </li> } </ul> </nav>
@* Form accessibility *@ <div class="form-group"> <label asp-for="Email" id="email-label"></label> <input asp-for="Email" aria-labelledby="email-label" aria-describedby="email-help email-error" /> <small id="email-help" class="form-text text-muted"> We'll never share your email. </small> <span asp-validation-for="Email" id="email-error" class="text-danger" role="alert"></span> </div>
@* Skip link for keyboard navigation *@ <a href="#main-content" class="skip-link">Skip to main content</a>