Załóżmy, że mamy pewien model, który posiada listę i chcemy wytworzyć radio buttony dla elementów tej listy. Rzuć okiem na poniższy przykład.
Główna klasa modelu zawierająca listę:
using System.Collections.Generic;
public class Team
{
public string Name { get; set; }
public List<Player> Players { get; set; }
}
Klasa dla elementów listy:
public class Player
{
public string Name { get; set; }
public string Level { get; set; }
}
Istnieją trzy akceptowalne wartości dla właściwości Level: BEG (Beginner), INT (Intermediate) oraz ADV (Advanced), chcemy więc wyświetlić trzy radio buttony (wraz z etykietami) dla każdego z zawodników w drużynie. Normalnie pewnie użylibyśmy enum dla property Level ale w imię prostoty przykładu zostaniemy przy typie string...
Metoda kontrolera zwracająca przykładowe dane:
public ActionResult Index()
{
var team = new Team() {
Name = "Some Team",
Players = new List<Player> {
new Player() {Name = "Player A", Level="BEG"},
new Player() {Name = "Player B", Level="INT"},
new Player() {Name = "Player C", Level="ADV"}
}
};
return View(team);
}
Oto widok Index.cshtml:
@model Team
<section>
<h1>@Model.Name</h1>
@Html.EditorFor(model => model.Players)
</section>
Zauważ, że markup dla właściwości Players nie jest tworzony wewnątrz pętli. Zamiast tego użyty jest EditorTemplate. Jest to zalecane podejście ponieważ sprawia, że kod jest bardziej przejrzysty i prostszy w utrzymaniu. Framwork jest na tyle sprtytny, że używa kodu z szablonu edytora dla każdego z zawodników na liście ponieważ Team.Players implementuje interfejs IEnumerable.
A oto wspomniany Players.cshtml EditorTemplate:
@model Player
<div>
<strong>@Model.Name:</strong>
@Html.RadioButtonFor(model => model.Level, "BEG")
@Html.LabelFor(model => model.Level, "Beginner")
@Html.RadioButtonFor(model => model.Level, "INT")
@Html.LabelFor(model => model.Level, "Intermediate")
@Html.RadioButtonFor(model => model.Level, "ADV")
@Html.LabelFor(model => model.Level, "Advanced")
</div>
Kod wygląda w porządku, używa silnie typowanych helperów opartych o wyrażenia lambda (dla wychwycenia błędów podczas kompilacji i łatwiejszego refaktoringu)... niestety, jest pewien problem: HTML wygenerowany przez taki kod ma bardzo poważną wadę. Sprawdź poniższy fragment kodu źródłowego strony, który został wygenerowany dla pierwszego zawodnika z listy:
<div>
<strong>Player A:</strong>
<input name="Players[0].Level" id="Players_0__Level" type="radio" checked="checked" value="BEG">
<label for="Players_0__Level">Beginner</label>
<input name="Players[0].Level" id="Players_0__Level" type="radio" value="INT">
<label for="Players_0__Level">Intermediate</label>
<input name="Players[0].Level" id="Players_0__Level" type="radio" value="ADV">
<label for="Players_0__Level">Advanced</label>
</div>
Identyfikator Players_0__Level jest użyty dla trzech różnych radio buttonów! Brak unikalności nie tylko narusza specyfikacje HTML i utrudnia oskryptowanie, powoduje też, że etykiety (tagi label) nie działają prawidłowo (tj. kliknięcie nie powoduje zaznaczenia odpowiedniego elementu input).
Na szczęście biblioteka MVC posiada klasę TemplateInfo z metodą GetFullHtmlFieldId. Metoda ta zwraca identyfikator dla elementu DOM. Identyfikator jest budowany poprzez połączenie argumentu przekazanego do metody z automatycznie wygenerowanym prefiksem. Prefiks uwzględnia poziom zagnieżdżenia i indeks elementu listy. Wewnętrznie, GetFullHtmlFieldId odwołuje się do właściwości TemplateInfo.HtmlFieldPrefix i metody TagBuilder.CreateSanitizedId więc w przypadku przekazania do sufiksu znaków niemogących być częścią id, znaki te zostaną usunięte.
Oto zmodyfikowany kod EditorTemplate:
@model Player
<div>
<strong>@Model.Name:</strong>
@{string rbBeginnerId = ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId("rbBeginner"); }
@Html.RadioButtonFor(model => model.Level, "BEG", new { id = rbBeginnerId })
@Html.LabelFor(model => model.Level, "Beginner", new { @for = rbBeginnerId} )
@{string rbIntermediateId = ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId("rbIntermediate"); }
@Html.RadioButtonFor(model => model.Level, "INT", new { id = rbIntermediateId })
@Html.LabelFor(model => model.Level, "Intermediate", new { @for = rbIntermediateId })
@{string rbAdvancedId = ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId("rbAdvanced"); }
@Html.RadioButtonFor(model => model.Level, "ADV", new { id = rbAdvancedId })
@Html.LabelFor(model => model.Level, "Advanced", new { @for = rbAdvancedId })
</div>
Wywołania metody ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId pozwalają na pozyskanie id dla radio buttonów, identyfikatory te są wykorzystane również do ustawienia atrybutów for elementów label. W MVC 3 nie było przeładowanej metody LabelFor, która akceptowałaby obiekt dla htmlAttributes. Na szczęście wersja 4 ma wbudowany stosowny overload.
Powyższy kod tworzy taki markup:
<div>
<strong>Player A:</strong>
<input name="Players[0].Level" id="Players_0__rbBeginner" type="radio" checked="checked" value="BEG">
<label for="Players_0__rbBeginner">Beginner</label>
<input name="Players[0].Level" id="Players_0__rbIntermediate" type="radio" value="INT">
<label for="Players_0__rbIntermediate">Intermediate</label>
<input name="Players[0].Level" id="Players_0__rbAdvanced" type="radio" value="ADV">
<label for="Players_0__rbAdvanced">Advanced</label>
</div>
Teraz inputy mają unikalne identyfikatory a labele poprawnie odwołują się do radio buttonów poprzez atrybut for. I w porządku :)
Przy okazji: dziwna nazwa „radio button” pochodzi od odbiorników radiowych, które posiadały panele z przyciskami do wyboru stacji (wciśnięcie jednego guzika automatycznie wybijało pozostałe).