Kolejny odcinek o Reflectorze i MVC, tym razem opowieść spod znaku "przecież to NIE MOŻE nie działać!". Oczywiście bezlitosna rzeczywistość twierdziła inaczej i jak zwykle w takich wypadkach bywa – to ona była górą. Zobaczmy cóż takiego się stało...
Jak zwykle dla uproszczenia stworzę bezsensowny projekcik specjalnie pod ten scenariusz, aby każdy mógł w prosty sposób odtworzyć cały proces. Praktyki stosowane podczas implementacji wcale nie muszą być godne naśladowania, po prostu chodzi o jak najprostszą demonstrację problemu.
Zadaniem moim było wyświetlenie rozwijanej listy elementów oraz zaznaczenie tego, którego ID zostało przekazane do akcji kontrolera. Wydaje się, że nic prostszego, zatem zróbmy to:
1) stwórzmy model, który pomoże nam w silnie typowany sposób przekazać niezbędne dane do widoku:
1: public class HomeIndexModel
2: {
3: public int? SelectedUserId { get; set; }
4: public ICollection<User> Users { get; set; }
5:
6: public HomeIndexModel(int? selectedUserId, ICollection<User> users)
7: {
8: SelectedUserId = selectedUserId;
9: Users = users;
10: }
11: }
2) zmodyfikujmy akcję Home/Index tak, aby była w stanie przyjąć odpowiedni parametr i przekazała dane do widoku:
1: public class HomeController : Controller
2: {
3: public ActionResult Index(int? id)
4: {
5: var users = new List<User> {new User(1, "User1"), new User(2, "User2")};
6: ViewData.Model = new HomeIndexModel(id, users);
7:
8: return View();
9: }
10: }
3) klasa User jest właściwie oczywista, ale dla pełnej jawności oto ona:
1: public class User
2: {
3: public int Id { get; set; }
4: public string Name { get; set; }
5:
6: public User(int id, string name)
7: {
8: Id = id;
9: Name = name;
10: }
11: }
4) wykorzystując projekt MVC Contrib tworzymy listę w bardzo wygodny sposób:
1: Users: <%= Html.DropDownList("users", Model.Users.ToSelectList(x => x.Id, x => x.Name, x => x.Id == Model.SelectedUserId)) %>
Dla niezorientowanych: metoda ToSelectList tworzy gotowe do wykorzystania elementy listy przy pomocy trzech wyrażeń lambda. Pierwsze służy do pobrania wartości elementu (value), drugie – wyświetlanej wartości (text), trzecie z kolei definiuje czy dany element ma być aktualnie wybrany czy też nie. Prosto i przyjemnie.
OKej, skoro mamy takie rozwiązanie, to je przetestujmy wchodząc na stronę Home/Index/2. I zonk:
Po kilku minutach zdumienia i tarcia brody (jak to inteligentnie musi wyglądać!) tknięty durnym impulsem zmieniłem przekazaną w pierwszym parametrze docelową nazwę listy z "users" na "x", i... zadziałało:
Na czas, że tak to brzydko określę, "płatny" to w zupełności wystarczyło. Jednak w czasie "wolnym" nie dawało mi to spokoju. Odpaliłem więc wieczorem Reflectora, załadowałem dllkę System.Web.Mvc i zabrałem się do "inwestygacji":
1) Wszystko zaczyna się w metodzie DropDownList, zatem do niej udamy się najsampierw:
2) Po kilku klikach i szybkich nawigacjach docieramy do sedna generacji tagu <select>, czyli prywatnej metody System.Web.Mvc.Html.SelectExtensions.SelectInternal. Od razu widać, że to jakaś skomplikowana potwora. Wiemy jednak jakie przekazujemy parametry, zatem dość łatwo możemy zidentyfikować potencjalnie interesujące źródła tego dziwnego zachowania:
Trochę ten rysunek pomazałem, ale już tłumaczę. Fragmenty 1, 2 oraz 3 wyeliminujemy: pierwsze dwa odpadają z powodu naszych parametrów, a trzeci dotyczy już plucia napisami, gdzie z pewnością wszystko jest w porządku (w końcu po małej modyfikacji strona działa jak trzeba). Zaznaczony fragment nr 4 wydaje się niezmiernie interesujący: ktoś zmienia wartość właściwości Selected dla elementów, które my tak pieczołowicie przygotowaliśmy w naszym aspx! Ma to jakiś związek ze zmienną set, która z kolei tworzona jest z source otrzymanego z obj2 uzyskanego przez wywołanie metody GetModelStateValue lub Eval. Nie jest źle, na pewno zbliżamy się do celu niczym łowczy do łani.
3) Klikamy losowo w jedną z nich, mi akurat trafiło się GetModelStateValue. Rzut oka wystarczył jednak, aby upewnić się, że to nie tu tkwi problem:
4) Przechodzimy zatem do Eval, i okazuje się że tu jest pies pogrzebany:
Dochodzimy do miejsca, gdzie pobierana jest wartość właściwości MODELU (czyli instancji naszej klasy HomeIndexModel) o nazwie odpowiadającej nazwie listy, do tego case-insensitive! W tym przypadku będzie to kolekcja użytkowników o nazwie Users typu ICollection<User>. Oznacza to ni mniej ni więcej, że zgodnie z tą logiką (z fragmentu "4!" z obrazka pod punktem 3) zaznaczone zostaną elementy spełniające warunek: item.Value == <anyUser>.ToString(). Łatwo się domyślić, że nie zostanie zaznaczone NIC, tak jak dało się zaobserwować.
Puenta: tworząc listę rozwijaną przy pomocy metody Html.DropDown i nadając jej nazwę występującą we właściwościach modelu możesz zostać zaskoczony niedziałaniem "serwerowego" zaznaczania elementów na liście. Ja rozwiązałem to zmieniając nazwę właściwości w modelu, jednak ten sam skutek odniosłaby zmiana nazwy listy.
Uff, jak teraz na to patrzę to dochodzę do wniosku że trochę przesadziłem... bo i co to kogo obchodzi?? Jeżeli dotarłeś aż do tego miejsca to gratuluję zawziętości, bo to chyba najnudniejszy post w mojej karierze:).
Bottom-line: reflector rulez, gdyż pozwala zaspokoić ciekawość dociekliwego programisty. Tutaj z pewnością prościej byłoby ściągnąć kod źródłowy MVC, ale kto by się spodziewał że zagrzebiemy się tak głęboko...