🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

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”:

  1. Przeglądarka zbiera dane ze wszystkich pól formularza (name atrybuty)
  2. Koduje je jako application/x-www-form-urlencoded (domyślnie) lub multipart/form-data (pliki)
  3. Wysyła HTTP POST do adresu z atrybutu action
  4. ASP.NET Core otrzymuje żądanie — Model Binding czyta name każ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 jako klucz=wartość&klucz2=wartość2
  • multipart/form-datawymagane 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 valueList<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

AtrybutGdzieZnaczenie
nameinput, select, textareaKlucz w danych wysłanych do serwera
iddowolny elementUnikalny identyfikator, powiązany z for w label
typeinputTyp pola: text, email, password, number, date, …
valueinputDomyślna wartość / wartość checkbox/radio
placeholdertext, email, password…Tekst podpowiedzi (znika po wpisaniu)
requiredwiększość inputówPole wymagane (walidacja HTML5)
disabledwiększość inputówPole wyłączone (nie wysyłane do serwera)
readonlytext, textareaPole tylko do odczytu (wysyłane do serwera)
autocompleteform, inputSugestie automatycznego uzupełniania
min/maxnumber, date, rangeZakres dozwolonych wartości
minlength/maxlengthtext, textareaDługość tekstu
patterntext, email, urlWyrażenie regularne
acceptfileDozwolone typy plików (tylko UX, nie bezpieczeństwo)
multiplefile, selectWielokrotny 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.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 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.

czytanie to początek

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ę →