ASP.NET CORE MVC - Layouts, Partial Views and Reusable UI

ASP.NET CORE MVC TUTORIAL SERIES · PART 14

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.

Objective

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.

Starting point

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.

In this tutorial
  1. Understand reusable Razor UI
  2. Review the shared layout
  3. Understand RenderBody()
  4. Review _ViewStart.cshtml and _ViewImports.cshtml
  5. Create _LoginPartial.cshtml
  6. Use the login partial from the layout
  7. Create a reusable Product action partial
  8. Use it from the Product Index view
  9. Understand partial model passing
  10. Review Bootstrap and conditional navigation
  11. Test all user states
  12. 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
Checkpoint

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.

Repeated markup ↓ Many files to maintain ↓ Inconsistent UI Reusable partial/layout ↓ One shared implementation ↓ Consistent UI

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.

_Layout.cshtml ├── Header / Navigation ├── RenderBody() │ ↓ │ Current View ├── Footer └── Scripts

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:

_Layout.cshtml + Views/Products/Index.cshtml ↓ Complete HTML page

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.

FilePurpose
_Layout.cshtmlShared visual shell for pages.
_ViewStart.cshtmlSelects the default layout.
_ViewImports.cshtmlShares 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>
Why inject SignInManager?

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.

_Layout.cshtml ↓ <partial name="_LoginPartial" /> ↓ Signed in? ┌────┴────┐ Yes No ↓ ↓ User + Register + Logout Login

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.

Remember

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
ProductIndexViewModel ↓ Model.Products ↓ foreach product ↓ _ProductActions.cshtml ↓ @model Product

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
Checkpoint

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

FeatureBest use
LayoutWhole-page shared structure such as navbar, container, footer and scripts.
Partial viewA smaller reusable fragment such as login controls or Product action links.
_ViewStartDefault layout selection.
_ViewImportsCommon 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

  1. Change the text Details in _ProductActions.cshtml to View.
  2. Refresh the Products page and confirm the change.
  3. Restore the text to Details.
  4. Test the navbar anonymously.
  5. Test it as a normal user.
  6. Test it as Admin.
  7. Confirm direct access to /Admin is still protected even though the navigation is conditional.

23. Knowledge Check

  1. What is the purpose of _Layout.cshtml?
  2. What does @RenderBody() do?
  3. What is the purpose of _ViewStart.cshtml?
  4. What does _ViewImports.cshtml provide?
  5. What is a partial view?
  6. How is a Product passed to _ProductActions.cshtml?
  7. Why is hiding the Admin link not sufficient authorization?
  8. Why are Bootstrap references normally placed in the layout?

24. Part 14 Summary

  • reviewed the shared MVC layout;
  • understood RenderBody();
  • reviewed _ViewStart.cshtml and _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

Purpose

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">
            &copy; 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
Final Part 14 checkpoint

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.

Next: Part 15 — Dashboard and Aggregate Data

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.