Wprowadzenie
W ASP.NET MVC rozbudowane formularze możemy utworzyć na różne sposoby. Na ogół staramy się wyrzucać powtarzające się elementy do oddzielnych plików, aby wykorzystywać je ponownie. Zauważyłem, że większość osób do tego celu wykorzystuje widoki Partial, które niestety często zamiast pomóc – powodują dodatkowe problemy. W tym wpisie postaram się pokazać Ci , dlaczego w przypadku formularzy lepiej użyć Editor Templates niż widoków Partial.
Przykład
Zacznę od przykładu, abyś wiedział lub wiedziała, co chcemy osiągnąć i jaki będziemy mieli problem. W testowej aplikacji chcemy umożliwić edycję danych użytkownika. Użytkownik, poza podstawowymi danymi, ma również dane adresowe. Z racji tego, że te dane możemy mieć w różnych miejscach naszej aplikacji (np. sam użytkownik może mieć adres zamieszkania, adres korespondencyjny, adres do faktury), chcemy ten fragment formularza mieć zdefiniowany w innym pliku, aby używać do wielokrotnie.
Przygotowałem dwa viewmodele – jeden dla danych adresowych, drugi dla danych użytkownika:
public class UserViewModel | |
{ | |
public string FirstName { get; set; } | |
public string LastName { get; set; } | |
public AddressViewModel Address { get; set; } = new AddressViewModel(); | |
} |
Jak widać klasa User zawiera właściwość typu AddressViewModel dla danych adresowych.
Zobaczmy, jak zachowa się formularz zbudowany przy wykorzystaniu widoku Partial.
Partial View
Do aplikacji dodałem bardzo prosty UsersController, który zawiera po dwie akcje dla każdego z przykładów (jedna akcja dla GET, druga dla POST). Akcje są bardzo proste i zwracają po prostu widok:
public class UsersController : Controller | |
{ | |
public ActionResult Partial() | |
{ | |
return View(new UserViewModel()); | |
} | |
[HttpPost] | |
public ActionResult Partial(UserViewModel viewModel) | |
{ | |
return View(viewModel); | |
} | |
public ActionResult EditorTemplate() | |
{ | |
return View(new UserViewModel()); | |
} | |
[HttpPost] | |
public ActionResult EditorTemplate(UserViewModel viewModel) | |
{ | |
return View(viewModel); | |
} | |
} |
Ciekawsze rzeczy dzieją się w samych widokach. Dla adresu utworzyłem nowy widok Partial w katalogu Views/Shared, aby można było go używać wielokrotnie w różnych kontrolerach. Widok wygenerowało Visual Studio na podstawie viewmodelu, w którym usunąłem cały kod związany z formularzem i zostawiłem same pola. Po tych zmianach widok wygląda tak:
@model PartialForms.ViewModels.Shared.AddressViewModel | |
<div class="form-group"> | |
@Html.LabelFor(model => model.Street, htmlAttributes: new { @class = "control-label col-md-2" }) | |
<div class="col-md-10"> | |
@Html.EditorFor(model => model.Street, new { htmlAttributes = new { @class = "form-control" } }) | |
@Html.ValidationMessageFor(model => model.Street, "", new { @class = "text-danger" }) | |
</div> | |
</div> | |
<div class="form-group"> | |
@Html.LabelFor(model => model.Number, htmlAttributes: new { @class = "control-label col-md-2" }) | |
<div class="col-md-10"> | |
@Html.EditorFor(model => model.Number, new { htmlAttributes = new { @class = "form-control" } }) | |
@Html.ValidationMessageFor(model => model.Number, "", new { @class = "text-danger" }) | |
</div> | |
</div> | |
<div class="form-group"> | |
@Html.LabelFor(model => model.City, htmlAttributes: new { @class = "control-label col-md-2" }) | |
<div class="col-md-10"> | |
@Html.EditorFor(model => model.City, new { htmlAttributes = new { @class = "form-control" } }) | |
@Html.ValidationMessageFor(model => model.City, "", new { @class = "text-danger" }) | |
</div> | |
</div> | |
<div class="form-group"> | |
@Html.LabelFor(model => model.Postal, htmlAttributes: new { @class = "control-label col-md-2" }) | |
<div class="col-md-10"> | |
@Html.EditorFor(model => model.Postal, new { htmlAttributes = new { @class = "form-control" } }) | |
@Html.ValidationMessageFor(model => model.Postal, "", new { @class = "text-danger" }) | |
</div> | |
</div> |
Możemy go użyć teraz w innym widoku i dodać do jakiegoś istniejącego formularza. I tak też zrobiłem. Utworzyłem nowy widok w katalogu Views/Users dla akcji Partial z kontrolera UsersController. Podobnie jak wcześniej Visual Studio wygenerowało widok na podstawie viewmodelu, a na koniec użyłem widoku Partial (Html.Partial), aby wygenerować pola dla danych adresowych przez przycisk wysłania formularza. Ostateczny kod widoku wygląda tak:
@model PartialForms.ViewModels.Users.UserViewModel | |
@{ | |
ViewBag.Title = "Partial"; | |
} | |
<h2>Partial</h2> | |
@using (Html.BeginForm()) | |
{ | |
@Html.AntiForgeryToken() | |
<div class="form-horizontal"> | |
<h4>UserViewModel</h4> | |
<hr /> | |
@Html.ValidationSummary(true, "", new { @class = "text-danger" }) | |
<div class="form-group"> | |
@Html.LabelFor(model => model.FirstName, htmlAttributes: new { @class = "control-label col-md-2" }) | |
<div class="col-md-10"> | |
@Html.EditorFor(model => model.FirstName, new { htmlAttributes = new { @class = "form-control" } }) | |
@Html.ValidationMessageFor(model => model.FirstName, "", new { @class = "text-danger" }) | |
</div> | |
</div> | |
<div class="form-group"> | |
@Html.LabelFor(model => model.LastName, htmlAttributes: new { @class = "control-label col-md-2" }) | |
<div class="col-md-10"> | |
@Html.EditorFor(model => model.LastName, new { htmlAttributes = new { @class = "form-control" } }) | |
@Html.ValidationMessageFor(model => model.LastName, "", new { @class = "text-danger" }) | |
</div> | |
</div> | |
@Html.Partial("Address", Model.Address) | |
<div class="form-group"> | |
<div class="col-md-offset-2 col-md-10"> | |
<input type="submit" value="Create" class="btn btn-default" /> | |
</div> | |
</div> | |
</div> | |
} | |
<div> | |
@Html.ActionLink("Back to List", "Index") | |
</div> | |
@section Scripts { | |
@Scripts.Render("~/bundles/jqueryval") | |
} |
Zwróć uwagę na linijkę 34, w której znajduje się użycie widoku Partial.
Sam widok w przeglądarce wygląda tak:
Aplikacja wyświetliła ładny formularz poprzez połączenie dwóch widoków.
Spójrzmy teraz na działanie tego widoku i to, w jaki sposób przesyła on dane do kontrolera.
Problem z widokiem Partial
Uzupełniłem dane w formularzu oraz przesłałem go do kontrolera. W akcji dla POST postawiłem breakpoint i zobaczyłem, jak uzupełnił dane model binder:
Widać, że dane użytkownika (z UserViewModel) zostały ładnie wczytane do obiektu, natomiast dane adresowe już nie. Każda właściwość ma wartość null.
Dzieje się tak, ponieważ widok Partial w zły sposób generuje hmla widoku. Najlepiej widać to w narzędziu developerskim Chrome’a:
Widać, że atrybut name inputa dla ulicy zawiera błędną wartość – („Street”), zamiast („Address.Street”). Dlatego model binder próbuje ustawić właściwość Street w UserViewModel, przez co właściwość Street w Address jest pusta.
Jednym ze sposobów rozwiązania tego problemu jest skorzystanie z tytułowych Editor Templates.
Editor Template
Czym są szablony edycji? Jest to specjalny typ widoków partial, który jest przetwarzany przez ASP.NET MVC w nieco inny sposób. Przede wszystkim jest on związany między innymi z metodą EditorFor, której używamy w widokach do generowania kontrolek dla poszczególnych pól. Gdy użyjemy tej metody, ASP.NET MVC podczas generowania widoku szuka Editor Template dla danego typu danych (np. string, int czy nawet dla naszych własnych typów).
Od strony kodu jest to taki sam widok Partial (niczego w nim nie musimy zmieniać). Znajduje się w trochę innej lokalizacji oraz ma inną nazwę. Widoki te znajdują w się katalogu EditorTemplates (w katalogu Shared lub w katalogu z widokami dla danego kontrolera). Jeżeli natomiast chodzi o nazwę widoku, to najlepiej jakby była taka sama, jak nazwa typu, dla którego tworzymy szablon. W moim przypadku jest to AddressViewModel.cshtml:
W samym widoku użytkownika musimy użyć wspomnianej wyżej metody EditorFor, aby ASP.NET MVC użyło szablonu zamiast widoku Partial. Akcja EditorTemplate i widok dla niej w przykładzie zawiera tę zmianę. Sam poprawiony widok wygląda tak:
@model PartialForms.ViewModels.Users.UserViewModel | |
@{ | |
ViewBag.Title = "EditorTemplate"; | |
} | |
<h2>EditorTemplate</h2> | |
@using (Html.BeginForm()) | |
{ | |
@Html.AntiForgeryToken() | |
<div class="form-horizontal"> | |
<h4>UserViewModel</h4> | |
<hr /> | |
@Html.ValidationSummary(true, "", new { @class = "text-danger" }) | |
<div class="form-group"> | |
@Html.LabelFor(model => model.FirstName, htmlAttributes: new { @class = "control-label col-md-2" }) | |
<div class="col-md-10"> | |
@Html.EditorFor(model => model.FirstName, new { htmlAttributes = new { @class = "form-control" } }) | |
@Html.ValidationMessageFor(model => model.FirstName, "", new { @class = "text-danger" }) | |
</div> | |
</div> | |
<div class="form-group"> | |
@Html.LabelFor(model => model.LastName, htmlAttributes: new { @class = "control-label col-md-2" }) | |
<div class="col-md-10"> | |
@Html.EditorFor(model => model.LastName, new { htmlAttributes = new { @class = "form-control" } }) | |
@Html.ValidationMessageFor(model => model.LastName, "", new { @class = "text-danger" }) | |
</div> | |
</div> | |
@Html.EditorFor(model => model.Address) | |
<div class="form-group"> | |
<div class="col-md-offset-2 col-md-10"> | |
<input type="submit" value="Create" class="btn btn-default" /> | |
</div> | |
</div> | |
</div> | |
} | |
<div> | |
@Html.ActionLink("Back to List", "Index") | |
</div> | |
@section Scripts { | |
@Scripts.Render("~/bundles/jqueryval") | |
} |
Jest on praktycznie identyczny jak poprzedni widok i różni się przede wszystkim linijką 34, w której jest „@Html.EditorFor(model => model.Address)” zamiast „@Html.Partial(„Address”, Model.Address)”.
Pod względem wizualnym wygenerowany widok jest identyczny i różni się on przede wszystkim wartościami atrybutu name inputów:
Gdy prześlemy dane do kontrola, widać, że wszystko ładnie zostało uzupełnione:
Przykład
Na githubie (https://github.com/danielplawgo/PartialForms) znajduje się przykład do tego wpisu. Testowa aplikacja, jak widać powyżej, zawiera dwie akcje ([adres]/Users/Partial oraz [adres]/Users/EditorTemplate), za pomocą których możesz sam/sama sprawdzić różnicę między działaniem widoku Partial a Editor Template.
Podsumowanie
Mam nadzieję, że po tym wpisie będziesz wiedział/wiedziała, dlaczego używanie widoków Partial do budowania formularzy to nienajlepszy pomysł. Niestety wiele razy widziałem aplikacje, w których programiści używali widoków Partial i później poświęcali wiele godzin, aby w jakiś sposób obejść ten problem. Na ogół tworzyli płaskie viewmodele, które później w kontrolerach mapowali (ręcznie lub za pomocą automappera) na zagnieżdżone obiekty. Utrzymanie tego kodu było później kosztowne, a można było to rozwiązać poprzez użycie Editor Templates.
W tym miejscu warto jeszcze wspomnieć, że w ASP.NET MVC istnieje drugi rodzaj szablonów (Display Templates), które działają w taki sam sposób (jedynie pliki znajdują w się w katalogu DisplayTemplates). Ich użycie odbywa się poprzez skorzystanie z metody DisplayFor.
A jakie są Twoje doświadczenia z widokami Partial oraz Editor Templates? Używasz tych drugich?
Jako alternatywa – w partialu można wywołać ViewData.TemplateInfo.HtmlFieldPrefix = „Address” i zadziała ?
Hej Paweł!
Masz racje można tak zrobić, ale według mnie na dłuższą metę takie rozwiązanie jest bardziej problematyczne niż Editor Templates. Szczególnie, gdy właściwości do adresu będą miały różne nazwy (np. użytkownik będzie miał 3 adresy: zamieszkania, korespondecyjny oraz do faktur).
Hmm wiekszym problemem wg mnie jest ze w takim projekcie dla jednej property potrzebujesz az 7 powtarzalnych linijek aby cos edytowac – sugerowalby jakis standardowy helper ktory by Ci wszystko dla jednej property robil (label, editorfor/inputbox, validationmessageFor) – bedzie bardziej przejrzyste i pozwoli pozbyc sie zbednych detali (dopoki nie robisz customowych rzeczy jest to wystarczajace)
Hej!
Masz rację, jest to kolejny problem, która warto rozwiązać, aby tworzenie formularzy było jeszcze łatwiejsze. Porusze to w jednym z kolejnych wpisów.
Dzięki za komentarz!