Skip to content

Tag Helpers & View Components

Key Points

  • Tag helpers are server-side attributes/elements that produce HTML — write <form asp-controller="Home" asp-action="Index"> instead of HTML helpers (@Html.BeginForm(...)).
  • View components are reusable view fragments with their own logic — like a mini-controller producing partial HTML. Use for headers, navigation, dynamic widgets.
  • Built-in tag helpers for forms, links, validation, environment-conditional rendering, image cache-busting, and more.
  • Custom tag helpers subclass TagHelper and override Process / ProcessAsync.
  • ViewComponent vs partial view vs section: partial = static include; section = layout extension; view component = logic + render.

Concepts (deep dive)

Built-in tag helpers — common ones

<!-- Form helpers -->
<form asp-controller="Account" asp-action="Login" method="post">
    <input asp-for="Email" />
    <span asp-validation-for="Email"></span>
    <input asp-for="Password" type="password" />
    <button type="submit">Sign in</button>
</form>

<!-- Link / anchor helpers -->
<a asp-page="/Orders/Index" asp-route-id="@Model.Id">View order</a>

<!-- Cache-busting on static files -->
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />

<!-- Environment-conditional content -->
<environment include="Development">
    <link rel="stylesheet" href="~/lib/bootstrap/dist/bootstrap.css" />
</environment>
<environment exclude="Development">
    <link rel="stylesheet" href="~/lib/bootstrap/dist/bootstrap.min.css" asp-append-version="true" />
</environment>

asp-for is the workhorse — generates name, id, value, validation attributes from the model property.

asp-append-version="true" adds a hash query string to the URL — browsers re-fetch when the file changes; can serve with long cache headers.

Validation tag helpers

<form asp-controller="Account" asp-action="Login">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    <input asp-for="Email" />
    <span asp-validation-for="Email" class="text-danger"></span>
</form>

@section Scripts {
    @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

asp-validation-for reads ModelState errors for the property; asp-validation-summary aggregates form-level errors. Combined with _ValidationScriptsPartial (jQuery validate), you get both server-side and client-side validation.

Custom tag helpers

[HtmlTargetElement("alert")]
public class AlertTagHelper : TagHelper
{
    public string? Type { get; set; } = "info";

    public override void Process(TagHelperContext ctx, TagHelperOutput output)
    {
        output.TagName = "div";
        output.Attributes.SetAttribute("class", $"alert alert-{Type}");
    }
}

Register globally in _ViewImports.cshtml:

@addTagHelper *, MyApp

Use:

<alert type="warning">Be careful!</alert>
<!-- Renders: <div class="alert alert-warning">Be careful!</div> -->

For async operations or content access, use ProcessAsync:

public override async Task ProcessAsync(TagHelperContext ctx, TagHelperOutput output)
{
    var content = await output.GetChildContentAsync();
    output.Content.SetHtmlContent(content.GetContent().ToUpper());
}

View components

public class CartViewComponent(ICartService cart) : ViewComponent
{
    public async Task<IViewComponentResult> InvokeAsync()
    {
        var cart = await cart.GetCurrentAsync();
        return View(cart);
    }
}
Views/Shared/Components/Cart/Default.cshtml

Invoke from a view:

@await Component.InvokeAsync("Cart")
<!-- Or, with parameters: -->
@await Component.InvokeAsync("Search", new { query = "abc" })

Or via tag helper syntax (after registering in _ViewImports.cshtml):

@addTagHelper *, MyApp

<vc:cart />
<vc:search query="abc" />

View component vs partial vs section

Construct Use case
Partial view (@await Html.PartialAsync("_Foo")) Static template re-use; same data passed in
Section (@section Scripts { ... }) Layout extension points
View component Self-contained widget with its own data fetch and logic

Use a partial if it's "just markup with a model passed in". Use a view component if the widget needs to fetch its own data, query a service, etc. Use a section to inject scripts/styles into a layout slot.

IsRequired, Order, lifecycle attributes

[HtmlTargetElement("img", Attributes = "src,asp-append-version")]
public class CacheBustingImageTagHelper : TagHelper
{
    public int Order => -1000;   // run before built-in helpers

    [HtmlAttributeName("src")] public string Src { get; set; } = "";
    public bool AspAppendVersion { get; set; }

    public override void Process(TagHelperContext ctx, TagHelperOutput output)
    {
        if (AspAppendVersion) /* compute version, append to src */;
    }
}

Order controls execution order when multiple tag helpers target the same element.

Component vs tag helper

In Blazor, components (<MyComponent />) are interactive. In MVC/Razor Pages, tag helpers (and view components via <vc:foo />) are server-rendered. Blazor and MVC tag helpers don't mix in a single rendering pass — Blazor's render mode is separate.


Code: correct vs wrong

❌ Wrong: typing names manually

<input name="Email" id="Email" value="@Model.Email" />
<span data-valmsg-for="Email" class="text-danger"></span>

✅ Correct: tag helpers do it for you

<input asp-for="Email" />
<span asp-validation-for="Email"></span>

❌ Wrong: hand-rolling cache busting

<link rel="stylesheet" href="/css/[email protected]" />

✅ Correct: asp-append-version

<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />

❌ Wrong: partial view doing data fetching

@{ var data = await ServiceLocator.Get<IFoo>().GetAsync(); }
@Html.PartialAsync("_FooPartial", data)

✅ Correct: view component

@await Component.InvokeAsync("Foo")

Design patterns for this topic

Pattern 1 — "Tag helpers over HTML helpers"

  • Intent: cleaner Razor; HTML-shaped syntax.

Pattern 2 — "View component for self-contained widgets"

  • Intent: logic + render in one place.

Pattern 3 — "Custom tag helpers for repeated markup"

  • Intent: turn <div class="alert alert-info"> boilerplate into <alert type="info">.

Pattern 4 — "asp-append-version + long cache headers"

  • Intent: static-asset cache busting without manual versioning.

Pros & cons / trade-offs

Construct Pros Cons
Tag helpers HTML-shaped; IDE-friendly Hidden compile-time magic
View components Self-contained widgets More code than partials
Partials Simple No own logic
Custom tag helpers DRY markup Not Blazor-compatible

When to use / when to avoid

  • Use tag helpers instead of HTML helpers (@Html.*) in modern code.
  • Use view components for widgets needing their own data.
  • Use partials for pure template reuse with passed-in data.
  • Avoid mixing partials and view components for the same purpose — pick one based on whether the widget owns its data.

Interview Q&A

Q1. Difference between tag helpers and HTML helpers? Tag helpers look like HTML attributes (<input asp-for="Email" />); HTML helpers are method calls (@Html.TextBoxFor(m => m.Email)). Tag helpers are preferred — better IDE support, more HTML-like.

Q2. What's asp-for? Binds a model property to a form input. Generates name, id, validation attributes.

Q3. What's a view component? A reusable view fragment with its own logic and view template — InvokeAsync() method does the data fetch, returns IViewComponentResult. Mini-controller for partial output.

Q4. When do you choose a view component over a partial? View component when the widget needs its own data fetching. Partial when you just pass in a model.

Q5. What does <environment> tag helper do? Conditionally renders content based on IHostEnvironment.EnvironmentName. Useful for dev-vs-prod asset bundles.

Q6. How do you create a custom tag helper? Subclass TagHelper; override Process/ProcessAsync. Register in _ViewImports.cshtml with @addTagHelper *, AssemblyName.

Q7. What's asp-append-version="true" for? Appends a hash of the file content to the URL — cache-busts when the file changes. Combine with long cache headers.

Q8. How do view components receive parameters? InvokeAsync(parameter1, parameter2, ...) — the framework binds parameters from the call.

Q9. Where does the view component view live? Views/Shared/Components/{ComponentName}/Default.cshtml (or {ViewName}.cshtml if you specify).

Q10. Tag helpers on Blazor components? Blazor uses components, not tag helpers. They don't interchange — render modes separate them.


Gotchas / common mistakes

  • ⚠️ Forgetting _ViewImports.cshtml to register tag helpers — they don't activate.
  • ⚠️ Custom tag helper not registered — silent fallback to literal HTML.
  • ⚠️ View component bypassing controller filters — auth must be at component or middleware level.
  • ⚠️ Heavy data fetch in a header view component — loads on every page.
  • ⚠️ Partial view doing data fetching — should be a view component instead.

Further reading