ASP.NET CORE MVC - Layouts, Partial Views and Reusable UI
Layouts, Partial Views and Reusable UI
Refactor repeated Razor markup into reusable UI components, understand the shared layout pipeline, and keep navigation aware of authentication, ownership and administrator roles.
By the end of this tutorial, learners will understand _Layout.cshtml, _ViewImports.cshtml, _ViewStart.cshtml and partial views, and will refactor the Product Management application so common interface elements are easier to maintain.
Part 13 completed the authentication and authorization stage. The application already has Identity login/logout, Product ownership and an Admin role. Part 14 does not change the database. It reorganises the presentation layer.
- Understand reusable Razor UI
- Review the shared layout
- Understand
RenderBody() - Review
_ViewStart.cshtmland_ViewImports.cshtml - Create
_LoginPartial.cshtml - Use the login partial from the layout
- Create a reusable Product action partial
- Use it from the Product Index view
- Understand partial model passing
- Review Bootstrap and conditional navigation
- Test all user states
- Compare full code in the appendix
1. Open and Verify the Part 13 Project
Open the Xubuntu Terminal and begin from the project folder:
cd ~/aspnet-mvc-tutorial/ProductManagement
pwd
dotnet build
code .
The expected project path is:
/home/xubuntu/aspnet-mvc-tutorial/ProductManagement
Continue only when Part 13 builds successfully and the Admin role works.
2. Why Reusable UI Matters
Without reusable UI, the same markup can appear in many views. A later change must then be repeated in every location.
ASP.NET Core Razor Views provide several mechanisms for this. This part focuses on layouts and partial views.
3. Understand _Layout.cshtml
Open:
code Views/Shared/_Layout.cshtml
The layout is the shared outer page used by MVC views. It normally contains the document structure, Bootstrap references, navbar, page container, footer and scripts.
The individual view supplies only the page-specific content.
4. Locate RenderBody()
Inside Views/Shared/_Layout.cshtml, find:
<main role="main" class="pb-3">
@RenderBody()
</main>
@RenderBody() is the position where the current view is inserted into the layout.
For example, requesting /Products results conceptually in:
5. Review _ViewStart.cshtml
Open:
code Views/_ViewStart.cshtml
Verify:
@{
Layout = "_Layout";
}
This tells MVC views to use Views/Shared/_Layout.cshtml by default. Individual views therefore do not need to repeat the layout declaration.
6. Review _ViewImports.cshtml
Open:
code Views/_ViewImports.cshtml
Verify that it contains:
@using ProductManagement
@using ProductManagement.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
_ViewImports.cshtml makes common namespaces and Tag Helpers available to views beneath the folder.
| File | Purpose |
|---|---|
_Layout.cshtml | Shared visual shell for pages. |
_ViewStart.cshtml | Selects the default layout. |
_ViewImports.cshtml | Shares Razor namespaces and Tag Helper imports. |
7. Create _LoginPartial.cshtml
Part 11 introduced Identity navigation. We will now make the login/logout area explicitly reusable.
Create the file if it does not already exist:
touch Views/Shared/_LoginPartial.cshtml
code Views/Shared/_LoginPartial.cshtml
Replace its contents with:
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<span class="nav-link text-dark">
Hello @User.Identity?.Name!
</span>
</li>
<li class="nav-item">
<form class="form-inline"
asp-area="Identity"
asp-page="/Account/Logout"
asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })">
<button type="submit"
class="nav-link btn btn-link text-dark">
Logout
</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark"
asp-area="Identity"
asp-page="/Account/Register">
Register
</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark"
asp-area="Identity"
asp-page="/Account/Login">
Login
</a>
</li>
}
</ul>
SignInManager.IsSignedIn(User) determines whether the current request belongs to a signed-in Identity user. The partial can then render either Login/Register or the signed-in user controls.
8. Use _LoginPartial from _Layout.cshtml
Open:
code Views/Shared/_Layout.cshtml
Find the closing </ul> for the main navigation inside the collapsible navbar. Immediately after that list, verify or add:
<partial name="_LoginPartial" />
The layout now delegates authentication controls to the partial rather than containing all login/logout markup itself.
9. Keep the Admin Navigation Conditional
Still in Views/Shared/_Layout.cshtml, find the Products navigation item. Immediately after it, keep the Part 13 Admin condition:
@if (User.IsInRole("Admin"))
{
<li class="nav-item">
<a class="nav-link text-dark"
asp-area=""
asp-controller="Admin"
asp-action="Index">
Admin
</a>
</li>
}
This produces an authentication-aware interface: normal users do not see the Admin navigation item, while administrators do.
Conditional navigation is not security. AdminController must remain protected with [Authorize(Roles = "Admin")].
10. Create a Reusable Product Action Partial
The Product Index currently contains authorization-aware Details/Edit/Delete markup inside every table row. Move that UI into a partial.
Create:
touch Views/Products/_ProductActions.cshtml
code Views/Products/_ProductActions.cshtml
Add:
@using Microsoft.AspNetCore.Identity
@using ProductManagement.Models
@inject UserManager<IdentityUser> UserManager
@model Product
<a asp-action="Details"
asp-route-id="@Model.Id">Details</a>
@if (User.Identity?.IsAuthenticated == true
&& (
Model.OwnerId == UserManager.GetUserId(User)
|| User.IsInRole("Admin")
))
{
<text> | </text>
<a asp-action="Edit"
asp-route-id="@Model.Id">Edit</a>
<text> | </text>
<a asp-action="Delete"
asp-route-id="@Model.Id">Delete</a>
}
This partial receives one Product through its own @model Product declaration.
11. Replace the Action Markup in Index.cshtml
Open:
code Views/Products/Index.cshtml
Find the <td> inside the foreach loop that currently contains Details, Edit and Delete links.
Replace that entire action cell with:
<td>
<partial name="_ProductActions"
model="product" />
</td>
The model="product" attribute passes the current Product from the loop to the partial.
12. Remove Identity Injection from Index.cshtml
Because the ownership-aware link logic now lives in _ProductActions.cshtml, Index.cshtml no longer needs its own UserManager injection.
At the top of Views/Products/Index.cshtml, remove these lines if they are present:
@using Microsoft.AspNetCore.Identity
@inject UserManager<IdentityUser> UserManager
Keep the page model:
@model ProductManagement.ViewModels.ProductIndexViewModel
13. Understand Partial View Model Passing
Inside the Index loop:
@foreach (var product in Model.Products)
{
<partial name="_ProductActions"
model="product" />
}
product is one Product. The partial declares:
@model Product
Therefore the partial can use:
@Model.Id
@Model.OwnerId
14. Review Bootstrap in the Shared Layout
Open Views/Shared/_Layout.cshtml and locate the Bootstrap stylesheet:
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
Near the bottom, locate the Bootstrap JavaScript bundle:
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
Bootstrap classes already used throughout the application include navbar, container, btn, table, form-control and form-select. Keeping Bootstrap references in the layout makes them available throughout the site.
15. Build Before Testing
Save the modified files:
Views/Shared/_Layout.cshtml
Views/Shared/_LoginPartial.cshtml
Views/Products/_ProductActions.cshtml
Views/Products/Index.cshtml
Then run:
dotnet build
The project should build with zero errors before browser testing.
16. Test as an Anonymous Visitor
Run:
dotnet run
Open /Products without signing in. Verify that the navbar shows Register and Login, the Admin link is absent, Product Details remains available, and Edit/Delete controls are absent.
17. Test as a Normal User
Sign in using a normal account. Verify that the navbar shows the signed-in identity and Logout, the Admin link remains absent, and Edit/Delete appear only for Products owned by the current user.
18. Test as Administrator
Sign out and sign in as the administrator from Part 13. Verify that the Admin navigation item appears, /Admin opens, and Edit/Delete controls appear for every Product.
19. Why Partial Views Improve Maintainability
Suppose you later rename Details to View or add another Product action. With the partial, the shared action UI is changed in one file:
Views/Products/_ProductActions.cshtml
Any view that uses that partial can then reuse the updated markup.
20. Layouts and Partials Have Different Jobs
| Feature | Best use |
|---|---|
| Layout | Whole-page shared structure such as navbar, container, footer and scripts. |
| Partial view | A smaller reusable fragment such as login controls or Product action links. |
_ViewStart | Default layout selection. |
_ViewImports | Common Razor imports and Tag Helpers. |
21. Troubleshooting
The partial view cannot be found
Verify the exact filename and location. Login partial: Views/Shared/_LoginPartial.cshtml. Product action partial: Views/Products/_ProductActions.cshtml.
Product partial reports a model type error
Verify the partial declares @model Product and Index passes model="product".
Tag Helpers appear as ordinary text or links are wrong
Open Views/_ViewImports.cshtml and confirm @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers exists.
Admin link does not appear
Verify the user is in the Admin role. If the role was assigned after login, log out and log in again.
Page content appears without the navbar
Check Views/_ViewStart.cshtml and verify Layout = "_Layout";.
22. Hands-On Exercise
- Change the text
Detailsin_ProductActions.cshtmltoView. - Refresh the Products page and confirm the change.
- Restore the text to
Details. - Test the navbar anonymously.
- Test it as a normal user.
- Test it as Admin.
- Confirm direct access to
/Adminis still protected even though the navigation is conditional.
23. Knowledge Check
- What is the purpose of
_Layout.cshtml? - What does
@RenderBody()do? - What is the purpose of
_ViewStart.cshtml? - What does
_ViewImports.cshtmlprovide? - What is a partial view?
- How is a Product passed to
_ProductActions.cshtml? - Why is hiding the Admin link not sufficient authorization?
- Why are Bootstrap references normally placed in the layout?
24. Part 14 Summary
- reviewed the shared MVC layout;
- understood
RenderBody(); - reviewed
_ViewStart.cshtmland_ViewImports.cshtml; - created a reusable Identity login partial;
- kept role-aware navigation in the layout;
- created a reusable Product action partial;
- passed a Product model into a partial;
- removed duplicated authorization-aware action markup; and
- tested anonymous, normal-user and administrator UI states.
Appendix — Full Code for Final Verification
Use this appendix after completing Part 14 to compare the complete new or substantially modified UI files.
Appendix A — Views/_ViewImports.cshtml
@using ProductManagement
@using ProductManagement.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Appendix B — Views/_ViewStart.cshtml
@{
Layout = "_Layout";
}
Appendix C — Views/Shared/_LoginPartial.cshtml
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<span class="nav-link text-dark">
Hello @User.Identity?.Name!
</span>
</li>
<li class="nav-item">
<form class="form-inline"
asp-area="Identity"
asp-page="/Account/Logout"
asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })">
<button type="submit"
class="nav-link btn btn-link text-dark">
Logout
</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark"
asp-area="Identity"
asp-page="/Account/Register">
Register
</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark"
asp-area="Identity"
asp-page="/Account/Login">
Login
</a>
</li>
}
</ul>
Appendix D — Views/Products/_ProductActions.cshtml
@using Microsoft.AspNetCore.Identity
@using ProductManagement.Models
@inject UserManager<IdentityUser> UserManager
@model Product
<a asp-action="Details"
asp-route-id="@Model.Id">Details</a>
@if (User.Identity?.IsAuthenticated == true
&& (
Model.OwnerId == UserManager.GetUserId(User)
|| User.IsInRole("Admin")
))
{
<text> | </text>
<a asp-action="Edit"
asp-route-id="@Model.Id">Edit</a>
<text> | </text>
<a asp-action="Delete"
asp-route-id="@Model.Id">Delete</a>
}
Appendix E — 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"] - ProductManagement</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/ProductManagement.styles.css" asp-append-version="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Product Management</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Products" asp-action="Index">Products</a>
</li>
@if (User.IsInRole("Admin"))
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Admin" asp-action="Index">Admin</a>
</li>
}
</ul>
<partial name="_LoginPartial" />
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
© 2026 - ProductManagement
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
Appendix F — Index.cshtml Action Cell
Inside the existing foreach loop in Views/Products/Index.cshtml, the final action cell should be:
<td>
<partial name="_ProductActions"
model="product" />
</td>
Appendix G — Final Verification Commands
cd ~/aspnet-mvc-tutorial/ProductManagement
dotnet build
dotnet run
Browser checks:
/Products
/Identity/Account/Login
/Identity/Account/Register
/Admin
If the shared layout renders on MVC pages, Login/Register/Logout change correctly with authentication state, the Admin link appears only for administrators, and the Product action partial preserves the ownership/Admin rules from Parts 12–13, Part 14 is complete.
Part 15 will use EF Core and LINQ aggregate operations such as CountAsync(), SumAsync(), AverageAsync() and GroupBy() to build a dashboard with Product, Category, quantity and inventory-value summaries.