Advanced Form Tag Helpers — Select, Checkbox and File Upload

Form-related Tag Helpers are the most frequently used in real MVC applications. Beyond the basic asp-for on text inputs, every form element type — select dropdowns, checkboxes, radio buttons, file uploads, textareas — has Tag Helper support with model binding built in. Getting these right is the difference between forms that work reliably and forms with subtle binding bugs that only surface with specific user inputs.

Select Dropdown with asp-items

// ── ViewModel with a list property for dropdown options ───────────────────
public class CreatePostViewModel
{
    [Required]
    public string Title { get; set; } = string.Empty;

    [Required]
    public int CategoryId { get; set; }

    // Options for the dropdown — populated by the controller
    public IEnumerable<SelectListItem> CategoryOptions { get; set; } = [];
}

// ── Controller — populate dropdown options ────────────────────────────────
[HttpGet]
public async Task<IActionResult> Create()
{
    var categories = await _categoryService.GetAllAsync();
    var vm = new CreatePostViewModel
    {
        CategoryOptions = categories.Select(c => new SelectListItem
        {
            Value = c.Id.ToString(),
            Text  = c.Name,
        })
    };
    return View(vm);
}

// ── View — select dropdown ─────────────────────────────────────────────────
@model CreatePostViewModel

<div class="mb-3">
    <label asp-for="CategoryId" class="form-label"></label>
    <select asp-for="CategoryId" asp-items="Model.CategoryOptions"
            class="form-select">
        <option value="">-- Select a category --</option>
    </select>
    <span asp-validation-for="CategoryId" class="text-danger"></span>
</div>
@* asp-for="CategoryId" sets id and name; asp-items populates the option elements;
   the selected option is pre-selected if CategoryId has a non-zero value *@

@* ── Enum dropdown — asp-items with Html.GetEnumSelectList ──────────────── *@
<select asp-for="Status" asp-items="Html.GetEnumSelectList<PostStatus>()"
        class="form-select">
</select>
Note: When re-displaying a form after a POST (validation failure), the CategoryOptions collection must be re-populated in the controller — it is not preserved across the POST because it is not submitted as form data. The most common bug with dropdown ViewModels: controller POST action re-displays the form when validation fails, but forgets to re-populate the dropdown options, causing the select to render empty. Always re-populate all SelectListItem collections before returning View(model) in a POST action.
Tip: Use Html.GetEnumSelectList<TEnum>() to automatically generate select options from an enum type. This saves manually building a SelectListItem list and stays in sync when enum values are added or renamed. Combine with the [Display(Name = "...")] attribute on enum values to control the displayed text: [Display(Name = "In Review")] InReview displays as “In Review” rather than “InReview”.
Warning: The checkbox hidden field is a subtle but important behaviour. For a boolean checkbox (<input asp-for="IsPublished" type="checkbox" />), ASP.NET Core’s Tag Helper automatically renders a hidden <input type="hidden" name="IsPublished" value="false" /> alongside the checkbox. When the checkbox is unchecked, only the hidden field is submitted (value=false). When checked, both are submitted; model binding picks the first matching value (true). Without this hidden field, unchecking a checkbox submits nothing for that field — model binding interprets the absence as null or default, not false.

All Input Types with asp-for

@model PostFormViewModel

@* ── Text input (default) ─────────────────────────────────────────────────── *@
<input asp-for="Title" class="form-control" />
@* renders: <input type="text" id="Title" name="Title" value="..." /> *@

@* ── Textarea ─────────────────────────────────────────────────────────────── *@
<textarea asp-for="Body" class="form-control" rows="8"></textarea>
@* renders: <textarea id="Body" name="Body">current value</textarea> *@

@* ── Checkbox ─────────────────────────────────────────────────────────────── *@
<div class="form-check">
    <input asp-for="IsPublished" type="checkbox" class="form-check-input" />
    <label asp-for="IsPublished" class="form-check-label"></label>
</div>
@* also renders: <input type="hidden" name="IsPublished" value="false" /> *@

@* ── Date input ───────────────────────────────────────────────────────────── *@
<input asp-for="ScheduledFor" type="date" class="form-control" />
@* [DataType(DataType.Date)] on the property sets type="date" automatically *@

@* ── File upload ──────────────────────────────────────────────────────────── *@
<input asp-for="CoverImage" type="file" accept="image/*" class="form-control" />
@* The form must have enctype="multipart/form-data" for file upload: *@
<form asp-action="Create" method="post" enctype="multipart/form-data">

@* ── Radio buttons ────────────────────────────────────────────────────────── *@
@foreach (var option in Model.PriorityOptions)
{
    <div class="form-check">
        <input asp-for="Priority" type="radio"
               value="@option.Value" class="form-check-input" />
        <label class="form-check-label">@option.Text</label>
    </div>
}

Common Mistakes

Mistake 1 — Not re-populating SelectListItem collections on validation failure

❌ Wrong — POST action returns View(model) with empty CategoryOptions; dropdown is empty:

if (!ModelState.IsValid) return View(model);  // model.CategoryOptions is empty!

✅ Correct — re-populate all option collections before returning View(model).

Mistake 2 — Missing enctype=”multipart/form-data” for file uploads

❌ Wrong — form POST without multipart encoding; IFormFile parameter is null:

<form asp-action="Create" method="post">  @* missing enctype! *@

✅ Correct — add enctype="multipart/form-data" to any form with file upload inputs.

🧠 Test Yourself

Why does asp-for on a checkbox render a hidden field in addition to the visible checkbox?