Formularze HTML5 w ASP.NET Core — Pola, Walidacja i Model Binding

Kąt widzenia: formularze HTML5 z perspektywy .NET fullstack — jakie typy pól, atrybuty walidacji HTML5 i jak dokładnie HTML formularz mapuje się na model C# przez ASP.NET Core model binding
Przykład przewodni: formularz rejestracji konta i formularz dodawania produktu do koszyka
Jak formularz HTML trafia do backendu .NET
Żeby zrozumieć formularze HTML, musisz wiedzieć, co się dzieje gdy użytkownik klika “Wyślij”:
- Przeglądarka zbiera dane ze wszystkich pól formularza (
nameatrybuty) - Koduje je jako
application/x-www-form-urlencoded(domyślnie) lubmultipart/form-data(pliki) - Wysyła HTTP POST do adresu z atrybutu
action - ASP.NET Core otrzymuje żądanie — Model Binding czyta
namekażdego pola i mapuje na właściwości modelu C#
<!-- HTML -->
<form action="/account/register" method="post">
<input type="email" name="Email" value="jan@example.com">
<input type="password" name="Password" value="Secret123!">
<button type="submit">Zarejestruj</button>
</form>
// ASP.NET Core — Model Binding łączy name="Email" z RegisterModel.Email
public class RegisterModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
[MinLength(8)]
public string Password { get; set; } = string.Empty;
}
// Razor Page
public async Task<IActionResult> OnPostAsync(RegisterModel model)
{
// model.Email == "jan@example.com"
// model.Password == "Secret123!"
}
Kluczowy punkt: atrybut name w HTML musi odpowiadać nazwie właściwości w modelu C# (lub ścieżce właściwości dla zagnieżdżonych obiektów). Model Binding jest case-insensitive.
Struktura formularza
<form
action="/account/register"
method="post"
enctype="application/x-www-form-urlencoded"
novalidate
>
@Html.AntiForgeryToken() <!-- lub asp-antiforgery="true" w Razor -->
<!-- pola formularza -->
<button type="submit">Zarejestruj</button>
</form>
method — GET vs POST
GET — dane dołączone do URL jako query string: ?search=klawiatura&maxPrice=500
- Widoczne w URL (można dodać zakładkę, udostępnić)
- Limit długości URL (~2000 znaków)
- Używaj dla: wyszukiwanie, filtry, paginacja
POST — dane w ciele żądania HTTP, niewidoczne w URL
- Brak limitu długości
- Używaj dla: tworzenie danych, modyfikacja stanu, logowanie, rejestracja, plik upload
enctype — kodowanie danych
application/x-www-form-urlencoded— domyślne. Tekst zakodowany jakoklucz=wartość&klucz2=wartość2multipart/form-data— wymagane gdy formularz zawiera<input type="file">text/plain— rzadko używane, nie zalecane
Anti-forgery token — ochrona przed CSRF
ASP.NET Core automatycznie waliduje token anty-CSRF w POST requestach. Musi być w formularzu:
<!-- Razor Pages — automatycznie gdy używasz <form method="post"> -->
<form method="post">
<!-- Razor automatycznie wstrzykuje hidden input z tokenem -->
</form>
<!-- MVC lub gdy potrzebujesz jawnie -->
<form method="post">
@Html.AntiForgeryToken()
<!-- generuje: <input type="hidden" name="__RequestVerificationToken" value="..."> -->
</form>
Bez tokenu — POST request zwróci 400 Bad Request (jeśli [ValidateAntiForgeryToken] jest aktywne, a jest domyślnie w Razor Pages).
Wszystkie typy <input>
Tekst i pochodne
<!-- Zwykły tekst -->
<input type="text" name="Username" placeholder="nazwa użytkownika"
value="jan_kowalski" autocomplete="username">
<!-- Email — przeglądarka waliduje format i pokazuje klawiaturę @ na mobile -->
<input type="email" name="Email" placeholder="jan@example.com"
autocomplete="email">
<!-- Hasło — maskuje znaki -->
<input type="password" name="Password" autocomplete="new-password">
<!-- Numer telefonu — klawiatura numeryczna na mobile -->
<input type="tel" name="Phone" placeholder="+48 123 456 789"
pattern="[+]?[0-9 ]{9,15}">
<!-- URL — waliduje format URL -->
<input type="url" name="Website" placeholder="https://example.com">
<!-- Wyszukiwanie — X do czyszczenia w niektórych przeglądarkach -->
<input type="search" name="q" placeholder="Szukaj produktów...">
Liczby
<!-- Liczba — strzałki w górę/dół, klawiatura numeryczna na mobile -->
<input type="number" name="Quantity"
min="1" max="99" step="1" value="1">
<!-- Cena (krok 0.01 dla groszy) -->
<input type="number" name="Price"
min="0" max="99999" step="0.01" value="299.00">
<!-- Suwak — wizualny kontroler zakresu -->
<input type="range" name="MaxPrice"
min="0" max="5000" step="50" value="1000">
<!-- Brak domyślnie wyświetlanej wartości — musisz ją pokazać przez JavaScript lub output element -->
<output for="MaxPrice" id="priceDisplay">1000 zł</output>
Data i czas
<!-- Data — datepicker w przeglądarce -->
<input type="date" name="BirthDate" min="1900-01-01" max="2010-12-31">
<!-- Data i czas lokalny -->
<input type="datetime-local" name="DeliveryDate"
min="2026-01-01T08:00" max="2026-12-31T20:00">
<!-- Tylko czas -->
<input type="time" name="PreferredCallTime" min="09:00" max="17:00" step="1800">
<!-- Miesiąc -->
<input type="month" name="ExpiryMonth">
<!-- Tydzień -->
<input type="week" name="DeliveryWeek">
Format wartości dla type="date" to zawsze YYYY-MM-DD (ISO 8601) — niezależnie od locale przeglądarki. W ASP.NET Core Model Binding mapuje to na DateTime lub DateOnly:
public class OrderModel
{
// name="DeliveryDate" mapuje na DateOnly
public DateOnly DeliveryDate { get; set; }
}
Checkbox i Radio
<!-- Checkbox — wielokrotny wybór -->
<fieldset>
<legend>Preferowane kanały kontaktu</legend>
<label>
<input type="checkbox" name="Channels" value="email" checked>
Email
</label>
<label>
<input type="checkbox" name="Channels" value="sms">
SMS
</label>
<label>
<input type="checkbox" name="Channels" value="push">
Powiadomienia push
</label>
</fieldset>
<!-- Radio — jeden wybór z grupy (ta sama name = ta sama grupa) -->
<fieldset>
<legend>Metoda dostawy</legend>
<label>
<input type="radio" name="DeliveryMethod" value="courier" checked>
Kurier (299 zł — 24h)
</label>
<label>
<input type="radio" name="DeliveryMethod" value="pickup">
Odbiór osobisty (gratis)
</label>
<label>
<input type="radio" name="DeliveryMethod" value="parcel-locker">
Paczkomat (12 zł — 48h)
</label>
</fieldset>
Checkbox i model binding w ASP.NET Core:
Wiele checkboxów z tą samą name + różne value → List<string> lub string[] w modelu:
public class ContactPreferencesModel
{
// name="Channels" z wieloma wartościami → List<string>
public List<string> Channels { get; set; } = new();
// name="DeliveryMethod" → jeden string
public string DeliveryMethod { get; set; } = "courier";
}
<label> wrappujący <input> — kliknięcie na etykietę zaznacza pole. Lepsza UX niż <label for="id">.
Ukryte pole
<!-- Przechowuje dane, które nie są widoczne dla użytkownika, ale wysyłane z formularzem -->
<input type="hidden" name="ProductId" value="42">
<input type="hidden" name="ReturnUrl" value="/products/keyboards">
W Razor Pages — popularne do przekazywania ID obiektu bez wyświetlania go:
<form method="post" asp-page-handler="AddToCart">
<input type="hidden" asp-for="ProductId">
<button type="submit">Do koszyka</button>
</form>
Plik
<!-- Wymagane: enctype="multipart/form-data" na formularzu -->
<form method="post" enctype="multipart/form-data" action="/products/upload-image">
<label for="productImage">Zdjęcie produktu (max 5 MB, JPG/PNG)</label>
<input type="file"
id="productImage"
name="ProductImage"
accept="image/jpeg,image/png"
required>
<button type="submit">Prześlij</button>
</form>
// ASP.NET Core — IFormFile mapuje na name="ProductImage"
public async Task<IActionResult> UploadImage(IFormFile productImage)
{
if (productImage.Length > 5 * 1024 * 1024)
return BadRequest("Plik za duży.");
var ext = Path.GetExtension(productImage.FileName).ToLowerInvariant();
if (ext is not ".jpg" and not ".jpeg" and not ".png")
return BadRequest("Niedozwolony format.");
// zapisz plik...
}
accept filtruje pliki w oknie dialogowym — nie jest walidacją bezpieczeństwa! Waliduj zawsze po stronie serwera.
Przycisk submit
<!-- Standardowy przycisk wysyłania formularza -->
<button type="submit">Zarejestruj</button>
<!-- Submit z wartością — value jest wysyłane z formularzem (np. do określenia akcji) -->
<button type="submit" name="Action" value="save">Zapisz</button>
<button type="submit" name="Action" value="save-and-close">Zapisz i zamknij</button>
<!-- Reset — czyści formularz do wartości domyślnych (rzadko używane) -->
<button type="reset">Wyczyść</button>
<!-- Przycisk bez domyślnej akcji — tylko dla JavaScript -->
<button type="button" id="previewBtn">Podgląd</button>
<!-- Wyłączony przycisk -->
<button type="submit" disabled>Prześlij (wypełnij formularz)</button>
<select> — lista wyboru
<!-- Pojedynczy wybór -->
<select name="Category">
<option value="">-- Wybierz kategorię --</option>
<option value="keyboards">Klawiatury</option>
<option value="mice">Myszy</option>
<option value="monitors" selected>Monitory</option>
</select>
<!-- Wielokrotny wybór (Ctrl+klik lub Shift+klik) -->
<select name="Tags" multiple size="5">
<option value="mechanical">Mechaniczne</option>
<option value="rgb">RGB</option>
<option value="wireless" selected>Bezprzewodowe</option>
<option value="gaming" selected>Gaming</option>
</select>
<!-- Grupowanie opcji -->
<select name="SubCategory">
<optgroup label="Klawiatury">
<option value="mechanical">Mechaniczne</option>
<option value="membrane">Membranowe</option>
</optgroup>
<optgroup label="Myszy">
<option value="gaming-mouse">Gaming</option>
<option value="office-mouse">Biurowe</option>
</optgroup>
</select>
W Razor — generowanie <select> z modelu:
<!-- asp-items generuje <option> z listy SelectListItem -->
<select asp-for="CategoryId" asp-items="Model.Categories">
<option value="">-- Wybierz --</option>
</select>
public class ProductFormModel
{
public int CategoryId { get; set; }
public IEnumerable<SelectListItem> Categories { get; set; } = [];
}
<textarea> — wieloliniowy tekst
<textarea
name="Description"
rows="6"
cols="60"
maxlength="2000"
placeholder="Opisz produkt (max 2000 znaków)..."
>Wstępna wartość tekstowa</textarea>
<textarea> nie jest void element — ma znacznik zamykający. Wartość domyślna jest między tagami (nie w atrybucie value).
<label> — etykieta pola
<!-- Sposób 1: label z for="id" — for musi odpowiadać id inputa -->
<label for="email">Adres email</label>
<input type="email" id="email" name="Email">
<!-- Sposób 2: label wrappujący input (nie wymaga id) -->
<label>
Adres email
<input type="email" name="Email">
</label>
Sposób 1 jest bardziej elastyczny (CSS, dodatkowe elementy między label a input).
Sposób 2 jest prostszy dla checkboxów i radio.
<fieldset> + <legend> dla grup powiązanych pól:
<fieldset>
<legend>Dane kontaktowe</legend>
<label for="firstName">Imię</label>
<input type="text" id="firstName" name="FirstName" required>
<label for="lastName">Nazwisko</label>
<input type="text" id="lastName" name="LastName" required>
<label for="email">Email</label>
<input type="email" id="email" name="Email" required>
</fieldset>
Walidacja HTML5 — atrybuty walidacyjne
HTML5 wprowadza walidację po stronie klienta przez atrybuty — bez JavaScriptu:
<form>
<!-- required — pole wymagane -->
<input type="text" name="Username" required>
<!-- minlength / maxlength — minimalna/maksymalna długość tekstu -->
<input type="password" name="Password" required minlength="8" maxlength="100">
<!-- min / max — zakres dla number i date -->
<input type="number" name="Quantity" min="1" max="99" required>
<input type="date" name="BirthDate" min="1900-01-01" max="2010-12-31" required>
<!-- pattern — wyrażenie regularne -->
<input type="text" name="PostalCode" pattern="\d{2}-\d{3}"
placeholder="00-000" title="Format: XX-XXX">
<!-- type — wbudowana walidacja formatu -->
<input type="email" name="Email" required> <!-- waliduje format email -->
<input type="url" name="Website"> <!-- waliduje URL -->
<button type="submit">Wyślij</button>
</form>
Przeglądarka wyświetla dymek z komunikatem błędu przy próbie wysłania niepoprawnego formularza — bez żadnego JavaScript.
Relacja z walidacją Data Annotations w ASP.NET Core
public class RegisterModel
{
[Required(ErrorMessage = "Email jest wymagany")]
[EmailAddress]
[MaxLength(200)]
public string Email { get; set; } = string.Empty;
[Required]
[MinLength(8, ErrorMessage = "Hasło musi mieć co najmniej 8 znaków")]
[MaxLength(100)]
public string Password { get; set; } = string.Empty;
[Range(18, 120, ErrorMessage = "Wiek musi być między 18 a 120")]
public int Age { get; set; }
}
ASP.NET Core asp-validation-for Tag Helper generuje atrybuty HTML5 na podstawie Data Annotations:
<input asp-for="Email" class="form-control">
<!-- Generuje: -->
<input type="email" id="Email" name="Email"
data-val="true"
data-val-required="Email jest wymagany"
data-val-email="Niepoprawny format email"
data-val-maxlength="Przekroczono maksymalną długość"
data-val-maxlength-max="200"
class="form-control">
Atrybuty data-val-* są używane przez jQuery Validation Unobtrusive — bibliotekę dołączoną do ASP.NET Core, która zapewnia walidację client-side na podstawie Data Annotations.
Zawsze waliduj po stronie serwera — walidacja HTML5 i JS jest dla UX, nie bezpieczeństwa. Można ją ominąć przez DevTools lub bezpośrednie żądanie HTTP.
Kompletny przykład: formularz rejestracji
<!-- Razor Page: Register.cshtml -->
@page
@model RegisterModel
@{
ViewData["Title"] = "Rejestracja";
}
<main>
<h1>Utwórz konto</h1>
<form method="post" asp-antiforgery="true" novalidate>
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
<fieldset>
<legend>Dane logowania</legend>
<div class="form-group">
<label asp-for="Email">Adres email</label>
<input asp-for="Email"
type="email"
class="form-control"
autocomplete="email"
placeholder="jan@example.com">
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Password">Hasło</label>
<input asp-for="Password"
type="password"
class="form-control"
autocomplete="new-password">
<span asp-validation-for="Password" class="text-danger"></span>
<small class="form-hint">Min. 8 znaków, wielka litera i cyfra.</small>
</div>
</fieldset>
<fieldset>
<legend>Dane osobowe</legend>
<div class="form-group">
<label asp-for="FirstName">Imię</label>
<input asp-for="FirstName" class="form-control" autocomplete="given-name">
<span asp-validation-for="FirstName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="BirthDate">Data urodzenia</label>
<input asp-for="BirthDate" type="date" class="form-control"
min="1900-01-01" max="2008-12-31">
<span asp-validation-for="BirthDate" class="text-danger"></span>
</div>
</fieldset>
<fieldset>
<legend>Preferencje</legend>
<label>
<input asp-for="AcceptsNewsletter" type="checkbox">
Chcę otrzymywać newsletter z ofertami
</label>
<div class="form-group">
<label asp-for="PreferredCategory">Ulubiona kategoria</label>
<select asp-for="PreferredCategory" asp-items="Model.Categories"
class="form-control">
<option value="">-- Wybierz --</option>
</select>
</div>
</fieldset>
<button type="submit" class="btn btn-primary">Zarejestruj się</button>
<a href="/login">Masz już konto? Zaloguj się</a>
</form>
</main>
@section Scripts {
<partial name="_ValidationScriptsPartial">
}
Atrybut novalidate na formularzu wyłącza wbudowaną walidację HTML5 — żeby walidacja jQuery (Unobtrusive) mogła przejąć kontrolę i pokazywać komunikaty w odpowiednich miejscach (przy polach), a nie w dymkach przeglądarki.
Tabela: najważniejsze atrybuty formularzy
| Atrybut | Gdzie | Znaczenie |
|---|---|---|
name | input, select, textarea | Klucz w danych wysłanych do serwera |
id | dowolny element | Unikalny identyfikator, powiązany z for w label |
type | input | Typ pola: text, email, password, number, date, … |
value | input | Domyślna wartość / wartość checkbox/radio |
placeholder | text, email, password… | Tekst podpowiedzi (znika po wpisaniu) |
required | większość inputów | Pole wymagane (walidacja HTML5) |
disabled | większość inputów | Pole wyłączone (nie wysyłane do serwera) |
readonly | text, textarea | Pole tylko do odczytu (wysyłane do serwera) |
autocomplete | form, input | Sugestie automatycznego uzupełniania |
min/max | number, date, range | Zakres dozwolonych wartości |
minlength/maxlength | text, textarea | Długość tekstu |
pattern | text, email, url | Wyrażenie regularne |
accept | file | Dozwolone typy plików (tylko UX, nie bezpieczeństwo) |
multiple | file, select | Wielokrotny wybór |
Podsumowanie
Formularz HTML to kontrakt między przeglądarką a serwerem: name atrybutu = klucz, value = wartość. ASP.NET Core Model Binding czyta te pary i mapuje na właściwości modelu C# — po nazwie, case-insensitive.
Walidacja HTML5 (atrybuty required, minlength, pattern) to warstwa UX — szybki feedback bez roundtrip do serwera. Data Annotations w C# to walidacja serwera — jedyna, której możesz ufać w bezpieczeństwie. Obie warstwy są potrzebne.
Tag Helpers ASP.NET Core (asp-for, asp-validation-for, asp-items) generują poprawny HTML z atrybutami walidacji na podstawie modelu C# — nie musisz ręcznie synchronizować name i id z modelem.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →
1 comment