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…