ASP.NET MVC – Reflector vs DropDownList

0

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…

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

Comments are closed.