Spis treści O autorze ...............................................................................................................17 O recenzencie ...
O autorze ...............................................................................................................17 O recenzencie technicznym ....................................................................................19
Część I Rozdział 1.
Wprowadzenie do ASP.NET MVC 4 ..........................................21 Zagadnienia ogólne ...............................................................................................23 Krótka historia programowania witryn WWW .............................................................................. 23 Tradycyjna technologia ASP.NET Web Forms ........................................................................ 23 Co poszło nie tak z ASP.NET Web Forms? .............................................................................. 25 Programowanie witryn WWW — stan obecny .............................................................................. 25 Standardy sieciowe oraz REST .................................................................................................... 26 Programowanie zwinne i sterowane testami ............................................................................ 26 Ruby on Rails ................................................................................................................................. 27 Sinatra ............................................................................................................................................. 27 Node.js ............................................................................................................................................ 28 Najważniejsze zalety ASP.NET MVC ............................................................................................... 28 Architektura MVC ........................................................................................................................ 28 Rozszerzalność .............................................................................................................................. 29 Ścisła kontrola nad HTML i HTTP ............................................................................................ 29 Łatwość testowania ....................................................................................................................... 30 Zawansowany system routingu .................................................................................................. 30 Zbudowany na najlepszych częściach platformy ASP.NET ................................................... 30 Nowoczesne API ........................................................................................................................... 31 ASP.NET MVC jest open source ................................................................................................ 31 Kto powinien korzystać z ASP.NET MVC? ..................................................................................... 31 Porównanie z ASP.NET Web Forms ......................................................................................... 31 Migracja z Web Forms do MVC ................................................................................................ 32 Porównanie z Ruby on Rails ....................................................................................................... 32 Porównanie z MonoRail .............................................................................................................. 32 Co nowego w ASP.NET MVC 4? ...................................................................................................... 33 Podsumowanie ..................................................................................................................................... 33
SPIS TREŚCI
Rozdział 2.
Pierwsza aplikacja MVC .........................................................................................35 Przygotowanie stacji roboczej ............................................................................................................ 35 Tworzenie nowego projektu ASP.NET MVC ................................................................................. 36 Dodawanie pierwszego kontrolera ............................................................................................. 38 Przedstawiamy ścieżki .................................................................................................................. 40 Generowanie stron WWW ................................................................................................................ 41 Tworzenie i generowanie widoku .............................................................................................. 41 Dynamiczne dodawanie treści .................................................................................................... 43 Tworzenie prostej aplikacji wprowadzania danych ........................................................................ 45 Przygotowanie sceny .................................................................................................................... 45 Projektowanie modelu danych ................................................................................................... 46 Łączenie metod akcji .................................................................................................................... 47 Budowanie formularza ................................................................................................................. 49 Obsługa formularzy ...................................................................................................................... 51 Dodanie kontroli poprawności ................................................................................................... 54 Kończymy ...................................................................................................................................... 60 Podsumowanie ..................................................................................................................................... 61
Rozdział 3.
Wzorzec MVC .........................................................................................................63 Historia MVC ....................................................................................................................................... 63 Wprowadzenie do wzorca MVC ....................................................................................................... 64 Budowa modelu domeny ............................................................................................................. 64 Implementacja MVC w ASP.NET .............................................................................................. 65 Porównanie MVC z innymi wzorcami ...................................................................................... 65 Przedstawiam wzorzec Smart UI ................................................................................................ 65 Modelowanie domeny ......................................................................................................................... 68 Przykładowy model domeny ....................................................................................................... 69 Wspólny język ............................................................................................................................... 69 Agregaty i uproszczenia ............................................................................................................... 70 Definiowanie repozytoriów ......................................................................................................... 71 Budowanie luźno połączonych komponentów ............................................................................... 73 Wykorzystanie wstrzykiwania zależności ................................................................................. 73 Przykład specyficzny dla MVC ................................................................................................... 75 Użycie kontenera wstrzykiwania zależności ............................................................................. 75 Zaczynamy testy automatyczne ......................................................................................................... 76 Zadania testów jednostkowych ................................................................................................... 77 Zadania testów integracyjnych ................................................................................................... 84 Podsumowanie ..................................................................................................................................... 84
Rozdział 4.
Najważniejsze cechy języka ...................................................................................85 Utworzenie przykładowego projektu ................................................................................................ 85 Użycie automatycznie implementowanych właściwości ............................................................... 86 Użycie inicjalizatorów obiektów i kolekcji ....................................................................................... 89 Użycie metod rozszerzających ........................................................................................................... 91 Stosowanie metod rozszerzających do interfejsów .................................................................. 93 Tworzenie filtrujących metod rozszerzających ........................................................................ 95 Użycie wyrażeń lambda ...................................................................................................................... 97 Automatyczne wnioskowanie typów .............................................................................................. 100 Użycie typów anonimowych ............................................................................................................ 100
6
SPIS TREŚCI
Wykonywanie zapytań LINQ .......................................................................................................... 102 Opóźnione zapytania LINQ ...................................................................................................... 105 Użycie metod asynchronicznych ..................................................................................................... 107 Użycie słów kluczowych async i await ..................................................................................... 108 Podsumowanie ................................................................................................................................... 109
Rozdział 5.
Praca z silnikiem Razor ........................................................................................111 Tworzenie projektu ........................................................................................................................... 111 Definiowanie modelu ................................................................................................................. 111 Definiowanie kontrolera ............................................................................................................ 112 Tworzenie widoku ...................................................................................................................... 113 Korzystanie z obiektów modelu ...................................................................................................... 114 Praca z układami ................................................................................................................................ 116 Tworzenie układu ....................................................................................................................... 116 Stosowanie układu ...................................................................................................................... 118 Użycie pliku ViewStart ............................................................................................................... 119 Użycie układów współdzielonych ............................................................................................ 119 Użycie wyrażeń Razor ....................................................................................................................... 123 Wstawianie wartości danych ..................................................................................................... 124 Przypisanie wartości atrybutu ................................................................................................... 125 Użycie konstrukcji warunkowych ............................................................................................ 127 Wyliczanie tablic i kolekcji ........................................................................................................ 129 Praca z przestrzenią nazw .......................................................................................................... 132 Podsumowanie ................................................................................................................................... 132
Rozdział 6.
Ważne narzędzia wspierające MVC .....................................................................133 Tworzenie przykładowego projektu ............................................................................................... 134 Utworzenie klas modelu ............................................................................................................ 134 Dodanie kontrolera .................................................................................................................... 135 Dodanie widoku .......................................................................................................................... 136 Użycie Ninject .................................................................................................................................... 136 Zrozumienie problemu .............................................................................................................. 137 Zaczynamy korzystać z Ninject ................................................................................................ 139 Konfiguracja wstrzykiwania zależności na platformie MVC ............................................... 141 Tworzenie łańcucha zależności ................................................................................................. 144 Definiowanie wartości właściwości i parametrów ................................................................. 146 Użycie łączenia warunkowego .................................................................................................. 148 Testy jednostkowe w Visual Studio ................................................................................................. 149 Tworzenie projektu testów jednostkowych ............................................................................ 150 Tworzenie testów jednostkowych ............................................................................................ 151 Uruchamianie testów (nieudane) ............................................................................................. 154 Implementacja funkcji ............................................................................................................... 155 Testowanie i poprawianie kodu ................................................................................................ 156 Użycie Moq ......................................................................................................................................... 157 Zrozumienie problemu .............................................................................................................. 158 Dodawanie Moq do projektu Visual Studio ........................................................................... 159 Dodanie obiektu imitacyjnego do testu jednostkowego ....................................................... 160 Tworzenie obiektu imitacji ........................................................................................................ 161 Tworzenie bardziej skomplikowanych obiektów Mock ....................................................... 163 Podsumowanie ................................................................................................................................... 166 7
SPIS TREŚCI
Rozdział 7.
SportsStore — kompletna aplikacja ....................................................................167 Zaczynamy .......................................................................................................................................... 168 Tworzenie rozwiązania i projektów w Visual Studio ............................................................ 168 Dodawanie referencji ................................................................................................................. 169 Konfigurowanie kontenera DI .................................................................................................. 171 Uruchamiamy aplikację ............................................................................................................. 172 Tworzenie modelu domeny ............................................................................................................. 173 Tworzenie abstrakcyjnego repozytorium ................................................................................ 174 Tworzenie imitacji repozytorium ............................................................................................. 174 Wyświetlanie listy produktów ......................................................................................................... 175 Dodawanie kontrolera ............................................................................................................... 176 Dodawanie widoku ..................................................................................................................... 177 Konfigurowanie domyślnej ścieżki .......................................................................................... 178 Uruchamianie aplikacji .............................................................................................................. 179 Przygotowanie bazy danych ............................................................................................................. 179 Tworzenie bazy danych ............................................................................................................. 180 Definiowanie schematu bazy danych ...................................................................................... 181 Dodawanie danych do bazy ....................................................................................................... 182 Tworzenie kontekstu Entity Framework ................................................................................. 183 Tworzenie repozytorium produktów ...................................................................................... 184 Dodanie stronicowania ..................................................................................................................... 186 Wyświetlanie łączy stron ........................................................................................................... 188 Ulepszanie adresów URL ........................................................................................................... 195 Dodawanie stylu ................................................................................................................................. 196 Definiowanie wspólnej zawartości w pliku układu ................................................................ 197 Dodanie stylów CSS .................................................................................................................... 197 Tworzenie widoku częściowego ............................................................................................... 199 Podsumowanie ................................................................................................................................... 200
SportsStore — ukończenie koszyka na zakupy ....................................................229 Użycie dołączania danych ................................................................................................................ 229 Tworzenie własnego łącznika modelu ..................................................................................... 229 Kończenie budowania koszyka ........................................................................................................ 234 Usuwanie produktów z koszyka ............................................................................................... 234 Dodawanie podsumowania koszyka ........................................................................................ 234
8
SPIS TREŚCI
Składanie zamówień .......................................................................................................................... 237 Rozszerzanie modelu domeny .................................................................................................. 237 Dodawanie procesu zamawiania .............................................................................................. 238 Implementowanie mechanizmu przetwarzania zamówień .................................................. 241 Rejestrowanie implementacji .................................................................................................... 243 Zakończenie pracy nad kontrolerem koszyka ........................................................................ 244 Wyświetlanie informacji o błędach systemu kontroli poprawności ................................... 248 Wyświetlanie strony podsumowania ....................................................................................... 249 Podsumowanie ................................................................................................................................... 249
Rozdział 10. SportsStore — administracja ...............................................................................251 Dodajemy zarządzanie katalogiem .................................................................................................. 251 Tworzenie kontrolera CRUD .................................................................................................... 251 Tworzenie nowego pliku układu .............................................................................................. 253 Implementowanie widoku listy ................................................................................................ 255 Edycja produktów ....................................................................................................................... 260 Tworzenie nowych produktów ................................................................................................. 271 Usuwanie produktów ................................................................................................................. 272 Podsumowanie ................................................................................................................................... 275
Rozdział 11. SportsStore — bezpieczeństwo i ostatnie usprawnienia .....................................277 Zabezpieczanie funkcji administracyjnych .................................................................................... 277 Realizacja uwierzytelniania z użyciem filtrów ........................................................................ 278 Tworzenie dostawcy uwierzytelniania ..................................................................................... 280 Tworzenie kontrolera AccountController .............................................................................. 281 Tworzenie widoku ...................................................................................................................... 283 Przesyłanie zdjęć ................................................................................................................................ 286 Rozszerzanie bazy danych ......................................................................................................... 286 Rozszerzanie modelu domeny .................................................................................................. 287 Tworzenie interfejsu użytkownika do przesyłania plików ................................................... 288 Zapisywanie zdjęć do bazy danych ........................................................................................... 289 Implementowanie metody akcji GetImage ............................................................................. 290 Wyświetlanie zdjęć produktów ................................................................................................. 292 Podsumowanie ................................................................................................................................... 293
Część II
ASP.NET MVC 4 — szczegółowy opis ....................................295
Rozdział 12. Przegląd projektu MVC ........................................................................................297 Korzystanie z projektów MVC z Visual Studio ............................................................................. 297 Przedstawienie konwencji MVC .............................................................................................. 301 Debugowanie aplikacji MVC ........................................................................................................... 302 Tworzenie projektu .................................................................................................................... 302 Uruchamianie debugera Visual Studio .................................................................................... 304 Przerywanie pracy aplikacji przez debuger Visual Studio .................................................... 306 Użycie opcji Edit and Continue ................................................................................................ 310 Podsumowanie ................................................................................................................................... 313
Rozdział 13. Routing URL .........................................................................................................315 Tworzenie projektu routingu ........................................................................................................... 316 Wprowadzenie do wzorców URL ................................................................................................... 318 9
SPIS TREŚCI
Tworzenie i rejestrowanie prostej ścieżki ....................................................................................... 319 Użycie prostej ścieżki ................................................................................................................. 324 Definiowanie wartości domyślnych ................................................................................................ 325 Użycie statycznych segmentów adresu URL ................................................................................. 327 Definiowanie własnych zmiennych segmentów ........................................................................... 331 Użycie własnych zmiennych jako parametrów metod akcji ................................................ 334 Definiowanie opcjonalnych segmentów URL ........................................................................ 335 Definiowanie ścieżek o zmiennej długości .............................................................................. 337 Definiowanie priorytetów kontrolerów na podstawie przestrzeni nazw ........................... 339 Ograniczenia ścieżek ......................................................................................................................... 342 Ograniczanie ścieżki z użyciem wyrażeń regularnych .......................................................... 342 Ograniczanie ścieżki do zbioru wartości ................................................................................. 343 Ograniczanie ścieżek z użyciem metod HTTP ....................................................................... 343 Definiowanie własnych ograniczeń ......................................................................................... 344 Routing żądań dla plików dyskowych ............................................................................................ 346 Konfiguracja serwera aplikacji .................................................................................................. 347 Definiowanie ścieżek dla plików na dysku .............................................................................. 349 Pomijanie systemu routingu ............................................................................................................ 350 Podsumowanie ................................................................................................................................... 351
Rozdział 14. Zaawansowane funkcje routingu ........................................................................353 Przygotowanie projektu .................................................................................................................... 353 Generowanie wychodzących adresów URL w widokach ............................................................. 354 Użycie systemu routingu do wygenerowania wychodzącego adresu URL ........................ 354 Użycie innych kontrolerów ....................................................................................................... 357 Przekazywanie dodatkowych parametrów .............................................................................. 358 Definiowanie atrybutów HTML ............................................................................................... 360 Generowanie w pełni kwalifikowanych adresów URL w łączach ........................................ 360 Generowanie adresów URL (nie łączy) ................................................................................... 361 Generowanie wychodzących adresów URL w metodach akcji ............................................ 362 Generowanie adresu URL na podstawie wybranej ścieżki ................................................... 363 Dostosowanie systemu routingu ..................................................................................................... 364 Tworzenie własnej implementacji RouteBase ........................................................................ 364 Tworzenie własnego obiektu obsługi ścieżki .......................................................................... 368 Korzystanie z obszarów .................................................................................................................... 369 Tworzenie obszaru ...................................................................................................................... 369 Wypełnianie obszaru .................................................................................................................. 371 Rozwiązywanie problemów z niejednoznacznością kontrolerów ....................................... 373 Generowanie łączy do akcji z obszarów .................................................................................. 374 Najlepsze praktyki schematu adresów URL .................................................................................. 375 Twórz jasne i przyjazne dla człowieka adresy URL ............................................................... 375 GET oraz POST — wybierz właściwie ..................................................................................... 376 Podsumowanie ................................................................................................................................... 376
Rozdział 15. Kontrolery i akcje .................................................................................................377 Wprowadzenie do kontrolerów ....................................................................................................... 377 Przygotowanie projektu ............................................................................................................. 377 Tworzenie kontrolera z użyciem interfejsu IController ....................................................... 378 Tworzenie kontrolera przez dziedziczenie po klasie Controller .......................................... 379 10
SPIS TREŚCI
Odczytywanie danych wejściowych ................................................................................................ 380 Pobieranie danych z obiektów kontekstu ................................................................................ 380 Użycie parametrów metod akcji ............................................................................................... 382 Tworzenie danych wyjściowych ...................................................................................................... 383 Wyniki akcji ................................................................................................................................. 385 Zwracanie kodu HTML przez generowanie widoku ............................................................. 388 Przekazywanie danych z metody akcji do widoku ................................................................. 391 Wykonywanie przekierowań .................................................................................................... 394 Zwracanie błędów i kodów HTTP ........................................................................................... 399 Podsumowanie ................................................................................................................................... 401
Rozdział 16. Filtry ....................................................................................................................403 Użycie filtrów ..................................................................................................................................... 403 Wprowadzenie do czterech podstawowych typów filtrów ................................................... 404 Dołączanie filtrów do kontrolerów i metod akcji .................................................................. 405 Tworzenie projektu ........................................................................................................................... 406 Użycie filtrów autoryzacji ................................................................................................................. 407 Użycie własnego filtra autoryzacji ............................................................................................ 409 Użycie wbudowanego filtra autoryzacji .................................................................................. 409 Użycie filtrów wyjątków ................................................................................................................... 410 Tworzenie filtra wyjątku ............................................................................................................ 411 Użycie filtra wyjątków ................................................................................................................ 412 Użycie widoku w celu reakcji na wyjątek ................................................................................ 414 Użycie wbudowanego filtra wyjątków ..................................................................................... 417 Użycie filtrów akcji ............................................................................................................................ 419 Implementacja metody OnActionExecuting .......................................................................... 420 Implementacja metody OnActionExecuted ........................................................................... 421 Używanie filtra wyniku ..................................................................................................................... 423 Użycie wbudowanych klas filtrów akcji i wyniku .................................................................. 424 Użycie innych funkcji filtrów ........................................................................................................... 425 Filtrowanie bez użycia atrybutów ............................................................................................. 426 Użycie filtrów globalnych .......................................................................................................... 427 Określanie kolejności wykonywania filtrów ........................................................................... 429 Użycie filtrów wbudowanych ........................................................................................................... 431 Użycie filtra RequireHttps ......................................................................................................... 432 Użycie filtra OutputCache ......................................................................................................... 432 Podsumowanie ................................................................................................................................... 435
Rozdział 17. Rozszerzanie kontrolerów ....................................................................................437 Tworzenie projektu ........................................................................................................................... 437 Tworzenie własnej fabryki kontrolerów ......................................................................................... 439 Przygotowanie kontrolera zapasowego ................................................................................... 441 Utworzenie klasy kontrolera ..................................................................................................... 442 Implementacja innych metod interfejsu ................................................................................. 442 Rejestrowanie własnej fabryki kontrolerów ............................................................................ 442 Wykorzystanie wbudowanej fabryki kontrolerów ........................................................................ 443 Nadawanie priorytetów przestrzeniom nazw ......................................................................... 444 Dostosowywanie sposobu tworzenia kontrolerów w DefaultControllerFactory .............. 445 Tworzenie własnego obiektu wywołującego akcje ........................................................................ 447 11
SPIS TREŚCI
Użycie wbudowanego obiektu wywołującego akcje ..................................................................... 449 Użycie własnych nazw akcji ...................................................................................................... 450 Selekcja metod akcji .................................................................................................................... 451 Poprawianie wydajności z użyciem specjalizowanych kontrolerów .......................................... 456 Użycie kontrolerów bezstanowych .......................................................................................... 456 Użycie kontrolerów asynchronicznych ................................................................................... 458 Podsumowanie ................................................................................................................................... 463
Rozdział 18. Widoki .................................................................................................................465 Tworzenie własnego silnika widoku ............................................................................................... 465 Tworzenie przykładowego projektu ........................................................................................ 467 Tworzenie własnej implementacji IView ................................................................................ 468 Tworzenie implementacji IViewEngine .................................................................................. 468 Rejestrowanie własnego silnika widoku .................................................................................. 470 Testowanie silnika widoku ........................................................................................................ 470 Korzystanie z silnika Razor .............................................................................................................. 472 Tworzenie przykładowego projektu ........................................................................................ 472 Sposób generowania widoków przez Razor ............................................................................ 473 Konfigurowanie wyszukiwania lokalizacji widoków ............................................................. 475 Dodawanie dynamicznych treści do widoku Razor ..................................................................... 477 Zastosowanie sekcji .................................................................................................................... 478 Użycie widoków częściowych ................................................................................................... 483 Użycie akcji podrzędnych .......................................................................................................... 486 Podsumowanie ................................................................................................................................... 488
Rozdział 19. Metody pomocnicze ............................................................................................489 Tworzenie przykładowego projektu ............................................................................................... 489 Tworzenie własnej metody pomocniczej ....................................................................................... 491 Tworzenie wewnętrznej metody pomocniczej HTML .......................................................... 491 Tworzenie zewnętrznej metody pomocniczej HTML ........................................................... 492 Zarządzanie kodowaniem ciągów tekstowych w metodzie pomocniczej .......................... 496 Użycie wbudowanych metod pomocniczych ................................................................................ 500 Przygotowania do obsługi formularzy ..................................................................................... 500 Określenie ścieżki używanej przez formularz ......................................................................... 507 Użycie metod pomocniczych do wprowadzania danych ...................................................... 508 Tworzenie znaczników select .................................................................................................... 513 Podsumowanie ................................................................................................................................... 515
Rozdział 20. Szablonowe metody pomocnicze ........................................................................517 Przegląd przykładowego projektu ................................................................................................... 517 Używanie szablonowych metod pomocniczych ............................................................................ 519 Generowanie etykiety i wyświetlanie elementów ................................................................... 522 Użycie szablonowych metod pomocniczych dla całego modelu ......................................... 525 Użycie metadanych modelu ............................................................................................................. 527 Użycie metadanych do sterowania edycją i widocznością .................................................... 528 Użycie metadanych dla etykiet ................................................................................................. 530 Użycie metadanych wartości danych ....................................................................................... 531 Użycie metadanych do wybierania szablonu wyświetlania .................................................. 533 Dodawanie metadanych do klasy zaprzyjaźnionej ................................................................ 535 Korzystanie z parametrów typów złożonych .......................................................................... 536 12
SPIS TREŚCI
Dostosowywanie systemu szablonowych metod pomocniczych ................................................ 538 Tworzenie własnego szablonu edytora .................................................................................... 538 Tworzenie szablonu ogólnego .................................................................................................. 539 Wymiana szablonów wbudowanych ....................................................................................... 540 Podsumowanie ................................................................................................................................... 541
Rozdział 21. Metody pomocnicze URL i Ajax ............................................................................543 Przegląd i przygotowanie projektu .................................................................................................. 543 Tworzenie podstawowych łączy i adresów URL ........................................................................... 545 Nieprzeszkadzający Ajax .................................................................................................................. 547 Tworzenie widoku formularza synchronicznego .................................................................. 547 Włączanie i wyłączanie nieprzeszkadzających wywołań Ajax ............................................. 549 Użycie nieprzeszkadzających formularzy Ajax ............................................................................. 550 Przygotowanie kontrolera ......................................................................................................... 550 Tworzenie formularza Ajax ....................................................................................................... 552 Sposób działania nieprzeszkadzających wywołań Ajax ........................................................ 553 Ustawianie opcji Ajax ........................................................................................................................ 554 Zapewnienie kontrolowanej degradacji .................................................................................. 554 Informowanie użytkownika o realizowanym żądaniu Ajax ................................................. 556 Wyświetlanie pytania przed wysłaniem żądania .................................................................... 557 Tworzenie łączy Ajax ........................................................................................................................ 558 Zapewnienie kontrolowanej degradacji dla łączy .................................................................. 559 Korzystanie z funkcji wywołania zwrotnego w Ajaksie ............................................................... 560 Wykorzystanie JSON ......................................................................................................................... 562 Dodanie obsługi JSON do kontrolera ...................................................................................... 562 Przetwarzanie JSON w przeglądarce ........................................................................................ 564 Przygotowanie danych do kodowania ..................................................................................... 565 Wykrywanie żądań Ajax w metodach akcji ............................................................................ 567 Podsumowanie ................................................................................................................................... 570
Rozdział 22. Dołączanie modelu ..............................................................................................571 Przygotowanie projektu .................................................................................................................... 571 Użycie dołączania modelu ................................................................................................................ 573 Użycie domyślnego łącznika modelu .............................................................................................. 575 Dołączanie typów prostych ....................................................................................................... 575 Dołączanie typów złożonych ..................................................................................................... 578 Dołączanie tablic i kolekcji ........................................................................................................ 584 Jawne wywoływanie dołączania modelu ........................................................................................ 589 Obsługa błędów dołączania modelu ........................................................................................ 590 Dostosowanie systemu dołączania modelu ................................................................................... 591 Tworzenie własnego dostawcy wartości .................................................................................. 591 Tworzenie własnego łącznika modelu ..................................................................................... 594 Rejestracja własnego łącznika modelu ..................................................................................... 596 Podsumowanie ................................................................................................................................... 597
Rozdział 23. Kontrola poprawności modelu ............................................................................599 Tworzenie projektu ........................................................................................................................... 599 Jawna kontrola poprawności modelu ............................................................................................. 602 Wyświetlenie użytkownikowi błędów podczas kontroli poprawności ............................... 603
13
SPIS TREŚCI
Wyświetlanie komunikatów kontroli poprawności ..................................................................... 604 Wyświetlanie komunikatów kontroli poprawności poziomu właściwości ........................ 608 Użycie alternatywnych technik kontroli poprawności ................................................................. 608 Kontrola poprawności w łączniku modelu ............................................................................. 609 Definiowanie zasad poprawności za pomocą metadanych .................................................. 610 Definiowanie modeli samokontrolujących się ....................................................................... 616 Użycie kontroli poprawności po stronie klienta ........................................................................... 618 Aktywowanie i wyłączanie kontroli poprawności po stronie klienta ................................. 618 Użycie kontroli poprawności po stronie klienta .................................................................... 619 Jak działa kontrola poprawności po stronie klienta? ............................................................. 620 Wykonywanie zdalnej kontroli poprawności ................................................................................ 621 Podsumowanie ................................................................................................................................... 624
Rozdział 24. Paczki i tryby wyświetlania .................................................................................625 Domyślne biblioteki skryptów ......................................................................................................... 625 Tworzenie przykładowej aplikacji ................................................................................................... 627 Zarządzanie skryptami i stylami ...................................................................................................... 630 Profilowanie wczytywania skryptów i arkuszy stylów .......................................................... 630 Używanie paczek stylów i skryptów ................................................................................................ 632 Stosowanie paczek ...................................................................................................................... 635 Używanie sekcji script ................................................................................................................ 637 Profilowanie wprowadzonych zmian ...................................................................................... 638 Przygotowanie aplikacji dla urządzeń mobilnych ........................................................................ 640 Aplikacja standardowa ............................................................................................................... 641 Użycie widoków i układów przeznaczonych dla urządzeń mobilnych .............................. 642 Tworzenie własnych trybów wyświetlania .............................................................................. 643 Podsumowanie ................................................................................................................................... 646
Rozdział 25. Web API ...............................................................................................................647 Zrozumienie Web API ...................................................................................................................... 647 Tworzenie aplikacji Web API .......................................................................................................... 648 Tworzenie modelu i repozytorium .......................................................................................... 648 Tworzenie kontrolera Home ..................................................................................................... 650 Utworzenie widoku i CSS .......................................................................................................... 651 Tworzenie kontrolera API ................................................................................................................ 653 Testowanie kontrolera API ....................................................................................................... 655 Jak działa kontroler API? .................................................................................................................. 656 Jak wybierana jest akcja kontrolera API? ................................................................................ 657 Mapowanie metod HTTP na metody akcji ............................................................................. 657 Tworzenie kodu JavaScript wykorzystującego interfejs Web API ............................................. 658 Tworzenie funkcji podstawowych ............................................................................................ 659 Dodanie obsługi edycji nowych rezerwacji ............................................................................. 661 Dodanie obsługi usuwania rezerwacji ..................................................................................... 663 Dodanie obsługi tworzenia rezerwacji ..................................................................................... 664 Podsumowanie ................................................................................................................................... 665
Przygotowanie do użycia Windows Azure .................................................................................... 671 Tworzenie witryny internetowej i bazy danych ..................................................................... 672 Przygotowanie bazy danych do zdalnej administracji .......................................................... 674 Tworzenie schematu bazy danych ........................................................................................... 674 Wdrażanie aplikacji ........................................................................................................................... 676 Podsumowanie ................................................................................................................................... 680
Adam Freeman jest doświadczonym specjalistą IT, który zajmował kierownicze stanowiska w wielu firmach, a ostatnio pracował jako dyrektor ds. technologii oraz dyrektor naczelny w międzynarodowym banku. Obecnie jest na emeryturze i poświęca swój czas na pisanie oraz bieganie.
ASP.NET MVC 4. ZAAWANSOWANE PROGRAMOWANIE
O recenzencie technicznym
Fabio Claudio Ferracchiati jest starszym konsultantem oraz starszym analitykiem-programistą korzystającym z technologii firmy Microsoft. Pracuje we włoskim oddziale (www.brainforce.it) firmy Brain Force (www.brainforce.com). Posiada certyfikaty Microsoft Certified Solution Developer for .NET, Microsoft Certified Application Developer for .NET, Microsoft Certified Professional. Jest autorem, współautorem i recenzentem technicznym wielu książek o różnej tematyce. W ciągu ostatnich dziesięciu lat pisał artykuły dla włoskich i międzynarodowych czasopism.
ASP.NET MVC 4. ZAAWANSOWANE PROGRAMOWANIE
CZĘŚĆ I
Wprowadzenie do ASP.NET MVC 4
ASP.NET MVC stanowi radykalną zmianę dla programistów WWW korzystających z platformy firmy Microsoft. Platforma ta jest oparta na jasnej architekturze, stosowaniu wzorców projektowych i łatwości testowania i nie próbuje ukrywać sposobu działania sieci WWW. Pierwsza część książki pomaga w zrozumieniu podstawowych zagadnień platformy ASP.NET MVC 4, przedstawia nowe funkcje, a także prezentuje jej praktyczne zastosowania.
ROZDZIAŁ 1.
Zagadnienia ogólne
ASP.NET MVC jest zaprojektowaną w firmie Microsoft platformą programowania witryn WWW, która łączy w sobie efektywność i schludność architektury model-widok-kontroler (MVC), najnowsze pomysły i techniki programowania zwinnego oraz najlepsze części istniejącej platformy ASP.NET. Jest to kompletna alternatywa dla tradycyjnych projektów ASP.NET Web Forms, mająca nad tą platformą znaczną przewagę, ujawniającą się we wszystkich projektach, poza najbardziej trywialnymi. W rozdziale tym wyjaśnimy, dlaczego Microsoft zajął się tworzeniem ASP.NET MVC, porównamy tę platformę z jej poprzednikami oraz rozwiązaniami alternatywnymi, a na koniec przedstawimy nowości w ASP.NET MVC 4.
Krótka historia programowania witryn WWW Aby zrozumieć wszystkie aspekty oraz cele projektowe platformy ASP.NET MVC, warto zapoznać się z historią programowania WWW — choć nie jest zbyt długa. Analizując platformy programowania witryn WWW oferowane przez firmę Microsoft, możemy zaobserwować stałe zwiększanie się ich możliwości oraz (niestety) złożoności. Jak można zauważyć w tabeli 1.1, każda nowa platforma uzupełniała pewne braki swojej poprzedniczki.
Tradycyjna technologia ASP.NET Web Forms W roku 2002 technologia ASP.NET była znacznym usprawnieniem w stosunku do poprzednich rozwiązań. Na rysunku 1.1 przedstawiony jest stos wprowadzonych wtedy technologii. W technologii Web Forms Microsoft próbował ukryć zarówno HTTP (wraz z jego bezstanowością), jak i HTML (który w tym czasie nie był znany wielu programistom) przez modelowanie interfejsu użytkownika (UI) za pomocą hierarchii serwerowych obiektów kontrolek. Każda kontrolka przechowywała własny stan pomiędzy żądaniami (z wykorzystaniem mechanizmu ViewState), automatycznie generowała własny kod HTML oraz pozwalała na automatyczne podłączanie zdarzeń klienckich (na przykład kliknięcie przycisku) do kodu obsługi działającego na serwerze. W efekcie technologia Web Forms stała się gigantyczną warstwą abstrakcji mającą za zadanie zrealizować klasyczny, sterowany zdarzeniami graficzny interfejs użytkownika (GUI) do obsługi sieci WWW. W założeniach programowanie witryn WWW powinno być zbliżone do programowania Windows Forms. Programiści nie musieli już korzystać z serii niezależnych żądań i odpowiedzi HTTP; mogli za to projektować swoje aplikacje na bazie obsługującego stan interfejsu użytkownika. Można było zapomnieć o mechanizmach WWW i ich bezstanowej naturze i zamiast tego budować interfejs, przeciągając kontrolki na formularz i wiedząc — lub co najmniej podejrzewając — że cały kod działa na serwerze.
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Tabela 1.1. Historia technologii programowania dla WWW firmy Microsoft Okres
Technologia
Zalety
Wady
Era jurajska
Common Gateway Interface (CGI)*
Prosta.
Działa poza serwerem WWW, więc wymaga dużo zasobów (tworzy osobny proces w systemie operacyjnym dla każdego żądania).
Elastyczna. Jedyna technologia w tym czasie.
Działa na niskim poziomie. Era brązu
Microsoft Internet Database Connector (IDC)
Działa wewnątrz serwera WWW.
1996
Active Server Pages (ASP)
Ogólnego przeznaczenia.
To tylko opakowanie dla zapytań SQL i szablony do formatowania wyników. Kod interpretowany. Zachęca to tworzenia „kodu spaghetti”.
2002/03
ASP.NET Web Forms 1.0/1.1
Kompilowany interfejs użytkownika z obsługą stanu.
Nieefektywne wykorzystanie łącza.
Dostęp do olbrzymiej infrastruktury.
Brzydki HTML. Problemy z testowaniem.
Zachęca do programowania obiektowego. 2005
ASP.NET Web Forms 2.0
2007
ASP.NET AJAX
2008
ASP.NET Web Forms 3.5
2009
ASP.NET MVC 1.0
2010
ASP.NET MVC 2.0 ASP.NET Web Forms 4.0
2011
ASP.NET MVC 3.0
2012
ASP.NET MVC 4.0 ASP.NET Web Forms 4.5
* CGI jest standardem podłączania serwera WWW do dowolnego programu wykonywalnego zwracającego dynamiczną zawartość. Specyfikacja jest utrzymywana przez organizację National Center for Supercomputing Applications (NCSA).
Rysunek 1.1. Stos technologii ASP.NET Web Forms
24
ROZDZIAŁ 1. ZAGADNIENIA OGÓLNE
Co poszło nie tak z ASP.NET Web Forms? Założenia technologii ASP.NET Web Forms były świetne, ale rzeczywistość okazała się bardziej skomplikowana. Po pewnym czasie stosowania Web Forms w rzeczywistych projektach ujawniły się pewne wady: Ciężar ViewState. Mechanizm pozwalający na przenoszenie stanu pomiędzy żądaniami (ViewState) powodował tworzenie gigantycznych bloków danych przesyłanych pomiędzy klientem i serwerem. Dane te mogą osiągać kilkaset kilobajtów nawet dla niewielkiej aplikacji WWW i są przesyłane w obie strony w każdym żądaniu, co może frustrować użytkowników strony zwiększeniem czasów odpowiedzi i zwiększać pasmo wykorzystywane przez serwer. Cykl życia strony. Mechanizm łączenia zdarzeń klienta z kodem obsługi na serwerze, będący częścią cyklu życia strony, jest niezwykle skomplikowany i delikatny. Niewielu programistów potrafiło manipulować hierarchią kontrolek bez powodowania błędów ViewState lub tajemniczego wyłączania niektórych bloków obsługi zdarzenia. Niewłaściwe rozdzielenie zadań. Model code-behind z ASP.NET pozwala oddzielić kod aplikacji od znaczników HTML i umieścić go w osobnej klasie. Powinno to być doceniane ze względu na oddzielanie warstwy logiki od prezentacji, ale w rzeczywistości programiści często byli zachęcani do mieszania kodu prezentacji (np. manipulowanie drzewem kontrolek serwera) z logiką aplikacji (np. manipulowaniem danymi w bazie) w jednej, monstrualnej wielkości klasie code-behind. W wyniku tego aplikacja była wrażliwa na błędy i mało profesjonalna. Ograniczona kontrola nad HTML. Kontrolki serwera generują swój wygląd w postaci HTML, ale niekoniecznie taki, jakiego sobie życzymy. W wersjach wcześniejszych od 4. wynikowy HTML zwykle nie trzymał się standardów WWW, nie korzystał ze stylów CSS, a kontrolki serwera generowały trudne do przewidzenia i skomplikowane wartości identyfikatorów; owe wartości z kolei były trudne do wykorzystania w kodzie JavaScript. Problemy te zostały w znacznej mierze usunięte w ASP.NET 4 i ASP.NET 4.5, ale nadal nie jest łatwo uzyskać taki kod HTML, jakiego oczekujemy. Słaba abstrakcja. Platforma Web Forms stara się ukryć szczegóły HTML i HTTP wszędzie, gdzie jest to możliwe. Przy próbie implementacji własnych mechanizmów często jesteśmy zmuszeni porzucić tę abstrakcję i wrócić do zdarzeń przesyłania danych lub też wykonywać inne nieeleganckie akcje pozwalające na wygenerowanie odpowiedniego kodu HTML. Dodatkowo cała ta abstrakcja może stać się frustrującą barierą dla zaawansowanego programisty WWW. Problemy z tworzeniem testów automatycznych. Gdy projektanci ASP.NET tworzyli swoją platformę, nie przypuszczali, że automatyczne testowanie wejdzie do standardowych mechanizmów tworzenia oprogramowania. Nie jest niespodzianką, że ściśle połączona architektura, jaką utworzyli, nie nadaje się do testowania jednostkowego. Również testy integracyjne mogą stanowić wyzwanie. ASP.NET stale się rozwija. Wersja 2.0 została wzbogacona o standardowe komponenty aplikacji, co pozwoliło znacznie zmniejszyć ilość kodu, jaki trzeba było napisać. Udostępnienie rozszerzenia Ajax w roku 2007 było odpowiedzią firmy Microsoft na szaleństwo Web 2.0 (Ajax), dzięki czemu możliwa była łatwa obsługa złożonej interakcji ze stroną kliencką. W wersji ASP.NET 4.0 skupiono się na tworzeniu bardziej przewidywalnego i zgodnego ze standardami sieciowymi kodu HTML. Z kolei w najnowszym wydaniu ASP.NET 4.5 wykorzystano pewne funkcje pochodzące z ASP.NET MVC i przeniesiono je do świata Web Forms. Dzięki temu udało się rozwiązać sporo uporczywych problemów, ale wielu ograniczeń technologii nie udało się wyeliminować.
Programowanie witryn WWW — stan obecny Po wydaniu pierwszej wersji Web Forms technologie programowania WWW poza firmą Microsoft szybko rozwijały się w kilku różnych kierunkach. Oprócz wspomnianej już technologii Ajax powstało jeszcze kilka innych ważnych rozwiązań.
25
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Standardy sieciowe oraz REST W ostatnich latach nie zmniejszył się nacisk na zachowanie zgodności ze standardami sieciowymi. Witryny internetowe są obecnie wykorzystywane w znacznie większej niż wcześniej liczbie różnych urządzeń i przeglądarek, a standardy sieciowe (dotyczące HTML, CSS i JavaScript itp.) zapewniają możliwość efektywnego korzystania z tych witryn — nawet przy użyciu lodówki podłączonej do internetu. Nowoczesne platformy sieciowe nie ignorują wymagań biznesowych oraz woli programistów, by utrzymać zgodność ze standardami sieciowymi. Coraz większą popularność zyskuje język HTML5 oferujący programistom potężne możliwości w zakresie tworzenia aplikacji sieciowych wykonujących po stronie klienta zadania, które wcześniej były przeznaczone do realizacji jedynie po stronie serwera. Wspomniane nowe możliwości oraz coraz większe dopracowanie bibliotek JavaScript takich jak jQuery, jQuery UI i jQuery Mobile oznacza, że standardy zyskały jeszcze większą wagę, a ich stosowanie ma krytyczne znaczenie dla każdej aplikacji sieciowej. Wskazówka W niniejszej książce poruszę tematy związane z HTML5, jQuery i jej bibliotekami pochodnymi, ale nie będę zagłębiać się w szczegóły, ponieważ wymienionym tematom można poświęcić osobne tomy. Jeżeli chcesz dowiedzieć się więcej o HTML5, JavaScript i jQuery, to zapoznaj się z innymi moimi książkami — wydawnictwo Helion ma w ofercie pozycję zatytułowaną HTML5. Przewodnik encyklopedyczny, a w ofercie wydawnictwa Apress znajdziesz Pro jQuery i Pro JavaScript for Web Apps.
W tym samym czasie dominującą architekturą dla współpracy aplikacji HTTP stała się architektura Representational State Transfer (REST), całkowicie przesłaniając SOAP (architektura stosowana początkowo w usługach sieciowych ASP.NET). REST definiuje aplikację jako zbiór zasobów (URI) reprezentujących encje domeny oraz operacji (metod HTTP) możliwych do wykonania na tych zasobach. Możemy na przykład dodać nowy produkt za pośrednictwem operacji PUT i adresu http://www.przyklad.pl/Produkty/Kosiarka lub usunąć dane klienta za pomocą operacji DELETE http://www.przyklad.pl/Klient/Arnold-Kowalski. Dzisiejsze aplikacje sieciowe nie tylko udostępniają HTML — równie często muszą one udostępniać dane JSON lub XML dla różnych technologii klienckich, takich jak Ajax, Silverlight czy rodzime aplikacje działające w smartfonach. Jest to realizowane w sposób naturalny poprzez REST i eliminuje historyczne różnice pomiędzy usługami i aplikacjami sieciowymi, ale wymaga takiego podejścia do obsługi HTTP oraz URL, które nie jest w łatwy sposób obsługiwane w ASP.NET Web Forms.
Programowanie zwinne i sterowane testami W ostatniej dekadzie rozwijało się nie tylko programowanie sieciowe — w obrębie tworzenia oprogramowania można zauważyć przesunięcie w kierunku metodologii zwinnych. Dla każdego programisty oznacza to coś innego, ale można powiedzieć o ogólnej zasadzie traktowania projektu tworzenia oprogramowania jako adaptowalnego procesu, w którym unika się nadmiernej biurokracji oraz sztywnego planowania. Entuzjazm związany z metodologiami zwinnymi zwykle jest skojarzony ze stosowaniem określonych praktyk i narzędzi (przeważnie open source) promujących i wspierających te praktyki. Programowanie sterowane testami (TDD) oraz jego najnowsze wcielenie programowanie sterowane zachowaniami (BDD) są oczywistymi przykładami. Założeniem tej metodologii jest projektowanie oprogramowania przez zdefiniowanie na początku przykładów oczekiwanego zachowania (nazywanych również testami lub specyfikacją), dzięki czemu w każdym momencie można zweryfikować stabilność i poprawność aplikacji przez wykonanie zbioru testów specyfikacji na danej implementacji. Nie brakuje narzędzi obsługujących TDD/BDD w .NET, ale zwykle nie sprawdzają się one zbyt dobrze w Web Forms: Narzędzia testów jednostkowych pozwalają określić zachowanie poszczególnych klas lub mniejszych jednostek kodu działających w izolacji. Mogą być one jednak efektywnie stosowane w aplikacjach zaprojektowanych jako zbiór jasno rozdzielonych, niezależnych modułów, dzięki czemu można je uruchamiać oddzielnie. Niestety, tylko niektóre aplikacje Web Forms mogą być testowane w ten sposób. Postępowanie zgodnie z zaleceniami platformy i umieszczanie logiki w metodach obsługi
26
ROZDZIAŁ 1. ZAGADNIENIA OGÓLNE
zdarzeń lub nawet użycie kontrolki serwerowej do bezpośredniego odpytania bazy danych powoduje zwykle ścisłe związanie logiki aplikacji ze środowiskiem uruchomieniowym Web Forms. Jest to zabójcze dla testowania jednostkowego. Narzędzia automatyzacji UI pozwalają symulować serie interakcji użytkownika w działającym egzemplarzu aplikacji. Teoretycznie mogą być one wykorzystywane w Web Forms, ale mogą przestać działać, jeżeli wprowadzimy zmiany w układzie strony. Jeżeli nie zostaną wykonane dodatkowe kroki, Web Forms zacznie generować całkowicie inne struktury HTML oraz identyfikatory elementów, co spowoduje, że nasze testy staną się bezużyteczne. Środowisko open source oraz niezależnych dostawców oprogramowania (ISV) dla .NET wytworzyło wiele świetnej jakości środowisk testów jednostkowych (NUnit i xUnit), platform zastępujących (Moq i Rhino Mock), kontenerów inwersji kontroli (Niniect i AutoFac), serwerów ciągłej integracji (Cruise Control i TeamCity), bibliotek obiektowo-relacyjnych (NHibernate i Subsonic) i wiele innych. Zwolennicy tych technologii zorganizowali nawet wspólny kanał komunikacyjny, publikując i organizując konferencje pod wspólną marką ALT.NET. Tradycyjna biblioteka ASP.NET Web Forms nie pozwala na łatwe stosowanie tych narzędzi i technik z powodu swojej monolitycznej budowy, więc Web Forms nie zdobyła zbyt dużego uznania wśród ekspertów oraz liderów technologii.
Ruby on Rails W roku 2004 Ruby on Rails był cichym projektem open source utrzymywanym przez nieznanych graczy. Nagle stał się bardzo znany i zmienił zasady programowania witryn WWW. Nie stało się to z powodu umieszczenia w Ruby on Rails nowych, rewolucyjnych technologii — ale dzięki użyciu istniejących składników i połączeniu ich w tak atrakcyjny i oczywisty sposób platforma ta błyskawicznie zdobyła uznanie. Ruby on Rails (lub po prostu Rails) wykorzystuje architekturę MVC (zostanie wkrótce omówiona). Dzięki zastosowaniu architektury MVC, działaniu zgodnemu z protokołem HTTP, a nie przeciw niemu, dzięki promowaniu konwencji zamiast konfiguracji oraz dzięki integracji narzędzia mapowania obiektowo-relacyjnego (ORM) aplikacje Rails mogą być szybko tworzone bez większych kosztów i bez wysiłku. Właśnie tak powinno wyglądać programowanie sieciowe — nagle okazało się, że przez te wszystkie lata walczyliśmy ze swoimi narzędziami, ale na szczęście teraz się to skończyło. Platforma Rails pokazała, że zgodność ze standardami sieciowymi oraz REST nie musi być trudna w realizacji. Pokazała również, że programowanie zwinne oraz TDD działa najlepiej, gdy platforma je wspiera. Pozostała część świata programowania sieciowego również to zauważyła.
Sinatra Dzięki Rails szybko pojawiło się wielu programistów korzystających z Ruby jako swojego głównego języka programowania. Jednak w tak niezwykle innowacyjnej społeczności pojawienie się odpowiedników Rails było tylko kwestią czasu. Najlepiej znanym jest środowisko Sinatra, udostępnione w roku 2007. W Sinatrze usunięto niemal całą standardową infrastrukturę Rails (routing, kontrolery, widoki itd.), a w zamian zaoferowano łączenie wzorców URL z blokami kodu Ruby. Użytkownik otwiera URL, co powoduje wykonanie bloku kodu Ruby, a wynikowe dane są wysyłane do przeglądarki — to wszystko. Jest to niezwykle prosty sposób programowania sieciowego, ale znalazł on swoje nisze w dwóch głównych obszarach. Po pierwsze, przy budowaniu usług sieciowych REST, gdzie pozwala szybko wykonać określone zadanie (zasady REST są przedstawione w rozdziale 25.). Po drugie, platforma Sinatra może być łączona z dużą liczbą technologii szablonów oraz ORM dostępnych na zasadach open source, zatem często jest używana jako podstawa tworzenia własnych środowisk dopasowanych do potrzeb architektonicznych konkretnych projektów. Sinatra nie zdobyła zbyt dużego udziału w rynku pełnym platform MVC, takich jak Rails (lub ASP.NET MVC). Wspominamy ją tutaj, aby pokazać, że programowanie sieciowe podlega stałemu trendowi upraszczania, a Sinatra stanowi opozycję dla innych platform oferujących coraz więcej funkcji.
27
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Node.js Innym znaczącym trendem jest użycie JavaScriptu jako podstawowego języka programowania. Technologia Ajax jako pierwsza uświadomiła nam, że JavaScript jest ważny; jQuery pokazuje, że może być potężny i elegancki, natomiast silnik JavaScript V8 firmy Google, że może być niezwykle szybki. Obecnie JavaScript staje się poważnym językiem programowania po stronie serwera. Służy jako język przechowywania i odpytywania danych dla kilku nierelacyjnych baz danych, w tym CouchDB i Mongo; jest ponadto wykorzystywany jako język ogólnego przeznaczenia dla platform serwerowych, takich jak Node.js. Node.js jest dostępny od roku 2009 i bardzo szybko zdobył powszechną akceptację. Architektonicznie jest podobny do platformy Sinatra, ale nie korzysta z wzorca MVC. Jest to bardziej niskopoziomowy sposób łączenia żądań HTTP z kodem. Jego najważniejszymi cechami są: Użycie JavaScript — programiści muszą korzystać z tylko jednego języka, od kodu klienta, poprzez logikę serwera, a nawet logikę dostępu do danych, po CouchDB lub podobne. Całkowita asynchroniczność — API Node.js nie daje żadnej możliwości zablokowania wątku w czasie oczekiwania na operacje wejścia-wyjścia czy jakiekolwiek inne. Wszystkie operacje wejścia-wyjścia są realizowane przez rozpoczęcie operacji, a po jej zakończeniu są uruchamiane metody wywołania zwrotnego. Powoduje to, że Node.js pozwala niezwykle efektywnie korzystać z zasobów systemu i obsługiwać dziesiątki tysięcy jednoczesnych żądań na procesor (alternatywne platformy zwykle są ograniczone do około 100 jednoczesnych żądań na procesor). Podobnie jak Sinatra, Node.js jest technologią niszową. Większość firm budujących aplikacje wymaga całej infrastruktury dostępnej w pełnych platformach, takich jak Ruby on Rails czy ASP.NET MVC. Wspominamy tutaj o Node.js, aby pokazać projekt ASP.NET MVC w kontekście aktualnych trendów. ASP.NET MVC zawiera na przykład kontrolery asynchroniczne (które opisujemy w rozdziale 17.). Jest to sposób na obsłużenie żądań HTTP z użyciem nieblokujących operacji wejścia-wyjścia, co pozwala na obsłużenie większej liczby żądań na procesor. Jak pokażemy, ASP.NET MVC dobrze daje się integrować ze złożonym kodem JavaScript działającym w przeglądarce.
Najważniejsze zalety ASP.NET MVC ASP.NET był ogromnym sukcesem komercyjnym, ale jak wspomniano, reszta świata poszła do przodu i nawet Microsoft zauważył, że platforma Web Forms zaczęła zarastać pajęczyną, a sam projekt zaczął jawić się jako staroświecki. W październiku roku 2007 na pierwszej konferencji ALT.NET w Austin, w Teksasie, wiceprezes Microsoftu, Scott Guthrie zapowiedział i zademonstrował całkiem nową platformę MVC, zbudowaną na rdzeniu ASP.NET, zaprojektowaną jako odpowiedź na ewolucję technologii takich jak Rails oraz reakcję na krytykę Web Forms. W kolejnych punktach pokażemy, w jaki sposób pokonano ograniczenia Web Forms i jak nowa platforma firmy Microsoft ponownie wróciła do czołówki produktów.
Architektura MVC Bardzo ważne jest odróżnienie wzorca architektonicznego MVC od platformy ASP.NET MVC. Wzorzec MVC nie jest nowy — powstał w roku 1978 w ramach projektu Smalltalk opracowanego w laboratoriach Xerox PARC — ale zdobył obecnie niezwykłą popularność jako architektura aplikacji sieciowych z następujących powodów: Interakcja użytkownika z aplikacją MVC naturalnie jest realizowana w następującym cyklu: użytkownik podejmuje akcję, a w odpowiedzi na nią aplikacja zmienia swój model danych i dostarcza użytkownikowi zaktualizowany widok. Następnie cykl się powtarza. Jest to bardzo wygodne dla aplikacji, które są w zasadzie serią żądań i odpowiedzi HTTP.
28
ROZDZIAŁ 1. ZAGADNIENIA OGÓLNE
Aplikacje sieciowe muszą łączyć w sobie kilka technologii (np. bazy danych, HTML oraz kod wykonywalny), zwykle podzielonych na zbiór warstw. Wzorzec ten, wynikający z tego połączenia, naturalnie przekłada się na koncepcje z MVC. Platforma ASP.NET MVC implementuje wzorzec MVC, zapewniając bardzo dobrą separację zadań. ASP.NET MVC implementuje nowoczesny wariant MVC, który szczególnie dobrze nadaje się do aplikacji sieciowych. Więcej na temat teorii i praktyki w tej architekturze przedstawimy w rozdziale 3. Przez użycie i zaadaptowanie wzorca MVC platforma ASP.NET MVC stała się silną konkurencją dla Ruby on Rails i podobnych oraz sprawiła, że wzorzec MVC znalazł się w głównym nurcie zainteresowań społeczności .NET. Dzięki wykorzystaniu doświadczeń i najlepszych praktyk wypracowanych w innych platformach ASP.NET MVC w wielu przypadkach daje znacznie więcej, niż może zaoferować Rails.
Rozszerzalność Wewnętrzne komponenty Twojego komputera są niezależnymi elementami, które współdziałają ze sobą poprzez standardowe, publicznie udokumentowane interfejsy, dzięki czemu możesz łatwo wyjąć kartę graficzną lub dysk twardy i zastąpić je innymi elementami, pochodzącymi od innego producenta, mając pewność, że będzie się dało je podłączyć i że będą działać poprawnie. W taki sam sposób platforma MVC jest zbudowana jako zbiór niezależnych komponentów — zgodnych z interfejsem .NET lub zbudowanych na klasach abstrakcyjnych — dzięki temu możemy łatwo wymienić system routingu, silnik widoku, kontroler lub dowolny inny element i zastąpić go własną implementacją. Projektanci platformy ASP.NET MVC udostępnili nam trzy opcje dla każdego komponentu MVC: użycie domyślnej implementacji komponentu (co powinno być wystarczające dla większości aplikacji), użycie klasy dziedziczącej po domyślnej implementacji w celu dostosowania jej działania, całkowitą wymianę komponentu i użycie nowej implementacji interfejsu lub abstrakcyjnej klasy bazowej. Jest to podobne do modelu dostawców w ASP.NET 2.0, ale zastosowanego znacznie szerzej — aż do rdzenia platformy MVC. Więcej informacji na temat różnych komponentów oraz tego, w jakim celu i w jaki sposób możemy je dostosowywać lub wymieniać, można znaleźć w kolejnych rozdziałach, zaczynając od 12.
Ścisła kontrola nad HTML i HTTP W ASP.NET MVC docenia się wagę tworzenia czystego i zgodnego ze standardami kodu HTML. Wbudowane metody pomocnicze HTML generują wyniki zgodne ze standardami, ale można również zauważyć bardziej znaczącą, filozoficzną zmianę w porównaniu z Web Forms. Zamiast tworzyć olbrzymie bloki HTML, nad którymi mamy niewielką kontrolę, możemy dzięki platformie MVC tworzyć proste, eleganckie znaczniki, do których się dodaje style CSS. Oczywiście, jeżeli chcesz skorzystać z gotowych kontrolek realizujących złożone elementy UI, takie jak kalendarze lub menu kaskadowe, stosowane w ASP.NET MVC podejście braku dodatkowych założeń pozwala na łatwe skorzystanie z najlepszych bibliotek open source, takich jak jQuery lub Yahoo! UI Library. Programiści JavaScript będą usatysfakcjonowani, gdy dowiedzą się, że ASP.NET MVC współpracuje z popularną biblioteką jQuery tak dobrze, że Microsoft udostępnia ją jako domyślny element w szablonie projektu ASP.NET MVC, a nawet pozwala na bezpośrednie odwołanie się do pliku .js jQuery umieszczonego na serwerach Content Delivery Network (CDN) firmy Microsoft. Strony wygenerowane dla ASP.NET MVC nie zawierają danych ViewState, więc mogą być znacznie mniejsze niż typowe strony ASP.NET Web Forms. Pomimo stosowanych obecnie szybkich połączeń internetowych zmniejszenie wykorzystania pasma skutkuje znacznie lepszym komfortem pracy użytkowników. Podobnie jak Ruby on Rails, ASP.NET MVC działa zgodnie z HTTP. Mamy pełną kontrolę nad żądaniami przekazywanymi między przeglądarką i serwerem, więc możemy dowolnie dostosować działanie interfejsu użytkownika. Ajax jest prosty i nie istnieją automatyczne przesyły wpływające na stan kodu po stronie klienta. Każdy programista, który skupia się na tworzeniu aplikacji sieciowych, niemal na pewno uzna nowe możliwości za upraszczające codzienną pracę i bardziej satysfakcjonujące.
29
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Łatwość testowania Architektura MVC ułatwia tworzenie aplikacji w taki sposób, aby były łatwe w utrzymaniu i testowaniu, ponieważ w naturalny sposób dzielimy różne zadania aplikacji na osobne i niezależne fragmenty kodu. Jednak architekci ASP.NET MVC nie zatrzymali się na tym. Aby wspierać testowanie jednostkowe, zbudowali model komponentów platformy tak, aby każdy z nich spełniał wymagania (i omijał ograniczenia) stosowanych obecnie metod testowania jednostkowego i narzędzi imitujących. Do Visual Studio zostały dodane kreatory projektów testów, zintegrowane z narzędziami testów jednostkowych, dostępnych na zasadach open source, takich jak NUnit, xUnit, oraz z własnym rozwiązaniem firmy Microsoft, MSTest. Jeżeli wcześniej nie tworzyłeś testów jednostkowych, dzięki kreatorom szybko je sobie przyswoisz. W książce tej przedstawimy przykłady tworzenia czystych i prostych testów jednostkowych dla kontrolerów i akcji ASP.NET MVC, korzystających z implementacji imitujących komponenty biblioteki, które pozwalają zasymulować różne scenariusze. Łatwość testowania nie jest związana wyłącznie z testowaniem jednostkowym. Aplikacje ASP.NET MVC dobrze współpracują również z narzędziami automatycznego testowania UI. Możliwe jest pisanie skryptów symulujących działania użytkownika bez konieczności zgadywania, jakie elementy struktury HTML, klasy CSS czy identyfikatory będą wygenerowane oraz kiedy zostaną zmienione.
Zaawansowany system routingu Wraz z ewolucją technologii aplikacji sieciowych ulepszane były również adresy URL. Adresy tego typu: /App_v2/Uzytkownik/Strona.aspx?action=show%20prop&prop_id=82742
spotyka się coraz rzadziej i są one zastępowane adresami w znacznie prostszym i jaśniejszym formacie: /do-wynajecia/krakow/2303-ul-dluga
Istnieje kilka powodów, dla których zajmowano się strukturą adresów URL. Po pierwsze, silniki wyszukiwania zdecydowanie większe znaczenie nadają słowom kluczowym znalezionym w adresach URL. Wyszukiwanie „wynajem kraków” z większym prawdopodobieństwem zwróci drugi z adresów. Po drugie, wielu użytkowników WWW jest na tyle zaawansowanych, aby rozumieć adresy URL. Docenią oni możliwość nawigowania przez wpisywanie adresów w przeglądarce. Po trzecie, gdy ktoś uważa, że rozumie adresy URL, istnieje większe prawdopodobieństwo, że będzie z nich korzystał (mając pewność, że adres nie ujawni jego danych osobistych) lub dzielił się nimi ze znajomymi czy nawet dyktował je przez telefon. Po czwarte, nie ujawniają one szczegółów technicznych, folderów ani struktury nazw aplikacji, więc można je zmienić w implementacji bez obawy o zepsucie wszystkich łączy. Proste adresy URL były trudne do implementacji we wcześniejszych bibliotekach, lecz obecnie ASP.NET MVC korzysta z możliwości System.Web.Routing do tworzenia prostych adresów URL. Daje to nam kontrolę nad schematem URL i jego relacjami z aplikacją, pozwala na swobodę przy tworzeniu adresów URL, które są zrozumiałe i użyteczne, i nie wymaga zachowania zgodności z predefiniowanym formatem. Oczywiście oznacza to, że można z łatwością zdefiniować nowoczesny schemat adresów URL zgodny z REST. Dokładny opis korzystania z systemu kierowania oraz najlepsze praktyki dotyczące adresów URL można znaleźć w rozdziałach 13. i 14.
Zbudowany na najlepszych częściach platformy ASP.NET Istniejąca platforma ASP.NET Microsoftu jest dojrzałym i sprawdzonym zestawem komponentów i usług pozwalających na tworzenie efektywnych i wydajnych aplikacji sieciowych. Po pierwsze, ponieważ ASP.NET MVC bazuje na platformie .NET, mamy możliwość pisania kodu w dowolnym języku .NET i dostęp do tych samych funkcji API — nie tylko samego MVC, ale również bogatej biblioteki klas .NET i dużego zestawu bibliotek firm trzecich. Po drugie, gotowe do wykorzystania funkcje platformy ASP.NET — takie jak strony wzorcowe, uwierzytelnianie, członkostwo, role, profile oraz internacjonalizacja — pozwalają na zmniejszenie ilości kodu
30
ROZDZIAŁ 1. ZAGADNIENIA OGÓLNE
do napisania i utrzymania w każdej aplikacji i są efektywne zarówno na platformie MVC, jak i w klasycznych projektach Web Forms. Niektóre wbudowane kontrolki serwerowe Web Forms, jak również własne kontrolki z wcześniejszych projektów ASP.NET, mogą być ponownie użyte w aplikacjach ASP.NET MVC (o ile nie korzystają z notacji specyficznych dla Web Forms, na przykład ViewState).
Nowoczesne API Od czasu debiutu w roku 2002 platforma .NET firmy Microsoft stale ewoluowała, obsługując, a nawet definiując najnowsze aspekty programowania. Platforma ASP.NET MVC 4 jest zbudowana na bazie .NET 4.5, więc jej API może korzystać z najnowszych usprawnień języka i środowiska uruchomieniowego, takich jak słowo kluczowe await, metody rozszerzające, wyrażenia lambda, typy anonimowe i dynamiczne oraz Language Integrated Query (LINQ). Wiele metod API platformy MVC oraz wzorców kodowania pozwala na tworzenie czytelniejszego kodu w porównaniu z wcześniejszymi platformami.
ASP.NET MVC jest open source W przeciwieństwie do poprzednich platform firmy Microsoft obecnie możemy pobrać oryginalny kod źródłowy ASP.NET MVC, a nawet zmodyfikować go i utworzyć własną wersję. Jest to niezwykle przydatne w przypadkach, gdy sesja debugowania prowadzi do komponentów systemowych i chcemy wejść do tego kodu (choćby w celu przeczytania komentarzy programisty), jak również w przypadku budowania zaawansowanych komponentów, gdy chcemy sprawdzić, czy istnieje określona możliwość lub w jaki sposób działa jeden z wbudowanych komponentów. Możliwość taka jest świetnym rozwiązaniem, jeżeli nie podoba nam się sposób działania określonej funkcji, znaleźliśmy błąd lub gdy po prostu chcemy uzyskać dostęp do elementu, który jest w inny sposób niedostępny. Jednak należy śledzić wprowadzane zmiany i ponownie je wprowadzać w przypadku zainstalowania nowej wersji platformy. ASP.NET MVC jest rozprowadzana na zasadach licencji Ms-PL (http://www.opensource.org/licenses/ms-pl.html), która jest zaaprobowana przez Open Source Initiative (OSI), co oznacza, że możemy zmieniać kod źródłowy, instalować go, a nawet redystrybuować nasze zmiany jako projekt pochodny. Kod źródłowy biblioteki MVC można pobrać z witryny http://aspnet.codeplex.com/.
Kto powinien korzystać z ASP.NET MVC? Tak jak w przypadku każdej nowej technologii, to, że wiemy o istnieniu platformy ASP.NET MVC, nie jest wystarczającym powodem, aby jej używać. Przedstawię tu porównanie platformy MVC z większością oczywistych jej odpowiedników. Próbowałem zachować maksymalną bezstronność, na jaką może się zdobyć osoba pisząca książkę na temat platformy MVC, ale wiem, że mój obiektywizm jest ograniczony. W kolejnych punktach znajduje się porównanie technologii. Przy wybieraniu platformy aplikacji sieciowej trzeba również wziąć pod uwagę umiejętności zespołu, zakres pracy związanej z migracją istniejących projektów oraz znajomość z źródłowych technologii.
Porównanie z ASP.NET Web Forms Już wcześniej przedstawiono tu słabości i ograniczenia platformy ASP NET Web Forms oraz sposoby ich uniknięcia przy użyciu ASP.NET MVC. Nie oznacza to, że platforma Web Forms jest martwa. Microsoft wielokrotnie deklarował, że obie technologie są aktywnie rozwijane i wspierane oraz że nie planuje wycofania Web Forms. W wielu przypadkach wybór pomiędzy tymi platformami jest często wyborem filozofii programowania. Weźmy pod uwagę następujące punkty: W Web Forms interfejs użytkownika pozwala na obsługiwanie stanu, dlatego utworzona jest złożona warstwa abstrakcji bazująca na HTTP i HTML przy użyciu danych ViewState oraz wywołań postback, 31
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
która ma za zadanie zapewnić efekt obsługi stanu. Pozwala to na zastosowanie stylu programowania znanego z Windows Forms, w którym kontrolki UI są umieszczane na formularzu i dodawany jest kod obsługi zdarzeń. MVC wykorzystuje prawdziwą, bezstanową naturę HTTP, współpracując z nią, a nie walcząc. Platforma MVC wymaga wiedzy na temat działania aplikacji sieciowej. Na bazie tej wiedzy daje ona proste, efektywne i nowoczesne podejście do tworzenia aplikacji sieciowych, których schludny kod jest w dłuższym okresie łatwiejszy do rozszerzania i utrzymania oraz jest wolny od dziwnych komplikacji i przykrych ograniczeń. Istnieją oczywiście przypadki, gdy platforma Web Forms jest co najmniej tak samo dobra jak MVC, o ile nie lepsza. Oczywistym przykładem jest mała aplikacja typu intranetowego, która w większości używa tabel dołączanych bezpośrednio do tabel bazy danych lub korzysta z kreatorów. Jeżeli nie musimy się martwić o użycie pasma ani optymalizację dla silników wyszukiwania, zalety modelu projektowania z użyciem technologii „przeciągnij i upuść” mogą przeważyć nad jego wadami. Z drugiej strony, jeżeli piszemy aplikację przeznaczoną dla publicznego internetu lub większą aplikację intranetową, powinna nas zainteresować efektywność wykorzystania łącza, lepsza zgodność pomiędzy przeglądarkami oraz lepsze wsparcie dla testów automatycznych oferowane przez MVC.
Migracja z Web Forms do MVC Jeżeli prowadzisz projekt ASP.NET Web Forms i rozważasz jego migrację do MVC, będziesz mile zaskoczony, że te dwie technologie mogą ze sobą współdziałać w jednej aplikacji. Daje to możliwość stopniowej migracji istniejącej aplikacji, szczególnie jeżeli aplikacja jest już podzielona na warstwy i model domeny lub logika biznesowa jest oddzielona od stron Web Forms. W niektórych przypadkach możemy nawet na etapie projektowania aplikacji zadecydować o zastosowaniu hybrydowego modelu z użyciem obu technologii.
Porównanie z Ruby on Rails Rails stał się pewnego rodzaju punktem odniesienia, z którym muszą być porównywane inne platformy. Korzystający z technologii Microsoft .NET uznają ASP.NET MVC za technologię łatwiejszą do zaadaptowania, natomiast pracujący z użyciem Python lub Ruby w systemach Linux lub Mac OS X uznają, że łatwiejszą ścieżką będzie Rails. Jest mało prawdopodobne, aby ktoś przeszedł z Rails do ASP.NET MVC, i odwrotnie. Istnieje jednak kilka różnic pomiędzy tymi technologiami. Rails jest pełną platformą programowania, obsługującą cały stos, od kontroli wersji bazy danych, poprzez ORM, obsługę żądań z użyciem akcji i kontrolerów, aż po wbudowane narzędzia testowania automatycznego. ASP.NET MVC z kolei skupia się wyłącznie na zadaniu obsługi żądań sieciowych w stylu MVC, z użyciem kontrolerów i akcji. Nie zawiera wbudowanego narzędzia ORM, wbudowanego narzędzia testów automatycznych ani systemu zarządzania migracjami bazy danych. Platforma .NET udostępnia gamę tych funkcji i można wykorzystać dowolną. Jeżeli chcemy użyć na przykład narzędzia ORM, możemy sięgnąć po NHibernate, Subsonic, Microsoft’s Entity Framework lub inne zaawansowane narzędzie. Jest to luksus korzystania z platformy .NET, choć powoduje to, że komponenty te nie są tak ściśle zintegrowane z ASP.NET MVC, jak ich odpowiedniki w Rails.
Porównanie z MonoRail MonoRail jest wcześniejszą platformą aplikacji sieciowych, bazującą na MVC i .NET, która wchodzi w skład projektu open source Castle i jest rozwijana od roku 2003. Platforma ta stanowi w wielu przypadkach prototyp ASP NET MVC. MonoRail pokazywał, jak dodać do ASP.NET architekturę MVC podobną do Rails, ustanawiać wzorce, praktyki i terminologię, które nadal istnieją w implementacji firmy Microsoft. Uważamy, że MonoRail nie jest poważnym konkurentem. Jest to prawdopodobnie najpopularniejsza platforma aplikacji sieciowych dla .NET napisana poza Redmond, która osiągnęła w miarę dużą popularność.
32
ROZDZIAŁ 1. ZAGADNIENIA OGÓLNE
Jednak od momentu udostępnienia ASP.NET MVC rzadko słyszy się o projekcie MonoRail. Entuzjazm środowiska .NET jest obecnie skupiony na ASP.NET MVC.
Co nowego w ASP.NET MVC 4? Wersja 4. platformy ASP.NET MVC zawiera wiele różnych usprawnień w stosunku do wersji 3. Przede wszystkim wprowadzono nowe, ważne funkcje, takie jak obsługa Web API (zostanie przedstawiona w rozdziale 25.) i obsługa urządzeń mobilnych (rozdział 24.), a także wiele użytecznych technik optymalizacyjnych stosowanych podczas wysyłania treści klientom (rozdział 24.). Oprócz tego wprowadzono wiele mniejszych usprawnień, takich jak uproszczona składnia widoków Razor, lepiej zorganizowany system dostarczania aplikacjom MVC podstawowych informacji konfiguracyjnych oraz kilka nowych opcji szablonów w projektach MVC oferowanych przez Visual Studio.
Podsumowanie W tym rozdziale opisałem, w jaki sposób ewoluowało programowanie sieciowe, od prymitywnego bagna programów CGI do najnowszych, wydajnych i zgodnych ze standardami platform programowania zwinnego. Przedstawiłem zalety, wady oraz ograniczenia ASP.NET Web Forms, głównej platformy sieciowej firmy Microsoft, wprowadzonej w roku 2002, oraz zmiany w branży programowania sieciowego, które wymusiły na firmie Microsoft opracowanie nowego produktu. Pokazałem, w jaki sposób platforma ASP.NET MVC rozwiązuje problemy ASP.NET Web Forms oraz jak nowoczesny projekt wspiera programistów, którzy chcą pisać kod wysokiej jakości. W następnym rozdziale przedstawię kod platformy MVC w działaniu oraz proste mechanizmy pozwalające osiągnąć opisane wcześniej korzyści. Zanim dotrzemy do rozdziału 7., będziesz gotowy do napisania realistycznej aplikacji z dziedziny handlu elektronicznego, zbudowanej z użyciem przejrzystej architektury, testów automatycznych i eleganckiego, minimalnego kodu znaczników.
33
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
34
ROZDZIAŁ 2.
Pierwsza aplikacja MVC
Najlepszym sposobem na docenienie środowiska programowego jest skorzystanie z niego. W tym rozdziale utworzymy prostą aplikację do wprowadzania danych, działającą w środowisku ASP.NET MVC. Krok po kroku pokażemy, jak powstaje aplikacja ASP.NET MVC. Aby zachować prostotę, pominę na razie część szczegółów technicznych, jednak nie obawiaj się — jeżeli MVC jest dla Ciebie nowością, znajdziesz tu wiele interesujących zagadnień. Gdy będziemy korzystać z pewnych mechanizmów bez ich wyjaśniania, zamieszczę odnośnik do rozdziału, w którym będzie można znaleźć wszystkie szczegóły.
Przygotowanie stacji roboczej Jedynym niezbędnym krokiem w procesie przygotowania stacji roboczej do tworzenia aplikacji z użyciem platformy ASP.NET MVC 4 jest zainstalowanie Visual Studio 2012. Wymienione narzędzie zawiera wszystko, czego potrzebujesz do rozpoczęcia pracy: wbudowany serwer pozwalający na uruchamianie aplikacji i usuwanie z niej błędów, pozbawione funkcji administracyjnych wydanie bazy danych SQL Server przydatne do opracowywania aplikacji opartych na bazie danych, narzędzia do przeprowadzania testów jednostkowych oraz — oczywiście — edytor kodu, kompilator i moduł przeznaczony do usuwania błędów. Microsoft oferuje kilka różnych wersji Visual Studio 2012, ale w niniejszej książce będziemy używali wydania całkowicie bezpłatnego: Visual Studio Express 2012 for Web. W płatnych wersjach Visual Studio firma Microsoft umieściła wiele przydatnych funkcji, których jednak nie będziemy używać w tej książce. Wszystkie rysunki znajdujące się w książce zostały wykonane w wydaniu Visual Studio 2012 Express, dostępnego bezpłatnie na stronie http://www.microsoft.com/visualstudio/plk/products/visual-studio-express-for-web. Istnieje kilka różnych wersji programu Visual Studio 2012 Express, a każda z nich jest przeznaczona do innego rodzaju programowania — upewnij się o pobraniu wersji Web pozwalającej na tworzenie aplikacji sieciowych w technologii ASP.NET MVC. Po zainstalowaniu narzędzia Visual Studio możesz natychmiast przystąpić do pracy. Microsoft naprawdę poprawił produkt w wersji Express i funkcje oferowane przez Visual Studio Express są w zupełności wystarczające do przećwiczenia materiału przedstawionego w niniejszej książce. Wskazówka W przykładach tworzonych na potrzeby tej książki użyty został system Windows 7, ale pozwalające na tworzenie aplikacji ASP.NET MVC 4 narzędzie Visual Studio 2012 może działać także w systemie Windows 8 oraz w systemach Windows wcześniejszych niż 7. Szczegółowe informacje na temat wymagań systemowych dla Visual Studio 2012 znajdziesz na podanej wcześniej stronie.
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Tworzenie nowego projektu ASP.NET MVC Zaczniemy od utworzenia nowego projektu MVC w Visual Studio. Z menu Plik wybierz Nowy Projekt…, co spowoduje otwarcie okna dialogowego Nowy projekt. Po wybraniu szablonu Sieć Web w sekcji Visual C# możemy zauważyć, że jeden z dostępnych typów projektów to Aplikacja sieci Web platformy ASP.NET MVC 4, pokazany na rysunku 2.1.
Rysunek 2.1. Szablon projektu MVC 4 w Visual Studio Ostrzeżenie Visual Studio 2012 pozwala na tworzenie aplikacji zarówno MVC 3, jak i MVC 4, więc oprócz nowych szablonów mamy również stare. Przy tworzeniu nowego projektu należy uważać, aby wybrać właściwy szablon.
Jako nazwy nowego projektu użyj PartyInvites i kliknij przycisk OK, aby kontynuować. Wyświetli się kolejne okno dialogowe pokazane na rysunku 2.2. Musimy w nim wybrać jeden z trzech typów szablonów projektu MVC. Poszczególne szablony projektów MVC pozwalają na tworzenie projektów różniących się standardowo umieszczonymi w nich funkcjami, takimi jak uwierzytelnianie, nawigacja i style wizualne. W tym rozdziale stawiamy na prostotę. Wybierz więc szablon Pusta, który powoduje utworzenie projektu zawierającego podstawową strukturę katalogów, ale bez żadnych plików wymaganych do zbudowania aplikacji MVC. Niezbędne pliki dodasz w trakcie lektury rozdziału, a ja wyjaśnię ich znaczenie. Kliknij przycisk OK, tworząc w ten sposób nowy projekt.
36
ROZDZIAŁ 2. PIERWSZA APLIKACJA MVC
Rysunek 2.2. Wybór typu projektu MVC 4 Uwaga W opcjach szablonu na rysunku 2.2 znajduje się menu rozwijane pozwalające na wybór silnika widoków dla projektu. Na platformie MVC 3 wprowadzono nowy, usprawniony silnik widoku o nazwie Razor, z którego będziemy korzystać w tej książce. Tobie zalecam to samo. Jeżeli jednak chcesz korzystać ze starszego silnika widoków ASP.NET (znanego pod nazwą silnika ASPX), możesz wybrać go właśnie w tym oknie. Dokładne omówienie silnika Razor i sposobu działania silnika widoku znajdziesz w rozdziałach 5. i 18.
Po utworzeniu projektu przez Visual Studio wyświetli się w oknie Eksplorator rozwiązania zestaw plików i katalogów. Jest to domyślna struktura dla projektu MVC 4. Możesz spróbować uruchomić teraz aplikację, wybierając Start Debugging z menu Debuguj (jeżeli wyświetli się monit informujący o konieczności włączenia debugowania, kliknij przycisk OK). Wyniki działania są przedstawione na rysunku 2.3. Zaczęliśmy od szablonu pustego projektu i aplikacja nie zawiera nic użytecznego do uruchomienia — zobaczymy zatem komunikat o błędzie 404.
Rysunek 2.3. Próba uruchomienia pustego projektu 37
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Zatrzymaj teraz debugowanie przez zamknięcie okna przeglądarki lub przez wybranie opcji Stop Debugging z menu Debuguj w Visual Studio. Visual Studio uruchamia przeglądarkę internetową w celu wyświetlenia projektu. Z poziomu paska narzędzi możesz wybrać używaną przeglądarkę internetową (rysunek 2.4). W omawianym przykładzie do dyspozycji jest Internet Explorer i Firefox.
Rysunek 2.4. Zmiana przeglądarki internetowej używanej przez Visual Studio do wyświetlenia uruchomionej aplikacji W książce będziemy używali przeglądarki Internet Explorer. Obecnie wszystkie nowoczesne przeglądarki internetowe są całkiem dobre, ale wykorzystamy IE, ponieważ ta przeglądarka znajduje się w standardowej instalacji systemu Windows.
Dodawanie pierwszego kontrolera W architekturze model-widok-kontroler (MVC) żądania przychodzące są obsługiwane przez kontrolery. W ASP.NET MVC kontrolery są zwykłymi klasami C# (zwykle dziedziczącymi po System.Web.Mvc.Controller, klasie bazowej kontrolerów dostępnej na platformie). Każda metoda publiczna w kontrolerze jest nazywana metodą akcji, co oznacza, że można ją wywołać poprzez WWW przy użyciu określonego adresu URL. Zgodnie z konwencją platformy ASP.NET MVC kontrolery umieszczamy w katalogu o nazwie Controllers, który jest utworzony przez Visual Studio przy konfigurowaniu projektu. Nie musisz postępować zgodnie z tą konwencją MVC i większością innych, ale zalecamy, abyś się do nich stosował — przynajmniej po to, by pomóc w zrozumieniu przykładów zamieszczonych w tej książce. Aby dodać kontroler do projektu, kliknij prawym przyciskiem myszy katalog Controllers w oknie Eksplorator rozwiązania, następnie wybierz z menu opcję Dodaj, a później Kontroler… (rysunek 2.5).
Rysunek 2.5. Dodawanie kontrolera do projektu MVC
38
ROZDZIAŁ 2. PIERWSZA APLIKACJA MVC
Gdy wyświetli się okno dialogowe Dodaj kontroler, jako nazwę wpisz HomeController (rysunek 2.6). Jest to kolejna konwencja — nazwy nadawane kontrolerom powinny być opisowe i kończyć się ciągiem Controller.
Rysunek 2.6. Określenie nazwy kontrolera Grupa opcji Opcje scaffoldingu pozwala na utworzenie kontrolera przy zastosowaniu szablonu z często wykorzystywanymi funkcjami. Nie będziemy używać tej funkcji, więc upewnij się, że w menu Szablon wybrana jest opcja Pusty kontroler MVC (rysunek 2.6). Kliknij przycisk Dodaj, aby utworzyć kontroler. Visual Studio utworzy w katalogu Controllers nowy plik C# o nazwie HomeController.cs i otworzy go do edycji. Zauważ, że znajduje się w nim klasa o nazwie HomeController, która dziedziczy po System.Web.Mvc.Controller. Domyślny kod pliku klasy wygenerowany przez Visual Studio został przedstawiony na listingu 2.1. Listing 2.1. Domyślny kod umieszczony w klasie HomeController using using using using using
namespace PartyInvites.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } } }
Dobrym sposobem rozpoczęcia pracy z MVC jest wprowadzenie kilku zmian w klasie kontrolera. Kod klasy w pliku HomeController.cs zmień w sposób pokazany na listingu 2.2 — zmiany zostały przedstawione pogrubioną czcionką, dzięki czemu łatwiej możesz je dostrzec. Listing 2.2. Zmodyfikowana klasa HomeController using System; using System.Collections.Generic; using System.Linq;
39
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
using System.Web; using System.Web.Mvc; namespace PartyInvites.Controllers { public class HomeController : Controller { public string Index() { return "Witaj, świecie"; } } }
Nie napisaliśmy na razie niczego ekscytującego, ale to wystarczy na rozpoczęcie znajomości z MVC. Zmodyfikowaliśmy metodę akcji o nazwie Index, która zwraca komunikat Witaj, świecie. Uruchom ponownie projekt przez wybranie Start Debugging z menu Debuguj. Przeglądarka wyświetli wynik działania metody akcji Index (rysunek 2.7).
Rysunek 2.7. Dane wyjściowe wygenerowane przez metodę akcji kontrolera
Przedstawiamy ścieżki Oprócz modeli, widoków i kontrolerów aplikacje MVC wykorzystują system routingu ASP.NET, który decyduje, w jaki sposób adres URL jest dopasowywany do określonego kontrolera i danej akcji. Gdy Visual Studio tworzy projekt MVC, dodaje na początek kilka domyślnych ścieżek. Możesz skorzystać z dowolnego z poniższych adresów URL, ale będziesz skierowany do akcji Index w HomeController. / /Home /Home/Index Jeżeli więc otworzymy w przeglądarce stronę http://naszserwer/ lub http://naszserwer/Home, otrzymamy wynik z metody Index zdefiniowanej w klasie HomeController. Obecnie adres URL to http://localhost:49157/, choć u Ciebie numer portu może być inny. Jeżeli do wymienionego adresu URL dołączysz człon /Home lub /Home/Index i naciśniesz klawisz Enter, wynikiem będzie wyświetlenie komunikatu Witaj, świecie. To dobry przykład zastosowania konwencji MVC. W tym przypadku konwencją jest nazywanie kontrolera HomeController, dzięki czemu stał się punktem startowym dla naszej aplikacji MVC. Przy tworzeniu domyślnych ścieżek dla nowego projektu zakłada się, że konwencja będzie zachowana. Ponieważ tak właśnie postąpiliśmy, otrzymaliśmy w prezencie obsługę wymienionych wcześniej adresów URL. Jeżeli nie trzymalibyśmy się konwencji, musielibyśmy zmodyfikować ścieżki, aby wskazywały na utworzony przez nas kontroler. W tym prostym przykładzie wystarczyła nam domyślna konfiguracja. Wskazówka Konfigurację routingu można zobaczyć i zmienić, otwierając plik Global.asax.cs. W rozdziale 7. utworzymy własne wpisy w systemie routingu, a w rozdziałach 13. i 14. powiem Ci więcej o jego możliwościach.
40
ROZDZIAŁ 2. PIERWSZA APLIKACJA MVC
Generowanie stron WWW Wynikiem poprzedniego przykładu nie był HTML — był to tylko tekst Witaj, świecie. Aby utworzyć odpowiedź HTML, będziemy potrzebować widoku.
Tworzenie i generowanie widoku Pierwszą czynnością do wykonania jest modyfikacja metody akcji Index w sposób pokazany na listingu 2.3. Listing 2.3. Modyfikowanie kontrolera w celu wygenerowania widoku using using using using using
namespace PartyInvites.Controllers { public class HomeController : Controller { public ViewResult Index() { return View(); } } }
Zmiany na listingu 2.3 są wyróżnione pogrubioną czcionką. Gdy zwracamy z metody akcji obiekt ViewResult, instruujemy aplikację MVC, aby wygenerowała widok. Obiekt ViewResult tworzymy przez wywołanie metody View bez parametrów. Informuje to MVC o konieczności wygenerowania domyślnego widoku dla akcji. Jeżeli w tym momencie uruchomisz aplikację, zobaczysz, że aplikacja MVC próbuje znaleźć domyślny widok do wykorzystania, jak wynika z komunikatu o błędzie przedstawionego na rysunku 2.8.
Rysunek 2.8. Aplikacja MVC próbuje znaleźć domyślny widok
41
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Ten komunikat jest bardziej pomocny niż większość innych. Nie tylko wyjaśnia, że MVC nie może znaleźć widoku dla naszej metody akcji, ale pokazuje, gdzie ten widok był wyszukiwany. Jest to kolejny przykład konwencji MVC — widoki są skojarzone z metodami akcji za pomocą konwencji nazewnictwa. Nasza metoda akcji ma nazwę Index i jak możemy wyczytać z rysunku 2.8, aplikacja MVC próbuje znaleźć w katalogu Views różne pliki o takiej nazwie. Aby utworzyć widok, kliknij prawym przyciskiem myszy metodę akcji w pliku kodu HomeController.cs (możesz kliknąć nazwę metody lub jej treść), a następnie wybierz Dodaj widok… z menu kontekstowego. Spowoduje to otwarcie okna dialogowego Dodaj widok, pokazanego na rysunku 2.9.
Rysunek 2.9. Okno dialogowe pozwalające na dodanie widoku Usuń zaznaczenie opcji Użyj układu lub strony wzorcowej. Nie będziemy korzystać z układów w tym przykładzie, ale pokażemy ich użycie w rozdziale 7. Kliknij przycisk Dodaj, a Visual Studio utworzy w katalogu Views/Home nowy plik widoku o nazwie Index.cshtml. Jeżeli wrócisz do komunikatu o błędzie na rysunku 2.8, zauważysz, że utworzony plik pasuje do jednej z przeszukiwanych lokalizacji. Wskazówka Rozszerzenie pliku .cshtml wskazuje na widok C#, który będzie przetwarzany przez Razor. Poprzednie wersje MVC korzystały z silnika widoku ASPX; w ich przypadku pliki miały rozszerzenie .aspx.
Plik Index.cshtml zostanie otwarty do edycji. Jak widać, zawiera on w większości HTML. Wyjątkiem jest poniższa deklaracja: @{ Layout = null; }
Jest to blok kodu, który będzie interpretowany przez silnik widoku Razor. To bardzo prosty przykład. Informujemy w ten sposób Razor, że nie będziemy korzystać ze strony wzorcowej. Zignorujmy Razor na moment. Zmodyfikuj plik Index.cshtml, dodając elementy zaznaczone pogrubieniem na listingu 2.4. Listing 2.4. Modyfikowanie kodu HTML widoku @{ Layout = null; }
42
ROZDZIAŁ 2. PIERWSZA APLIKACJA MVC
Indeks
Witaj, świecie (z widoku)
Wprowadzona zmiana powoduje wyświetlenie innego prostego komunikatu. Wybierz Start Debugging z menu Debuguj, aby uruchomić aplikację i przetestować nasz widok. Powinieneś zobaczyć ekran podobny do tego z rysunku 2.10.
Rysunek 2.10. Testowanie widoku Gdy na początku utworzyliśmy metodę akcji Index, zwracała ona wartość w postaci ciągu tekstowego. Oznaczało to, że aplikacja MVC nie robiła nic poza przekazaniem ciągu znaków do przeglądarki. Teraz, gdy metoda Index zwraca ViewResult, instruujemy aplikację MVC, aby wygenerowała widok i zwróciła kod HTML. Nie wskazujemy, który widok ma być użyty, więc do jego automatycznego wyszukania wykorzystywana jest konwencja nazewnictwa. Zgodnie z konwencją widok ma taką nazwę jak skojarzona metoda akcji i znajduje się w katalogu o nazwie kontrolera — ~/Views/Home/Index.cshtml. Poza tekstem oraz obiektem ViewResults możemy również zwracać inne wyniki z metod akcji. Jeżeli na przykład zwrócimy RedirectResult, przeglądarka wykona przekierowanie do innego adresu URL. Gdy zwrócimy HttpUnauthorizedResult, wymusimy operację zalogowania użytkownika. Obiekty te są nazywane wynikami akcji i wszystkie dziedziczą po klasie bazowej ActionResult. System wyników akcji pozwala hermetyzować często spotykane odpowiedzi i wielokrotnie używać ich w akcjach. Więcej informacji na ich temat i bardziej złożone przykłady użycia będą przedstawiane w kolejnych rozdziałach książki.
Dynamiczne dodawanie treści Oczywiście, głównym zadaniem platformy aplikacji sieciowych jest zapewnienie możliwości dynamicznego tworzenia i wyświetlania treści. W ASP.NET MVC zadaniem kontrolera jest skonstruowanie danych, a zadaniem widoku jest wygenerowanie kodu HTML. Dane są przekazywane z kontrolera do widoku. Jednym ze sposobów przekazania danych z kontrolera do widoku jest użycie obiektu ViewBag. Jest to składnik bazowej klasy Controller. ViewBag jest dynamicznym obiektem, do którego można przypisywać dowolne właściwości, udostępniając ich wartości w dowolnym generowanym następnie widoku. Na listingu 2.5 pokazane jest przekazywanie prostych danych dynamicznych w taki sposób. Listing 2.5. Ustawianie danych widoku using System; using System.Collections.Generic; using System.Linq;
43
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
using System.Web; using System.Web.Mvc; namespace PartyInvites.Controllers { public class HomeController : Controller { public ViewResult Index() { int hour = DateTime.Now.Hour; ViewBag.Greeting = hour < 17 ? "Dzień dobry" : "Dobry wieczór"; return View(); } } }
Dane są dostarczane widokowi poprzez przypisanie wartości właściwości ViewBag.Greeting. ViewBag to przykład obiektu dynamicznego, natomiast właściwość Greeting nie istnieje aż do chwili przypisania jej wartości. Dzięki temu dane z kontrolera do widoku można przekazywać w niezwykle elastyczny sposób bez konieczności wcześniejszego definiowania klas. Do właściwości ViewBag.Greeting odwołujemy się ponownie w widoku, ale tym razem w celu pobrania jej wartości, co zostało przedstawione na listingu 2.6. Zmiany wprowadzone w kodzie oznaczono pogrubioną czcionką. Listing 2.6. Pobieranie danych z ViewBag @{ Layout = null; } Indeks
@ViewBag.Greeting, świecie (z widoku)
Nowością na listingu 2.6 jest wyrażenie Razor. Podczas wywołania metody View w metodzie Index kontrolera platforma ASP.NET odszukuje plik widoku Index.cshtml i nakazuje silnikowi widoku Razor przetworzenie treści wymienionego pliku. Razor szuka wyrażeń, np. takich jak dodane na listingu, i przetwarza je. W omawianym przykładzie przetworzenie wyrażenia oznacza wstawienie do widoku wartości przypisanej właściwości ViewBag.Greeting. Nie ma nic specjalnego w nazwie właściwości Greeting — można ją zamienić na dowolną inną nazwę, a wynik będzie taki sam. Oczywiście, w ten sposób można przekazywać z kontrolera do widoku wiele wartości. Gdy ponownie uruchomisz projekt, możesz zobaczyć swój pierwszy dynamiczny widok MVC, pokazany na rysunku 2.11.
44
ROZDZIAŁ 2. PIERWSZA APLIKACJA MVC
Rysunek 2.11. Dynamiczna odpowiedź z MVC
Tworzenie prostej aplikacji wprowadzania danych W dalszej części tego rozdziału powiem więcej na temat podstawowych funkcji MVC i pokażę, jak zbudować prostą aplikację wprowadzania danych. Moim celem jest zademonstrowanie MVC w działaniu, więc pominę wyjaśnienia, jak funkcjonują stosowane mechanizmy. Bez obaw — omówię je dokładniej w dalszych rozdziałach.
Przygotowanie sceny Wyobraźmy sobie, że Twoja przyjaciółka organizuje przyjęcie sylwestrowe i poprosiła Cię o utworzenie witryny pozwalającej zaproszonym gościom na wysyłanie potwierdzeń przybycia. Poprosiła Cię o następujące cztery główne funkcje: stronę domową pokazującą informacje na temat przyjęcia, formularz, który może być używany do wysłania potwierdzenia, kontrolę poprawności formularza potwierdzenia, co pozwoli na wyświetlenie strony podziękowania, potwierdzenia wysyłane pocztą elektroniczną do gospodarza przyjęcia. W kolejnych punktach rozbudujemy projekt MVC utworzony na początku rozdziału i dodamy do niego wymienione funkcje. Możemy szybko zrealizować pierwszy element z listy przez zastosowanie przedstawionego już mechanizmu — wystarczy dodać kod HTML z listingu 2.7 do istniejącego widoku, a otrzymamy informacje o przyjęciu. Listing 2.7. Wyświetlanie informacji o przyjęciu @{ Layout = null; } Przyjęcie sylwestrowe
@ViewBag.Greeting, świecie (z widoku)
Zapraszamy na wspaniałe przyjęcie. (Do zrobienia: trzeba to ulepszyć, dodać zdjęcia i inne takie).
45
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Projekt jest rozpoczęty. Jeżeli uruchomimy aplikację, wyświetlą się informacje o przyjęciu — a właściwie wyświetli się miejsce na te informacje, ale przecież doskonale wiesz, o co chodzi (rysunek 2.12).
Rysunek 2.12. Dodawanie widoku HTML
Projektowanie modelu danych W nazwie architektury MVC litera M pochodzi od słowa model, najważniejszej części aplikacji. Model jest reprezentacją obiektów świata rzeczywistego, procesów i zasad kierujących modelowanymi obiektami, czyli domeną aplikacji. Model, nazywany często modelem domeny, zawiera obiekty C# (określane obiektami domeny), które tworzą jądro naszej aplikacji, a metody pozwalają nam manipulować tymi obiektami. Widoki i kontrolery w spójny sposób udostępniają domenę naszym klientom. Dobrze zaprojektowana aplikacja MVC zaczyna się od dobrze zaprojektowanego modelu, na którym się następnie opieramy, dodając kontrolery i widoki. Nie musimy wymagać zbyt wiele od modelu aplikacji PartyInvites, ale znajduje się tu jedna klasa domeny. Nazwiemy ją GuestResponse. Obiekt ten będzie odpowiedzialny za przechowywanie, kontrolę poprawności oraz potwierdzanie zaproszenia.
Dodawanie klasy modelu Zgodnie z konwencją MVC klasy składające się na model są umieszczane w katalogu ~/Models. Kliknij Models w oknie Eksplorator rozszerzenia i wybierz Dodaj, a następnie Klasa… z menu kontekstowego. Wpisz nazwę GuestResponse.cs i kliknij przycisk Dodaj. Zmień zawartość klasy, aby odpowiadała przedstawionej na listingu 2.8. Wskazówka Jeżeli nie możesz dodać klasy, to prawdopodobnie projekt jest aktualnie uruchomiony w Visual Studio. Pamiętaj, że Visual Studio nie pozwala na wprowadzanie zmian w uruchomionej aplikacji.
Listing 2.8. Klasa domeny GuestResponse namespace PartyInvites.Models { public class GuestResponse { public string Name { get; set; } public string Email { get; set; } public string Phone { get; set; } public bool? WillAttend { get; set; } } }
Wskazówka Być może zauważyłeś, że właściwość WillAtend jest typu bool, oznaczona jako nullable, co oznacza, że może przyjmować wartości true, false lub null. Powód zastosowania takiego typu wyjaśnimy w punkcie „Dodanie kontroli poprawności”, w dalszej części rozdziału.
46
ROZDZIAŁ 2. PIERWSZA APLIKACJA MVC
Łączenie metod akcji Jednym z celów naszej aplikacji jest dołączenie formularza RSVP (skrót ten pochodzi z języka francuskiego i oznacza prośbę o odpowiedź — potwierdzenie lub odrzucenie zaproszenia), więc potrzebujemy dodać do niego łącze w naszym widoku Index.cshtml, jak pokazano na listingu 2.9. Listing 2.9. Dodanie łącza do formularza RSVP @{ Layout = null; } Przyjęcie sylwestrowe
@ViewBag.Greeting, świecie (z widoku)
Zapraszamy na wspaniałe przyjęcie. (Do zrobienia: trzeba to ulepszyć, dodać zdjęcia i inne takie).
@Html.ActionLink("Wyślij RSVP", "RsvpForm")
Html.ActionLink jest metodą pomocniczą HTML. Platforma MVC zawiera zbiór wbudowanych metod pomocniczych, które są wygodnym sposobem generowania łączy HTML, pól tekstowych, pól wyboru, list, a nawet własnych kontrolek. Metoda ActionLink ma dwa parametry: pierwszym jest tekst do wyświetlenia w łączu, a drugim akcja wykonywana po kliknięciu łącza przez użytkownika. Pozostałe metody pomocnicze HTML przedstawię w rozdziałach od 19. do 21. Dodane przez nas łącze jest pokazane na rysunku 2.13.
Rysunek 2.13. Dodawanie łącza do widoku Jeżeli umieścisz kursor myszy na łączu w przeglądarce, zauważysz, że łącze wskazuje na adres http://naszserwer/Home/RsvpForm. Metoda Html.ActionLink przeanalizowała konfigurację routingu adresów URL i określiła, że /Home/RsvpForm jest prawidłowym adresem URL dla akcji o nazwie Rsvp w kontrolerze o nazwie HomeController. Zwróć uwagę, że w przeciwieństwie do tradycyjnych aplikacji ASP.NET adresy URL MVC nie odpowiadają fizycznym plikom. Każda metoda akcji posiada własny adres URL, a MVC korzysta z systemu routingu ASP.NET do przekształcenia tych adresów na akcje.
47
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Tworzenie metody akcji Gdy klikniesz nowe łącze, zobaczysz komunikat o błędzie 404. Dzieje się tak, ponieważ nie utworzyliśmy jeszcze metody akcji odpowiadającej adresowi URL /Home/RsvpForm. Zrealizujemy to, dodając metodę o nazwie RsvpForm do naszej klasy HomeController, która jest zamieszczona na listingu 2.10. Listing 2.10. Dodanie nowej metody akcji do kontrolera using using using using using
namespace PartyInvites.Controllers { public class HomeController : Controller { public ViewResult Index() { int hour = DateTime.Now.Hour; ViewData["greeting"] = hour < 17 ? "Dzień dobry" : "Dobry wieczór"; return View(); } public ViewResult RsvpForm() { return View(); } } }
Dodawanie widoku ściśle określonego typu Dodamy teraz widok dla naszej metody akcji RsvpForm, ale w nieco inny sposób — utworzymy widok ściśle określonego typu. Widok ściśle określonego typu jest przeznaczony do wizualizacji wartości określonego typu domeny i jeżeli określimy typ, na którym chcemy pracować (GuestResponse w tym przykładzie), platforma MVC będzie w stanie utworzyć kilka wygodnych skrótów, które ułatwią nam pracę. Ostrzeżenie Zanim zrobisz cokolwiek innego, upewnij się, że projekt MVC jest skompilowany. Jeżeli utworzyłeś klasę GuestResponse, ale nie skompilowałeś jej, MVC nie będzie w stanie utworzyć widoku ściśle określonego typu dla danego typu. Aby skompilować aplikację, wybierz Kompiluj rozwiązanie z menu Kompilacja w Visual Studio.
Kliknij prawym przyciskiem myszy wewnątrz metody akcji RsvpForm i z menu kontekstowego wybierz Dodaj widok…. W oknie dialogowym Dodaj widok zaznacz opcję Utwórz widok silnie typizowany i wybierz GuestResponse z rozwijanego menu Klasa modelu. Usuń zaznaczenie opcji Użyj układu lub strony wzorcowej i upewnij się, że wybrany jest silnik widoku Razor, a opcja Szablon scaffoldu ma wartość Empty (rysunek 2.14). Kliknij przycisk Dodaj, aby utworzyć nowy widok. Visual Studio utworzy nowy plik o nazwie RvspForm.cshtml i otworzy go do edycji. Domyślny kod wspomnianego pliku przedstawiono na listingu 2.11. Jak widać, jest to szkielet pliku HTML z wyrażeniem Razor @model. Jak pokażemy za moment, jest to klucz do widoku ściśle określonego typu i oferowanych przez niego udogodnień.
48
ROZDZIAŁ 2. PIERWSZA APLIKACJA MVC
Rysunek 2.14. Dodawanie widoku ściśle określonego typu Listing 2.11. Domyślny kod wygenerowany w pliku RsvpForm.cshtml @model PartyInvites.Models.GuestResponse @{ Layout = null; } RsvpForm
Budowanie formularza Teraz, gdy utworzyliśmy widok ściśle określonego typu, możemy zmodyfikować zawartość pliku RsvpForm.cshtml, budując formularz HTML do edycji obiektów GuestResponse. Umieść w widoku kod przedstawiony na listingu 2.12. Listing 2.12. Tworzenie widoku z formularzem @model PartyInvites.Models.GuestResponse @{ Layout = null; }
49
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
RSVP @using (Html.BeginForm()) {
Imię i nazwisko: @Html.TextBoxFor(x => x.Name)
Twój e-mail: @Html.TextBoxFor(x => x.Email)
Twój telefon: @Html.TextBoxFor(x => x.Phone)
Czy przyjdziesz na przyjęcie? @Html.DropDownListFor(x => x.WillAttend, new[] { new SelectListItem() {Text = "Tak, przyjdę.", Value = bool.TrueString}, new SelectListItem() {Text = "Nie, nie przyjdę.", Value = bool.FalseString} }, "Wybierz opcję")
}
Dla każdej właściwości klasy modelu GuestResponse używany metody pomocniczej HTML generującej odpowiednią kontrolkę. Metody te pozwalają na określenie właściwości, do której odnosi się element wejściowy, za pomocą wyrażenia lambda, takiego jak: @Html.TextBoxFor(x => x.Phone)
Metoda pomocnicza HTML TextBoxFor generuje HTML, który z kolei tworzy element wejściowy, ustawia parametr type na text, a atrybuty id oraz name na Phone — nazwę wybranej właściwości klasy domeny:
Ta wygodna funkcja działa dzięki zastosowaniu ściśle określonego typu widoku RsvpForm i wskazaniu typu GuestResponse jako typu wyświetlanego w tym widoku. Dlatego też metoda pomocnicza HTML dzięki wyrażeniu @model zna żądany przez nas typ danych dla odczytywanej właściwości. Nie przejmuj się, jeżeli nie znasz jeszcze wyrażeń lambda w języku C#. Ich omówienie znajduje się w rozdziale 4. Alternatywą użycia wyrażeń lambda jest odwołanie się do nazwy właściwości modelu za pomocą ciągu znaków w następujący sposób: @Html.TextBox("Email")
Zauważyłem, że korzystanie z wyrażeń lambda uniemożliwia błędne wpisanie nazwy właściwości typu modelu. Dzieje się tak dzięki mechanizmowi IntelliSense z Visual Studio wyświetlającemu listę, z której można wybrać odpowiednią właściwość (rysunek 2.15). Inną wygodną metodą pomocniczą jest Html.BeginForm, która generuje znacznik formularza HTML skonfigurowany do przesłania danych do metody akcji. Ponieważ nie przekazywaliśmy żądanych parametrów do metody pomocniczej, zakłada się, że chcemy przesłać dane do tego samego adresu URL. Przydatną sztuczką jest ujęcie całego formularza wewnątrz instrukcji using z C# w następujący sposób: @using (Html.BeginForm()) { ... tu zawartość formularza ...
Normalnie konstrukcja taka powoduje, że obiekt jest usuwany po wyjściu z zakresu. Jest ona często wykorzystywana do połączeń z bazami danych, dzięki czemu są one zamykane natychmiast po zakończeniu działania zapytania (to zastosowanie słowa kluczowego using różni się od udostępniania klas z przestrzeni nazw w zakresie klasy).
50
ROZDZIAŁ 2. PIERWSZA APLIKACJA MVC
Rysunek 2.15. IntelliSense w Visual Studio dla wyrażeń lambda w metodach pomocniczych HTML Zamiast usuwania obiektu metoda pomocnicza Html.BeginForm zamyka znacznik HTML formularza po wyjściu z zakresu. Oznacza to, że metoda pomocnicza Html.BeginForm tworzy obie części elementu form w następujący sposób:
Nie przejmuj się, jeżeli nie znasz mechanizmu usuwania obiektów w języku C#. Naszym celem jest pokazanie, jak można tworzyć formularze za pomocą metod pomocniczych HTML. Aby wyświetlić formularz z widoku RsvpForm, uruchom aplikację i kliknij łącze Wyślij RSVP. Wynik jest pokazany na rysunku 2.16.
Rysunek 2.16. Widok RsvpForm Uwaga Książka ta nie jest poświęcona CSS ani projektowaniu stron WWW. W większości przypadków będziemy tworzyć przykłady, których wygląd może wydawać się przestarzały (choć wolę nazywać go klasycznym, co jest mniej deprecjonujące). Widoki MVC generują czysty kod HTML i mamy pełną kontrolę nad układem elementów oraz przypisanymi im klasami, więc nie ma problemu z użyciem narzędzi projektowych lub gotowych szablonów, dzięki którym Twoje widoki będą po prostu piękne.
Obsługa formularzy Nie poinformowaliśmy jeszcze MVC, co należy zrobić z danymi formularza przesłanymi do serwera, dlatego kliknięcie przycisku Wyślij RSVP kasuje wartości wprowadzone do formularza. Dzieje się tak, ponieważ formularz wysyła dane do metody akcji RsvpForm w kontrolerze HomeController, który powoduje po prostu ponowne wygenerowanie widoku. 51
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Uwaga Możesz być zaskoczony tym, że wprowadzone dane są tracone przy powtórnym generowaniu widoku. Jeżeli tak się dzieje, prawdopodobnie tworzyłeś aplikację przy użyciu ASP.NET Web Forms, gdzie w takiej sytuacji dane są automatycznie zachowywane. Wkrótce pokażemy, jak osiągnąć ten sam efekt w MVC.
Aby odebrać i przetworzyć przesłane dane formularza, zastosujemy sprytną sztuczkę. Dodamy drugą metodę akcji RsvpForm, tworząc następującą parę: Metoda odpowiadająca na żądanie HTTP GET — żądanie GET jest generowane w momencie, gdy ktoś kliknie łącze. Ta wersja akcji będzie odpowiedzialna za wyświetlenie początkowego, pustego formularza, gdy ktoś pierwszy raz otworzy /Home/RsvpForm. Metoda odpowiadająca na żądanie HTTP GET — domyślnie formularze generowane za pomocą Html.BeginForm() są przesyłane przez przeglądarkę jako żądanie POST. Ta wersja akcji będzie odpowiedzialna za odebranie wysłanych danych i wykonanie na nich pewnych akcji. Obsługa żądań GET oraz POST w osobnych metodach C# pozwala utrzymać porządek w kodzie, ponieważ metody te mają inne przeznaczenie. Obie metody akcji są wywoływane z użyciem tego samego adresu URL, ale platforma MVC zapewnia wywołanie odpowiedniej metody w zależności od tego, czy obsługiwane jest żądanie GET, czy POST. Na listingu 2.13 przedstawione są zmiany, jakie należy zastosować w klasie HomeController. Listing 2.13. Dodawanie metody akcji obsługującej żądania POST using using using using using using
namespace PartyInvites.Controllers { public class HomeController : Controller { public ViewResult Index() { int hour = DateTime.Now.Hour; ViewData["greeting"] = hour < 17 ? "Dzień dobry" : "Dobry wieczór"; return View(); } [HttpGet] public ViewResult RsvpForm() { return View(); } [HttpPost] public ViewResult RsvpForm(GuestResponse guestResponse) { // do zrobienia: wyślij zawartość guestResponse do organizatora przyjęcia return View("Thanks", guestResponse); } } }
52
ROZDZIAŁ 2. PIERWSZA APLIKACJA MVC
Do istniejącej metody akcji RsvpForm dodaliśmy atrybut HttpGet. Informuje on platformę MVC, że metoda ta powinna być używana wyłącznie dla żądań GET. Następnie dodaliśmy przeciążoną wersję RsvpForm, która oczekuje parametru GuestResponse i ma dodany atrybut HttpPost. Atrybut ten informuje platformę MVC, że nowa metoda będzie obsługiwała żądania POST. Zwróć uwagę, że zaimportowaliśmy przestrzeń nazw PartyInvites.Models. Dzięki temu możemy odwołać się do typu GuestResponse bez konieczności podawania pełnej przestrzeni nazw w nazwie klasy. Sposób działania kodu po wprowadzonych modyfikacjach zostanie omówiony w kolejnych punktach.
Użycie dołączania modelu Pierwsza przeciążona wersja metody akcji RsvpForm generuje ten sam domyślny widok co poprzednio. Generuje formularz pokazany na rysunku 2.16. Druga przeciążona wersja jest bardziej interesująca. Jest ona wywoływana w odpowiedzi na żądanie HTTP POST, a typ GuestResponse jest klasą C#. W jaki sposób dane POST są połączone z tą klasą? Odpowiedzią jest dołączanie modelu, czyli niezwykle przydatna funkcja ASP.NET MVC, która zapewnia automatyczną analizę przychodzących danych i dzięki porównaniu par klucz-wartość żądania HTTP z nazwami właściwości oczekiwanego typu .NET wypełniane są właściwości typu modelu domeny. Proces ten jest przeciwieństwem użycia metod pomocniczych HTML — w czasie tworzenia wysyłanych do klienta danych formularza generujemy elementy wprowadzania danych, w których wartości atrybutów id oraz name są dziedziczone po nazwach właściwości klas modelu. Dla porównania — w przypadku dołączania modelu nazwy elementów wprowadzania danych są używane do ustawiania wartości właściwości w egzemplarzu klasy modelu, która jest z kolei przekazywana do metody akcji obsługującej żądania POST. Dołączanie modelu jest potężną i modyfikowalną funkcją, eliminującą konieczność ręcznego obsługiwania żądań HTTP i pozwalającą nam operować na obiektach C# zamiast na wartościach z tablic Request.Form[] oraz Request.QueryString[]. Obiekt GuestResponse przekazywany jako parametr naszej metody akcji jest automatycznie wypełniany danymi z pól formularza. Więcej informacji na temat tego mechanizmu, w tym o sposobach jego modyfikowania, można znaleźć w rozdziale 22.
Generowanie dowolnych widoków Druga wersja metody akcji RsvpForm pokazuje również, w jaki sposób w odpowiedzi na żądanie można wygenerować dowolny szablon widoku zamiast widoku domyślnego. Wiersz, o którym mówimy, to: return View("Thanks", guestResponse);
To wywołanie metody View informuje MVC o konieczności znalezienia widoku o nazwie Thanks i przekazania do niego obiektu GuestResponse. Aby utworzyć wskazany widok, kliknij prawym przyciskiem myszy wewnątrz metody w HomeController i wybierz Dodaj widok… z menu kontekstowego. Jako nazwę formularza podaj Thanks (rysunek 2.17). Aby utworzyć kolejny widok ściśle określonego typu, zaznacz odpowiednią opcję w oknie dialogowym Dodaj widok. Wybrana dla widoku klasa danych musi odpowiadać klasie przekazanej do widoku za pomocą metody View, więc z menu rozwijanego wybierz GuestResponse. Upewnij się, że opcja Użyj układu lub strony wzorcowej nie jest zaznaczona, że na liście Wyświetl aparat wybrana jest opcja Razor, a w rozwijanym menu Szablon scaffoldu ustawiona jest opcja Empty. Kliknij Dodaj, aby utworzyć nowy widok. Ponieważ widok jest skojarzony z kontrolerem Home, MVC tworzy widok ~/Views/Home/Thanks.cshtml. Zmodyfikuj kod nowo utworzonego pliku w taki sposób, aby jego zawartość odpowiadała przedstawionej na listingu 2.14. Kod, który trzeba dodać, oznaczono pogrubioną czcionką. Listing 2.14. Widok Thanks @model PartyInvites.Models.GuestResponse @{ }
Layout = null;
53
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Rysunek 2.17. Dodawanie widoku Thanks Dziękujemy
Dziękujemy, @Model.Name!
@if (Model.WillAttend == true) { @:Cieszymy się, że przyjdziesz do nas. Napoje są już w lodówce! } else { @:Przykro nam, że nie możesz się zjawić, ale dziękujemy za informację. }
Widok Thanks używa silnika Razor do wyświetlenia danych na podstawie wartości właściwości obiektu GuestResponse przekazanego do metody View w metodzie akcji RsvpForm. Operator @Model z Razor korzysta z typu
modelu domeny skojarzonego z silnie typowanym widokiem. Aby odwołać się do wartości właściwości w obiekcie domeny, korzystamy z Model.NazwaWłaściwości. Aby uzyskać na przykład wartość właściwości Name, używamy Model.Name. Nie przejmuj się, jeżeli składnia Razor nie ma dla Ciebie sensu — wyjaśnimy ją w rozdziale 5. Teraz, po utworzeniu widoku Thanks, mamy działający przykład. Uruchom aplikację w Visual Studio, kliknij łącze Wyślij RSVP, dodaj dane do formularza, a następnie kliknij przycisk Wyślij RSVP. Zobaczysz wynik pokazany na rysunku 2.18 (choć może być inny, jeżeli nie nazywasz się Paweł i nie możesz przyjść na przyjęcie).
Dodanie kontroli poprawności Jak można zauważyć, do tej pory nie wykonywaliśmy żadnej kontroli poprawności. Można wpisać dowolne dane w polu na adres e-mail, a nawet przesłać całkowicie pusty formularz.
54
ROZDZIAŁ 2. PIERWSZA APLIKACJA MVC
Rysunek 2.18. Wygenerowany widok Thanks W aplikacji MVC kontrola poprawności jest zwykle przeprowadzana w modelu domeny, a nie w interfejsie użytkownika. Oznacza to, że definiujemy kryteria kontroli poprawności w jednym miejscu i że działa ona wszędzie, gdzie użyta jest klasa modelu. ASP NET MVC obsługuje deklaratywne zasady kontroli poprawności definiowane za pomocą atrybutów z przestrzeni nazw System.ComponentModel.DataAnnotations. Na listingu 2.15 przedstawiony jest sposób zastosowania tych atrybutów w klasie modelu GuestResponse. Listing 2.15. Stosowanie kontroli poprawności w klasie modelu GuestResponse using System.ComponentModel.DataAnnotations; namespace PartyInvites.Models { public class GuestResponse { [Required(ErrorMessage = "Proszę podać swoje imię i nazwisko.")] public string Name { get; set; } [Required(ErrorMessage = "Proszę podać adres e-mail.")] [RegularExpression(".+\\@.+\\..+", ErrorMessage = "Proszę podać prawidłowy adres e-mail.")] public string Email { get; set; } [Required(ErrorMessage = "Proszę podać numer telefonu.")] public string Phone { get; set; } [Required(ErrorMessage = "Proszę określić, czy weźmiesz udział.")] public bool? WillAttend { get; set; } } }
Zasady poprawności są zaznaczone pogrubioną czcionką. Platforma MVC automatycznie wykrywa atrybuty kontroli poprawności i korzysta z nich do weryfikowania danych w procesie dołączania modelu. Zwróć uwagę, że zaimportowaliśmy przestrzeń nazw zawierającą atrybuty kontroli poprawności, dzięki czemu można się do nich odwoływać bez potrzeby stosowania nazw kwalifikowanych. Wskazówka Jak wcześniej wspomnieliśmy, dla właściwości WillAttend zastosowaliśmy odmianę nullable typu bool. Dzięki temu możemy zastosować atrybut Required. Jeżeli użylibyśmy zwykłego typu bool, wartość otrzymana poprzez dołączanie modelu mogłaby przyjmować wyłącznie wartość true lub false i nie bylibyśmy w stanie
55
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
stwierdzić, czy użytkownik faktycznie wybrał wartość. Typ nullable bool posiada trzy możliwe wartości: true, false oraz null. Wartość null jest wykorzystywana, jeżeli użytkownik nie wybrał wartości, i powoduje, że atrybut Required raportuje błąd weryfikacji.
Aby sprawdzić, czy wystąpiły problemy w procesie kontroli poprawności, korzystamy w klasie kontrolera z właściwości ModelState.IsValid. Na listingu 2.16 pokazuję, w jaki sposób należy to wykonać w metodzie akcji RsvpForm obsługującej żądania POST. Listing 2.16. Sprawdzanie błędów kontroli poprawności formularza ... [HttpPost] public ViewResult RsvpForm(GuestResponse guestResponse) { if (ModelState.IsValid) { // do zrobienia: wyślij zawartość guestResponse do organizatora przyjęcia return View("Thanks", guestResponse); } else { // błąd kontroli poprawności, więc ponownie wyświetlamy formularz wprowadzania danych return View(); } } ...
Jeżeli nie wystąpiły błędy weryfikacji, możemy poprosić platformę MVC o wygenerowanie widoku Thanks, tak jak poprzednio. Jeżeli pojawiły się błędy weryfikacji, generujemy widok RsvpForm przez wywołanie metody View bez parametrów. Wyświetlenie samego formularza w przypadku wystąpienia błędów nie jest zbyt użyteczne. Musimy wyświetlić użytkownikowi błędy kontroli poprawności i tym samym poinformować go o przyczynach odrzucenia wartości podanych w formularzu. Dlatego też zastosujemy w widoku RsvpForm metodę pomocniczą Html.ValidationSummary (listing 2.17). Listing 2.17. Użycie metody pomocniczej Html.ValidationSummary @model PartyInvites.Models.GuestResponse @{ Layout = null; } RsvpForm
Czy przyjdziesz na przyjęcie? @Html.DropDownListFor(x => x.WillAttend, new[] { new SelectListItem() {Text = "Tak, przyjdę.", Value = bool.TrueString}, new SelectListItem() {Text = "Nie, nie przyjdę.", Value = bool.FalseString} }, "Wybierz opcję")
}
Jeżeli nie wystąpiły błędy, metoda Html.ValidationSummary tworzy w formularzu ukryty element listy. Platforma MVC dodaje komunikaty o błędach zdefiniowane za pomocą atrybutów kontroli poprawności, a następnie powoduje, że lista staje się widoczna. Ten sposób działania jest przedstawiony na rysunku 2.19.
Rysunek 2.19. Podsumowanie weryfikacji danych Użytkownik nie zobaczy widoku Thanks, jeżeli nie będą spełnione wszystkie ograniczenia zdefiniowane w klasie GuestResponse. Zwróć uwagę, że dane wprowadzone do formularza zostały zachowane i ponownie pokazane, gdy widok się wyświetlił z dołączonym elementem podsumowania weryfikacji. Dzieje się tak dzięki dołączaniu modelu. Uwaga Jeżeli używałeś wcześniej platformy ASP.NET Web Forms, na pewno wiesz, że korzysta ona z „kontrolek serwerowych”, które zachowują swój stan przez serializowanie wartości i ich przechowywanie w ukrytym polu o nazwie __VIEWSTATE. Mechanizm dołączania modelu w ASP.NET MVC nie ma absolutnie nic wspólnego z koncepcją kontrolek serwerowych, przesyłów zwrotnych ani ViewState. ASP.NET MVC nigdy nie umieszcza ukrytego pola __VIEWSTATE w generowanych stronach HTML.
Wyróżnianie pól z błędami Wbudowane metody pomocnicze HTML odpowiedzialne za tworzenie pól tekstowych, list rozwijanych i innych mają jeszcze jedną przyjemną właściwość współdziałającą z dołączaniem modelu. Ten sam mechanizm, który pomaga metodom pomocniczym ponownie użyć wcześniej wprowadzonych wartości, może być również wykorzystywany do wyróżniania pól, w których wystąpił błąd kontroli poprawności.
57
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Gdy dla właściwości modelu klasy jest wykrywany błąd kontroli poprawności, metody pomocnicze HTML generują nieco inny kod HTML. Poniżej zamieszczony jest przykładowy kod HTML generowany przez wywołanie Html.TextBoxFor(x => x.Name) w przypadku braku błędu weryfikacji:
Poniżej natomiast znajduje się HTML wygenerowany przez to samo wywołanie, gdy użytkownik nie wpisał wartości (co jest błędem kontroli poprawności, ponieważ do właściwości Name w klasie modelu GuestResponse dodaliśmy atrybut Required):
Różnicę zaznaczono pogrubioną czcionką. Metoda pomocnicza dodała klasę CSS o nazwie input-validation-error. Możemy wykorzystać ten fakt i utworzyć arkusz stylów zawierający style
dla wymienionej klasy oraz dla innych klas stosowanych przez pozostałe metody pomocnicze HTML. Wedle konwencji stosowanej w projektach ASP.NET MVC, wszelka treść statyczna jest umieszczana w katalogu o nazwie Content. Możesz utworzyć wymieniony katalog, klikając prawym przyciskiem myszy projekt PartyInvites w oknie Eksploratora rozwiązania, a następnie wybierając opcję Dodaj/Nowy folder z menu kontekstowego. Aby utworzyć nowy styl, kliknij prawym przyciskiem myszy katalog Content, wybierz opcję Dodaj/Nowy element… z menu kontekstowego, a następnie Arkusz stylów w wyświetlonym oknie dialogowym. Nowo utworzonemu arkuszowi stylów nadajemy nazwę Site.css. Wymieniona nazwa jest używana przez Visual Studio podczas tworzenia projektu na podstawie szablonu innego niż Pusta. Zawartość arkusza stylów Site.css przedstawiono na listingu 2.18. Listing 2.18. Zawartość pliku arkusza stylów Content/Site.css .field-validation-error { color: #f00; } .field-validation-valid { display: none; } .input-validation-error { border: 1px solid #f00; background-color: #fee; } .validation-summary-errors { font-weight: bold; color: #f00; } .validation-summary-valid { display: none; }
Aby użyć tego arkusza stylów, musimy dodać nowe odwołanie do nagłówka widoku RsvpForm w postaci przedstawionej na listingu 2.19. Elementy link do widoku dodajesz w taki sam sposób jak do zwykłych statycznych plików HTML. Listing 2.19. Dodanie elementu link do widoku RsvpForm @model PartyInvites.Models.GuestResponse @{ Layout = null; }
Czy przyjdziesz na przyjęcie? @Html.DropDownListFor(x => x.WillAttend, new[] { new SelectListItem() {Text = "Tak, przyjdę.", Value = bool.TrueString}, new SelectListItem() {Text = "Nie, nie przyjdę.", Value = bool.FalseString} }, "Wybierz opcję")
}
Wskazówka Jeżeli korzystałeś z platformy ASP.NET MVC 3, prawdopodobnie spodziewałeś się konieczności dodania pliku CSS do widoku za pomocą atrybutu href, np.: @Href("~/Content/Site.css") lub @Href. Content("~/Content/Site.css"). W przypadku platformy ASP.NET MVC 4 silnik Razor automatycznie wykrywa atrybuty rozpoczynające się od ~/ i automatycznie wstawia wywołania @Href lub @Url.
Teraz, gdy użytkownik wyśle dane powodujące błąd kontroli poprawności, zobaczy jasno wyróżnioną przyczynę problemów (rysunek 2.20).
Rysunek 2.20. Automatyczne wyróżnianie błędów kontroli poprawności 59
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Kończymy Ostatnim wymaganiem względem omawianej tutaj przykładowej aplikacji jest wysłanie wypełnionego zgłoszenia RSVP do organizatora przyjęcia. Moglibyśmy to zrobić, dodając metodę akcji w celu utworzenia i wysłania wiadomości e-mail przy użyciu klas obsługi poczty elektronicznej dostępnych na platformie .NET. Zamiast tego wykorzystamy klasę pomocniczą WebMail. Nie wchodzi ona w skład platformy MVC, ale pozwoli nam dokończyć ten przykład bez wchodzenia w szczegóły konfiguracji kolejnych formularzy do wysyłania poczty. Uwaga Użyliśmy tu klasy pomocniczej WebMail, ponieważ pozwala ona zademonstrować wysyłanie wiadomości e-mail przy minimalnym wysiłku. Zwykle jednak umieszczamy tego typu funkcje w metodzie akcji. Powody wyjaśnimy przy opisie wzorca architektury MVC w rozdziale 3.
Chcemy, aby wiadomość e-mail została wysłana w czasie generowania widoku Thanks. Na listingu 2.20 pokazane są wymagane zmiany. Listing 2.20. Użycie klasy pomocniczej WebMail @model PartyInvites.Models.GuestResponse @{ Layout = null; } Dziękujemy @{ try { WebMail.SmtpServer = "smtp.przyklad.pl"; WebMail.SmtpPort = 587; WebMail.EnableSsl = true; WebMail.UserName = "nazwaUżytkownikaSMTP"; WebMail.Password = "hasłoUżytkownikaSMTP"; WebMail.From = "[email protected]"; WebMail.Send("[email protected]", "Powiadomienie RSVP", Model.Name + ((Model.WillAttend ?? false) ? "" : "nie") + "przyjdzie"); } catch (Exception) { @:Przepraszamy - nie możemy wysłać wiadomości RSVP. } }
Dziękujemy, @Model.Name!
@if (Model.WillAttend == true) { @:Cieszymy się, że przyjdziesz do nas. Napoje są już w lodówce!
60
ROZDZIAŁ 2. PIERWSZA APLIKACJA MVC
} else { @:Przykro nam, że nie możesz się zjawić, ale dziękujemy za informację. }
Dodaliśmy tu blok kodu Razor, który korzysta z klasy pomocniczej WebMail do skonfigurowania parametrów naszego serwera pocztowego, w tym nazwy serwera, do użycia połączenia szyfrowanego oraz konta użytkownika. Po podaniu tych wszystkich szczegółów zastosowaliśmy metodę WebMail.Send do wysłania wiadomości. Cały kod wysyłania poczty umieściliśmy w bloku try...catch, dzięki czemu będziemy mogli poinformować użytkownika, gdy wiadomość e-mail nie będzie mogła być wysłana. Jest to realizowane przez dodanie bloku tekstu do zawartości widoku Thanks. Lepszym rozwiązaniem jest wyświetlenie osobnego widoku błędu w przypadku problemów z wysłaniem wiadomości, ale w naszej pierwszej aplikacji MVC chcieliśmy zachować prostotę.
Podsumowanie W rozdziale tym utworzyliśmy nowy projekt MVC i użyliśmy go do skonstruowania prostej aplikacji MVC przeznaczonej do obsługi formularza, dzięki której mogłeś zapoznać się z architekturą i mechanizmami platformy MVC. Pominęliśmy kilka ważnych funkcji (w tym składnię silnika Razor, routing oraz zautomatyzowane testowanie), ale wrócimy do nich w kolejnych rozdziałach. W następnym rozdziale przedstawię architekturę MVC, wzorce projektowe i techniki, z których będziemy korzystać w całej książce.
61
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
62
ROZDZIAŁ 3.
Wzorzec MVC
W rozdziale 7. skupimy się na tworzeniu bardziej złożonych przykładów z użyciem ASP.NET. Zanim zagłębisz się w szczegółach platformy ASP.NET MVC, musisz poznać wzorzec projektowy MVC oraz powody jego stosowania. Po przeczytaniu tego rozdziału będziesz znał następujące zagadnienia: wzorzec architektury MVC, modele domeny i repozytoria, tworzenie luźno powiązanych systemów korzystających z kontenera wstrzykiwania zależności (DI), podstawy testowania automatycznego. Być może spotkałeś się z nimi wcześniej lub niektóre już dobrze znasz, szczególnie jeżeli używałeś zaawansowanych funkcji C# oraz ASP.NET. Jeśli nie, to zachęcam Cię do dokładnego przestudiowania tego rozdziału. Dobre zrozumienie zagadnień związanych z MVC pozwoli Ci efektywnie korzystać z dalszej części książki.
Historia MVC Termin model-widok-kontroler jest używany od końca lat 70. ubiegłego stulecia. Powstał w ramach projektu Smalltalk w Xerox PARC, gdzie oznaczał sposób organizowania wczesnych aplikacji GUI. Niektóre aspekty oryginalnego wzorca MVC są związane z koncepcjami języka Smalltalk, takimi jak ekrany i narzędzia, ale szersze koncepcje nadal dobrze pasują do aplikacji i szczególnie dobrze nadają się do aplikacji sieciowych. Interakcja z aplikacją MVC jest realizowana zgodnie z naturalnym cyklem akcji użytkownika i aktualizacji widoku, gdzie zakłada się bezstanowość widoku. Odpowiada to nieźle żądaniom i odpowiedziom HTTP, które są podstawą aplikacji sieciowej. Dodatkowo MVC wymusza separację zadań — model domeny oraz logika kontrolera są oddzielone od interfejsu użytkownika. W aplikacji sieciowej oznacza to, że operacje na HTML-u są oddzielone od reszty aplikacji, dzięki czemu jej utrzymanie i testowanie staje się prostsze. Środowisko Ruby on Rails doprowadziło do wznowienia zainteresowania wzorcem MVC. Od czasu jego udostępnienia powstało wiele innych platform MVC ujawniających zalety tego wzorca — w tym oczywiście ASP.NET MVC.
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Wprowadzenie do wzorca MVC Mówiąc najogólniej, wzorzec MVC oznacza, że nasza aplikacja będzie podzielona na co najmniej trzy osobne fragmenty: Modele, reprezentujące dane, które użytkownicy przeglądają lub modyfikują. Czasami korzystamy z prostych modeli widoku, które wyłącznie przechowują dane przesyłane pomiędzy kontrolerem a widokiem, a w innych przypadkach tworzymy bardziej zaawansowane modele domeny, które hermetyzują informacje, operacje i zasady rządzące tematem (domeną biznesową) naszej aplikacji. Widoki, które opisują sposób wyświetlania obiektów modelu w interfejsie użytkownika. Kontrolery, które obsługują przychodzące żądania, wykonują operacje na modelu domeny oraz wybierają widok do wyświetlenia użytkownikowi. Modele odzwierciedlają świat, w którym działa nasza aplikacja. Na przykład w aplikacji bankowej model domeny może reprezentować konta bankowe i limity kredytowe, może zawierać operacje takie jak przelew środków, a zasady mogą wymagać, aby konta pozostawały w swoich limitach kredytowych. W modelu muszą być również zachowane stan i spójność danych — na przykład wszystkie transakcje muszą być dodane do konta, klient nie może wypłacić więcej pieniędzy, niż ma prawo, ani więcej pieniędzy, niż posiada bank. Modele są definiowane poprzez operacje, za które nie są odpowiedzialne. Nie należy do nich generowanie interfejsu użytkownika ani przetwarzanie żądań — to zadania widoków i kontrolerów. Widoki zawierają logikę wymaganą do wyświetlenia elementów modelu użytkownikowi i nic więcej. Nie mają one bezpośredniego odwołania do modelu oraz nie komunikują się z modelem w żaden bezpośredni sposób. Kontrolery są łącznikami pomiędzy widokami i modelem. Żądania pochodzą od klienta i są obsługiwane przez kontroler, który wybiera odpowiedni widok do wyświetlenia i w razie potrzeby operację do wykonania na widoku. Każdy z elementów architektury jest dobrze zdefiniowany i niezależny, co jest odpowiedzią na rozdzielenie zadań. Logika manipulowania danymi w modelu znajduje się wyłącznie w modelu, logika wyświetlania wyłącznie w widoku, a kod obsługujący żądania klientów znajduje się wyłącznie w kontrolerze. Przez zachowanie jasnego rozdzielenia zadań nasza aplikacja będzie łatwiejsza do utrzymania oraz późniejszego rozszerzania, niezależnie od tego, jak bardzo się rozrośnie.
Budowa modelu domeny Najważniejszą częścią aplikacji MVC jest model domeny. Model tworzymy przez zidentyfikowanie encji ze świata rzeczywistego, operacji oraz zasad występujących w przemyśle lub aktywnościach, jakie będą wspierane przez naszą aplikację, co jest nazywane domeną. Następnie tworzymy programową reprezentację domeny — model domeny. Dla naszych celów model domeny jest zbiorem typów C# (klas, struktur itd.), nazywanych wspólnie typami domeny. Operacje na domenie są reprezentowane przez metody zdefiniowane w typach domeny, a zasady domeny są wyrażane poprzez logikę wewnątrz tych metod oraz, jak pokazaliśmy w poprzednim rozdziale, przez dodanie do metod atrybutów C#. Gdy tworzymy egzemplarz typu domeny w celu reprezentowania określonego fragmentu danych, tworzymy obiekt domeny. Modele domeny są zwykle trwałe i długowieczne. Istnieje wiele różnych sposobów osiągnięcia tego celu, ale najczęściej wybieranym jest użycie relacyjnej bazy danych. Krótko mówiąc, model domeny jest pojedynczą, autorytatywną definicją danych biznesowych i procesów w aplikacji. Trwały model domeny jest również autorytatywną definicją stanu reprezentacji naszej domeny. Podejście z użyciem modelu domeny rozwiązuje wiele problemów, jakie napotykamy przy stosowaniu wzorca Smart UI. Nasza logika biznesowa znajduje się w jednym miejscu. Jeżeli potrzebujemy wykonać manipulacje na danych z modelu lub dodać nowy proces albo zasadę, to model domeny jest jedyną zmienianą częścią aplikacji. Wskazówka Częstym sposobem na wymuszanie oddzielenia modelu domeny od reszty aplikacji ASP.NET MVC jest umieszczenie modelu w osobnym podzespole C#. Możemy dzięki temu utworzyć referencje do modelu domeny z innych części aplikacji i jednocześnie upewnić się, że nie istnieją odwołania w odwrotnym kierunku. Jest to szczególnie przydatne w dużych projektach. Podejście to zostanie zastosowane w przykładowej aplikacji, którą zaczniemy budować w rozdziale 7.
64
ROZDZIAŁ 3. WZORZEC MVC
Implementacja MVC w ASP.NET W ASP.NET MVC kontrolery są klasami C# zwykle dziedziczącymi po klasie System.Web.Mvc.Controller. Każda metoda publiczna w klasie dziedziczącej po Controller jest nazywana metodą akcji i jest skojarzona z adresem URL poprzez system routingu ASP.NET. Gdy żądanie jest wysyłane do adresu URL skojarzonego z metodą akcji, wykonywane są instrukcje z klasy kontrolera, które przeprowadzają operacje na modelu domeny, a następnie wybierają widok do pokazania klientowi. Na rysunku 3.1 przedstawiono interakcję pomiędzy kontrolerem, modelem i widokiem.
Rysunek 3.1. Interakcje w aplikacji MVC Platforma ASP.NET MVC oferuje możliwość wyboru silnika widoku. Wcześniejsze wersje platformy MVC korzystały ze standardowego silnika widoku ASP.NET, który przetwarza strony ASPX przy użyciu uproszczonej wersji znaczników z Web Forms. W MVC 3 został wprowadzony silnik widoku Razor, który korzysta z całkowicie innej składni (opisanej w rozdziale 5.). Visual Studio zapewnia obsługę IntelliSense dla obu silników, dzięki czemu można w łatwy sposób wstawiać i kontrolować dane przekazane przez kontroler. ASP.NET MVC nie narzuca żadnych ograniczeń dotyczących implementacji modelu domeny. Można tworzyć model przy użyciu zwykłych obiektów C# i implementować mechanizmy trwałości z wykorzystaniem dowolnych baz danych, bibliotek ORM lub innych narzędzi danych obsługiwanych przez .NET. Visual Studio tworzy katalog /Models w czasie przetwarzania szablonu projektu MVC. Jest to wystarczające w odniesieniu do prostych projektów, ale w przypadku bardziej złożonych aplikacji zwykle definiuje się modele w osobnym projekcie Visual Studio. Implementację modelu domeny omówimy w dalszej części tego rozdziału.
Porównanie MVC z innymi wzorcami MVC nie jest oczywiście jedynym wzorcem architektury oprogramowania. Istnieje wiele innych, z których część jest lub była niezwykle popularna. Na temat MVC można się wiele dowiedzieć, patrząc na inne wzorce. W kolejnych punktach krótko przedstawię różne podejścia do budowania aplikacji i porównam je z MVC. Niektóre z tych wzorców są bliskimi odmianami MVC. Nie sugeruję, że MVC jest doskonałym wzorcem we wszystkich sytuacjach. Jestem zwolennikiem wybierania najlepszego podejścia do rozwiązania konkretnego problemu. Jak będzie można zauważyć, istnieją sytuacje, w których konkurencyjne wzorce są równie użyteczne jak MVC lub nawet lepsze. Zachęcam do podejmowania rozważnego wyboru wzorca. Już fakt, że czytasz tę książkę, sugeruje, że przekonałeś się do wzorca MVC, ale uważasz, że zawsze lepiej jest zdobyć możliwie szeroką perspektywę.
Przedstawiam wzorzec Smart UI Jednym z najczęściej stosowanych wzorców projektowych jest smart UI. Większość programistów tworzyło aplikacje smart UI w pewnym punkcie swojej kariery — ja oczywiście też. Również Windows Forms oraz ASP.NET Web Forms korzystają z tego wzorca. Aby zbudować aplikację smart UI, programiści tworzą interfejs użytkownika, zwykle przeciągając zestaw komponentów lub kontrolek na obszar projektowania. Kontrolki raportują interakcje z użytkownikiem przez generowanie zdarzeń dla kliknięcia przycisków, naciśnięcia klawiszy, przesunięcia myszy itd. Programista dodaje kod odpowiadający na te zdarzenia w metodach obsługi zdarzeń będących niewielkimi blokami kodu wywoływanymi w momencie wygenerowania zdarzenia w komponencie. W ten sposób otrzymujemy monolityczną
65
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
aplikację, której schemat jest pokazany na rysunku 3.2. Kod obsługujący interfejs użytkownika oraz kod obsługujący zasady biznesowe są ze sobą wymieszane bez stosowania zasady separacji zadań. Kod ten definiuje akceptowalne wartości dla wprowadzanych danych, wykonuje zapytania na danych lub modyfikuje konto użytkownika i przeprowadza wiele innych operacji w niewielkich, połączonych ze sobą fragmentach wykonywanych w kolejności wywoływania zdarzeń.
Rysunek 3.2. Wzorzec smart UI Największą wadą tego projektu są problemy z jego utrzymaniem i rozszerzaniem. Mieszanie modelu domeny i kodu logiki biznesowej z kodem interfejsu użytkownika powoduje powstawanie powtórzeń, w których ten sam fragment logiki biznesowej jest kopiowany i wklejany do nowych komponentów. Wyszukanie wszystkich powtórzonych fragmentów i ich wyodrębnienie może być trudne. W złożonej aplikacji smart UI niemal niemożliwe jest dodanie nowych funkcji bez uszkodzenia istniejących. Testowanie aplikacji smart UI może być skomplikowane. Jedynym sposobem jest symulowanie interakcji z użytkownikiem, co jest dalekie od ideału, ponieważ zapewnienie pełnego pokrycia testami jest trudne. W świecie MVC wzorzec smart UI jest często nazywany antywzorcem — czymś, co powinno być unikane za wszelką cenę. Antypatia ta powstała po części dlatego, że programiści szukali w MVC alternatywy, ponieważ czuli, że nie warto poświęcać części swojej kariery na tworzenie i utrzymanie aplikacji smart UI. Dotyczy to również mnie; noszę piętno tych długich lat, ale nie odrzucam całkowicie wzorca smart UI. Nie wszystko jest w nim złe, zauważam także pozytywne aspekty. Aplikacje smart UI można tworzyć bardzo szybko i bez trudu. Producenci komponentów oraz narzędzi projektowania włożyli dużo pracy w ułatwienie tworzenia aplikacji i nawet najmniej doświadczony programista może w kilka godzin wyprodukować profesjonalnie wyglądającą i względnie funkcjonalną aplikację. Największa słabość aplikacji smart UI — problemy z jej obsługą — nie występuje w małych projektach. Jeżeli zamierzasz wytworzyć proste narzędzie dla niewielkiej grupy odbiorców, aplikacja smart UI może być doskonałym zadaniem. Dodatkowa złożoność aplikacji MVC nie ma tu uzasadnienia. Oprócz tego smart UI doskonale nadaje się do prototypowania interfejsu użytkownika — te narzędzia wizualne są naprawdę dobre. Jeżeli pracujesz z klientem i chcecie nakreślić wymagania dotyczące wyglądu i przepływu sterowania w interfejsie, użycie narzędzi smart UI może być dobrym sposobem testowania różnych pomysłów.
Architektura model-widok W przypadku aplikacji smart UI obszarem, w którym zwykle powstają problemy, jest logika biznesowa, nierzadko stająca się tak rozproszona w aplikacji, że wprowadzanie zmian lub dodawanie funkcji staje się trudnym procesem. Usprawnieniem w tym zakresie może być zastosowanie architektury model-widok, w której logika biznesowa jest wyodrębniona w osobnym modelu domeny. W ten sposób dane, procesy oraz zasady są skoncentrowane w jednej części aplikacji (rysunek 3.3).
Rysunek 3.3. Wzorzec model-widok Architektura model-widok jest znacznym usprawnieniem w stosunku do monolitycznego wzorca smart UI. Jest na przykład łatwiejsza w obsłudze. Jednak powstają dwa problemy. Pierwszy wynika z faktu, że interfejs
66
ROZDZIAŁ 3. WZORZEC MVC
użytkownika oraz model domeny są ze sobą ściśle zintegrowane, co powoduje, że trudno jest wykonywać testy jednostkowe pojedynczego komponentu. Drugi wynika z praktyki, a nie z definicji wzorca. Model zwykle zawiera dużo kodu dostępu do danych (nie musi oczywiście tak być), przez co nie zawiera wyłącznie danych biznesowych, operacji i zasad.
Klasyczna architektura trójwarstwowa Aby rozwiązać problemy dotyczące architektury model-widok, powstał wzorzec architektury trójwarstwowej, w której kod obsługi trwałości jest oddzielony od modelu domeny i znajduje się w osobnym komponencie, nazywanym warstwą dostępu do danych (DAL). Wzorzec ten pokazano na rysunku 3.4.
Rysunek 3.4. Wzorzec architektury trójwarstwowej Jest to duży krok naprzód. Architektura trójwarstwowa jest najczęściej wykorzystywanym wzorcem dla aplikacji biznesowych. Nie narzuca ograniczeń na implementację interfejsu użytkownika i zapewnia dobrą separację zadań, bez wprowadzania zbytnich komplikacji. Przy odrobinie uwagi warstwa DAL może być zdefiniowana tak, że testowanie jednostkowe będzie względnie proste. Można wskazać oczywiste podobieństwa pomiędzy klasyczną aplikacją trójwarstwową a opartą na wzorcu MVC. Różnica powstaje w przypadku, gdy warstwa interfejsu użytkownika jest bezpośrednio związana z biblioteką GUI działającą na podstawie zdarzeń (taka jak Windows Forms lub ASP.NET Web Forms), ponieważ niemal niemożliwe staje się wykonywanie testów jednostkowych. Interfejs użytkownika aplikacji trójwarstwowej może być bardzo złożony, zatem powstaje wiele kodu, który nie jest rygorystycznie przetestowany. W najgorszym scenariuszu brak wymuszania dyscypliny w warstwie interfejsu powoduje, że aplikacja trójwarstwowa zostaje zdegradowana do odpychającej aplikacji smart UI, nieposiadającej prawdziwej separacji zadań. Powstaje wtedy najgorszy możliwy wynik: niedająca się testować i trudna w obsłudze aplikacja, która jest nadmiernie złożona.
Odmiany MVC Przedstawiłem już podstawowe zasady budowy aplikacji MVC, szczególnie w odniesieniu do ich implementacji za pomocą ASP.NET MVC. Pojawiły się również inne interpretacje tego wzorca, w których architektura została rozszerzona, skorygowana lub w inny sposób dostosowana do określonego zakresu i tematu projektu. W kolejnych punktach krótko omówię dwie najbardziej znane odmiany architektury MVC. Zapoznanie się z tymi odmianami nie jest konieczne do pracy z ASP.NET MVC. Dodałem je, aby omówienie było kompletne.
Architektura model-widok-prezenter Architektura model-widok-prezenter (MVP) jest odmianą MVC, która nieco lepiej pasuje do platform GUI zachowujących stan, takich jak Windows Forms lub ASP.NET Web Forms. Jest to wartościowa próba wykorzystania najlepszych aspektów wzorca smart UI i uniknięcia związanych z nim problemów. W tym wzorcu prezenter ma takie same zadania jak kontroler MVC, ale wchodzi w ściślejszą relację z widokiem zachowującym stan i bezpośrednio zarządza wartościami wyświetlanymi w komponentach UI, zgodnie z danymi wprowadzanymi przez użytkownika oraz podejmowanymi przez niego akcjami. Istnieją dwie implementacje tego wzorca: Pasywna implementacja widoku, w której widok nie zawiera logiki. W kontenerze są kontrolki UI bezpośrednio manipulowane przez prezenter. Implementacja z kontrolerem nadzorującym, w której widok może być odpowiedzialny za część logiki prezentacji, takiej jak dołączanie danych na podstawie przekazanych mu źródeł danych w modelu. 67
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Różnica pomiędzy tymi dwoma podejściami odnosi się do stopnia inteligencji widoku. W obu przypadkach prezenter jest oddzielony od technologii GUI, więc jego logika jest prostsza i nadaje się do testowania jednostkowego.
Architektura model-widok-widok-model Architektura model-widok-widok-model (MVVM) jest najnowszą odmianą MVC. Powstała w roku 2005 w firmie Microsoft, w zespole pracującym nad technologią, która jest stosowana teraz w Windows Presentation Foundation (WPF) oraz Silverlight. W MVVM modele i widoki mają te same zadania co ich odpowiedniki w MVC. Różnicą jest koncepcja modelu widoku, który stanowi abstrakcyjną reprezentację interfejsu użytkownika. Model widoku jest najczęściej klasą C#, która udostępnia właściwości dla danych wyświetlanych w interfejsie oraz operacje na tych danych, które mogą być wywołane z interfejsu. W przeciwieństwie do kontrolerów w MVC lub prezenterów w MVP model widoku MVVM nie ma informacji na temat widoku (ani żadnej specyficznej technologii UI). Zamiast tego widok MVVM korzysta z funkcji dołączania, zapewnianej przez WCF i Silverlight, która łączy właściwości widoku (czyli listy rozwijane lub efekty kliknięcia przycisku) z właściwościami udostępnianymi przez model widoku. Cały wzorzec MVVM jest ściśle związany z mechanizmem dołączania z WCF, więc nie zawsze ma sens stosowanie go w innych technologiach. Uwaga W MVC również wykorzystywany jest termin model widoku, ale określa on prostą klasę modelu, która jest używana wyłącznie do przekazania danych z kontrolera do widoku. Odróżniamy modele widoku od modelu domeny, który jest złożoną reprezentacją danych, operacji i zasad.
Modelowanie domeny Już wcześniej wyjaśniłem, że sensowne jest użycie obiektów ze świata rzeczywistego oraz procesów i zasad z dziedziny oprogramowania i umieszczenie ich w komponencie nazywanym modelem domeny. Komponent ten jest sercem naszego oprogramowania. Wszystko inne, w tym kontrolery i widoki, są tylko szczegółami technicznymi pozwalającymi na interakcję z modelem domeny. ASP.NET MVC nie wymusza na nas użycia żadnej specyficznej technologii modelowania domeny. Polega na rozwiązaniach odziedziczonych po platformie .NET oraz jej ekosystemu. Jednak istnieją infrastruktura oraz konwencje pomagające połączyć klasy modelu z kontrolerami, widokami i samą biblioteką MVC. Wykorzystywane są trzy podstawowe funkcje: Dołączanie modelu jest mechanizmem bazującym na konwencji, który pozwala automatycznie wypełniać obiekty modelu danymi przychodzącymi, zwykle pochodzącymi z wywołania POST. Metadane modelu pozwalają na opisywanie znaczenia klas modelu. Na przykład można dostarczyć zrozumiałe dla człowieka opisy właściwości lub używać podpowiedzi na temat tego, co powinno być wyświetlane. Platforma MVC może następnie automatycznie wygenerować interfejs użytkownika dla klas modelu w widoku. Kontrola poprawności jest realizowana w czasie dołączania i stosowania zasad zdefiniowanych jako metadane. Wspominałem już o dołączaniu modelu oraz kontroli poprawności przy opisie naszej pierwszej aplikacji w rozdziale 2. — wrócimy do dokładniejszego opisu tych zagadnień w rozdziałach 22. oraz 23. Odłóżmy na razie implementację architektury MVC w ASP.NET i zajmijmy się modelowaniem domeny jako osobnym zadaniem. Utworzymy prosty model domeny za pomocą .NET i SQL Server, przy użyciu kilku podstawowych technik programowania sterowanego domeną (DDD).
68
ROZDZIAŁ 3. WZORZEC MVC
Przykładowy model domeny Prawdopodobnie uczestniczyłeś już w burzy mózgów przy tworzeniu modelu domeny dla wcześniejszych projektów. Zwykle bierze w niej udział co najmniej jeden programista i co najmniej jeden ekspert biznesowy. Zużywają oni ogromne ilości kawy, ciastek i pisaków do tablicy. Po pewnym czasie powstaje pierwszy szkic modelu domeny. (W naszym opisie procesu tworzenia szkicu modelu domeny pominęliśmy wiele godzin kłótni i dyskusji, które na tym etapie wydają się nieuniknione. Wystarczy wspomnieć, że programiści poświęcają kilka pierwszych godzin na kwestionowanie funkcji proponowanych przez ekspertów biznesowych, które są wzięte bezpośrednio z literatury fantastycznonaukowej, natomiast eksperci biznesowi wyrażają w tym czasie swoje zaskoczenie i obawy co do oszacowania czasu i kosztów aplikacji, które zbliżają się do tych, które NASA wzięłaby pod uwagę przy planowaniu podróży na Marsa. Do rozwiązania takich patowych konfliktów niezbędna jest kawa. W końcu udaje się osiągnąć kompromis, ponieważ nadmiar płynów powoduje, że wszyscy chcą zakończyć spotkanie.) Jeżeli chcemy na przykład utworzyć aplikację aukcyjną, możemy zacząć od diagramu pokazanego na rysunku 3.5.
Rysunek 3.5. Pierwszy szkic modelu domeny dla systemu aukcyjnego Diagram ten wskazuje, że model domeny składa się ze zbioru użytkowników (Member), którzy zawierają zbiór ofert (Bid), a każda oferta dotyczy jednego przedmiotu (Item). Przedmiot może zawierać wiele ofert od różnych użytkowników.
Wspólny język Najważniejszą zaletą implementacji modelu domeny jako osobnego komponentu jest możliwość projektowania go z użyciem dowolnego języka i dowolnej terminologii. Warto opracować terminologię dla encji, operacji i relacji, mającą sens nie tylko dla programistów, ale również dla ekspertów biznesowych (ekspertów domeny). Być może wybrałbyś terminy użytkownicy i role, ale Twoim ekspertom domeny bardziej odpowiadałyby słowa agenci i zezwolenia. Nawet jeżeli modelujesz koncepcje, dla których eksperci domeny nie mają odpowiedniego określenia, to model stanowi porozumienie dotyczące wspólnego języka, bez którego nie możemy być pewni, że modelujemy procesy i relacje, o których myśleli eksperci domeny. Dlaczego ten „wspólny język” jest tak wartościowy? Programiści w naturalny sposób używają języka kodu programu (nazwy klas, tabele baz danych itp.). Eksperci biznesowi nie rozumieją tych terminów, ale i nie jest to konieczne. Ekspert biznesowy z niewielką wiedzą techniczną jest bardzo niebezpieczny, ponieważ stale dopasowuje on wymagania do swojej znajomości możliwości technologii. Jeżeli tak się stanie, nigdy naprawdę nie zrozumiemy, czego od nas wymagają użytkownicy. Podejście to pomaga uniknąć nadmiernej generalizacji w oprogramowaniu. My, programiści, mamy tendencję do modelowania nie tylko określonej rzeczywistości biznesowej, ale każdej możliwej rzeczywistości. W przykładzie z aukcją może to być zastępowanie słów użytkownik i przedmiot ogólniejszym słowem zasób, połączonych nie za pomocą ofert, ale relacji. Jeżeli nie ograniczymy modelu domeny w ten sam sposób, w jaki działa określony biznes, odrzucamy wszystkie znane sposoby jego działania i w przyszłości będziemy mieli problemy z zaimplementowaniem funkcji, które będą wyglądać jak dziwne przypadki specjalne w naszym eleganckim metaświecie. Ograniczenia są w rzeczywistości wiedzą na temat domeny. Połączenie pomiędzy wspólnym językiem i modelem domeny nie może być płytkie. Eksperci DDD twierdzą, że jakakolwiek zmiana we wspólnym języku jest zmianą w modelu. Jeżeli pozwolimy na rozejście się naszej wiedzy na temat domeny biznesowej z istniejącym kodem modelu, utworzymy język pośredni, który odwzorowuje
69
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
model na domenę, co w dłuższym okresie wróży katastrofę. Doprowadzimy tylko do powstania specjalnej klasy osób, które potrafią komunikować się w obu językach, i to one zaczną dobierać wymagania na podstawie niekompletnej wiedzy na temat tych języków.
Agregaty i uproszczenia Spójrzmy jeszcze raz na przykładowy diagram modelu aukcyjnego (rysunek 3.5). Jak się okazuje, nie oferuje on zbyt dużo pomocy, jeżeli zaczniemy go implementować z użyciem C# i SQL Server. Jeżeli załadujemy obiekt użytkownika do pamięci, to czy powinniśmy również załadować wszystkie jego oferty oraz wszystkie przedmioty skojarzone z tymi przedmiotami i wszystkie inne oferty dla tych przedmiotów i użytkowników, którzy złożyli te oferty? Gdy coś usuniemy, to jak głęboko powinniśmy wykonać kaskadowe usuwanie w grafie obiektów? Jeżeli chcemy dodać zasady poprawności, które będą rządziły relacjami pomiędzy obiektami, to gdzie należy umieścić te role? Jeżeli zamiast użycia relacyjnej bazy danych zdecydujemy się na użycie bazy dokumentów, to które grupy obiektów będą stanowiły jeden element? Nie wiemy tego, a nasz model domeny nie daje nam żadnych odpowiedzi. Sposobem stosowanym w DDD na zmniejszenie tej złożoności jest zorganizowanie encji domeny w grupy nazywane agregatami. Na rysunku 3.6 pokazane jest użycie agregatu w przykładzie modelu systemu aukcyjnego.
Rysunek 3.6. Model systemu aukcyjnego z agregatami Agregaty grupują w sobie kilka obiektów modelu domeny. Każdy agregat posiada główną encję definiującą cały agregat, która działa jako „szef” agregatu podczas kontroli poprawności i jej zapisywania. Agregat jest pojedynczym elementem w przypadku zmiany danych, więc należy wybierać agregaty, które pozostają w logicznych relacjach w rzeczywistych procesach biznesowych, oraz tworzyć operacje, które logicznie odpowiadają rzeczywistym procesom biznesowym. Innymi słowy, powinniśmy tworzyć agregaty przez łączenie obiektów, które są jednocześnie modyfikowane. Kluczowa zasada DDD mówi, że obiekty poza agregatem mogą jedynie zawierać trwałe referencje do głównej encji, a nie do encji wewnątrz agregatu (faktycznie wartości identyfikatorów dla encji innych niż główna nie muszą być unikatowe poza agregatem, a w bazie dokumentów nie muszą nawet posiadać identyfikatorów). Zasada ta powoduje, że obiekty wewnątrz agregatu są traktowane jako jeden element. W tym przykładzie użytkownicy (Members) i przedmioty (Items) są encjami głównymi agregatów, ponieważ muszą być niezależnie dostępne, natomiast oferty (Bids) są interesujące wyłącznie w kontekście przedmiotu. Oferty mogą przechowywać odwołania do użytkowników (będących encjami głównymi), ale użytkownicy nie odwołują się bezpośrednio do ofert (niebędących encjami głównymi), ponieważ spowodowałoby to naruszenie granicy agregatu przedmiotów.
70
ROZDZIAŁ 3. WZORZEC MVC
Jedną z zalet agregatów jest uproszczenie relacji pomiędzy obiektami w modelu domeny. Agregaty często dają nam również dodatkową wiedzę na temat modelowanej domeny. Tworzenie agregatów ogranicza relacje między obiektami modelu domeny, dzięki czemu przypominają one bardziej relacje występujące w rzeczywistej domenie. Na listingu 3.1 przedstawiony jest nasz model domeny wyrażony w postaci kodu C#. Listing 3.1. Model domeny aukcji zapisany w C# public class Member { public string LoginName { get; set; } // klucz unikatowy public int ReputationPoints { get; set; } } public class Item { public int ItemID { get; private set; } // klucz unikatowy public string Title { get; set; } public string Description { get; set; } public DateTime AuctionEndDate { get; set; } public IList Bids { get; set; } } public class Bid { public Member Member { get; set; } public DateTime DatePlaced { get; set; } public decimal BidAmount { get; set; } }
Zwróć uwagę, że jesteśmy w stanie oddać jednokierunkową naturę relacji pomiędzy ofertami (Bid) i użytkownikami (Member). Mogliśmy również zamodelować kilka innych ograniczeń. Zauważ też, że klasa Bid jest niezmienna (co odzwierciedla często stosowaną w aukcjach zasadę, że po złożeniu oferty nie może być ona modyfikowana). Zastosowanie agregacji pozwoliło nam na utworzenie użyteczniejszego i dokładniejszego modelu domeny, który można łatwo reprezentować w C#. Agregaty pozwalają dodać do modelu domeny pewną strukturę oraz zwiększyć jego dokładność. Umożliwiają łatwiejsze kontrolowanie poprawności (obiekt główny agregatu może sprawdzić poprawność całego agregatu) i są oczywistą jednostką zapisu. Ponieważ agregaty są atomowymi elementami modelu domeny, stają się również jednostkami zarządzania transakcjami oraz usuwania kaskadowego w bazach danych. Z drugiej strony dodają one ograniczenia, które czasami wydają się sztuczne — ponieważ często są sztuczne. Agregaty występują naturalnie w bazach danych dokumentów, ale nie są koncepcją z SQL Server ani większości narzędzi ORM, więc aby je poprawnie zaimplementować, zespół potrzebuje dyscypliny i efektywnej komunikacji.
Definiowanie repozytoriów Wcześniej czy później musimy pomyśleć o umieszczeniu naszych obiektów domeny w systemie przechowywania danych. Zwykle realizuje się to przez użycie relacyjnej czy obiektowej bazy danych albo bazy dokumentów. Przechowywanie danych nie jest częścią naszego modelu domeny. Jest zadaniem niezależnym lub inaczej zadaniem ortogonalnym w kontekście separacji zadań. Oznacza to, że nie chcemy mieszać kodu przechowywania danych z kodem modelu domeny. Standardową metodą zachowania tej separacji pomiędzy modelem domeny a systemem przechowywania jest zdefiniowanie repozytoriów. Nie są one niczym więcej jak obiektową reprezentacją bazy danych (lub magazynu bazującego na plikach), danych pobieranych za pomocą usługi WWW bądź dowolnego innego mechanizmu. Zamiast pracować bezpośrednio na bazie danych, model domeny wywołuje metody zdefiniowane w repozytorium, które z kolei wykonują odwołania do bazy danych w celu zapisania lub pobrania danych modelu. Pozwala to odizolować model od implementacji trwałości. Często stosuje się konwencję, zgodnie z którą definiujemy osobne modele danych dla każdego agregatu, ponieważ agregaty są naturalną jednostką trwałości. W przypadku naszych aukcji możemy utworzyć dwa repozytoria — jedno dla obiektów Member i jedno dla Item (zwróć uwagę, że nie potrzebujemy repozytorium
71
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
dla obiektów Bid, gdyż oferty będą zapisywane w ramach agregatu Item). Przykład definicji takich repozytoriów jest przedstawiony na listingu 3.2. Listing 3.2. Klasy C# repozytoriów dla klas domeny Member oraz Item public class MembersRepository { public void AddMember(Member member) { /* napisz mnie */ } public Member FetchByLoginName(string loginName) { /* napisz mnie */ } public void SubmitChanges() { /* Napisz mnie */ } } public class ItemsRepository { public void AddItem(Item item) { /* napisz mnie */ } public Item FetchByID(int itemID) { /* napisz mnie */ return null; } public IList ListItems(int pageSize,int pageIndex) { /* napisz mnie */ return null; } public void SubmitChanges() { /* napisz mnie */ } }
Zwróć uwagę, że te repozytoria zajmują się wyłącznie wczytywaniem i zapisywaniem danych; nie zawierają one żadnej logiki domeny. Aby dokończyć klasy repozytoriów, należy dodać do nich kod wykonujący operacje zapisu i odczytu danych, zgodne z wybranym mechanizmem przechowywania. W rozdziale 7. rozpoczniemy budowanie bardziej złożonej i realistycznej aplikacji MVC i pokażemy wtedy, jak użyć Entity Framework do implementacji naszych repozytoriów.
72
ROZDZIAŁ 3. WZORZEC MVC
Budowanie luźno połączonych komponentów Jak wspomniałem, jedną z najważniejszych zasad wzorca MVC jest separacja zadań. Chcemy, aby komponenty naszej aplikacji miały możliwie niewiele zależności, którymi będziemy musieli zarządzać. W idealnej sytuacji każdy komponent nie „wie” nic o innych komponentach i współpracuje z innymi obszarami aplikacji jedynie za pośrednictwem abstrakcyjnych interfejsów. Jest to nazywane luźnym powiązaniem; zasada ta ułatwia testowanie i modyfikowanie aplikacji. Przedstawię prosty przykład. Jeżeli piszemy komponent o nazwie MyEmailSender, którego zadaniem jest wysyłanie poczty elektronicznej, powinniśmy utworzyć interfejs IEmailSender, definiujący wszystkie publiczne funkcje wymagane do wysyłania poczty. Każdy komponent w naszej aplikacji, który powinien wysłać e-mail — na przykład klasa do resetowania hasła o nazwie PasswordResetHelper — może wysłać wiadomość, odwołując się wyłącznie do metod tego interfejsu. Jak pokazano na rysunku 3.7, nie istnieje bezpośrednia zależność pomiędzy PasswordResetHelper a MyEmailSender.
Rysunek 3.7. Użycie interfejsów do rozdzielania komponentów Przez wprowadzenie interfejsu IEmailSender zapewniamy, że nie będzie występowała zależność pomiędzy PasswordResetHelper i MyEmailSender. Możemy wymienić MyEmailSender na innego dostawcę poczty elektronicznej, a nawet użyć imitacji do testowania. Uwaga Nie każda relacja musi być rozdzielona przez użycie interfejsu. Decyzja ta zależy od stopnia złożoności aplikacji, rodzaju wymaganego testowania oraz tego, w jakim stopniu konieczne jest zapewnienie jej długoterminowej obsługi. Możemy na przykład zdecydować się, aby w małej i prostej aplikacji ASP.NET MVC nie oddzielać kontrolerów od modelu domeny.
Wykorzystanie wstrzykiwania zależności Interfejsy pomagają nam rozdzielać komponenty, ale nadal napotykamy problem — C# nie zawiera wbudowanego mechanizmu pozwalającego na łatwe tworzenie obiektów implementujących interfejsy, poza tworzeniem obiektów konkretnych komponentów. Musimy więc korzystać z kodu przedstawionego na listingu 3.3. Listing 3.3. Tworzenie konkretnej klasy w celu uzyskania implementacji interfejsu public class PasswordResetHelper { public void ResetPassword() { IEmailSender mySender = new MyEmailSender(); //...wywołanie metod interfejsu w celu skonfigurowania szczegółów wiadomości e-mail... mySender.SendEmail(); } }
Jesteśmy w połowie drogi do osiągnięcia luźno powiązanych komponentów. Klasa PasswordResetHelper wykorzystuje interfejs IEmailService do wysyłania wiadomości e-mail, ale przy tworzeniu obiektów implementujących tę usługę musimy użyć klasy MyEmailSender. Tylko pogorszyliśmy sprawę. Teraz klasa PasswordResetHelper zależy od IEmailSender oraz MyEmailSender (rysunek 3.8).
73
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Rysunek 3.8. Komponenty są i tak ściśle powiązane Potrzebujemy sposobu na uzyskanie obiektów implementujących odpowiedni interfejs bez potrzeby bezpośredniego tworzenia konkretnego obiektu. Rozwiązaniem tego problemu jest mechanizm wstrzykiwania zależności (ang. dependency injection — DI), nazywany również odwróceniem kontroli (ang. inversion of control — IoC). DI jest wzorcem projektowym, który pozwala dokończyć osiągnięcie luźnego powiązania komponentów przez dodanie do naszego prostego przykładu interfejsu IEmailSender. DI jest jednym z głównych elementów efektywnego programowania z użyciem MVC. Wzorzec DI składa się z dwóch części. Pierwszą jest możliwość usunięcia z naszych komponentów wszystkich zależności od klas konkretnych — w tym przypadku PasswordResetHelper. Realizujemy to przez przekazanie implementacji wymaganego interfejsu do konstruktora klasy (listing 3.4). Listing 3.4. Usuwanie zależności z klasy PasswordResetHelper public class PasswordResetHelper { private IEmailSender emailSender; public PasswordResetHelper(IEmailSender emailSenderParam) { emailSender = emailSenderParam; } public void ResetPassword() { IEmailSender mySender = new MyEmailSender(); //...wywołanie metod interfejsu w celu skonfigurowania szczegółów wiadomości e-mail... mySender.SendEmail(); } }
W ten sposób usunęliśmy zależność pomiędzy PasswordResetHelper i MyEmailSender. Konstruktor klasy PasswordResetHelper wymaga podania jako parametru obiektu implementującego IEmailSender, ale nie „wie”, jakiej klasy jest ten obiekt, i nie jest odpowiedzialny za jego utworzenie. Zależności są wstrzykiwane do klasy PasswordResetHelper w czasie działania aplikacji. Egzemplarz pewnej klasy implementującej interfejs IEmailSender zostanie utworzony i przekazany do konstruktora PasswordResetHelper w czasie tworzenia obiektu. W trakcie kompilacji nie istnieje zależność pomiędzy PasswordResetHelper a jakąkolwiek klasą implementującą interfejs IEmailSender. Uwaga Przedstawiona tu klasa PasswordResetHelper wymaga przekazania zależności jako parametrów konstruktora. Jest to nazywane wstrzykiwaniem za pomocą konstruktora. Alternatywnie można pozwolić, aby zewnętrzny kod dostarczał zależności z użyciem właściwości udostępnionych do zapisu — jest to nazywane wstrzykiwaniem za pomocą settera.
Ponieważ obsługa zależności jest realizowana w czasie działania aplikacji, można wtedy zdecydować, której implementacji interfejsu powinniśmy użyć. Można wybrać dostawcę poczty elektronicznej lub wstrzyknąć implementację testową. W ten sposób osiągnęliśmy oczekiwaną relację zależności pokazaną na rysunku 3.8.
74
ROZDZIAŁ 3. WZORZEC MVC
Przykład specyficzny dla MVC Wróćmy do przykładowego systemu aukcyjnego i zastosujmy w nim DI. Naszym celem jest utworzenie klasy kontrolera, AdminController, który korzysta z repozytorium MembersRepository, ale bez wiązania AdminController z MembersRepository. Zacznijmy od zdefiniowania interfejsu, który pozwoli nam rozdzielić nasze dwie klasy — nazwijmy go IMembersRepository — i zmieńmy klasę MembersRepository, aby implementowała ten interfejs, jak pokazano na listingu 3.5. Listing 3.5. Interfejs IMembersRepository public interface IMembersRepository { void AddMember(Member member); Member FetchByLoginName(string loginName); void SubmitChanges(); } public class MembersRepository : IMembersRepository { public void AddMember(Member member) { /* napisz mnie */ } public Member FetchByLoginName(string loginName) { /* napisz mnie */ } public void SubmitChanges() { /* napisz mnie */ } }
Teraz możesz napisać klasę kontrolera, która zależy od interfejsu IMembersRepository (listing 3.6). Listing 3.6. Klasa AdminController public class AdminController : Controller { IMembersRepository membersRepository; public AdminController(IMembersRepository membersRepository) { this.membersRepository = membersRepository; } public ActionResult ChangeLoginName(string oldLogin, string newLogin) { Member member = membersRepository.FetchByLoginName(oldLogin); member.LoginName = newLogin; membersRepository.SubmitChanges(); // … tutaj generowanie widoku return View(); } }
Ta klasa AdminControler wymaga przekazania do niej implementacji IMembersRepository jako parametru konstruktora. Będzie ona wstrzyknięta w czasie działania aplikacji, co pozwoli klasie AdminController na operowanie na obiekcie klasy implementującej interfejs bez konieczności wiązania się z tą implementacją.
Użycie kontenera wstrzykiwania zależności Rozwiązaliśmy już nasz problem z zależnościami — oczekujemy, że zależności będą wstrzyknięte do konstruktorów w czasie działania aplikacji. Jednak pozostał jeden problem. W jaki sposób utworzyć konkretną implementację interfejsu bez tworzenia zależności w innym miejscu aplikacji? Rozwiązaniem jest użycie kontenera DI, nazywanego również kontenerem IoC. Jest to komponent, który służy jako broker pomiędzy zależnościami wymaganymi przez klasę, taką jak PasswordResetHelper, a konkretnymi implementacjami tych zależności, takich jak MyEmailSender.
75
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Rejestrujemy zbiór interfejsów lub typów abstrakcyjnych, z których aplikacja będzie korzystała za pośrednictwem kontenera DI, oraz wskazujemy konkretne klasy, które będą tworzone w celu spełnienia zależności. Zarejestrujemy więc w kontenerze interfejs IEmailSender i wskażemy klasę MyEmailSender jako tę, która powinna być konkretyzowana w przypadku konieczności użycia implementacji IEmailSender. Jeżeli będziemy potrzebowali IEmailSender, tak jak przy tworzeniu egzemplarza PasswordResetHelper, przejdziemy do kontenera DI i otrzymamy implementację klasy, która została zaimplementowana jako domyślna konkretna implementacja tego interfejsu — w tym przypadku MyEmailSender. Nie musimy tworzyć samodzielnie kontenera DI. Istnieje kilka świetnych implementacji dostępnych bezpłatnie na zasadach open source. Jedna z nich ma nazwę Ninject; informacje na jej temat można znaleźć na stronie www.ninject.org. Wprowadzenie do Ninject znajduje się w rozdziale 6. Wskazówka Microsoft utworzył własny kontener DI o nazwie Unity. Będę jednak korzystać z Ninject, ponieważ lubię ten produkt, a przy okazji pokażę możliwość łączenia różnych narzędzi w MVC. Jeżeli chcesz dowiedzieć się więcej na temat Unity, zapoznaj się z witryną unity.codeplex.com.
Rola kontenera DI wydaje się bardzo prosta, ale jest to złudne. Dobry kontener DI posiada trzy sprytne funkcje: Obsługa łańcucha zależności — jeżeli zażądamy komponentu, który posiada zależności (np. parametry konstruktora), kontener będzie rekurencyjnie je obsługiwał. Jeśli więc konstruktor dla klasy MyEmailSender wymaga implementacji interfejsu INetworkTransport, kontener DI utworzy domyślną implementację tego interfejsu, przekaże ją do konstruktora MyEmailSender i zwróci jako wynik domyślną implementację IEmailSender. Zarządzanie czasem życia obiektów — jeżeli zażądamy komponentu więcej niż jeden raz, to czy powinniśmy otrzymać za każdym razem ten sam egzemplarz, czy zawsze nowy? Dobry kontener zwykle pozwala na skonfigurowanie cyklu życia komponentów, pozwalając wybrać pomiędzy singletonem (za każdym razem ten sam egzemplarz), nietrwałym (nowy egzemplarz za każdym razem), egzemplarzem na wątek, egzemplarzem na żądanie HTTP, egzemplarzem z puli itd. Konfiguracja wartości parametrów konstruktora — jeżeli na przykład konstruktor klasy INetworkTransport oczekuje ciągu znaków o nazwie serverName, w konfiguracji kontenera DI można ustawić jego wartość. Jest to surowy, ale prosty system konfiguracyjny, który pozwala uniknąć przekazywania w kodzie ciągów połączenia, adresów serwerów i tym podobnych wartości. Być może masz ochotę na napisanie własnego kontenera DI. Uważamy, że jest to doskonały projekt eksperymentalny, o ile masz nieco czasu i chcesz nauczyć się dużo nowych rzeczy na temat refleksji w .NET oraz C#. Jeżeli potrzebujesz po prostu kontenera DI do aplikacji MVC, zalecamy jedno ze znanych rozwiązań, na przykład Ninject.
Zaczynamy testy automatyczne Platforma ASP.NET MVC jest zaprojektowana tak, aby w maksymalnym stopniu ułatwić konfigurowanie testów automatycznych oraz korzystanie z metodologii programowania, takich jak programowanie sterowane testami, które zostaną przedstawione w dalszej części rozdziału. ASP.NET MVC tworzy idealną platformę dla testowania automatycznego, a w Visual Studio jest kilka świetnych funkcji wspomagających testowanie. Pozwalają one projektować i wykonywać testy łatwo i szybko. Mówiąc najogólniej, programiści aplikacji sieciowych skupiają się obecnie na dwóch rodzajach testów automatycznych. Pierwszym rodzajem są testy jednostkowe, które są sposobem na specyfikowanie i weryfikowanie działania poszczególnych klas (lub innych małych jednostek kodu) w izolacji od reszty aplikacji. Drugim typem są testy integracyjne, które pozwalają specyfikować i weryfikować działanie wielu współpracujących ze sobą komponentów, a nawet całej aplikacji sieciowej.
76
ROZDZIAŁ 3. WZORZEC MVC
Oba rodzaje testowania mogą być niezmiernie wartościowe w aplikacji sieciowej. Testy jednostkowe łatwo się tworzy i przeprowadza, są niezwykle precyzyjne, jeżeli pracujemy nad algorytmami, logiką biznesową lub innymi elementami zaplecza. Z kolei wartością testów integracyjnych jest możliwość modelowania tego, w jaki sposób użytkownik posługuje się UI, oraz możliwość objęcia całego stosu technologii wykorzystywanych przez aplikację, w tym serwera WWW i bazy danych. Testy integracyjne zwykle są lepsze do wykrywania nowych błędów, które powstały w starych funkcjach; nazywa się to testowaniem regresyjnym.
Zadania testów jednostkowych W środowisku .NET tworzymy osobny projekt testowy w pliku rozwiązania Visual Studio, w którym będziemy przechowywać osprzęt testów. Projekt ten zostanie utworzony przy dodaniu pierwszego testu jednostkowego lub będzie utworzony automatycznie przy tworzeniu projektu MVC z użyciem szablonu Aplikacja internetowa. Osprzęt testu jest klasą C#, która definiuje zbiór metod testowych — jedna metoda testowa przypada na zachowanie, które chcemy zweryfikować. Projekt testowy może zawierać wiele klas osprzętu. Uwaga Sposób tworzenia projektu testowego oraz wypełniania go testami przedstawię w rozdziale 6. Celem tego rozdziału jest jedynie wprowadzenie koncepcji testowania jednostkowego i zaprezentowanie budowy osprzętu testów i sposobu jego wykorzystania.
Na listingu 3.7 zamieszczona jest przykładowa klasa osprzętu testów, dzięki której możemy testować działanie metody ChangeLoginName() z klasy AdminController, zdefiniowanych na listingu 3.6. Listing 3.7. Przykładowa klasa osprzętu testów [TestClass] public class AdminControllerTest { [TestMethod] public void CanChangeLoginName() { // przygotowanie (skonfigurowanie scenariusza) Member bob = new Member() { LoginName = "Bogdan" }; FakeMembersRepository repositoryParam = new FakeMembersRepository(); repositoryParam.Members.Add(bob); AdminController target = new AdminController(repositoryParam); string oldLoginParam = bob.LoginName; string newLoginParam = "Anastazja"; // działanie (wykonanie operacji) target.ChangeLoginName(oldLoginParam, newLoginParam); // asercje (weryfikacja wyniku) Assert.AreEqual(newLoginParam, bob.LoginName); Assert.IsTrue(repositoryParam.DidSubmitChanges); } private class FakeMembersRepository : IMembersRepository { public List Members = new List(); public bool DidSubmitChanges = false; public void AddMember(Member member) { throw new NotImplementedException(); }
77
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
public Member FetchByLoginName(string loginName) { return Members.First(m => m.LoginName == loginName); } public void SubmitChanges() { DidSubmitChanges = true; } } }
Osprzętem testów jest metoda CanChangeLoginName. Zwróć uwagę, że metoda ta jest oznaczona atrybutem TestMethod, a klasa, w której ona się znajduje, AdminControllerTest, jest oznaczona atrybutem TestClass. W ten sposób Visual Studio wyszukuje osprzęt testów. Metoda testowa CanChangeLoginName jest napisana zgodnie z wzorcem arrange/act/assert (AAA). Pierwszym etapem jest przygotowanie (arrange) warunków testowych, drugi etap to działanie (act), w którym jest wywoływana testowana operacja, a w ostatnim etapie asercji (assert) weryfikowane są wyniki działania. Zapewnienie tej spójności układu kodu testującego ułatwia szybkie czytanie, co można docenić w przypadku napisania setek testów. Klasa osprzętu testów zamieszczona na listingu 3.7 korzysta ze specyficznej dla testu implementacji interfejsu IMembersRepository, która symuluje określone warunki — w tym przypadku, gdy w repozytorium znajduje się tylko jeden użytkownik, Bogdan. Tworzenie imitacji repozytorium oraz obiektu Member jest realizowane w części testu przygotowanie. Następnie wywoływana jest testowana metoda, AdminController.ChangeLoginName. Jest to część testu działanie. Na koniec sprawdzamy wyniki przy użyciu pary wywołań Assert; jest to część testu asercje. Uruchomimy test za pomocą menu Test w Visual Studio i otrzymamy obraz stanu realizacji testów (rysunek 3.9).
Rysunek 3.9. Widok stanu realizacji testów jednostkowych Jeżeli osprzęt testów zostanie wykonany bez zgłoszenia żadnego nieobsłużonego wyjątku i wszystkie instrukcje Assert zostaną wykonane bez problemów, w oknie Test Result pojawi się zielone światło. W przeciwnym razie
będzie widać czerwone światło wraz z informacją, co poszło źle. Uwaga Zauważ, jak zastosowanie DI pomogło w testowaniu jednostkowym. Byliśmy w stanie utworzyć implementację imitującą repozytorium i wstrzyknąć ją do kontrolera, aby osiągnąć bardzo specyficzny scenariusz. To jeden z powodów tego, że jestem ogromnym zwolennikiem DI.
Może się wydawać, że to sporo kodu do zweryfikowania jednej prostej metody, ale nawet w odniesieniu do złożonych przypadków więcej kodu już nie potrzeba. Jeżeli kiedykolwiek będziesz miał zamiar pominąć tego typu małe testy, powinieneś pamiętać, że pomagają one wykryć błędy, które czasami pozostają niezauważone w trakcie bardziej skomplikowanych testów.
78
ROZDZIAŁ 3. WZORZEC MVC
W książce pokażę przykłady złożonych i zwięzłych testów, w których usprawnieniem jest między innymi wyeliminowanie specyficznych dla testu klas imitacji, takich jak FakeMembersRepository, przy wykorzystaniu narzędzi imitujących. W rozdziale 6. wyjaśnię, w jaki sposób możemy to zrealizować.
Użycie programowania sterowanego testami i zasady czerwone-zielone-refaktoryzacja W przypadku programowania sterowanego testami (TDD) testy jednostkowe pomagają projektować kod. To może wydawać się dziwne, jeżeli korzystałeś z testowania po zakończeniu kodowania, ale jest całkiem sensowne. Kluczowa koncepcja w tym rodzaju programowania jest nazywana czerwone-zielone-refaktoryzacja. Zgodne z nią działanie wygląda następująco: Zdecyduj, czy potrzebujesz dodać do aplikacji nową funkcję lub metodę. Napisz test, który sprawdzi poprawność działania nowej funkcji. Uruchom test i sprawdź, czy pojawia się czerwone światło. Napisz kod implementujący nową funkcję. Wykonaj test ponownie i poprawiaj kod do momentu uzyskania zielonego światła. Jeżeli jest to wymagane, refaktoryzuj kod. Na przykład zreorganizuj instrukcje, zmień nazwy zmiennych itd. Uruchom testy, aby potwierdzić, że zmiany nie zakłóciły działania aplikacji. Taka procedura jest powtarzana przy dodawaniu każdej funkcji. Rozważmy następujący przykład. Wyobraźmy sobie funkcję, dzięki której możemy dodać ofertę do przedmiotu, ale nowa oferta musi być wyższa niż wszystkie poprzednie oferty dla tego przedmiotu. Na początek dodamy zrąb metody do klasy Item (listing 3.8). Listing 3.8. Dodawanie zrębu metody do klasy Item using System; using System.Collections.Generic; namespace TheMVCPattern.Models { public class Item { public int ItemID { get; private set; public string Title { get; set; } public string Description { get; set; public DateTime AuctionEndDate { get; public IList Bids { get; private
} // klucz unikatowy } set; } set; }
public void AddBid(Member memberParam, decimal amountParam) { throw new NotImplementedException(); } } }
Jest oczywiste, że metoda AddBid, zaznaczona czcionką pogrubioną, nie przynosi zamierzonych efektów, ale to dopiero początek. Kluczowe w TDD jest testowanie poprawności kodu przed zaimplementowaniem funkcji. Przetestujemy trzy różne aspekty działania nowej funkcji, które mamy zamiar zaimplementować: jeżeli nie ma ofert, można podać dowolną wartość, jeżeli istnieją oferty, można podać wyższą wartość, jeżeli istnieją oferty, nie można podać niższej wartości. W tym celu utworzymy trzy metody testowe zamieszczone na listingu 3.9.
79
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Listing 3.9. Trzy metody osprzętu testów [TestMethod()] public void CanAddBid() { // przygotowanie - skonfigurowanie scenariusza Item target = new Item(); Member memberParam = new Member(); Decimal amountParam = 150M; // działanie - wykonanie operacji target.AddBid(memberParam, amountParam); // asercje - weryfikacja wyniku Assert.AreEqual(1, target.Bids.Count()); Assert.AreEqual(amountParam, target.Bids[0].BidAmount); } [TestMethod()] [ExpectedException(typeof(InvalidOperationException))] public void CannotAddLowerBid() { // przygotowanie Item target = new Item(); Member memberParam = new Member(); Decimal amountParam = 150M; // działanie target.AddBid(memberParam, amountParam); target.AddBid(memberParam, amountParam - 10); } [TestMethod()] public void CanAddHigherBid() { // przygotowanie Item target = new Item(); Member firstMember = new Member(); Member secondMember = new Member(); Decimal amountParam = 150M; // działanie target.AddBid(firstMember, amountParam); target.AddBid(secondMember, amountParam + 10); // asercje Assert.AreEqual(2, target.Bids.Count()); Assert.AreEqual(amountParam + 10, target.Bids[1].BidAmount); }
Utworzyliśmy osobny test jednostkowy dla każdej z oczekiwanych funkcji. Metody testowe są budowane zgodnie z wzorcem przygotowanie-działanie-asercje, za pomocą którego tworzymy, testujemy i kontrolujemy poprawność jednego aspektu danego kodu. Metoda CannotAddLowerBid nie posiada asercji, ponieważ prawidłowym wynikiem testu jest zgłoszenie wyjątku, który kontrolujemy za pomocą atrybutu ExpectedException w metodzie testowej.
80
ROZDZIAŁ 3. WZORZEC MVC
Uwaga Zwróć uwagę, w jaki sposób test wykonywany w metodzie testu jednostkowego CannotAddLowerBid kształtuje naszą implementację metody AddBid. Kontrolujemy wynik testu przez sprawdzenie, czy został zgłoszony wyjątek i czy jest on typu System.InvalidOperationException. Napisanie testu jednostkowego przed napisaniem kodu pozwala nam pomyśleć o różnych rodzajach wyniku, zanim zagłębimy się w implementacje.
Jak można oczekiwać, wszystkie te testy nie zostaną wykonane prawidłowo, jak pokazano na rysunku 3.10.
Rysunek 3.10. Wykonanie testów jednostkowych po raz pierwszy Możemy teraz napisać pierwszą wersję metody AddBid (listing 3.10). Listing 3.10. Implementacja metody AddBid using System; using System.Collections.Generic; namespace TheMVCPattern.Models { public class Item { public int ItemID { get; private set; } // klucz unikatowy public string Title { get; set; } public string Description { get; set; } public DateTime AuctionEndDate { get; set; } public IList Bids { get; set; } public Item() { Bids = new List(); } public void AddBid(Member memberParam, decimal amountParam) { Bids.Add(new Bid() { BidAmount = amountParam, DatePlaced = DateTime.Now, Member = memberParam }); } } }
Do klasy Item dodaliśmy pierwszą implementację metody AddBid. Uzupełniliśmy ją również o prosty konstruktor, dzięki czemu możemy tworzyć obiekty Item, zapewniając prawidłową inicjalizację kolekcji obiektów Bid. Ponowne uruchomienie testów daje lepsze wyniki (rysunek 3.11).
81
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Rysunek 3.11. Wykonanie testów jednostkowych dla naszej początkowej implementacji Dwa z trzech testów zostały wykonane prawidłowo. Nie udało się wykonać prawidłowo testu CannotAddLowerBid. Nie dodaliśmy żadnych warunków sprawdzających, czy oferta jest wyższa niż poprzednia.
Musimy zmienić naszą implementację, dodając odpowiednie warunki w sposób pokazany na listingu 3.11. Listing 3.11. Ulepszanie implementacji metody AddBid using System; using System.Collections.Generic; using System.Linq; namespace TheMVCPattern.Models { public class Item { public int ItemID { get; private set; } // klucz unikatowy public string Title { get; set; } public string Description { get; set; } public DateTime AuctionEndDate { get; set; } public IList Bids { get; set; } public Item() { Bids = new List(); } public void AddBid(Member memberParam, decimal amountParam) { if (Bids.Count() == 0 || amountParam > Bids.Max(e => e.BidAmount)) { Bids.Add(new Bid() { BidAmount = amountParam, DatePlaced = DateTime.Now, Member = memberParam }); } else { throw new InvalidOperationException("Zbyt mała wartość oferty"); } } } }
Możesz zauważyć, że zasygnalizowaliśmy błędną sytuację w taki sposób, aby zrealizować założenia testu jednostkowego napisanego przed rozpoczęciem kodowania; zgłosiliśmy wyjątek InvalidOperationException, gdy otrzymana oferta okazała się zbyt niska.
82
ROZDZIAŁ 3. WZORZEC MVC
Uwaga Do sprawdzenia poprawności oferty użyliśmy Language Integrated Query (LINQ). Nie przejmuj się, jeżeli nie znasz LINQ lub użytych przez nas wyrażeń lambda (notacja =>). Wprowadzenie do funkcji C#, które są niezbędne przy programowaniu w środowisku MVC, znajdziesz w rozdziale 6.
Za każdym razem, gdy zmieniamy implementację metody AddBid, ponownie wykonujemy testy jednostkowe. Wyniki są pokazane na rysunku 3.12.
Rysunek 3.12. Poprawne wyniki testów jednostkowych Sukces! Napisaliśmy nową funkcję, która przechodzi wszystkie testy jednostkowe. Ostatnim krokiem jest poświęcenie chwili na upewnienie się, czy nasze testy faktycznie kontrolują wszystkie aspekty działania implementowanej funkcji. Jeżeli tak jest, zakończyliśmy pracę. Jeżeli nie, powinniśmy dodać więcej testów i powtórzyć cykl, aż będziemy pewni, że mamy wyczerpujący zestaw testów i implementację spełniającą je wszystkie. Omówienie to jest kwintesencją TDD. Istnieje wiele powodów, aby zarekomendować TDD jako styl programowania, ale najważniejsze jest chyba zmuszenie programisty do pomyślenia o tym, jaki skutek przyniosą zmiana lub rozszerzenie, zanim zacznie on pisać kod. Zawsze mamy jasny cel przed sobą i metody sprawdzenia, czy już go osiągnęliśmy. Jeżeli mamy testy jednostkowe pokrywające pozostałą część aplikacji, możemy być pewni, że modyfikacje nie zmienią jej zachowania w innym miejscu.
Nawrócenie na testowanie jednostkowe Jeżeli nie stosujesz testów jednostkowych w kodzie, możesz uznać przedstawiony proces za dziwny i nieefektywny — więcej pisania, więcej testowania, więcej iteracji. Jeżeli jednak korzystasz z testów jednostkowych, wiesz już, jakie są wyniki: mniej błędów, lepiej zaprojektowane oprogramowanie i mniej niespodzianek przy wprowadzaniu zmian. Przejście od nietestera do testera może być trudne. Oznacza przystosowanie się do nowych zasad i trzymanie się ich tak długo, aż zobaczymy korzyści. Nasze pierwsze próby wprowadzenia testowania nie udały się z powodu nieoczekiwanych przesunięć daty oddania zadania. Trudno jest przekonać się do wykonywania czegoś, co uważamy za dodatkowe zajęcie, gdy pracujemy pod presją czasu. Stałem się zwolennikiem testowania jednostkowego i jestem przekonany, że jest to świetny styl programowania. Jeżeli nie próbowałeś tego wcześniej lub próbowałeś, ale się poddałeś, ASP.NET MVC idealnie nadaje się do zaadaptowania testowania jednostkowego. Zespół projektowy w firmie Microsoft niezwykle ułatwił testowanie jednostkowe przez wydzielenie kluczowych klas z bazowych technologii, dzięki czemu możemy tworzyć imitacje kluczowych funkcji i testować sytuacje brzegowe, co byłoby bardzo trudne do zrealizowania, gdyby tego udogodnienia nie wprowadzono. W książce tej będę pokazywać przykłady testowania jednostkowego aplikacji MVC. Zachęcam do ich analizowania i prób samodzielnego pisania testów.
83
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Zadania testów integracyjnych W przypadku aplikacji sieciowych większość najczęściej wykorzystywanych podejść do testów integracyjnych opiera się na automatyzacji interfejsu użytkownika. Termin ten odnosi się do symulowania lub automatyzacji przeglądarki WWW w celu sprawdzenia całego stosu technologii przez zreprodukowanie akcji wykonywanych przez użytkownika, takich jak kliknięcia przycisków, korzystanie z łączy albo wysyłanie danych formularza. Dwoma najlepszymi produktami open source dla programistów .NET zapewniającymi automatyzację przeglądarki są: Selenium RC (http://seleniumhq.org/) — zawierający aplikację „serwera” Java, która może wysyłać polecenia automatyzacji do przeglądarek Internet Explorer, Firefox, Safari lub Opera oraz klientów .NET, Python, Ruby i wielu innych, dzięki czemu można pisać skrypty testowe w wybranym języku. Selenium to produkt zaawansowany i dojrzały; jego jedyną wadą jest konieczność uruchomienia serwera Java. WatiN (http://watin.sourceforge.net/) — jest biblioteką .NET, która wysyła polecenia automatyzacji do przeglądarki Internet Explorer lub Firefox. API tego produktu nie jest tak zaawansowane jak w przypadku Selenium, ale obsługuje większość wspólnych scenariuszy i jest łatwe do skonfigurowania — wystarczy dołączyć jeden plik DLL. Testowanie integracyjne jest idealnym uzupełnieniem testowania jednostkowego. Testowanie jednostkowe świetnie nadaje się do kontrolowania funkcjonowania poszczególnych komponentów serwera, natomiast testowanie integracyjne umożliwia tworzenie testów skupiających się na działaniach użytkownika. Pozwala dzięki temu ujawnić problemy, które wynikają z interakcji między komponentami — stąd termin testowanie integracyjne. Ponieważ testowanie integracyjne dla aplikacji WWW jest realizowane za pośrednictwem przeglądarki, można sprawdzać, czy kod JavaScriptu działa w oczekiwany sposób, co jest bardzo trudne w przypadku testowania jednostkowego. Istnieją również wady testowania integracyjnego — zabiera ono więcej czasu. Dłużej trwa budowanie testów i dłużej są one wykonywane. Ponadto testy integracyjne mogą być wrażliwe. Jeżeli zmienimy identyfikator komponentu sprawdzanego w teście, nie zostanie on wykonany prawidłowo. Ze względu na wymagane nakłady i dodatkowy czas testy integracyjne są często wykonywane w kluczowych punktach projektu — na przykład po tygodniowym zatwierdzeniu kodu albo po zakończeniu głównych bloków funkcyjnych. Testowanie integracyjne jest równie użyteczne jak testowanie jednostkowe i może ujawnić problemy niewykrywane przez testy jednostkowe. Czas wymagany na utworzenie i wykonanie testów integracyjnych jest czasem dobrze zainwestowanym i zalecamy dodać te testy do procesu programowania. W książce tej nie będę jednak przedstawiać testów integracyjnych. Nie dlatego, że uważam je za niepotrzebne — tak nie jest, dlatego zalecam ich stosowanie — ale dlatego, że wykraczają poza ramy książki. Platforma ASP.NET MVC została zaprojektowana w taki sposób, aby ułatwić testowanie jednostkowe, więc muszę je omówić, aby przybliżyć pełny proces budowania dobrej aplikacji MVC. Testowanie integracyjne jest osobną dziedziną i wszystko, co możemy powiedzieć o testowaniu integracyjnym dowolnej aplikacji WWW, odnosi się również do MVC.
Podsumowanie W tym rozdziale przedstawiłem wzorzec architektury MVC i porównałem go z innymi znanymi wzorcami. Omówiłem znaczenie modelu domeny, a następnie utworzyliśmy prosty przykład. Wprowadziłem też DI, pozwalający na rozdzielenie komponentów w celu zapewnienia jasnego podziału pomiędzy częściami naszej aplikacji. Zademonstrowałem kilka prostych testów jednostkowych i wyjaśniłem, jak oddzielić luźno sprzężone komponenty oraz jak DI ułatwia testowanie jednostkowe. Przy okazji wyraziłem mój entuzjazm dla TDD i pokazałem, jak pisać testy jednostkowe przed napisaniem kodu aplikacji. Na koniec wspomniałem o testowaniu integracyjnym i porównałem je z testowaniem jednostkowym.
84
ROZDZIAŁ 4.
Najważniejsze cechy języka
C# jest bogatym językiem i nie każdy programista zna wszystkie jego cechy, z których będziemy korzystać w tej książce. W tym rozdziale przedstawię krótko te cechy języka C#, które dobry programista MVC musi znać. Jeżeli potrzebujesz dokładniejszego omówienia C# lub LINQ, to zajrzyj do innych napisanych przeze mnie książek — kompletnym przewodnikiem po C# jest Introducing Visual C#; w celu zapoznania się z LINQ sięgnij do Pro LINQ in C#, a dokładne omówienie programowania asynchronicznego na platformie .NET znajdziesz w Pro .Net Parallel Programming in C#. Wszystkie wymienione książki zostały wydane przez Apress.
Utworzenie przykładowego projektu Aby zademonstrować funkcje języka C#, trzeba rozpocząć od utworzenia w Visual Studio nowego projektu (Aplikacja sieci Web platformy ASP.NET MVC 4) opartego na szablonie Pusta. Nowemu projektowi nadaj nazwę LanguageFeatures. Omawiane tutaj funkcje nie są stosowane wyłącznie w aplikacjach MVC, ale narzędzie Visual Studio Express 2012 for Web nie pozwala na tworzenie projektów generujących dane wyjściowe w konsoli. Jeżeli chcesz wypróbować przykłady przedstawione w rozdziale, to musisz utworzyć aplikację MVC. Do zaprezentowania wspomnianych funkcji języka potrzebny będzie prosty kontroler. Dlatego też, wykorzystując technikę, którą omówiłem w rozdziale 2., utwórz plik HomeController.cs w katalogu Controllers. Początkowy kod kontrolera Home został pokazany na listingu 4.1. Listing 4.1. Początkowy kod kontrolera HomeController using using using using using using
namespace LanguageFeatures.Controllers { public class HomeController : Controller { public string Index() { return "Przejście do adresu URL pokazującego przykład";
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
} } }
Dla każdego przykładu utworzymy metody akcji. W przypadku metody akcji Index wartością zwrotną jest prosty ciąg tekstowy. Aby mieć możliwość wyświetlania wyników działania metod akcji, konieczne jest dodanie widoku do katalogu Views/Home. W omawianym przykładzie plik widoku ma nazwę Result.cshtml. Kod wymienionego pliku widoku został przedstawiony na listingu 4.2. Listing 4.2. Kod w pliku widoku Result.cshtml @model String @{ Layout = null; } Result
@Model
Jak możesz się przekonać, to jest widok o ściśle określonym typie — w omawianym przypadku typ modelu to String. W rozdziale nie będą przedstawiane zbyt skomplikowane przykłady i wyniki ich działania mogą zostać wyświetlone w postaci prostych ciągów tekstowych.
Użycie automatycznie implementowanych właściwości Właściwości w C# umożliwiają udostępnienie danych z klasy niezależnie od sposobu ich ustawiania i odczytywania. Na listingu 4.3 zamieszczony jest prosty przykład w klasie o nazwie Product, którą musimy dodać do katalogu Models projektu LanguageFeatures. Listing 4.3. Definiowanie właściwości using using using using
namespace LanguageFeatures.Models { public class Product { private string name; public string Name { get { return name; } set { name = value; } } } }
86
ROZDZIAŁ 4. NAJWAŻNIEJSZE CECHY JĘZYKA
Właściwość o nazwie Name jest zaznaczona czcionką pogrubioną. Instrukcje wewnątrz bloku get (nazywane getterami) są wykonywane w momencie odczytu wartości właściwości, a instrukcje wewnątrz bloku set (settery) są wykonywane, gdy do właściwości jest przypisywana wartość (specjalna zmienna Value reprezentuje przypisywaną wartość). Właściwość jest konsumowana przez inne klasy, jakby była polem (listing 4.4). Na listingu 4.4 przedstawiono również metodę akcji AutoProperty dodaną do kontrolera Home. Listing 4.4. Korzystanie z właściwości using using using using using using
namespace LanguageFeatures.Controllers { public class HomeController : Controller { public string Index() { return "Przejście do adresu URL pokazującego przykład"; } public ViewResult AutoProperty() { // utworzenie nowego obiektu Product Product myProduct = new Product(); // ustawienie wartości właściwości myProduct.Name = "Kajak"; // odczytanie właściwości string productName = myProduct.Name; // Wygenerowanie widoku. return View("Result", (object)String.Format("Nazwa produktu: {0}", productName)); } } }
Jak można zauważyć, wartość właściwości jest odczytywana i zapisywana jak zwykłe pole. Zalecane jest użycie właściwości zamiast pól, ponieważ możemy zmieniać instrukcje w blokach get i set bez potrzeby zmiany którejkolwiek klasy zależnej od tej właściwości. Wskazówka Prawdopodobnie zauważyłeś, że drugi argument metody View został rzutowany na postać object. Powód jest prosty: metoda View jest przeciążona i akceptuje dwa argumenty String, które mają inne znaczenie i mogą akceptować typy String i object. Aby uniknąć wywołania niewłaściwego argumentu, stosujemy wyraźne rzutowanie na postać object. Do metody View i jej przeciążeń powrócimy w rozdziale 18.
Efekt działania przykładu możesz zobaczyć po uruchomieniu projektu i przejściu do adresu URL /Home/AutoProperty (który powoduje wywołanie metody akcji AutoProperty i stanowi wzorzec testowania wszystkich przykładów przedstawionych w rozdziale). Ponieważ jedynie przekazujemy ciąg tekstowy z metody akcji do widoku, to dane wyjściowe przedstawione zostaną w postaci tekstu, a nie rysunku. Wywołanie wymienionej wcześniej metody akcji powoduje wygenerowanie komunikatu: Nazwa produktu: Kajak
87
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Wszystko dobrze, ale jeżeli klasa ma sporo właściwości, praca staje się nużąca, a wszystkie metody getterów i setterów realizują to samo zadanie — zarządzają dostępem do pola. W efekcie otrzymujemy kod, który na pewno nie jest zwięzły (listing 4.5). Na listingu 4.5 pokazano właściwości w takiej postaci, w jakiej znajdują się w pliku Product.cs. Listing 4.5. Rozwlekła definicja właściwości using using using using
namespace LanguageFeatures.Controllers { public class Product { private int productID; private string name; private string description; private decimal price; private string category; public int ProductID { get { return productID; } set { productID = value; } } public string Name { get { return name; } set { name = value; } } public string Description { get { return description; } set { description = value; } } //…i tak dalej… } }
Często się zdarza, że oczekujemy elastyczności właściwości, ale w danym momencie nie potrzebujemy getterów ani setterów. Rozwiązaniem jest użycie automatycznie implementowanych właściwości, nazywanych również właściwościami automatycznymi. W przypadku właściwości automatycznych możemy utworzyć szablon właściwości opartej na polu prywatnym bez konieczności definiowania tego pola ani specyfikowania kodu gettera lub settera (listing 4.6). Listing 4.6. Użycie automatycznie implementowanych właściwości using using using using
Przy korzystaniu z właściwości automatycznych należy pamiętać o kilku zagadnieniach. Po pierwsze, nie definiujemy treści gettera ani settera. Po drugie, nie definiujemy pola, na którym operuje właściwość. Obie te operacje realizuje za nas kompilator C# przy kompilacji klasy. Użycie właściwości automatycznych nie różni się niczym od zastosowania zwykłych właściwości — kod z listingu 4.4 nadal będzie działać. Dzięki wykorzystaniu właściwości automatycznych oszczędzamy sobie nieco pisania, tworzymy kod łatwiejszy do odczytu i jednocześnie zachowujący elastyczność zapewnianą przez użycie standardowych właściwości. Jeżeli zajdzie potrzeba zmiany sposobu implementacji właściwości, można wrócić do zwykłego formatu. Wyobraźmy sobie zmianę w sposobie tworzenia właściwości Name pokazaną na listingu 4.7. Listing 4.7. Powrót z właściwości automatycznej do standardowej using using using using
namespace LanguageFeatures.Controllers { public class Product { private string name; public int ProductID { get; set; } public string Name { get { return ProductID + name;} set { name = value; } } public string Description { get; set;} public decimal Price { get; set; } public string Category { set; get;} } }
Uwaga Należy zwrócić uwagę, że przy powrocie do właściwości standardowej konieczne jest zaimplementowanie zarówno gettera, jak i settera. C# nie obsługuje mieszania getterów i setterów w stylu właściwości automatycznych i standardowych.
Użycie inicjalizatorów obiektów i kolekcji Innym nużącym zadaniem programistycznym jest tworzenie nowych obiektów i przypisywanie wartości ich właściwościom (listing 4.8). Na listingu 4.8 przedstawiono również metodę akcji CreateProduct dodaną do kontrolera Home. Listing 4.8. Konstruowanie i inicjowanie obiektów z właściwościami using using using using using
using System.Web.Mvc; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public string Index() { return "Przejście do adresu URL pokazującego przykład"; } public ViewResult AutoProperty() { // … polecenia zostały pominięte w celu zachowania zwięzłości… } public ViewResult CreateProduct() { // tworzenie nowego obiektu Product Product myProduct = new Product(); // ustawienie wartości właściwości myProduct.ProductID = 100; myProduct.Name = "Kajak"; myProduct.Description = "Łódka jednoosobowa"; myProduct.Price = 275M; myProduct.Category = "Sporty wodne"; return View("Result", (object)String.Format("Kategoria: {0}", myProduct.Category)); } } }
Aby utworzyć obiekt Product i przekazać go do metody ProcessProduct, musimy przejść przez trzy etapy: utworzenie obiektu, ustawienie wartości parametrów, a następnie wywołanie metody. Na szczęście możemy użyć funkcji inicjalizatora obiektów, która pozwala wykonać wszystko w jednym kroku (listing 4.9). Listing 4.9. Użycie funkcji inicjalizatora obiektów ... public ViewResult CreateProduct() { // tworzenie nowego obiektu Product Product myProduct = new Product { ProductID = 100, Name = "Kajak", Description = "Łódka jednoosobowa", Price = 275M, Category = "Sporty wodne" }); return View("Result", (object)String.Format("Kategoria: {0}", myProduct.Category)); } ...
Klamry ({}) za wywołaniem konstruktora Product stanowią inicjalizator. W procesie tworzenia obiektu możemy przekazać wartości do tych parametrów. Ta sama funkcja pozwala nam inicjować zawartość kolekcji i tablic w czasie ich tworzenia (listing 4.10).
90
ROZDZIAŁ 4. NAJWAŻNIEJSZE CECHY JĘZYKA
Listing 4.10. Inicjowanie kolekcji i tablic using using using using using using
namespace LanguageFeatures.Controllers { public class HomeController : Controller { public string Index() { return "Przejście do adresu URL pokazującego przykład"; } // … inne metody akcji zostały pominięte w celu zachowania zwięzłości… public ViewResult CreateCollection() { string[] stringArray = { "jabłko", "pomarańcza", "gruszka" }; List intList = new List { 10, 20, 30, 40 }; Dictionary myDict = new Dictionary { { "jabłko", 10 }, { "pomarańcza", 20 }, { "gruszka", 30 } }; return View("Result", (object)stringArray[1]); } } }
Na listingu 4.10 zademonstrowałem sposób tworzenia i inicjowania tablicy oraz dwóch klas z biblioteki kolekcji generycznych. Funkcja ta jest tylko udogodnieniem składniowym — dzięki niej C# jest przyjemniejszy w użyciu, nie ma żadnego innego wpływu na działanie kodu i nie oferuje żadnych dodatkowych korzyści.
Użycie metod rozszerzających Metody rozszerzające są dobrym sposobem dodawania metod do klas, których nie jesteśmy właścicielami, przez co nie możemy ich bezpośrednio modyfikować. Na listingu 4.11 zamieszczona jest dodana do katalogu Models klasa ShoppingCart reprezentująca kolekcję obiektów Products. Listing 4.11. Klasa ShoppingCart using using using using
namespace LanguageFeatures.Models { public class ShoppingCart { public List Products { get; set; } } } ShoppingCart jest klasą osłonową dla kolekcji List obiektów Products (w tym przykładzie taki prosty przypadek jest wystarczający). Załóżmy, że musimy określić całkowitą wartość obiektów Products zawartych
91
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
w kolekcji ShoppingCart, ale nie możemy zmodyfikować tej klasy — może ona znajdować się w bibliotece dostarczanej przez zewnętrzną firmę i możemy nie mieć kodu źródłowego do tej biblioteki. Na szczęście można użyć metody rozszerzającej, która pozwala nam uzyskać potrzebne działanie. Na listingu 4.12 przedstawiono klasę MyExtensionMethods, którą również trzeba dodać do katalogu Models. Listing 4.12. Definiowanie metody rozszerzającej namespace LanguageFeatures.Models { public static class MyExtensionMethods { public static decimal TotalPrices(this ShoppingCart cartParam) { decimal total = 0; foreach (Product prod in cartParam.Products) { total += prod.Price; } return total; } } }
Słowo kluczowe this przed pierwszym parametrem oznacza metodę TotalPrices jako metodę rozszerzającą. Pierwszy parametr informuje .NET, do której klasy powinna być zastosowana metoda rozszerzająca — w tym przypadku ShoppingCart. Aby odwołać się do egzemplarza ShoppingCart, do którego została zastosowana metoda rozszerzająca, korzystamy z parametru cartParam. Nasza metoda przegląda obiekty Products z ShoppingCart i zwraca sumę wartości właściwości Product.Price. Na listingu 4.13 przedstawiono sposób użycia metody rozszerzającej w nowej metodzie akcji o nazwie UseExtension, która została dodana do kontrolera Home. Listing 4.13. Stosowanie metody rozszerzającej using using using using using using
namespace LanguageFeatures.Controllers { public class HomeController : Controller { public string Index() { return "Przejście do adresu URL pokazującego przykład"; } // … inne metody akcji zostały pominięte w celu zachowania zwięzłości… public ViewResult CreateCollection() { // tworzenie i wypełnianie ShoppingCart ShoppingCart cart = new ShoppingCart { Products = new List { new Product {Name = "Kajak", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Price = 48.95M}, new Product {Name = "Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Price = 34.95M} } }; // pobranie całkowitej wartości produktów w koszyku
Uwaga Metody rozszerzające nie pozwalają na łamanie zasad dostępu zdefiniowanych dla metod, pól oraz właściwości tej klasy. Można rozszerzać działanie klasy za pomocą metody rozszerzającej, ale wyłącznie przy użyciu składowych klasy, do których i tak mamy dostęp.
W kodzie z listingu 4.13 tworzymy obiekt ShoppingCart i wypełniamy go obiektami Products, korzystając z funkcji inicjalizatora obiektów. Jak widać, po prostu wywołujemy metodę, jakby była częścią klasy ShoppingCart. Trzeba pamiętać, że metoda rozszerzająca nie była zdefiniowana w tej samej klasie, na rzecz której była zastosowana. Platforma .NET wyszukuje klasy rozszerzające, które znajdują się w zakresie bieżącej klasy, czyli które wchodzą w skład tej samej przestrzeni nazw lub przestrzeni nazw użytej w instrukcji using. Wynik działania kodu z listingu 4.13 jest następujący: Razem: 378,40 zł
Stosowanie metod rozszerzających do interfejsów Możemy również tworzyć metody rozszerzające odnoszące się do interfejsu, co pozwala wywoływać metody rozszerzające w kontekście wszystkich klas implementujących ten interfejs. Na listingu 4.14 przedstawiona jest zmieniona klasa ShoppingCart, która teraz implementuje interfejs IEnumerable. Listing 4.14. Implementowanie interfejsu w klasie ShoppingCart using using using using using
namespace LanguageFeatures.Models { public class ShoppingCart : IEnumerable { public List Products { get; set; } public IEnumerator GetEnumerator() { return Products.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator();
93
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
} } }
Możemy teraz zmienić naszą metodę rozszerzającą, aby operowała na IEnumerable, jak pokazano na listingu 4.15. Listing 4.15. Metoda rozszerzająca, która operuje na interfejsie using System.Collections.Generic; namespace LanguageFeatures.Models { public static class MyExtensionMethods { public static decimal TotalPrices(this IEnumerable productEnum) { decimal total = 0; foreach (Product prod in productEnum) { total += prod.Price; } return total; } } }
Typ pierwszego parametru zmieniliśmy na IEnumerable, co oznacza, że pętla foreach w treści metody działa bezpośrednio na obiekcie parametru. Jest to jedyna zmiana w tej metodzie rozszerzającej. Przejście na interfejs oznacza, że możemy obliczyć całkowitą wartość obiektów Products znajdujących się w dowolnej kolekcji IEnumerable, do których zalicza się obiekt ShoppingCart, ale także tablice obiektów Product (listing 4.16). Listing 4.16. Stosowanie metody rozszerzającej dla różnych implementacji tego samego interfejsu using using using using using using
namespace LanguageFeatures.Controllers { public class HomeController : Controller { public string Index() { return "Przejście do adresu URL pokazującego przykład"; } // … inne metody akcji zostały pominięte w celu zachowania zwięzłości… public ViewResult UseExtensionEnumerable() { IEnumerable products = new ShoppingCart { Products = new List { new Product {Name = "Kajak", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Price = 48.95M}, new Product {Name = "Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Price = 34.95M} } };
94
ROZDZIAŁ 4. NAJWAŻNIEJSZE CECHY JĘZYKA
// tworzenie i wypełnianie tablicy obiektów Product Product[] productArray = { new Product {Name = "Kajak", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Price = 48.95M}, new Product {Name = "Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Price = 34.95M} }; // pobranie całkowitej wartości produktów do koszyka decimal cartTotal = products.TotalPrices(); decimal arrayTotal = products.TotalPrices(); return View("Result", (object)String.Format("Razem koszyk: {0}, Razem tablica: {1}", cartTotal, arrayTotal)); } } }
Uwaga Implementacja interfejsu IEnumerable w tablicach C# jest nieco dziwna. Nie znajdziemy jej na liście typów implementujących ten interfejs w dokumentacji MSDN. Obsługa jest realizowana przez kompilator, więc kod dla wcześniejszych wersji C# nadal będzie się kompilował. Dziwne, ale prawdziwe. Można użyć w tym przykładzie innej klasy kolekcji generycznych, ale chciałem pokazać Czytelnikowi także najciemniejsze zakamarki specyfikacji C#. Również dziwne, ale prawdziwe.
Jeżeli skompilujemy i uruchomimy klasę z listingu 4.16, otrzymamy wynik zamieszczony poniżej, który pokazuje, że metoda rozszerzająca zwraca tę samą wartość niezależnie od sposobu przechowywania obiektów Product: Razem koszyk: $378.40 Razem tablica: $378.40
Tworzenie filtrujących metod rozszerzających Ostatnim zagadnieniem dotyczącym metod rozszerzających jest ich użycie do filtrowania kolekcji obiektów. Metody rozszerzające działające na IEnumerable, które zwracają również wartość IEnumerable, mogą korzystać ze słowa kluczowego yield do zastosowania kryterium selekcji dla elementów danych źródłowych w celu wytworzenia zmniejszonego zestawu wyników. Metoda taka jest przedstawiona na listingu 4.17 i została dodana do klasy MyExtensionMethods. Listing 4.17. Filtrująca metoda rozszerzająca using System.Collections.Generic; namespace LanguageFeatures.Models { public static class MyExtensionMethods { public static decimal TotalPrices(this IEnumerable productEnum) { decimal total = 0; foreach (Product prod in productEnum) { total += prod.Price; } return total;
95
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
} public static IEnumerable FilterByCategory( this IEnumerable productEnum, string categoryParam) { foreach (Product prod in productEnum) { if (prod.Category == categoryParam) { yield return prod; } } } } }
Ta metoda rozszerzająca o nazwie FilterByCategory oczekuje dodatkowego parametru pozwalającego na podanie warunku filtrowania w czasie wywołania metody. Obiekty Product, których właściwość Category pasuje do parametru, są zwracane w wynikowej kolekcji IEnumerable, a te, które nie pasują, są pomijane. Użycie tej metody jest pokazane na listingu 4.18. Listing 4.18. Użycie filtrującej metody rozszerzającej using using using using using using
namespace LanguageFeatures.Controllers { public class HomeController : Controller { public string Index() { return "Przejście do adresu URL pokazującego przykład"; } // … inne metody akcji zostały pominięte w celu zachowania zwięzłości… public ViewResult UseExtensionEnumerable() { IEnumerable products = new ShoppingCart { Products = new List { new Product {Name = "Kajak", Category="Sporty wodne", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Category="Sporty wodne", Price = 48.95M}, new Product {Name = "Piłka nożna", Category="Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Category="Piłka nożna", Price = 34.95M} } }; decimal total = 0; foreach (Product prod in products.FilterByCategory("Piłka nożna")) { total += prod.Price; } return View("Result", (object)String.Format("Razem: {0}", total)); } } }
96
ROZDZIAŁ 4. NAJWAŻNIEJSZE CECHY JĘZYKA
Gdy wywołamy metodę FilterByCategory na obiekcie ShoppingCart, zostaną zwrócone wyłącznie produkty z kategorii Piłka nożna. Jeżeli uruchomisz projekt i użyjesz metody akcji UseFilterExtensionMethod, wówczas otrzymasz przedstawione poniżej dane pokazujące sumę cen produktów kategorii Piłka nożna: Razem: 54,45 zł
Użycie wyrażeń lambda Aby metoda FilterByCategory stała się ogólniejsza, możemy zastosować delegata. Dzięki temu delegat będzie wywołany dla każdego obiektu Product, który może być odfiltrowany w dowolnie wybrany sposób (listing 4.19). Na listingu przedstawiono metodę rozszerzającą Filter, która została dodana do klasy MyExtensionMethods. Listing 4.19. Użycie delegata w metodzie rozszerzającej using System; using System.Collections.Generic; namespace LanguageFeatures.Models { public static class MyExtensionMethods { public static decimal TotalPrices(this IEnumerable productEnum) { decimal total = 0; foreach (Product prod in productEnum) { total += prod.Price; } return total; } public static IEnumerable FilterByCategory( this IEnumerable productEnum, string categoryParam) { foreach (Product prod in productEnum) { if (prod.Category == categoryParam) { yield return prod; } } } public static IEnumerable Filter( this IEnumerable productEnum, Func selectorParam) { foreach (Product prod in productEnum) { if (selectorParam(prod)) { yield return prod; } } } } }
Użyliśmy Func jako parametru filtrowania, dzięki czemu nie musimy definiować delegata jako typu. Delegat oczekuje obiektu Product jako parametru i zwraca wartość typu bool równą true, jeżeli dany Product ma być dołączony do wyniku. Do skorzystania z tej metody potrzebny jest dosyć rozbudowany kod, pokazany na listingu 4.20. Na listingu przedstawiono zmiany, które zostały wprowadzone w metodzie rozszerzającej UseFilterExtensionMethod.
97
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Listing 4.20. Użycie filtrującej metody rozszerzającej z parametrem Func ... public ViewResult UseFilterExtensionMethod() { // tworzenie i wypełnianie ShoppingCart IEnumerable products = new ShoppingCart { Products = new List { new Product {Name = "Kajak", Category="Sporty wodne", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Category="Sporty wodne", Price = 48.95M}, new Product {Name = "Piłka nożna", Category="Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Category="Piłka nożna", Price = 34.95M} } }; Func categoryFilter = delegate(Product prod) { return prod.Category == "Piłka nożna"; }; decimal total = 0; foreach (Product prod in products.Filter(categoryFilter)) { total += prod.Price; } return View("Result", (object)String.Format("Razem: {0}", total)); } ...
Wykonaliśmy krok naprzód, ponieważ możemy teraz filtrować obiekty Product za pomocą dowolnego wyrażenia zdefiniowanego przy użyciu delegata, ale musimy zdefiniować Func dla każdego wyrażenia, jakiego chcemy użyć, co nie jest idealnym rozwiązaniem. Mniej rozbudowanym sposobem jest użycie wyrażeń lambda, które zapewniają zwięzły format wyrażania treści metody w delegacie. Możemy zastąpić nim naszą definicję delegata, jak pokazano na listingu 4.21. Listing 4.21. Użycie wyrażeń lambda do zastąpienia definicji delegata ... public ViewResult UseFilterExtensionMethod() { // tworzenie i wypełnianie ShoppingCart IEnumerable products = new ShoppingCart { Products = new List { new Product {Name = "Kajak", Category="Sporty wodne", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Category="Sporty wodne", Price = 48.95M}, new Product {Name = "Piłka nożna", Category="Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Category="Piłka nożna", Price = 34.95M} } }; Func categoryFilter = prod => prod.Category == "Piłka nożna"; decimal total = 0; foreach (Product prod in products.Filter(categoryFilter)) { total += prod.Price; } return View("Result", (object)String.Format("Razem: {0}", total)); } ...
98
ROZDZIAŁ 4. NAJWAŻNIEJSZE CECHY JĘZYKA
Wyrażenie lambda jest zaznaczone czcionką pogrubioną. Parametr jest definiowany bez specyfikowania typu, który zostanie automatycznie wywnioskowany. Znaki => powinny być czytane jako „trafia do” i łączą parametr z wynikowym wyrażeniem lambda. W naszym przykładzie parametr Product o nazwie prod trafia do wyrażenia typu bool, które jest prawdziwe, jeżeli właściwość Category parametru prod jest równa Piłka nożna. Możemy jeszcze zwięźlej zapisać nasze wyrażenie przez całkowite usunięcie Func (listing 4.22). Listing 4.22. Wyrażenie lambda bez Func ... public ViewResult UseFilterExtensionMethod() { IEnumerable products = new ShoppingCart { Products = new List { new Product {Name = "Kajak", Category="Sporty wodne", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Category="Sporty wodne", Price = 48.95M}, new Product {Name = "Piłka nożna", Category="Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Category="Piłka nożna", Price = 34.95M} } }; decimal total = 0; foreach (Product prod in products .Filter(prod => prod.Category == "Piłka nożna")) { total += prod.Price; } return View("Result", (object)String.Format("Razem: {0}", total)); } ...
W tym przykładzie przekazaliśmy wyrażenie lambda jako parametr metody Filter. Jest to naturalny sposób wyrażania filtra, jaki chcemy zastosować. Możemy łączyć wiele filtrów przez rozszerzanie wyrażania lambda (listing 4.23). Listing 4.23. Rozszerzanie wyrażenia filtrującego za pomocą wyrażenia lambda ... public ViewResult UseFilterExtensionMethod() { IEnumerable products = new ShoppingCart { Products = new List { new Product {Name = "Kajak", Category="Sporty wodne", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Category="Sporty wodne", Price = 48.95M}, new Product {Name = "Piłka nożna", Category="Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Category="Piłka nożna", Price = 34.95M} } }; decimal total = 0; foreach (Product prod in products .Filter(prod => prod.Category == "Piłka nożna" || prod.Price > 20)) { total += prod.Price; }
Użyte w powyższym listingu wyrażenie lambda spowoduje dopasowanie obiektów Product należących do kategorii Piłka nożna lub tych, których właściwość Price ma wartość większą niż 20.
Inne formy wyrażeń lambda Nie musimy wyrażać logiki naszego delegata w postaci wyrażenia lambda. Możemy również wywołać metodę, jak pokazano poniżej: prod => EvaluateProduct(prod)
Jeżeli potrzebujemy wyrażenia lambda dla delegata posiadającego wiele parametrów, musimy ująć parametry w nawiasy w następujący sposób: (prod, count) => prod.Price > 20 && count > 0
Jeżeli w wyrażeniu lambda potrzebujemy wielu instrukcji, możemy skorzystać z nawiasów klamrowych ({}) i zakończyć je instrukcją return: (prod, count) => { //…wiele instrukcji kodu return result; }
Nie musisz wykorzystywać wyrażeń lambda w swoim kodzie, ale są one dobrym sposobem na łatwe i czytelne wyrażanie złożonych funkcji. Bardzo je lubię, więc spotkasz je w wielu miejscach w całej książce.
Automatyczne wnioskowanie typów Słowo kluczowe var z języka C# pozwala na zdefiniowanie zmiennej lokalnej bez jawnego określania typu zmiennej, jak pokazano na listingu 4.24. Nazywa się to wnioskowaniem typów lub niejawnym typowaniem. Listing 4.24. Użycie wnioskowania typów ... var myVariable = new Product { Name = "Kajak", Category = "Sporty wodne", Price = 275M }; string name = myVariable.Name; // prawidłowo int count = myVariable.Count; // błąd kompilacji ...
Nieprawdą jest, że myVariable nie posiada typu. Chcemy jedynie, aby kompilator wywnioskował go z kodu. Jak pokazałem w zamieszczonym powyżej kodzie, kompilator pozwala na korzystanie ze składników wywnioskowanej klasy — w tym przypadku Product.
Użycie typów anonimowych Łącząc inicjalizatory obiektów z wnioskowaniem typów, można konstruować proste obiekty przechowujące dane bez potrzeby definiowania odpowiedniej klasy lub struktury. Na listingu 4.25 pokazany jest przykład takiej konstrukcji.
100
ROZDZIAŁ 4. NAJWAŻNIEJSZE CECHY JĘZYKA
Listing 4.25. Tworzenie typu anonimowego ... var myAnonType = new { Name = "MVC", Category = "Wzorzec" }; ...
W przykładzie tym myAnonType jest obiektem typowanym anonimowo. Nie oznacza to, że jest to typ dynamiczny, tak jak w przypadku dynamicznie typowanych zmiennych JavaScript. Oznacza to jedynie, że definicja typu będzie utworzona automatycznie przez kompilator. Nadal wymuszane jest silne typowanie. Można odczytywać i zapisywać tylko te właściwości, które zostały zdefiniowane w inicjalizatorze. Kompilator C# generuje klasę, bazując na nazwach i typach parametrów w inicjalizatorze. Dwa anonimowo typowane obiekty mające właściwości o tych samych nazwach i typach będą przypisane do tej samej, wygenerowanej automatycznie klasy. Oznacza to, że można tworzyć tablice anonimowo typowanych obiektów, jak pokazano na listingu 4.26, w którym przedstawiono metodę akcji CreateAnonArray dodaną do kontrolera Home. Listing 4.26. Tworzenie tablicy anonimowo typowanych obiektów using using using using using using using
namespace LanguageFeatures.Controllers { public class HomeController : Controller { public string Index() { return "Przejście do adresu URL pokazującego przykład"; } // … inne metody akcji zostały pominięte w celu zachowania zwięzłości… public ViewResult CreateAnonArray() { var oddsAndEnds = new[] { new { Name = "MVC", Category = "Wzorzec"}, new { Name = "Kapelusz", Category = "Odzież"}, new { Name = "Jabłko", Category = "Owoc"} }; StringBuilder result = new StringBuilder(); foreach (var item in oddsAndEnds) { result.Append(item.Name).Append(" "); } return View("Result", (object)result.ToString()); } } }
Należy zwrócić uwagę, że do deklaracji tablicy zostało użyte słowo kluczowe var. Musimy z niego skorzystać, ponieważ nie możemy jawnie podać typu, jak w przypadku standardowo typowanej tablicy. Choć nie zdefiniowaliśmy klasy dla żadnego z tych obiektów, nadal możemy przeglądać zawartość tablicy i odczytywać wartość właściwości Name z każdego obiektu. Jest to ważne, gdyż bez tej funkcji nie można tworzyć tablic
101
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
anonimowo typowanych obiektów. Mówiąc dokładniej, moglibyśmy utworzyć tablicę, ale nie bylibyśmy w stanie zrobić z nią niczego użytecznego. Po uruchomieniu projektu i wywołaniu omawianej metody akcji otrzymasz następujące dane wyjściowe: MVC Kapelusz Jabłko
Wykonywanie zapytań LINQ Wszystkie opisane do tej pory funkcje są wykorzystywane w bibliotece LINQ. Uwielbiam LINQ. Jest to wspaniały i dziwnie kuszący dodatek do .NET. Jeżeli nigdy nie używałeś LINQ, wiele straciłeś. Zapewnia on składnię podobną do składni SQL, pozwalającą na wykonywanie w klasach operacji na danych. Wyobraźmy sobie sytuację, w której mamy kolekcję obiektów Product i chcemy znaleźć trzy o najwyższej cenie, a następnie wyświetlić ich nazwy i ceny. Bez LINQ potrzebujemy kodu zbliżonego do przedstawionego na listingu 4.27, gdzie przedstawiono metodę akcji FindProducts, którą należy dodać do kontrolera Home. Listing 4.27. Wykonywanie zapytań bez użycia LINQ ... public ViewResult FindProducts() { Product[] products = { new Product {Name = "Kajak", Category="Sporty wodne", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Category="Sporty wodne", Price = 48.95M}, new Product {Name = "Piłka nożna", Category="Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Category="Piłka nożna", Price = 34.95M} }; // definiowanie tablicy do przechowywania wyników Product[] results = new Product[3]; // posortowanie tablicy Array.Sort(products, (item1, item2) => { return Comparer.Default.Compare(item1.Price, item2.Price); }); // odczytanie pierwszych trzech pozycji w tablicy Array.Copy(products, results, 3); // przygotowanie danych wyjściowych StringBuilder result = new StringBuilder(); foreach (Product p in foundProducts) { result.AppendFormat("Cena: {0} ", p.Price); } return View("Result", (object)result.ToString()); } ...
Z użyciem LINQ można znacznie uprościć proces pobierania danych, co przedstawiono na listingu 4.28. Listing 4.28. Użycie LINQ do pobierania danych ... public ViewResult FindProducts() { Product[] products = { new Product {Name = "Kajak", Category="Sporty wodne", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Category="Sporty wodne", Price = 48.95M}, new Product {Name = "Piłka nożna", Category="Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Category="Piłka nożna", Price = 34.95M}
102
ROZDZIAŁ 4. NAJWAŻNIEJSZE CECHY JĘZYKA
}; var results = from product in products orderby product.Price descending select new { product.Name, product.Price }; // przygotowanie danych wyjściowych int count = 0; StringBuilder result = new StringBuilder(); foreach (var p in foundProducts) { result.AppendFormat("Cena: {0} ", p.Price); if (++count == 3) { break; } } return View("Result", (object)result.ToString()); } ...
Jest to znacznie elegantsze rozwiązanie. Składnia przypominająca SQL jest zaznaczona czcionką pogrubioną. Sortujemy obiekty Product w porządku malejącym i za pomocą słowa kluczowego select zwracamy typ anonimowy zawierający potrzebne nam właściwości. Ten styl korzystania z LINQ jest nazywany składnią zapytania i zna go większość programistów. Niestety, przy użyciu tej metody zapytanie zwraca jeden anonimowo typowany obiekt dla każdego obiektu Product w tablicy źródłowej, więc musimy później zająć się wybieraniem pierwszych trzech elementów i wyświetleniem wyników. Jeżeli poświęcimy prostotę składni zapytania, możemy uzyskać z LINQ znacznie więcej. Alternatywą jest notacja kropki, która bazuje na metodach rozszerzających. Na listingu 4.29 przedstawione jest użycie tej alternatywnej składni do przetwarzania obiektów Product. Listing 4.29. Użycie notacji kropki w LINQ ... public ViewResult FindProducts() { Product[] products = { new Product {Name = "Kajak", Category="Sporty wodne", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Category="Sporty wodne", Price = 48.95M}, new Product {Name = "Piłka nożna", Category="Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Category="Piłka nożna", Price = 34.95M} }; var results = products .OrderByDescending(e => e.Price) .Take(3) .Select(e => new { e.Name, e.Price }); StringBuilder result = new StringBuilder(); foreach (Product p in foundProducts) { result.AppendFormat("Cena: {0} ", p.Price); } return View("Result", (object)result.ToString()); } ...
103
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Przyznaję, że to zapytanie LINQ, zaznaczone czcionką pogrubioną, nie wygląda tak elegancko jak zapisane z zastosowaniem składni zapytania, ale nie wszystkie funkcje LINQ mają odpowiadające im słowa kluczowe C#. Zaawansowani programiści LINQ muszą korzystać z metod rozszerzających. Każda z metod rozszerzających LINQ użytych na listingu 4.29 jest stosowana do IEnumerable i zwraca IEnumerable, co pozwala na łączenie ze sobą metod w celu uzyskiwania złożonych zapytań. Uwaga Wszystkie metody rozszerzające LINQ znajdują się w przestrzeni nazw System.Linq, która musi być zadeklarowana za pomocą słowa kluczowego using. Visual Studio automatycznie dodaje przestrzeń nazw System.Linq do klas kontrolera, ale może wystąpić potrzeba jej ręcznego dodania w innych komponentach projektu MVC.
Metoda OrderByDescending zmienia kolejność obiektów w źródle danych. W tym przypadku wyrażenie lambda zwraca wartość, jakiej chcemy użyć do porównania. Metoda Take zwraca zdefiniowaną liczbę obiektów od początku wyniku (tego nie mogliśmy zrealizować z wykorzystaniem składni zapytania). Metoda Select pozwala nam wykonać projekcję wyniku — definiuje oczekiwany wynik. W tym przypadku wykonujemy projekcję na obiekt anonimowy zawierający właściwości Name oraz Price. Zwróć uwagę, że nie musieliśmy nawet określać nazw właściwości w typie anonimowym. C# wywnioskował je na podstawie właściwości wybranych w metodzie Select. W tabeli 4.1 zebrane są najużyteczniejsze metody rozszerzające LINQ. Zapytania LINQ wykorzystujemy w całej książce, więc być może będziesz chciał wrócić do tej tabeli, gdy zobaczysz metodę rozszerzającą, z którą wcześniej się nie spotkałeś. Wszystkie metody LINQ zamieszczone w tabeli 4.1 operują na IEnumerable. Tabela 4.1. Niektóre przydatne metody rozszerzające LINQ Metoda rozszerzająca
Opis
Opóźniona
All
Zwraca true, jeżeli wszystkie obiekty w źródle danych pasują do predykatu.
Nie
Any
Zwraca true, jeżeli co najmniej jeden obiekt w źródle danych pasuje do predykatu.
Nie
Contains
Zwraca true, jeżeli źródło danych zawiera podany obiekt lub wartość.
Nie
Count
Zwraca liczbę elementów w źródle danych.
Nie
First
Zwraca pierwszy element ze źródła danych.
Nie
FirstOrDefault
Zwraca pierwszy element ze źródła danych lub wartość domyślną, jeżeli nie ma żadnych elementów.
Nie
Last
Zwraca ostatni element ze źródła danych.
Nie
LastOrDefault
Zwraca ostatni element ze źródła danych lub wartość domyślną, jeżeli nie ma żadnych elementów.
Nie
Max Min
Zwraca największą lub najmniejszą wartość zdefiniowaną przez wyrażenie lambda.
Nie
OrderBy OrderByDescending
Sortuje źródło danych, bazując na wartości zwracanej przez wyrażenie lambda.
Tak
Reverse
Odwraca kolejność elementów w źródle danych.
Tak
Select
Wykonuje projekcję wyników z zapytania.
Tak
SelectMany
Wykonuje projekcję każdego elementu danych w sekwencji elementów, a następnie łączy wszystkie wynikowe sekwencje w jedną.
Tak
Single
Zwraca pierwszy element ze źródła danych lub zgłasza wyjątek, jeżeli znalezione zostaną co najmniej dwa elementy.
Nie
104
ROZDZIAŁ 4. NAJWAŻNIEJSZE CECHY JĘZYKA
Tabela 4.1. Niektóre przydatne metody rozszerzające LINQ (ciąg dalszy) Metoda rozszerzająca
Opis
Opóźniona
SingleOrDefault
Zwraca pierwszy element ze źródła danych albo wartość domyślną, jeżeli nie ma żadnych elementów, lub zgłasza wyjątek, jeżeli znalezione zostaną co najmniej dwa elementy.
Nie
Skip SkipWhile
Pomija podaną liczbę elementów lub pomija elementy pasujące do predykatu.
Tak
Sum
Sumuje wartości wybrane przez predykat.
Nie
Take TakeWhile
Wybiera podaną liczbę elementów od początku źródła danych lub wybiera element, dopóki predykat pasuje do elementu.
Tak
ToArray ToDictionary ToList
Konwertuje źródło danych na tablicę lub kolekcję innego typu.
Nie
Where
Odrzuca elementy źródła danych, które nie pasują do predykatu.
Tak
Opóźnione zapytania LINQ Na pewno zauważyłeś, że w tabeli 4.1 znajduje się kolumna o nazwie Opóźniona. Występuje tu interesująca odmiana w sposobie wykonywania metod rozszerzających w zapytaniach LINQ. Zapytanie, które zawiera wyłącznie metody opóźnione, nie jest wykonywane, dopóki elementy wyniku IEnumerable nie zaczną być przeglądane (listing 4.30). Na listingu pokazano prostą zmianę wprowadzoną w metodzie akcji FindProducts. Listing 4.30. Użycie opóźnionych metod rozszerzających LINQ w zapytaniu ... public ViewResult FindProducts() { Product[] products = { new Product {Name = "Kajak", Category="Sporty wodne", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Category="Sporty wodne", Price = 48.95M}, new Product {Name = "Piłka nożna", Category="Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Category="Piłka nożna", Price = 34.95M} }; var results = products.OrderByDescending(e => e.Price) .Take(3) .Select(e => new { e.Name, e.Price }); products[2] = new Product { Name = "Stadion", Price = 79600M }; StringBuilder result = new StringBuilder(); foreach (Product p in foundProducts) { result.AppendFormat("Cena: {0} ", p.Price); } return View("Result", (object)result.ToString()); } ...
Po zdefiniowaniu zapytania LINQ zmieniamy jeden z obiektów w tablicy Product i przeglądamy wyniki zapytania. Oto rezultat tego przykładu: Cena: 79500 Cena: 275 Cena 48.95
105
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Jak można zauważyć, zapytanie nie jest wykonywane do momentu przeglądania wyniku, więc wprowadzona przez nas zmiana — dodanie do tablicy Product obiektu Stadion — jest uwzględniana w wyniku. Wskazówka Jedną z interesujących cech opóźnionych metod rozszerzających LINQ jest to, że zapytania są wykonywane od początku za każdym razem, gdy jest przeglądany wynik. Oznacza to możliwość nieustannego wykonywania zapytań podczas zmiany źródła danych.
Dla porównania — użycie którejkolwiek z nieopóźnionych metod rozszerzających powoduje, że zapytanie LINQ jest wykonywane natychmiast. Przykład jest zamieszczony na listingu 4.31, w którym przedstawiono metodę akcji SumProducts dodaną do kontrolera Home. Listing 4.31. Natychmiast wykonywane zapytanie LINQ ... public ViewResult SumProducts() { Product[] products = { new Product {Name = "Kajak", Category="Sporty wodne", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Category="Sporty wodne", Price = 48.95M}, new Product {Name = "Piłka nożna", Category="Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Category="Piłka nożna", Price = 34.95M} }; var results = products.Sum(e => e.Price); products[2] = new Product { Name = "Stadion", Price = 79500M }; return View("Result", (object)String.Format("Suma: {0:c)", results)); } ...
W przykładzie tym wykorzystana jest metoda Sum, co powoduje otrzymanie następującego wyniku: Suma: 378,40 zł
Jak można zauważyć, element Stadion o znacznie większej cenie nie został uwzględniony w wyniku. Wynik działania metody Sum jest obliczany tuż po jej wywołaniu, a jej działanie nie jest opóźnione aż do chwili użycia wyników.
LINQ i interfejs IQueryable LINQ ma wiele odmian, choć jego użycie jest zawsze prawie takie samo. Jedną z tych odmian jest LINQ to Objects, która została zastosowana w przykładach z tego rozdziału. LINQ to Objects pozwala na odpytywanie obiektów C# znajdujących się w pamięci. Inna odmiana, LINQ to XML, umożliwia bardzo wygodne i efektywne tworzenie, przetwarzanie i odpytywanie danych XML. Parallel LINQ jest nadzbiorem LINQ to Objects obsługującym wykonywanie zapytań LINQ w sposób równoległy, na wielu procesorach lub rdzeniach. Szczególnie interesująca dla nas jest odmiana LINQ to Entities, która pozwala na wykonywanie zapytań LINQ na danych uzyskanych za pomocą Entity Framework. Entity Framework jest platformą ORM firmy Microsoft, będącą częścią szerszej platformy ADO.NET. ORM umożliwia korzystanie z danych relacyjnych z zastosowaniem obiektów C# i właśnie tego mechanizmu użyjemy w tej książce do obsługi danych zapisanych w bazie danych. Wykorzystanie Entity Framework oraz LINQ to Entities przedstawię w następnym rozdziale, ale przy okazji wprowadzenia do LINQ chcę wspomnieć o interfejsie IQueryable. Interfejs ten dziedziczy po IEnumerable i jest stosowany do oznaczenia wyniku zapytania wykonanego na określonym źródle danych. W naszych przykładach będzie to baza danych SQL Server. Nie trzeba korzystać
106
ROZDZIAŁ 4. NAJWAŻNIEJSZE CECHY JĘZYKA
bezpośrednio z IQueryable. Jedną z przyjemnych cech LINQ jest możliwość wykonania tego samego zapytania na wielu typach źródeł danych (obiekty, XML, bazy danych itd.). Gdy zauważysz przykłady użycia IQueryable w przykładach zamieszczonych w kolejnych rozdziałach, to będzie to sygnał, że chcę jasno oznaczyć operacje na danych pochodzących z bazy danych.
Użycie metod asynchronicznych Jednym z największych dodatków do języka C# na platformie .NET 4.5 są wprowadzone usprawnienia w zakresie obsługi metod asynchronicznych. Metody asynchroniczne wykonują swoje zadania w tle oraz informują o zakończeniu pracy. Dzięki temu kod może przeprowadzać inne operacje, podczas gdy metoda asynchroniczna działa w tle. Metody asynchroniczne to bardzo ważne narzędzia pozwalające zarówno na usunięcie wąskich gardeł w kodzie, jak i wykorzystanie przez aplikację zalet płynących z posiadania wielu procesorów i wielu rdzeni procesorów, które mogą działać jednocześnie. Język C# i platforma .NET zapewniają doskonałą obsługę metod asynchronicznych. Jednak odpowiedzialny za to kod często jest rozwlekły i programiści, którzy wcześniej nie stosowali programowania równoległego, zwykle grzęzną w nietypowej składni. Jako prosty przykład może posłużyć kod przedstawiony na listingu 4.32, w którym pokazano metodę asynchroniczną o nazwie GetPageLength. Wymieniona metoda została zdefiniowana w klasie MyAsyncMethod dodanej do katalogu Models. Listing 4.32. Prosty przykład metody asynchronicznej using System.Net.Http; using System.Threading.Tasks; namespace LanguageFeatures.Models { public class MyAsyncMethods { public static Task GetPageLength() { HttpClient client = new HttpClient(); var httpTask = client.GetAsync("http://apress.com"); // w trakcie oczekiwania na zakończenie działania żądania HTTP // można przeprowadzić inne operacje return httpTask.ContinueWith((Task antecedent) => { return antecedent.Result.Content.Headers.ContentLength; }); } } }
Przedstawiona powyżej prosta metoda asynchroniczna używa obiektu System.Net.Http.HttpClient w celu pobrania treści strony głównej wydawnictwa Apress i zwraca jej wielkość. Fragment metody, który może budzić największe wątpliwości, został oznaczony pogrubioną czcionką i stanowi przykład tak zwanej kontynuacji zadania. Platforma .NET jako obiekty Task przedstawia operacje przeznaczone do asynchronicznego wykonania. Wymienione obiekty mają typy ściśle określone na podstawie wyniku wygenerowanego w tle. Dlatego też po wywołaniu metody HttpClient.GetAsync wartość zwrotna będzie typu Task. W ten sposób platforma informuje, że żądanie zostanie wykonane w tle, a wynikiem wykonania wspomnianego żądania będzie obiekt HttpResponseMessage.
107
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Wskazówka Używając słów takich jak tło, pomijam wiele szczegółów, aby przedstawić jedynie najważniejsze dla świata MVC koncepcje. Ogólnie rzecz biorąc, oferowana przez platformę .NET obsługa metod asynchronicznych i programowania równoległego jest doskonała. Zachęcam Cię więc do poznania oferowanych możliwości, co pozwoli Ci na tworzenie naprawdę wydajnych aplikacji, które będą mogły w pełni wykorzystać komputery wyposażone w wiele procesorów lub w procesory wielordzeniowe. Do metod asynchronicznych w MVC powrócimy jeszcze w rozdziale 17.
Większość programistów ma największe problemy z kontynuacją, czyli mechanizmem pozwalającym na wskazanie operacji do wykonania po ukończeniu zadania działającego w tle. W omawianym przykładzie zastosowano metodę ContinueWith do przetworzenia obiektu HttpResponseMessage zwróconego przez metodę HttpClient.GetAsync. W metodzie ContinueWith użyte zostało wyrażenie lambda odpowiedzialne za zwrot wartości właściwości przechowującej informacje o wielkości treści otrzymanej z serwera WWW wydawnictwa Apress. Zwróć uwagę na dwukrotne użycie słowa kluczowego return: ... return httpTask.ContinueWith((Task antecedent) => { return antecedent.Result.Content.Headers.ContentLength; }); ...
Ten fragment może sprawić największe trudności. Pierwsze użycie słowa kluczowego return oznacza zwrot obiektu Task, który gdy zadanie zostanie zakończone, zwróci (return) wartość przechowywaną w nagłówku ContentLength. Nagłówek ContentLength zwraca wynik typu long? (wartość long, którą nie może być null). Oznacza to, że wynikiem działania metody GetPageLength jest Task, np.: ... public static Task GetPageLength() { ...
Nie przejmuj się, jeśli w pełni nie rozumiesz omówionego powyżej fragmentu kodu — sprawia on trudności wielu osobom. Skomplikowane operacje asynchroniczne mogą łączyć ze sobą wiele zadań za pomocą metody ContinueWith, której kod w takim przypadku może stać się trudny w odczycie i jeszcze trudniejszy w obsłudze.
Użycie słów kluczowych async i await Firma Microsoft wprowadziła w języku C# dwa nowe słowa kluczowe mające ułatwić programistom używanie metod asynchronicznych takich jak HttpClient.GetAsync. Wspomniane nowe słowa kluczowe to async i await — wykorzystamy je teraz w celu uproszczenia omówionej wcześniej metody. Zmodyfikowaną wersję metody GetPageLength przedstawiono na listingu 4.33. Listing 4.33. Użycie słów kluczowych async i await using System.Net.Http; using System.Threading.Tasks; namespace LanguageFeatures.Models { public class MyAsyncMethods { public async static Task GetPageLength() { HttpClient client = new HttpClient(); var httpMessage = await client.GetAsync("http://apress.com"); // w trakcie oczekiwania na zakończenie działania żądania HTTP // można przeprowadzić inne operacje
Słowo kluczowe await zostało użyte podczas wywoływania metody asynchronicznej. Informuje ono kompilator C# o konieczności poczekania na wynik działania Task, który zostanie zwrócony przez metodę GetAsync. Dopiero wtedy nastąpi wykonanie pozostałych poleceń znajdujących się w tej samej metodzie. Zastosowanie słowa kluczowego await daje możliwość potraktowania wyniku zwróconego przez metodę GetASync dokładnie w taki sam sposób, jakby został zwrócony przez zwykłą metodę. Zwrócony obiekt HttpResponseMessage zostaje po prostu przypisany zmiennej. Co ważniejsze, następnie można użyć słowa kluczowego return w zwykły sposób i wygenerować dane wyjściowe z innej metody — w omawianym przypadku to wartość właściwości ContentLength. To znacznie naturalniejszy sposób wyszukiwania metod, a ponadto zwalnia programistów z konieczności przejmowania się metodą ContinueWith oraz wielokrotnym użyciem słowa kluczowego return. Kiedy używasz słowa kluczowego await, do sygnatury metody musisz dodać słowo kluczowe async, jak to przedstawiono w przykładzie. Typ wyniku zwracanego przez metodę nie ulega zmianie — w omawianym przypadku metoda GetPageLength nadal zwraca Task. Wynika to z faktu, że słowa kluczowe await i async są implementowane z użyciem pewnych sprytnych technik kompilatora. Pozwala to na zastosowanie naturalniejszej składni, ale jednocześnie nie zmienia sposobu działania metod, w których wymienione słowa kluczowe są stosowane. Komponent wywołujący metodę GetPageLength nadal będzie musiał pracować z wynikiem typu Task, ponieważ operacja działająca w tle powoduje wygenerowanie wartości long innej niż null. Programista może oczywiście zdecydować się na użycie słów kluczowych await i async. Uwaga Prawdopodobnie zauważyłeś brak przykładu MVC pozwalającego na przetestowanie słów kluczowych async i await. Wynika to z faktu, że metody asynchroniczne w kontrolerach ASP.NET MVC wymagają specjalnej techniki. Jednak zanim ją przedstawię w rozdziale 17., mam wiele innych informacji do zaprezentowania.
Podsumowanie W rozdziale tym zaczęliśmy od przeglądu kluczowych funkcji języka C#, które musi znać każdy efektywny programista MVC. Funkcje te są połączone ze sobą w LINQ, którego będziemy używać do pobierania danych w tej książce. Jak wspomniałem, jestem wielkim zwolennikiem LINQ, odgrywającego ważną rolę w aplikacjach MVC. W rozdziale przedstawiłem także nowe słowa kluczowe async i await, które znacznie ułatwiają pracę z metodami asynchronicznymi. Do tego tematu powrócimy w rozdziale 17., w którym pokażę Ci zaawansowane techniki pozwalające na zastosowanie programowania asynchronicznego w kontrolerach aplikacji ASP.NET MVC. W następnym rozdziale przyjrzymy się silnikowi widoku Razor, który jest mechanizmem pozwalającym na dynamiczne wstawianie danych w widokach.
109
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
110
ROZDZIAŁ 5.
Praca z silnikiem Razor
Razor to nazwa nowego silnika widoku wprowadzonego przez Microsoft w ASP.NET MVC 3 i usprawnionego w MVC 4 (choć zmiany są naprawdę niewielkie). Silnik widoku ASP.NET przetwarza strony WWW, szukając specjalnych poleceń, najczęściej odpowiedzialnych za dynamiczne umieszczanie treści w danych wyjściowych wysyłanych do przeglądarki internetowej. Microsoft zapewnia obsługę dwóch silników widoku. Pierwszy to ASPX działający wraz ze znacznikami <% i %>, które od wielu lat są znane wszystkim programistom ASP.NET. Drugi to Razor działający jedynie na fragmentach treści oznaczonych znakiem @. Jeżeli jednak znasz składnię <% %>, nie będziesz miał zbyt wielu problemów z Razorem, choć istnieje w nim kilka nowych zasad. W tym rozdziale przedstawię krótki przewodnik po składni Razora, dzięki czemu będziesz mógł rozpoznać nowe elementy, gdy się na nie natkniesz. Nie będę zamieszczać tu kompletnego podręcznika silnika Razor; będzie to raczej szybki kurs składni. W dalszych rozdziałach książki omówię kolejne elementy silnika Razor. Wskazówka Razor jest ściśle powiązany z programowaniem na platformie ASP.NET MVC. Jednak wraz z wprowadzeniem ASP.NET 4.5 silnik widoku Razor zapewnia także obsługę stron internetowych w technologii ASP.NET.
Tworzenie projektu W celu przybliżenia działania i składni silnika Razor utworzymy w Visual Studio nowy projekt w oparciu o szablon Aplikacja sieci Web platformy ASP.NET MVC 4. Następnie wybierz szablon projektu Pusta. Projektowi nadaj nazwę Razor.
Definiowanie modelu Skorzystamy tu z bardzo prostego modelu domeny, który będzie zawierał jedną klasę domeny o nazwie Product (używaliśmy jej w poprzednim rozdziale). Dodaj do katalogu Models plik o nazwie Product.cs, a następnie umieść w nim kod z listingu 5.1. Listing 5.1. Tworzenie klasy prostego modelu domeny namespace Razor.Models { public class Product {
Definiowanie kontrolera Kliknij prawym przyciskiem myszy katalog Controllers w projekcie i wybierz Dodaj, a następnie Kontroler… z menu kontekstowego. Podaj nazwę HomeController i wybierz opcję Pusty kontroler MVC w sekcji Szablon (rysunek 5.1).
Rysunek 5.1. Tworzenie kontrolera ProductController Kliknij przycisk Dodaj, aby utworzyć klasę kontrolera, a następnie w klasie tej umieść kod z listingu 5.2. Listing 5.2. Prosty kontroler using using using using
namespace Razor.Controllers { public class HomeController : Controller { Product myProduct = new Product { ProductID = 1, Name = "Kajak", Description = "Jednoosobowa łódka", Category = "Sporty wodne", Price = 275M }; public ActionResult Index() { return View(myProduct); } } }
112
ROZDZIAŁ 5. PRACA Z SILNIKIEM RAZOR
Zdefiniowaliśmy metodę akcji o nazwie Index, w której następuje utworzenie i przypisanie wartości właściwościom obiektu Product. Wymieniony obiekt zostaje przekazany metodzie View, więc w trakcie generowania widoku będzie użyty jako model. W trakcie wywoływania metody View nie podajemy nazwy pliku widoku, a tym samym zostanie użyty domyślny widok dla danej metody akcji (ten widok utworzymy w kolejnym punkcie).
Tworzenie widoku Aby utworzyć widok, kliknij prawym przyciskiem myszy metodę Index w klasie HomeController, a następnie wybierz Dodaj widok… z menu kontekstowego. Zaznacz opcję tworzenia widoku o ściśle określonym typie i wybierz klasę Product z menu rozwijanego (rysunek 5 2).
Rysunek 5.2. Dodawanie widoku Index Uwaga Jeżeli nie widzisz klasy Product na liście rozwijanej, skompiluj projekt i ponownie spróbuj utworzyć widok. Visual Studio nie rozpoznaje klas modelu, jeżeli nie są skompilowane.
Upewnij się o odznaczeniu opcji użycia układu lub strony wzorcowej, jak pokazano na rysunku 5.2. Kliknij Dodaj, aby utworzyć widok, który powinien pojawić się w katalogu Views/Product jako Index.cshtml. Plik widoku zostanie wyświetlony w edytorze — możesz się przekonać, że to dokładnie taki sam podstawowy plik widoku, jaki utworzyliśmy w poprzednim rozdziale. Kod pliku widoku został przedstawiony na listingu 5.3. Listing 5.3. Prosty widok Razor @model Razor.Models.Product @{ Layout = null; }
113
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Index
W kolejnych podrozdziałach zostaną przedstawione różne aspekty widoku Razor oraz pewne możliwości, jakie on oferuje. W trakcie poznawania widoku Razor dobrze jest pamiętać, że widok istnieje w celu przedstawienia użytkownikowi jednej lub większej liczby części modelu. To oznacza wygenerowanie kodu HTML przeznaczonego do wyświetlenia danych pochodzących z jednego lub więcej obiektów. Jeżeli będziesz pamiętał, że zawsze próbujemy utworzyć stronę HTML, którą będzie można wysłać klientowi, wówczas działanie silnika Razor nabierze dla Ciebie większego sensu. Uwaga W tym podrozdziale zostaną powtórzone pewne informacje, które przedstawiono już w rozdziale 2. Chcę tutaj — dla wygody Czytelnika — zebrać w jednym miejscu wszelkie informacje o konkretnych funkcjach widoku Razor.
Korzystanie z obiektów modelu Zacznijmy od pierwszego wiersza w widoku: ... @model Razor.Models.Product ...
Instrukcje Razor zaczynają się od znaku @. W tym przypadku definiujemy model widoku, który zostanie przekazany widokowi z metody akcji. W ten sposób będziemy mogli się odwoływać do metod, pól i właściwości za pomocą właściwości @Model (listing 5.4). Na listingu pokazano prostą zmianę wprowadzoną w omawianym widoku. Listing 5.4. Odwołanie do obiektu modelu w widoku Razor @model Razor.Models.Product @{ Layout = null; } Index
@Model.Name
114
ROZDZIAŁ 5. PRACA Z SILNIKIEM RAZOR
Uwaga Zwróć uwagę, że gdy definiowaliśmy typ modelu, stosowaliśmy @model (mała litera m), a gdy odwoływaliśmy się do obiektu modelu — @Model (wielka litera M). To jest nieco zawiłe, gdy rozpoczynasz pracę z silnikiem Razor, ale bardzo szybko do tego przywykniesz.
Gdy uruchomimy aplikację, zobaczymy wynik widoczny na rysunku 5.3. Nie trzeba podawać konkretnego adresu URL, ponieważ wedle domyślnej konwencji w aplikacji MVC, jeśli żądanie jest skierowane do głównego adresu URL (/), wówczas następuje przekierowanie do metody akcji Index w kontrolerze Home. W rozdziale 13. dowiesz się, jako można zmienić to zachowanie aplikacji.
Rysunek 5.3. Efekt odczytania wartości właściwości i jej wyświetlenia w widoku Poprzez użycie wyrażenia @model informujemy aplikację MVC, z jakiego rodzaju obiektem będziemy pracować, a Visual Studio może na wiele sposobów wykorzystać te informacje. Przede wszystkim, w trakcie tworzenia kodu widoku Visual Studio będzie podpowiadać nazwy po wpisaniu słowa kluczowego @model i kropki, jak pokazano na rysunku 5.4.
Rysunek 5.4. Visual Studio podpowiada nazwy, które można wprowadzić w wyrażeniu @Model To jest bardzo podobne do działania opisanego w rozdziale 4. mechanizmu automatycznego uzupełniania dla wyrażeń lambda przekazywanych do metod pomocniczych HTML. Równie użyteczną funkcją jest podświetlanie przez Visual Studio błędów, które pojawiają się podczas odwoływania się do obiektów widoku modelu. Przykład możesz zobaczyć na rysunku 5.5 — w przedstawionej sytuacji próbujemy odwołać się do metody @Model.NieistniejącaWłaściwość. Narzędzie Visual Studio sprawdziło, że klasa Product wskazana jako model nie posiada wymienionej właściwości, więc w edytorze kodu została ona podkreślona jako błędna.
115
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Rysunek 5.5. Visual Studio zgłasza problem z wyrażeniem @Model
Praca z układami Innym wyrażeniem Razor w pliku widoku Index.cshtml jest: ... @{ Layout = null; } ...
To jest przykład bloku kodu Razor, który pozwala na umieszczanie poleceń C# w widoku. Blok kodu rozpoczyna się od znaków @{ i kończy znakiem }, natomiast znajdujące się w nim polecenia są wykonywane w trakcie generowania widoku. Przedstawiony powyżej blok kodu powoduje przypisanie wartości null właściwości Layout. Jak to zostanie szczegółowo objaśnione w rozdziale 18., w aplikacji ASP.NET MVC widoki są kompilowane na postać klas C#, a używana klasa bazowa definiuje właściwość Layout. Dokładny sposób działania poznasz w rozdziale 18., ale teraz musisz pamiętać o jednym: efektem przypisania wartości null właściwości Layout jest poinformowanie platformy MVC, że widok jest niezależny i że będzie generował całą treść, którą trzeba zwrócić klientowi. Niezależne widoki doskonale sprawdzają się w prostych aplikacjach, ale rzeczywiste projekty mogą posiadać dziesiątki widoków. Układ to szablon zawierający kod znaczników używany do zapewnienia spójności witryny internetowej — wspomniany kod może gwarantować dołączanie wymaganych bibliotek JavaScript, a także odpowiadać za zachowanie spójnego wyglądu i działania aplikacji sieciowej.
Tworzenie układu W celu utworzenia układu kliknij prawym przyciskiem myszy katalog Views w oknie eksploratora rozwiązania i wybierz opcję Dodaj/Nowy element… z menu kontekstowego, a następnie wskaż szablon Strona układu MVC 4 (Razor) jak pokazano na rysunku 5.6. Jako nazwę dla tworzonego pliku podaj _BasicLayout.cshtml i kliknij przycisk Dodaj, tworząc w ten sposób plik. Zawartość pliku utworzonego przez Visual Studio przedstawiono na listingu 5.5. Listing 5.5. Początkowa zawartość pliku układu @ViewBag.Title
@RenderBody()
116
ROZDZIAŁ 5. PRACA Z SILNIKIEM RAZOR
Rysunek 5.6. Utworzenie nowego układu
Uwaga Pliki widoków rozpoczynające się od podkreślenia (_) nie są nigdy zwracane użytkownikom, co pozwala na używanie nazw plików do rozróżniania widoków przeznaczonych do wygenerowania oraz obsługujących je plików. Układy będące plikami obsługującymi są poprzedzone znakiem podkreślenia.
Układ to specjalna postać widoku. Jak możesz zobaczyć, w powyższym listingu wyrażenie @ zostało oznaczone pogrubioną czcionką. Wywołanie metody @RenderBody powoduje wstawienie do kodu znaczników układu zawartości widoku wskazanego przez metodę akcji. Drugie wyróżnione w układzie wyrażenie Razor powoduje wyszukanie w ViewBag właściwości o nazwie Title w celu pobrania treści dla elementu . Wszystkie elementy układu będą zastosowane we wszystkich widokach używających danego układu. Dlatego też układy w zasadzie są szablonami. Na listingu 5.6 przedstawiono układ wzbogacony o prosty kod znaczników. Listing 5.6. Dodanie elementów do układu @ViewBag.Title
Dodano kilka elementów oraz zastosowano pewne style CSS względem elementu
zawierającego wyrażenie @RenderBody. Dzięki temu powinno być jasne, która treść pochodzi z układu, a która z widoku.
Stosowanie układu Aby zastosować układ w widoku, konieczne jest przypisanie wartości właściwości Layout. Można również usunąć elementy dostarczane przez strukturę kompletnej strony HTML, ponieważ będą one pobierane z układu. Po zastosowaniu układu (listing 5.7) plik Index.cshtml został znacznie uproszczony. Listing 5.7. Użycie właściwości Layout do wskazania pliku widoku @model Razor.Models.Product @{ ViewBag.Title = "Nazwa produktu"; Layout = "~/Views/_BasicLayout.cshtml"; } Nazwa produktu: @Model.Name
Wskazówka Na listingu przypisana została także wartość właściwości ViewBag.Title, która będzie użyta jako treść dla elementu w dokumencie HTML wysyłanym użytkownikowi. To jest opcjonalna, ale dobra praktyka. Jeżeli wymienionej właściwości nie będzie przypisana wartość, platforma MVC po prostu zwróci pusty element .
Zmiana jest całkiem duża, nawet w przypadku tak prostego widoku. Możemy skoncentrować się na przedstawieniu użytkownikowi danych pochodzących z obiektu modelu widoku, a struktura dokumentu HTML została wyeliminowana z pliku. Użycie układu wiąże się z wieloma zaletami. Układ pozwala na uproszczenie widoków (jak możesz zobaczyć w powyższym listingu). Ponadto układ pozwala na utworzenie współdzielonego kodu HTML, który następnie będzie stosowany w wielu widokach. To znacznie ułatwia obsługę kodu, ponieważ ewentualna modyfikacja kodu HTML przeprowadzona w jednym miejscu (układ) zostanie odzwierciedlona we wszystkich widokach stosujących dany układ. Aby zobaczyć układ w działaniu, po prostu uruchom omawianą aplikację. Wynik pokazano na rysunku 5.7.
Rysunek 5.7. Efekt zastosowania prostego układu w widoku
118
ROZDZIAŁ 5. PRACA Z SILNIKIEM RAZOR
Użycie pliku ViewStart Nadal mamy niewielki problem do rozwiązania, jakim jest konieczność podawania pliku układu w każdym widoku, w którym ma być on zastosowany. Oznacza to, że jeśli wystąpi konieczność zmiany nazwy pliku układu, wówczas trzeba będzie odszukać każdy stosujący go widok, a następnie wprowadzić odpowiednią zmianę. To jest proces podatny na wprowadzenie błędów i jednocześnie zupełne przeciwieństwo ogólnej łatwości obsługi motywów na platformie ASP.NET MVC. Rozwiązaniem problemu jest użycie pliku ViewStart. W trakcie generowania widoku platforma MVC szuka pliku o nazwie _ViewStart.cshtml. Zawartość wymienionego pliku będzie traktowana tak, jakby znajdowała się w samym pliku widoku. Możemy więc wykorzystać tę funkcję do automatycznego przypisania wartości właściwości Layout. Aby utworzyć plik ViewStart, musisz dodać nowy plik układu do katalogu Views, stosując kroki przedstawione we wcześniejszej części rozdziału. Nowemu plikowi nadaj nazwę _ViewStart.cshtml, a następnie umieść w nim kod przedstawiony na listingu 5.8. Listing 5.8. Utworzenie pliku ViewStart @{ Layout = "~/Views/_BasicLayout.cshtml"; }
Tak przygotowany plik ViewStart zawiera zdefiniowaną wartość właściwości Layout, co oznacza możliwość usunięcia tego polecenia z pliku Index.cshtml (listing 5.9). Listing 5.9. Uaktualnienie pliku widoku, aby używał pliku ViewStart @model Razor.Models.Product @{ ViewBag.Title = "Nazwa produktu"; } Nazwa produktu: @Model.Name
W żaden sposób nie trzeba wskazywać chęci użycia pliku ViewStart. Platforma MVC automatycznie wyszukuje plik ViewStart i używa go. Wartości zdefiniowane w pliku ViewStart mają pierwszeństwo, co ułatwia ich nadpisywanie. Ostrzeżenie Trzeba koniecznie zrozumieć różnicę pomiędzy pominięciem właściwości Layout w pliku widoku a przypisaniem jej wartości null. Jeżeli widok jest niezależny i nie chcesz używać układu, wówczas właściwości Layout przypisz wartość null. Natomiast jeżeli pominiesz właściwość Layout, platforma MVC przyjmie założenie, że chcesz użyć układu i wykorzysta wartość odczytaną w pliku ViewStart.
Użycie układów współdzielonych Aby szybko przekonać się, jak można współdzielić układy, do kontrolera Home dodamy nową metodę akcji o nazwie NameAndPrice. Definicję wymienionej metody znajdziesz na listingu 5.10, w którym przedstawiono zmiany wprowadzone w pliku /Controllers/HomeController.cs. Listing 5.10. Dodanie nowej metody akcji do kontrolera HomeController using Razor.Models; using System; using System.Web.Mvc;
119
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
namespace Razor.Controllers { public class HomeController : Controller { Product myProduct = new Product { ProductID = 1, Name = "Kajak", Description = "Jednoosobowa łódka", Category = "Sporty wodne", Price = 275M }; public ActionResult Index() { return View(myProduct); } public ActionResult NameAndPrice() { return View(myProduct); } } }
Nowa metoda akcji po prostu przekazuje obiekt myProduct metodzie widoku, podobnie jak w przypadku metody akcji Index. Takiego rozwiązania nie powinieneś stosować w rzeczywistych aplikacjach, tutaj chciałem zademonstrować funkcjonalność silnika Razor i ten prosty przykład doskonale się do tego nadaje. W edytorze kliknij prawym przyciskiem myszy metodę NameAndPrice, a następnie z menu kontekstowego wybierz opcję Dodaj widok…. W wyświetlonym oknie dialogowym zaznacz pole wyboru Utwórz widok silnie typizowany i wybierz opcję Product (Razor.Models) z rozwijanego menu. Upewnij się także o zaznaczeniu opcji Użyj układu lub strony wzorcowej, jak pokazano na rysunku 5.8.
Rysunek 5.8. Utworzenie widoku korzystającego z układu 120
ROZDZIAŁ 5. PRACA Z SILNIKIEM RAZOR
Zwróć uwagę na komunikat znajdujący się pod polem wyboru Użyj układu lub strony wzorcowej. Informuje on, że powinieneś pozostawić pole tekstowe puste, jeśli widok, którego użyjesz, wskazałeś w pliku ViewStart. Jeśli jednak klikniesz przycisk dodawania (wielokropek), widok zostanie utworzony bez polecenia C# odpowiedzialnego za przypisanie wartości właściwości Layout. W omawianym przykładzie wyraźnie wskażemy widok, więc kliknij przycisk z wielokropkiem, który znajdziesz po prawej stronie pola tekstowego. Visual Studio wyświetli na ekranie kolejne okno dialogowe (rysunek 5.9) pozwalające na wybór pliku układu.
Rysunek 5.9. Wybór pliku układu Wedle konwencji dla projektu MVC pliki układów powinny być umieszczane w katalogu Views, którego zawartość automatycznie wyświetla okno dialogowe. Pamiętaj, że to jednak tylko konwencja. Dlatego też po lewej stronie okna dialogowego znajdziesz wyświetloną strukturę katalogów projektu, na wypadek gdybyś zdecydował się nie stosować do konwencji. Na obecnym etapie mamy zdefiniowany tylko jeden plik układu, więc wybierz _BasicLayout.cshtml i kliknij przycisk OK, tym samym powracając do okna dialogowego dodawania widoku. Jak możesz zobaczyć na rysunku 5.10, nazwa pliku układu została umieszczona w polu tekstowym.
Rysunek 5.10. Wybór pliku układu podczas tworzenia nowego widoku
121
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Po kliknięciu przycisku Dodaj nastąpi utworzenie pliku /Views/Home/NameAndPrice.cshtml. Zawartość wymienionego pliku przedstawiono na listingu 5.11. Listing 5.11. Uaktualnienie pliku widoku, aby używał pliku ViewStart @model Razor.Models.Product @{ ViewBag.Title = "NameAndPrice"; Layout = "~/Views/_BasicLayout.cshtml"; }
NameAndPrice
Visual Studio używa nieco innej domyślnej treści dla pliku widoku, dla którego wskażesz układ. Jak jednak możesz zobaczyć na listingu, kod zawiera dokładnie te same wyrażenia Razor, których wcześniej użyliśmy podczas przypisywania układu widokowi. Aby zakończyć omawiany przykład, na listingu 5.12 przedstawiono prostą zmianę w pliku NameAndPrice.cshtml, po wprowadzeniu której widok będzie wyświetlał dane pochodzące z obiektu modelu widoku. Listing 5.12. Modyfikacja układu NameAndPrice @model Razor.Models.Product @{ ViewBag.Title = "NameAndPrice"; Layout = "~/Views/_BasicLayout.cshtml"; }
NameAndPrice
Nazwa produktu to @Model.Name, jego cena to @Model.Price zł
Jeżeli uruchomisz aplikację i przejdziesz do adresu URL /Home/NameAndPrice, wówczas otrzymasz wynik pokazany na rysunku 5.11. Zgodnie z oczekiwaniami współdzielone elementy i style zdefiniowane w układzie zostały zastosowane w widoku. W ten sposób dowiedziałeś się, w jaki sposób można wykorzystać układ w charakterze szablonu pozwalającego na zapewnienie spójnego wyglądu i działania (choć niewątpliwie prostego i nieatrakcyjnego w omawianym przykładzie).
Rysunek 5.11. Treść z pliku układu zastosowana w widoku NameAndPrice
122
ROZDZIAŁ 5. PRACA Z SILNIKIEM RAZOR
Uwaga Ten sam wynik otrzymasz po pozostawieniu pustego pola tekstowego w oknie dialogowym dodawania nowego widoku. W ten sposób polegasz na pliku ViewStart. W omawianym przykładzie wyraźnie wskazano plik, aby Ci pokazać, jak Visual Studio pomaga w podejmowaniu decyzji.
Użycie wyrażeń Razor Skoro poznałeś już podstawy z zakresu widoków i układów, to teraz naszą uwagę możemy skierować na inne rodzaje wyrażeń obsługiwanych przez Razor oraz sposoby ich używania podczas tworzenia treści widoków. W dobrej aplikacji platformy ASP.NET MVC istnieje wyraźny podział pomiędzy rolami pełnionymi przez metody akcji i widoki. Reguły wspomnianego podziału — zresztą bardzo proste — zostały przedstawione w tabeli 5.1. Tabela 5.1. Zadania metod akcji i widoków Komponent
Wykonuje
Nie wykonuje
Metoda akcji
Przekazuje widokowi obiekt modelu widoku
Przekazuje widokowi sformatowane dane
Widok
Używa obiektu modelu widoku do przedstawienia treści użytkownikowi
Zmienia dowolny aspekt obiektu modelu widoku
Do tego tematu będziemy nieustannie powracali w książce. Aby móc wykorzystać możliwości platformy ASP.NET MVC, konieczne jest pełne poszanowanie zasady zachowania rozdziału pomiędzy różnymi częściami aplikacji. Jak się przekonasz, silnik Razor oferuje całkiem potężne możliwości, łącznie z użyciem poleceń C# — nie wolno Ci jednak używać silnika Razor do przeprowadzania logiki biznesowej lub jakiegokolwiek manipulowania obiektami modelu domeny. Ponadto nie powinieneś formatować danych przekazywanych do widoku przez metodę akcji. Zamiast tego pozwól widokowi na ustalenie, jakie dane powinny zostać wyświetlone. Bardzo prosty przykład takiej implementacji został przedstawiony w poprzednim podrozdziale. Zdefiniowaliśmy metodę akcji o nazwie NameAndPrice, która wyświetlała wartości właściwości Name i Price obiektu Product. Wprawdzie doskonale wiedzieliśmy, wartości których właściwości powinny zostać wyświetlone, ale jednak modelowi widoku przekazywaliśmy kompletny obiekt Product: ... public ActionResult NameAndPrice() { return View(myProduct); } ...
Następnie wykorzystaliśmy w widoku wyrażenie Razor @Model w celu pobrania wartości interesujących nas właściwości: ... Nazwa produktu to @Model.Name, jego cena to @Model.Price zł ...
Przeznaczony do wyświetlenia ciąg tekstowy moglibyśmy utworzyć w metodzie akcji i przekazać widokowi jako obiekt modelu widoku. Wprawdzie takie rozwiązanie działa, ale podkopuje zalety wzorca MVC i zmniejsza możliwość udzielenia w przyszłości odpowiedzi na zmiany. Jak już wspomniano, do omawianego zagadnienia jeszcze powrócimy. Powinieneś pamiętać, że platforma ASP.NET MVC nie posiada mechanizmów wymuszających poprawne stosowanie wzorca MVC. Dlatego też musisz być świadomy efektów podejmowanych decyzji projektowych i dotyczących kodu.
123
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Wstawianie wartości danych Najprostszym zadaniem, jakie można wykonać przy użyciu wyrażenia Razor, jest wstawienie wartości danych w kodzie znaczników. Wyrażenie @Model możesz wykorzystać w celu odwołania się do właściwości i metod zdefiniowanych przez obiekt modelu widoku. Inna możliwość to użycie wyrażenia @ViewBag w celu dynamicznego odwołania się do zdefiniowanych właściwości za pomocą (przedstawionej w rozdziale 2.) funkcji ViewBag. Przykłady użycia obu wymienionych wyrażeń już widziałeś. Jednak w celu zachowania porządku do kontrolera Home dodano nową metodę akcji o nazwie DemoExpression. Zadaniem wymienionej metody jest przekazanie danych do widoku za pomocą obiektu modelu i ViewBag. Definicję nowej metody akcji przedstawiono na listingu 5.13. Listing 5.13. Metoda akcji DemoExpression using Razor.Models; using System; using System.Web.Mvc; namespace Razor.Controllers { public class HomeController : Controller { Product myProduct = new Product { ProductID = 1, Name = "Kajak", Description = "Jednoosobowa łódka", Category = "Sporty wodne", Price = 275M }; public ActionResult Index() { return View(myProduct); } public ActionResult NameAndPrice() { return View(myProduct); } public ActionResult DemoExpression() { ViewBag.ProductCount = 1; ViewBag.ExpressShip = true; ViewBag.ApplyDiscount = false; ViewBag.Supplier = null; return View(myProduct); } } }
Ponadto utworzono ściśle określonego typu widok o nazwie DemoExpression.cshtml, który wykorzystamy do przedstawienia podstawowych typów wyrażeń. Zawartość pliku widoku znajdziesz na listingu 5.14.
W powyższym przykładzie została utworzona prosta tabela HTML, a właściwości obiektu modelu i ViewBag wykorzystano do wstawienia wartości w komórkach tabeli. Na rysunku 5.12 pokazano wynik uruchomienia aplikacji i przejścia do adresu URL /Home/DemoExpression. Tutaj stosujemy jedynie proste wyrażenia Razor, z których już wcześniej korzystaliśmy.
Rysunek 5.12. Użycie prostych wyrażeń Razor w celu wstawienia danych w kodzie znaczników HTML Otrzymany wynik nie jest ładny pod względem graficznym, ponieważ nie zastosowaliśmy żadnych stylów CSS dla elementów HTML generowanych przez widok. Celem przykładu jest jednak pokazanie sposobu użycia wyrażeń Razor do wyświetlenia danych przekazanych widokowi przez metodę akcji.
Przypisanie wartości atrybutu Wszystkie przedstawione dotąd przykłady miały zdefiniowaną treść elementów, ale wyrażenia Razor możesz wykorzystać także do przypisania wartości atrybutów elementu. Na listingu 5.15 przedstawiono zmodyfikowaną wersję widoku DemoExpression, który teraz używa właściwości ViewBag w celu przypisania wartości atrybutów. Sposób, w jaki Razor obsługuje atrybuty na platformie ASP.NET MVC 4, jest całkiem elegancki. To jest jeden z obszarów, na którym wprowadzono spore usprawnienia względem ASP.NET MVC 3.
125
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Listing 5.15. Użycie wyrażenia Razor w celu przypisania wartości atrybutu @model Razor.Models.Product @{ ViewBag.Title = "DemoExpression"; Layout = "~/Views/_BasicLayout.cshtml"; }
Właściwość
Wartość
Nazwa
@Model.Name
Cena
@Model.Price
Ilość w magazynie
@ViewBag.ProductCount
Element posiada atrybuty danych
Rabat: Express: Dostawca:
Rozpoczęliśmy od użycia prostych wyrażeń Razor w celu przypisania wartości pewnych atrybutów data w elemencie
. Atrybuty danych, które są atrybutami o nazwach poprzedzonych prefiksem name-, przez wiele lat były nieformalnym sposobem tworzenia własnych atrybutów i w końcu stały się formalną częścią standardu HTML5. W przykładzie wykorzystano właściwości ViewBag ApplyDiscount, ExpressShip i Supplier do przypisania wartości wspomnianym atrybutom. Uruchom omawianą aplikację, wywołaj metodę docelową i spójrz na kod źródłowy, na podstawie którego została wygenerowana strona. Powinieneś dostrzec, że wyrażenie Razor przypisało wartość atrybutom, np.: ...
Element posiada atrybuty danych
...
Wartości False i True odpowiadają wartościom boolowskim w ViewBag. W przypadku właściwości o wartości null wygenerowany został pusty ciąg tekstowy — to rozsądne rozwiązanie zastosowane przez Razor. Jeszcze ciekawiej robi się, gdy spojrzysz na drugi fragment kodu dodany do widoku, czyli serię pól wyboru. Wartościami atrybutu checked wspomnianych pól wyboru są nazwy właściwości ViewBag użyte w atrybutach danych. Wygenerowany fragment kodu HTML przedstawia się następująco: ... Rabat: Express: Dostawca: ...
Na platformie ASP.NET MVC 4 silnik Razor potrafi wykryć sposób użycia atrybutu takiego jak checked, w którym obecność atrybutu, a nie wartość, zmienia konfigurację elementu. Jeżeli Razor wstawi False, null lub pusty ciąg tekstowy jako wartość atrybutu checked, wówczas przeglądarka internetowa wygeneruje to pole wyboru jako zaznaczone. Dlatego też zamiast wstawiać wartość False lub null, Razor po prostu całkowicie usuwa atrybut z elementu i tym samym zapewnia zachowanie spójności widoku danych, jak pokazano na rysunku 5.13.
126
ROZDZIAŁ 5. PRACA Z SILNIKIEM RAZOR
Rysunek 5.13. Efekt usunięcia atrybutów, których obecność konfiguruje element
Użycie konstrukcji warunkowych Razor potrafi przetwarzać konstrukcje warunkowe, co oznacza możliwość dostosowania danych wyjściowych widoku na podstawie wartości podawanych w danych. Docieramy więc do kolejnej potężnej funkcji silnika Razor oferującej możliwość tworzenia skomplikowanych i elastycznych układów, które mimo wszystko pozostaną względnie proste do odczytu i obsługi. Na listingu 5.16 przedstawiono uaktualnioną wersję pliku widoku DemoExpression.cshtml, w którym zastosowano konstrukcję warunkową. Listing 5.16. Użycie konstrukcji warunkowej @model Razor.Models.Product @{ ViewBag.Title = "DemoExpression"; Layout = "~/Views/_BasicLayout.cshtml"; }
Właściwość
Wartość
Nazwa
@Model.Name
Cena
@Model.Price
Ilość w magazynie
@switch ((int)ViewBag.ProductCount) { case 0: @: Brak break; case 1: Mało (@ViewBag.ProductCount) break; default: @ViewBag.ProductCount
127
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
break; }
Aby rozpocząć konstrukcję warunkową, umieść znak @ przed poleceniem warunkowym języka C#, którym w omawianym przykładzie jest switch. Blok kodu konstrukcji warunkowej zamyka nawias klamrowy }, podobnie jak w przypadku zwykłego bloku kodu C#. Wskazówka Zwróć uwagę na konieczność rzutowania wartości właściwości ViewBag.ProductCount na int, aby było możliwe jej użycie w poleceniu switch. Ten krok jest wymagany, ponieważ polecenie switch może działać jedynie z określonymi typami i nie ma możliwości obliczenia wartości właściwości dynamicznej bez jej rzutowania, jak to przedstawiono w powyższym przykładzie.
Wewnątrz bloku kodu Razor można umieścić elementy HTML i wartości danych poprzez zwykłe zdefiniowanie wyrażeń HTML i Razor, np. w następujący sposób: ... Mało (@ViewBag.ProductCount) ... @ViewBag.ProductCount ...
Elementów i wyrażeń nie trzeba ujmować w cudzysłów lub oznaczać ich w jakikolwiek inny specjalny sposób — silnik Razor interpretuje je jako dane wyjściowe do przetworzenia w zwykły sposób. Jednak jeśli chcesz wstawić dosłowny tekst do widoku i ten tekst nie jest opakowany żadnym elementem HTML, wówczas musisz odrobinę pomóc silnikowi Razor i poprzedzić tekst prefiksem @:, np.: ... @: Brak ...
Prefiks @:uniemożliwia silnikowi Razor interpretowanie tekstu jako polecenia C#, co jest domyślnym zachowaniem silnika Razor po napotkaniu tekstu. Wynik działania konstrukcji warunkowej pokazano na rysunku 5.14.
Rysunek 5.14. Użycie polecenia warunkowego switch w widoku Razor
128
ROZDZIAŁ 5. PRACA Z SILNIKIEM RAZOR
Konstrukcje warunkowe pełnią ważną rolę w widokach Razor, ponieważ pozwalają na dostosowanie treści do wartości danych otrzymywanych przez widok z metod akcji. Na listingu 5.17 przedstawiono jeszcze jeden przykład polecenia warunkowego w widoku Razor. Tym razem jest to polecenie if umieszczone w pliku widoku DemoExpression.cshtml. Jak możesz przypuszczać, jest to bardzo często używane polecenie warunkowe. Listing 5.17. Użycie polecenia if w widoku Razor @model Razor.Models.Product @{ ViewBag.Title = "DemoExpression"; Layout = "~/Views/_BasicLayout.cshtml"; }
Właściwość
Wartość
Nazwa
@Model.Name
Cena
@Model.Price
Ilość w magazynie
@if (ViewBag.ProductCount == 0) { @: Brak } else if (ViewBag.ProductCount == 1) { Mało (@ViewBag.ProductCount) } else { @ViewBag.ProductCount }
Powyższe polecenie warunkowe powoduje wygenerowanie takiego samego wyniku jak w przypadku przedstawionego wcześniej polecenia switch. Celem było pokazanie Ci możliwości łączenia konstrukcji warunkowych języka C# z widokami Razor. Sposób działania całości zostanie szczegółowo objaśniony w rozdziale 18., w którym dokładnie przyjrzymy się widokom.
Wyliczanie tablic i kolekcji Tworząc aplikacje w technologii ASP.NET MVC, często będziesz spotykał się z koniecznością wyświetlenia treści tablicy lub pewnego innego rodzaju kolekcji obiektów i wygenerowania danych opisujących poszczególne obiekty. Aby zademonstrować tego rodzaju rozwiązanie, w kontrolerze Home zdefiniowano nową metodę akcji o nazwie DemoArray, której kod znajdziesz na listingu 5.18. Listing 5.18. Metoda akcji DemoArray using using using using
public class HomeController : Controller { Product myProduct = new Product { ProductID = 1, Name = "Kajak", Description = "Jednoosobowa łódka", Category = "Sporty wodne", Price = 275M }; // … inne metody akcji zostały pominięte w celu zachowania zwięzłości… public ActionResult DemoArray() { Product[] array = { new Product {Name = "Kajak", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Price = 48.95M}, new Product {Name = "Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Price = 34.95M} }; return View(array); } } }
Przedstawiona metoda akcji tworzy obiekt Product[] zawierający pewne proste wartości danych. Następnie wymieniony obiekt jest przekazywany metodzie View, aby dane zostały wygenerowane za pomocą widoku domyślnego. Podczas tworzenia widoku Visual Studio nie oferuje opcji dla tablic i kolekcji, więc w oknie dialogowym Dodaj widok musisz ręcznie wprowadzić wszystkie szczegółowe informacje. Przykład pokazano na rysunku 5.15. Jak widzisz, tworzony jest widok DemoArray.cshtml, a typ modelu widoku to Razor.Models.Product[].
Rysunek 5.15. Ręczne wprowadzenie ustawień typu tworzonego widoku
130
ROZDZIAŁ 5. PRACA Z SILNIKIEM RAZOR
Zawartość pliku widoku DemoArray.cshtml przedstawiono na listingu 5.19. W wymienionym listingu umieszczono także kilka dodatkowych poleceń odpowiedzialnych za wygenerowanie użytkownikowi informacji o produkcie. Listing 5.19. Zawartość pliku widoku DemoArray.cshtml @model Razor.Models.Product[] @{ ViewBag.Title = "DemoArray"; Layout = "~/Views/_BasicLayout.cshtml"; } @if (Model.Length > 0) {
Produkt
Cena
@foreach (Razor.Models.Product p in Model) {
@p.Name
@p.Price zł
}
} else {
Brak danych produktu
}
Polecenie @if zostało użyte w celu zróżnicowania treści na podstawie wielkości wykorzystywanej tablicy, natomiast wyrażenie @foreach umożliwiło pobranie treści z tablicy oraz wygenerowanie rekordu w tabeli HTML dla każdego obiektu pobranego z tablicy. Jak możesz się przekonać, użyte wyrażenia odpowiadają stosowanym w języku C#. W pętli foreach została utworzona zmienna lokalna o nazwie p, a następnie za pomocą wyrażeń @p.Name i @p.Price odwołaliśmy się do właściwości danego obiektu. Jeżeli tablica jest pusta, wynikiem będzie wygenerowanie elementu
wraz z odpowiednim komunikatem. Jeśli tablica zawiera jakiekolwiek elementy, dla każdego z nich zostanie wygenerowany jeden wiersz w tabeli HTML. W omawianym przykładzie dane są statyczne, dlatego też zawsze otrzymasz taki sam wynik, który został pokazany na rysunku 5.16.
Rysunek 5.16. Wygenerowanie elementów za pomocą konstrukcji pętli
131
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Praca z przestrzenią nazw Zapewne zauważyłeś, że w pętli foreach w poprzednim przykładzie do klasy Product musieliśmy odwoływać się za pomocą pełnej nazwy: ... @foreach (Razor.Models.Product p in Model) { ...
To może być irytujące w skomplikowanych widokach, w których używa się wielu odniesień do modelu widoku oraz innych klas. Istnieje możliwość uprzątnięcia widoku przez zastosowanie wyrażenia @using i dołączenie przestrzeni nazw do kontekstu danego widoku — dokładnie tak samo jak w przypadku zwykłych klas C#. Na listingu 5.20 przedstawiono sposób zastosowania wyrażenia @using w utworzonym wcześniej pliku widoku DemoArray.cshtml. Listing 5.20. Zastosowanie wyrażenia @using @using Razor.Models @model Product[] @{ ViewBag.Title = "DemoArray"; Layout = "~/Views/_BasicLayout.cshtml"; } @if (Model.Length > 0) {
Produkt
Cena
@foreach (Product p in Model) {
@p.Name
@p.Price zł
}
} else {
Brak danych produktu
}
W widoku można umieścić wiele wyrażeń @using. W powyższym przykładzie wyrażenie @using zostało użyte w celu zaimportowania przestrzeni nazw Razor.Models. Dzięki temu możemy usunąć przestrzeń nazw z wyrażenia @model oraz z wnętrza pętli foreach.
Podsumowanie W tym rozdziale przedstawiłem ogólny opis silnika widoku Razor oraz sposób jego użycia do generowania treści HTML. Dowiedziałeś się, jak odwoływać się do danych przekazanych z kontrolera poprzez obiekt modelu widoku oraz poprzez ViewBag. Poznałeś sposoby użycia wyrażeń Razor w celu przygotowania danych wyjściowych na podstawie aktualnych danych roboczych. Wiele innych sposobów użycia silnika Razor zobaczysz w pozostałej części książki. Z kolei w rozdziale 18. szczegółowo omówię działanie mechanizmu widoku na platformie ASP.NET MVC. W następnym rozdziale przyjrzymy się kluczowym narzędziom ułatwiającym tworzenie i testowanie aplikacji MVC.
132
ROZDZIAŁ 6.
Ważne narzędzia wspierające MVC
W niniejszym rozdziale przedstawimy trzy narzędzia, które powinny się znaleźć w arsenale każdego programisty MVC. Wszystkie one zostały wspomniane w poprzednich rozdziałach: kontener DI, platforma testów jednostkowych oraz narzędzie imitujące. Na potrzeby tej książki wybrałem trzy konkretne implementacje tych narzędzi, ale dostępne są liczne odpowiedniki każdego z nich. Jeżeli nie przyzwyczaisz się do produktów proponowanych przeze mnie, to na pewno znajdziesz coś, co pasuje do Twojego sposobu myślenia i pracy. Jak wspomniałem w rozdziale 3., Ninject jest moim preferowanym kontenerem DI. Jest prosty, elegancki i łatwy w użyciu. Dostępne są bardziej złożone rozwiązania alternatywne, ale lubię sposób, w jaki Ninject działa przy minimalnej koniecznej konfiguracji. Przedstawiam tu przecież punkty startowe, a nie stanowię prawa. Po prostu uznałem, że bardzo łatwo można zbudować nasz kontener DI za pomocą Ninject. Jeżeli nie lubisz Ninject, polecam zapoznać się z Unity, który jest udostępniany przez Microsoft. Przy testowaniu jednostkowym używam mechanizmów wbudowanych w Visual Studio. Wcześniej korzystałem z NUnit, który jest najpopularniejszą platformą testów jednostkowych dla .NET. Lubię NUnit, ale Microsoft znacznie poprawił obsługę testów jednostkowych w Visual Studio (a sam moduł jest teraz dostępny nawet w bezpłatnych wydaniach Visual Studio). Ostatecznie więc platforma testów jednostkowych jest ściśle powiązana z resztą zintegrowanego środowiska programistycznego (IDE), co niewątpliwie jest dobrą wiadomością. Trzecim wybranym narzędziem jest Moq — zestaw narzędzi imitacyjnych. Za pomocą Moq tworzymy implementacje interfejsów, które są wykorzystywane w naszych testach jednostkowych. Programiści kochają lub nienawidzą Moq — środek nie istnieje. Możesz uznać ten produkt za elegancki i ekspresyjny albo przeklinać go przy każdej próbie użycia. Jeżeli nie będziesz w stanie go znieść, sugeruję zapoznać się z Rhino Mocks. Przedstawię każde z tych trzech narzędzi i zademonstruję ich najważniejsze funkcje. Nie zamieszczam tu wyczerpującego omówienia tych narzędzi — mógłbym z łatwością napisać o tym osobną publikację — ale zamieszczone tu informacje pozwolą rozpocząć pracę i co najważniejsze, zrozumieć przykłady zamieszczone w pozostałej części książki. Uwaga W rozdziale przyjęto założenie, że Czytelnik chce skorzystać z wszystkich udogodnień oferowanych przez platformę ASP.NET MVC, łącznie z możliwością użycia architektury obsługującej intensywne testowanie oraz kładącej nacisk na tworzenie aplikacji, które są łatwe do modyfikacji i późniejszej obsługi. Uwielbiam taki rodzaj aplikacji i nie tworzę aplikacji pozbawionych wymienionych cech. Zdaję jednak sobie sprawę, że niektórzy Czytelnicy po prostu chcą poznać funkcje oferowane przez platformę MVC bez zagłębiania się w filozofię i metodologię. Nie zamierzam Cię przekonywać do stosowania mojego podejścia — to decyzja osobista i sam wiesz najlepiej, jak przygotowywać własne projekty. Sugeruję Ci jednak przynajmniej pobieżne przejrzenie rozdziału, abyś mógł przekonać się, jakie możliwości oferuje platforma MVC. Jeżeli nie chcesz stosować testów jednostkowych podczas tworzenia aplikacji, od razu możesz przejść do kolejnego rozdziału, w którym dowiesz się, jak zbudować rzeczywistą aplikację.
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Tworzenie przykładowego projektu Pracę rozpoczynamy od utworzenia prostego, przykładowego projektu na potrzeby niniejszego rozdziału. Utwórz więc nowy projekt na podstawie szablonu Aplikacja sieci Web platformy ASP.NET MVC 4 i następnie wybierz szablon projektu Pusta, aby otrzymać projekt MVC pozbawiony treści początkowej. Ten sam rodzaj projektu tworzyliśmy we wcześniejszych rozdziałach. Projektowi nadaj nazwę EssentialTools.
Utworzenie klas modelu Kolejnym krokiem jest dodanie do katalogu Models pliku klasy o nazwie Product.cs o treści przedstawionej na listingu 6.1. To jest dokładnie ta sama klasa modelu, której używaliśmy w poprzednich rozdziałach. Jedyna różnica to zmiana w przestrzeni nazw, określającej teraz projekt EssentialTools. Listing 6.1. Klasa modelu namespace EssentialTools.Models { public class Product { public int ProductID { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } public string Category { set; get; } } }
Konieczne jest również utworzenie klasy odpowiedzialnej za zsumowanie wartości kolekcji obiektów Product. Do katalogu Models dodaj więc plik klasy o nazwie LinqValueCalculator.cs o treści przedstawionej na listingu 6.2. Listing 6.2. Klasa LinqValueCalculator using System.Collections.Generic; using System.Linq; namespace EssentialTools.Models { public class LinqValueCalculator { public decimal ValueProducts(IEnumerable products) { return products.Sum(p => p.Price); } } }
W klasie LinqValueCalculator została zdefiniowana pojedyncza metoda o nazwie ValueProducts, która używa metody LINQ Sum do zsumowania wartości właściwości Price wszystkich obiektów Product przekazanych metodzie (to użyteczna i często używana funkcja LINQ). Ostatnia klasa modelu to ShoppingCart, która przedstawia kolekcję obiektów Product i używa klasy LinqValueCalculator do ustalenia wartości całkowitej. Utwórz nowy plik klasy o nazwie ShoppingCart.cs i umieść w nim treść przedstawioną na listingu 6.3. Listing 6.3. Klasa ShoppingCart using System.Collections.Generic; namespace EssentialTools.Models {
134
ROZDZIAŁ 6. WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
public class ShoppingCart { private LinqValueCalculator calc; public ShoppingCart(LinqValueCalculator calcParam) { calc = calcParam; } public IEnumerable Products { get; set; } public decimal CalculateProductTotal() { return calc.ValueProducts(Products); } } }
Dodanie kontrolera Do katalogu Controllers dodaj nowy kontroler o nazwie HomeController i umieść w nim kod przedstawiony na listingu 6.4. Metoda akcji Index powoduje utworzenie tablicy obiektów Product i używa klasy LinqValueCalculator do zsumowania wartości całkowitej produktów przekazanych metodzie View. Ponieważ w trakcie wywoływania metody View nie zostaje podana nazwa widoku, platforma użyje widoku domyślnego. Listing 6.4. Kod kontrolera HomeController using EssentialTools.Models; using System.Linq; using System.Web.Mvc; namespace EssentialTools.Controllers { public class HomeController : Controller { Product[] products = { new Product {Name = "Kajak", Category="Sporty wodne", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Category="Sporty wodne", Price = 48.95M}, new Product {Name = "Piłka nożna", Category="Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Category="Piłka nożna", Price = 34.95M} }; public ActionResult Index() { LinqValueCalculator calc = new LinqValueCalculator(); ShoppingCart cart = new ShoppingCart(calc) { Products = products }; decimal totalValue = cart.CalculateProductTotal(); return View(totalValue); } } }
135
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Dodanie widoku Ostatnim dodatkiem do projektu jest widok. Kliknij prawym przyciskiem myszy metodę akcji Index w kontrolerze Home, a następnie wybierz opcję Dodaj widok… z menu kontekstowego. Utwórz widok o nazwie Index o ściśle określonym typie decimal. Po utworzeniu pliku widoku umieść w nim kod przedstawiony na listingu 6.5. Listing 6.5. Kod w pliku widoku Index.cshtml @model decimal @{ Layout = null; } Index
Wartość całkowita wynosi @Model zł
To jest bardzo prosty przykład użycia wyrażenia @Model w celu wyświetlenia wartości decimal otrzymanej z metody akcji. Jeżeli uruchomisz projekt, zobaczysz wartość całkowitą obliczoną przez klasę LinqValueCalculator, jak pokazano na rysunku 6.1.
Rysunek 6.1. Testowanie przykładowej aplikacji Wprawdzie to bardzo prosty projekt, ale wystarczający do przedstawienia różnych narzędzi i technik, które zostaną omówione w rozdziale.
Użycie Ninject W rozdziale 3. przedstawiliśmy temat DI. Dla przypomnienia — chcemy zapewnić rozłączenie komponentów aplikacji MVC przez użycie interfejsów oraz kontenera DI. W kolejnych punktach dokładnie omówię problem, jaki wprowadziliśmy w przykładowej aplikacji. Pokażę również, jak używać Ninject, czyli mojego ulubionego oprogramowania kontenera DI, które można wykorzystać do rozwiązania wspomnianego problemu.
136
ROZDZIAŁ 6. WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Zrozumienie problemu W przykładowej aplikacji mamy do czynienia z problemem, który można rozwiązać za pomocą kontenera DI. Utworzona przed chwilą przykładowa aplikacja opiera się na trzech ściśle powiązanych klasach. Klasa ShoppingCart jest ściśle powiązana z klasą LinqValueCalculator, natomiast klasa HomeController jest ściśle powiązana z klasami ShoppingCart i LinqValueCalculator. Oznacza to, że jeśli będziesz chciał zastąpić klasę LinqValueCalculator inną, wówczas będziesz musiał znaleźć wszystkie odniesienia do niej w klasach ściśle powiązanych z zastępowaną LinqValueCalculator. Nie jest to problemem w przypadku prostych aplikacji, takich jak przedstawiona w rozdziale, ale w rzeczywistych projektach operacja może stać się żmudna i podatna na wprowadzenie błędów, zwłaszcza jeśli chcesz użyć innej implementacji kalkulatora, zamiast jedynie zastąpić jedną klasę inną klasą.
Zastosowanie interfejsu Część problemu można rozwiązać przez zastosowanie interfejsu C# w celu oddzielenia funkcji kalkulatora od jego implementacji. Aby zademonstrować tego rodzaju rozwiązanie, trzeba dodać plik IValueCalculator.cs do katalogu Models i utworzyć interfejs przedstawiony na listingu 6.6. Listing 6.6. Interfejs IValueCalculator using System.Collections.Generic; namespace EssentialTools.Models { public class LinqValueCalculator : IValueCalculator { public decimal ValueProducts(IEnumerable products) { return products.Sum(p => p.Price); } } }
Następnie przygotowany interfejs można zaimplementować w klasie LinqValueCalculator, jak przedstawiono na listingu 6.7. Listing 6.7. Zastosowanie interfejsu w klasie LinqValueCalculator using System.Collections.Generic; using System.Linq; namespace EssentialTools.Models { public class LinqValueCalculator : IValueCalculator { public decimal ValueProducts(IEnumerable products) { return products.Sum(p => p.Price); } } }
Interfejs pozwala na rozluźnienie powiązania pomiędzy klasami ShoppingCart i LinqValueCalculator, co zostało przedstawione na listingu 6.8.
137
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
Listing 6.8. Zastosowanie interfejsu w klasie ShoppingCart using System.Collections.Generic; namespace EssentialTools.Models { public class ShoppingCart { private IValueCalculator calc; public ShoppingCart(IValueCalculator calcParam) { calc = calcParam; } public IEnumerable Products { get; set; } public decimal CalculateProductTotal() { return calc.ValueProducts(Products); } } }
Osiągnęliśmy pewien postęp, ale język C# wymaga wskazania implementacji klasy dla interfejsu podczas inicjalizacji, co jest oczywiste, ponieważ musi dokładnie wiedzieć, której implementacji klasy chcemy użyć. To oznacza, że nadal mamy problem w kontrolerze Home podczas tworzenia obiektu LinqValueCalculator: ... public ActionResult Index() { IValueCalculator calc = new LinqValueCalculator(); ShoppingCart cart = new ShoppingCart(calc) { Products = products }; decimal totalValue = cart.CalculateProductTotal(); return View(totalValue); } ...
Naszym celem użycia Ninject jest osiągnięcie rozwiązania, w którym wystarczy zadeklarować użycie interfejsu IValueCalculator, a wymagane szczegóły implementacji nie będą częścią kodu w kontrolerze Home.
Dodawanie Ninject do projektu Visual Studio Najłatwiejszym sposobem dodania Ninject do projektu jest użycie wbudowanego w Visual Studio mechanizmu obsługi NuGet, co znacznie ułatwia instalację i aktualizację pakietów. Wybierz opcję menu Narzędzia/Menedżer pakietów bibliotek/Zarządzaj pakietami NuGet dla rozwiązania…. Na ekranie zostanie wyświetlone okno dialogowe zarządzania pakietami NuGet. Kliknij Online po lewej stronie, a następnie w polu wyszukiwania znajdującym się w prawym górnym rogu okna wpisz Ninject. Wyświetli się kilka pozycji pokazanych na rysunku 6.2. (Możesz otrzymać inne wyniki wyszukiwania, co odzwierciedli uaktualnienia pakietów wprowadzone od czasu napisania tego rozdziału). Kliknij przycisk Install znajdujący się po prawej stronie pozycji. Visual Studio rozpocznie proces pobierania biblioteki i jej instalacji w pakiecie — podzespół Ninject zostanie pobrany i umieszczony w katalogu Odwołania projektu.
138
ROZDZIAŁ 6. WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Rysunek 6.2. Dodawanie Ninject do projektu Visual Studio NuGet to doskonały sposób pobierania i obsługi pakietów używanych w Visual Studio, gorąco zachęcam do jego stosowania. Jednak na potrzeby niniejszej książki musimy zastosować inne podejście, ponieważ użycie NuGet powoduje dodanie do projektu ogromnej ilości informacji dotyczących zależności i śledzenia, aby zapewnić synchronizację i aktualność pakietów. Wspomniane dane dodatkowe podwajają wielkość naszego prostego przykładowego projektu. Z tego powodu kod źródłowy udostępniony na witrynie wydawnictwa Helion przez wielu Czytelników byłby uznany za zbyt duży. Zamiast tego pobierz najnowszą wersję biblioteki z witryny Ninject (http://www.ninject.org/) i zainstaluj ją ręcznie. W Visual Studio wybierz opcję menu Projekt/Dodaj odwołanie…, a następnie po kliknięciu przycisku Przeglądaj… przejdź do katalogu, w którym rozpakowałeś pobrany pakiet Ninject. Zaznacz plik Ninject.dll w celu jego ręcznego dodania do projektu. Efekt jest taki sam jak w przypadku użycia menedżera pakietów NuGet, ale oczywiście bez zalet w postaci prostych uaktualnień i zarządzania zależnościami pakietu. Uwaga Chcę w tym miejscu wyraźnie podkreślić, że jedynym powodem przedstawionego tutaj ręcznego dodania pakietu Ninject jest ograniczenie wielkości materiałów dołączonych do książki. We własnych projektach stosuję NuGet, ponieważ kilka dodatkowych megabajtów naprawdę nie robi żadnej różnicy. Tobie również zalecam używanie NuGet.
Zaczynamy korzystać z Ninject W celu uzyskania podstawowej funkcjonalności Ninject konieczne jest wykonanie trzech kroków — wszystkie zostały przedstawione na listingu 6.9. W wymienionym listingu zaprezentowano zmiany, które trzeba wprowadzić w kontrolerze Home. Listing 6.9. Dodanie podstawowej funkcjonalności Ninject do metody akcji Index using EssentialTools.Models; using System.Linq; using System.Web.Mvc;
139
CZĘŚĆ I WPROWADZENIE DO ASP.NET MVC 4
using Ninject; namespace EssentialTools.Controllers { public class HomeController : Controller { Product[] products = { new Product {Name = "Kajak", Category="Sporty wodne", Price = 275M}, new Product {Name = "Kamizelka ratunkowa", Category="Sporty wodne", Price = 48.95M}, new Product {Name = "Piłka nożna", Category="Piłka nożna", Price = 19.50M}, new Product {Name = "Flaga narożna", Category="Piłka nożna", Price = 34.95M} }; public ActionResult Index() { IKernel ninjectKernel = new StandardKernel(); ninjectKernel.Bind().To(); IValueCalculator calc = ninjectKernel.Get(); ShoppingCart cart = new ShoppingCart(calc) { Products = products }; decimal totalValue = cart.CalculateProductTotal(); return View(totalValue); } } }
Pierwszym krokiem jest utworzenie obiektu kernel Ninject, pozwalającego nam komunikować się z platformą i żądać implementacji interfejsów. Poniżej przedstawiono polecenie, które na listingu 6.9 tworzy wspomniany obiekt: ... IKernel ninjectKernel = new StandardKernel(); ...
Konieczne jest utworzenie implementacji interfejsu Ninject.IKernel, co też czynimy poprzez utworzenie nowego egzemplarza klasy StandardKernel. Dzięki temu możemy przejść do kroku drugiego, czyli konfiguracji związku pomiędzy interfejsami w aplikacji i implementacjami klas, z którymi będziemy pracować. Poniżej przedstawiono polecenie, które na listingu 6.9 jest odpowiedzialne za wykonanie kroku drugiego: ... ninjectKernel.Bind().To(); ...
Ninject używa typu parametrów C# w celu utworzenia związku: interfejs, z którym chcemy pracować, konfigurujemy jako typ parametru dla metody Bind i wywołujemy metodę To względem otrzymanych wyników. Implementację klasy, którą chcemy ustanowić, konfigurujemy jako typ parametru metody To. Powyższe polecenie informuje Ninejct, że kiedy prosimy o implementację interfejsu IValueCalculator, spełnienie żądania powinno polegać na utworzeniu nowego egzemplarza klasy LinqValueCalculator. Ostatnim krokiem jest faktyczne użycie Ninject, co odbywa się za pomocą metody Get w następujący sposób: ... IValueCalculator calc = ninjectKernel.Get(); ...
140
ROZDZIAŁ 6. WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Typ parametru użyty dla metody Get informuje Ninject o interesującym nas interfejsie. Wynikiem działania metody jest egzemplarz typu implementacji wskazany chwilę wcześniej w metodzie To.
Konfiguracja wstrzykiwania zależności na platformie MVC Wynikiem wykonania trzech kroków przedstawionych w poprzednim listingu jest określenie w Ninject, która implementacja klasy powinna zostać użyta do spełnienia żądania interfejsu IValueCalculator. Oczywiście w żaden sposób jeszcze nie usprawniliśmy aplikacji, ponieważ wspomniana wiedza nadal pozostaje zdefiniowana w kontrolerze Home, co oznacza dalsze ścisłe powiązanie kontrolera Home z klasą LinqValueCalculator. W kolejnych punktach pokażę Ci, jak osadzić Ninject w sercu przykładowej aplikacji MVC. Dzięki temu możliwe stanie się uproszczenie kontrolera i rozszerzenie wpływu Ninject na całą aplikację. Ponadto dowiesz się, jak skonfigurować kilka użytecznych przykładów kolejnych funkcji Ninject.
Tworzenie mechanizmu rozwiązywania zależności Pierwszą zmianą, którą trzeba wprowadzić, jest utworzenie własnego mechanizmu rozwiązywania zależności. Platforma MVC wykorzystuje mechanizmy rozwiązywania zależności w celu tworzenia egzemplarzy klas potrzebnych do obsługi żądań. Dzięki utworzeniu własnego mechanizmu mamy gwarancję użycia Ninject za każdym razem, gdy obiekt będzie tworzony. Do omawianego projektu dodaj nowy katalog o nazwie Infrastructure, a następnie umieść w nim nowy plik o nazwie NinjectDependencyResolver.cs. W nowo dodanym pliku powinien znaleźć się kod przedstawiony na listingu 6.10. Listing 6.10. Kod, który należy umieścić w pliku NinjectDependencyResolver.cs using using using using using using using using