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
Labelczyta te same atrybuty[Display]coValidationMessageiInputText, 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.Name → DisplayNameAttribute.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
Labelgeneruje 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 atrybutid: to fundament pod automatyczne łączenie zLabelbez żadnej konfiguracji DisplayNameto 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
