Blazor .NET 11 Preview 1: Label i DisplayName | Szkoła Dotneta
#Blazor#.NET

Blazor .NET 11 Preview 1: Label i DisplayName

Kajetan Duszyński

Kajetan Duszyński

Microsoft MVP, .NET Expert

calendar_today 05 marca 2026 schedule 7 min czytania visibility 0 wyświetleń

Dostępność formularzy w Blazorze zawsze wymagała trochę gimnastyki. Ręczne synchronizowanie atrybutu for na <label> z atrybutem id na <input> wyglądało jak praca mechaniczna, i nią była. .NET 11 Preview 1 naprawia to w elegancki sposób: dwa nowe komponenty, Label i DisplayName, automatyzują to co wcześniej pisałeś z palca przy każdym formularzu.

Label: dostępne etykiety bez ręcznego for/id

Nowy komponent Label generuje etykiety dostępne semantycznie, obsługując dwa klasyczne wzorce powiązania etykiety z polem.

Pierwsza kwestia, którą warto zrozumieć: komponent automatycznie wyciąga nazwę wyświetlaną z atrybutu [Display(Name = "...")] lub [DisplayName("...")] na modelu. Jeśli żadnego atrybutu nie ma, używa po prostu nazwy właściwości. Zero hardkodowanego tekstu w znacznikach.

Wzorzec zagnieżdżony (implicit association)

Gdy opakujesz input wewnątrz Label, asocjacja jest niejawna, przeglądarka i screen reader wiedzą, że etykieta dotyczy zagnieżdżonego pola:

<Label For="() => model.CustomerName">
    <InputText @bind-Value="model.CustomerName" />
</Label>

Renderuje się jako:

<label>
    Customer name
    <input value="..." />
</label>

Tekst etykiety pochodzi z [Display(Name = "Customer name")] na właściwości, albo z samej nazwy CustomerName jeśli atrybutu nie ma.

Wzorzec for/id (explicit association)

Drugi wzorzec: etykieta i input jako oddzielne elementy, połączone przez for i id. To podejście wymagane między innymi gdy stylizujesz etykietę i pole niezależnie:

<Label For="() => model.CustomerName" />
<InputText @bind-Value="model.CustomerName" />

Renderuje się jako:

<label for="CustomerName">Customer name</label>
<input id="CustomerName" value="..." />

Kluczowy szczegół: wszystkie wbudowane komponenty Input* w .NET 11 automatycznie generują atrybut id wyprowadzony z wyrażenia @bind-Value. Właśnie to umożliwia automatyczne łączenie: Label zna nazwę właściwości z wyrażenia For, InputText generuje id z tego samego wyrażenia. Koordynacja jest po stronie frameworka, nie programisty.

Label w EditForm z walidacją

Komponent współpracuje bezpośrednio z EditForm i DataAnnotationsValidator:

<EditForm Model="order" OnValidSubmit="HandleSubmit">
    <DataAnnotationsValidator />
    
    <div class="form-group">
        <Label For="() => order.OrderNumber" />
        <InputText @bind-Value="order.OrderNumber" />
        <ValidationMessage For="() => order.OrderNumber" />
    </div>
    
    <div class="form-group">
        <Label For="() => order.DeliveryDate" />
        <InputDate @bind-Value="order.DeliveryDate" />
        <ValidationMessage For="() => order.DeliveryDate" />
    </div>
    
    <button type="submit">Zapisz zamówienie</button>
</EditForm>

@code {
    private Order order = new();
    
    private void HandleSubmit()
    {
        // save logic
    }
}

Model z atrybutami:

public class Order
{
    [Display(Name = "Order number")]
    [Required(ErrorMessage = "Order number is required")]
    public string OrderNumber { get; set; } = string.Empty;

    [Display(Name = "Delivery date")]
    [Required]
    public DateTime? DeliveryDate { get; set; }
}

Komponent Label czyta te same atrybuty [Display] co ValidationMessage i InputText, jedno źródło prawdy dla całego formularza.

Wyrażenia lambda For="() => model.Pole" są spójne z tym co już używasz przy ValidationMessage. Jeśli znasz ten wzorzec, Label po prostu wchodzi obok.

DisplayName i parity z MVC

Drugi nowy komponent rozwiązuje inny problem: wyświetlanie nazw pól poza kontekstem formularza. W MVC mieliśmy do tego @Html.DisplayNameFor(). W Blazorze do tej pory trzeba było albo hardkodować tekst, albo pisać własne rozwiązanie przez refleksję.

DisplayName działa tak samo jak Label jeśli chodzi o rozwiązywanie nazwy: DisplayAttribute.NameDisplayNameAttribute.DisplayName → nazwa właściwości. Różnica: nie generuje <label>, tylko czysty tekst, dlatego nadaje się wszędzie tam gdzie <label> byłby semantycznie niepoprawny.

Nagłówki tabeli

Najczęstszy przypadek użycia:

@using Microsoft.AspNetCore.Components.Forms

<table class="table">
    <thead>
        <tr>
            <th><DisplayName For="() => product.Name" /></th>
            <th><DisplayName For="() => product.Price" /></th>
            <th><DisplayName For="() => product.ReleaseDate" /></th>
            <th><DisplayName For="() => product.StockLevel" /></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var p in products)
        {
            <tr>
                <td>@p.Name</td>
                <td>@p.Price.ToString("C")</td>
                <td>@p.ReleaseDate.ToString("d")</td>
                <td>@p.StockLevel</td>
            </tr>
        }
    </tbody>
</table>

Model:

public class Product
{
    [Display(Name = "Product name")]
    public string Name { get; set; } = string.Empty;

    [Display(Name = "Net price")]
    public decimal Price { get; set; }

    [Display(Name = "Release date")]
    public DateTime ReleaseDate { get; set; }

    [Display(Name = "Stock level")]
    public int StockLevel { get; set; }
}

Zamiast <th>Product name</th>, tekst który musi być zsynchronizowany ręcznie z atrybutem modelu, masz <DisplayName For="() => product.Name" />. Zmieniasz [Display(Name = "...")] w jednym miejscu i nagłówki tabeli aktualizują się automatycznie.

DisplayName w EditForm: własny układ etykiet

Komponent przydaje się też gdy chcesz pełną kontrolę nad strukturą etykiety, ale nadal czerpać nazwy z modelu:

<EditForm Model="product">
    <div class="form-group">
        <label class="form-label font-semibold">
            <DisplayName For="() => product.Name" />
            <span class="text-red-500 ml-1">*</span>
        </label>
        <InputText @bind-Value="product.Name" class="form-control" />
    </div>
</EditForm>

W tym przypadku Label byłby za restrykcyjny, chcesz dodać gwiazdkę przy wymaganym polu bez modyfikowania generowanego HTML. DisplayName daje tylko tekst, resztę kontrolujesz sam.

Lokalizacja: jeden atrybut, wiele języków

Oba komponenty respektują lokalizację przez DisplayAttribute.ResourceType:

public class Order
{
    [Display(Name = "OrderNumberLabel", ResourceType = typeof(FormResources))]
    public string OrderNumber { get; set; } = string.Empty;
}

Gdy aplikacja jest skonfigurowana z IStringLocalizer i plikami zasobów, Label i DisplayName automatycznie pobierają zlokalizowaną nazwę. Działa tak samo jak lokalizacja w MVC, spójna mechanika w całym ekosystemie ASP.NET Core.

Jak to zastosować w projekcie

Kilka obserwacji po zapoznaniu się ze specyfikacją:

Formularz z InputText i Label: pattern replace. W większości projektów masz już [Display] na modelach do walidacji i komunikatów błędów. Label po prostu wchodzi w miejsce istniejącego <label for="...">.

Tabele z DisplayName: szczególnie wartościowe w projektach gdzie model zmienia się często. Zamiast łapać rozbieżności między etykietami w widoku a nazwami w modelu, DisplayName eliminuje problem strukturalnie.

Wzorzec zagnieżdżony vs for/id: w nowych formularzach zagnieżdżony wzorzec jest czystszy i ma mniej ruchomych części. For/id ma sens gdy Tailwind albo inne narzędzia CSS wymagają oddzielnych elementów do stylizacji. Oba wzorce są w pełni obsługiwane.

Migracja istniejących formularzy: nie ma przymusu. Label jest opcjonalny, stare <label> z ręcznym for dalej działają. Migrację warto zacząć od formularzy które już mają [Display] na modelu, tam koszt przejścia jest zerowy.

@* Przed *@
<label for="email">Adres e-mail</label>
<InputText id="email" @bind-Value="model.Email" />

@* Po *@
<Label For="() => model.Email" />
<InputText @bind-Value="model.Email" />

Zakładając [Display(Name = "Adres e-mail")] na właściwości, rezultat HTML identyczny, ale tekst pochodzi z modelu.

Podsumowanie

  • Label generuje dostępne etykiety formularzy: obsługuje zagnieżdżony wzorzec (implicit association) i wzorzec for/id (explicit association); tekst z [Display] / [DisplayName] albo nazwy właściwości
  • Wszystkie komponenty Input* generują teraz automatycznie atrybut id: to fundament pod automatyczne łączenie z Label bez żadnej konfiguracji
  • DisplayName to odpowiednik @Html.DisplayNameFor() z MVC, przydatny wszędzie poza <label>, szczególnie w nagłówkach tabel i złożonych layoutach etykiet
  • Lokalizacja działa przez DisplayAttribute.ResourceType, spójnie z resztą ASP.NET Core
  • Oba komponenty są addytywne, nie ma potrzeby migracji istniejących formularzy jednocześnie; można wprowadzać je stopniowo