Uploaded by User1629

Freeman A. - ASP.NET MVC 5. Zaawansowane programowanie

advertisement
Spis treści
O autorze ................................................................................................................17
O recenzencie technicznym .....................................................................................18
Rozdział 1.
ASP.NET MVC w szerszym kontekście . ...................................................................19
Krótka historia programowania witryn WWW . ............................................................................ 19
Co poszło nie tak z ASP.NET Web Forms? . ............................................................................ 20
Programowanie witryn WWW — stan obecny . ............................................................................ 21
Standardy sieciowe oraz REST . .................................................................................................. 21
Programowanie zwinne i sterowane testami . .......................................................................... 22
Ruby on Rails ................................................................................................................................ 22
Node.js ........................................................................................................................................... 22
Najważniejsze zalety ASP.NET MVC ...............................................................................................23
Architektura MVC ....................................................................................................................... 23
Rozszerzalność ............................................................................................................................. 24
Ścisła kontrola nad HTML i HTTP . .......................................................................................... 24
Łatwość testowania ...................................................................................................................... 24
Zaawansowany system routingu . .............................................................................................. 25
Zbudowany na najlepszych częściach platformy ASP.NET . ................................................. 25
Nowoczesne API .......................................................................................................................... 26
ASP.NET MVC jest open source . .............................................................................................. 26
Co powinienem wiedzieć? ................................................................................................................. 26
Jaka jest struktura książki? ................................................................................................................. 27
Część I. Wprowadzenie do ASP.NET MVC 5 . ........................................................................ 27
Część II. Szczegółowe omówienie platformy ASP.NET MVC . ............................................. 27
Co nowego w ASP.NET MVC 5? . .................................................................................................... 27
Gdzie znajdę przykładowe fragmenty kodu? . ................................................................................. 28
Jakiego oprogramowania będę potrzebował? . ................................................................................ 28
Bootstrap .............................................................................................................................................. 29
Podsumowanie .................................................................................................................................... 29
SPIS TREŚCI
Rozdział 2.
Pierwsza aplikacja MVC ..........................................................................................31
Przygotowanie Visual Studio . ........................................................................................................... 31
Tworzenie nowego projektu ASP.NET MVC . ............................................................................... 31
Dodawanie pierwszego kontrolera . ........................................................................................... 34
Poznajemy trasy ........................................................................................................................... 37
Generowanie stron WWW ............................................................................................................... 37
Tworzenie i generowanie widoku . ............................................................................................ 37
Dynamiczne dodawanie treści ....................................................................................................41
Tworzenie prostej aplikacji wprowadzania danych . ...................................................................... 42
Przygotowanie sceny ................................................................................................................... 42
Projektowanie modelu danych . ................................................................................................. 43
Łączenie metod akcji ................................................................................................................... 44
Budowanie formularza ................................................................................................................ 47
Zdefiniowanie początkowego adresu URL . ............................................................................. 49
Obsługa formularzy ..................................................................................................................... 50
Dodanie kontroli poprawności ...................................................................................................53
Nadanie stylu zawartości ............................................................................................................ 58
Kończymy przykład ..................................................................................................................... 63
Podsumowanie .................................................................................................................................... 64
Rozdział 3.
Wzorzec MVC ..........................................................................................................65
Historia MVC ...................................................................................................................................... 65
Wprowadzenie do wzorca MVC . ..................................................................................................... 66
Budowa modelu domeny ............................................................................................................ 66
Implementacja MVC w ASP.NET . ............................................................................................ 67
Porównanie MVC z innymi wzorcami . .................................................................................... 67
Budowanie luźno połączonych komponentów . ............................................................................. 70
Wykorzystanie wstrzykiwania zależności . ............................................................................... 71
Użycie kontenera wstrzykiwania zależności . ........................................................................... 72
Zaczynamy testy automatyczne . ....................................................................................................... 74
Zadania testów jednostkowych ...................................................................................................74
Zadania testów integracyjnych . ................................................................................................. 79
Podsumowanie .................................................................................................................................... 79
Rozdział 4.
Najważniejsze cechy języka ....................................................................................81
Utworzenie przykładowego projektu . .............................................................................................. 81
Dodanie podzespołu System.Net.Http . .................................................................................... 83
Użycie automatycznie implementowanych właściwości . ................................................................. 83
Użycie inicjalizatorów obiektów i kolekcji . ..................................................................................... 86
Użycie metod rozszerzających . ......................................................................................................... 88
Stosowanie metod rozszerzających do interfejsów . ................................................................ 90
Tworzenie filtrujących metod rozszerzających . ...................................................................... 92
Użycie wyrażeń lambda ..................................................................................................................... 93
Automatyczna inferencja typów . ...................................................................................................... 97
Użycie typów anonimowych ............................................................................................................. 97
Wykonywanie zapytań LINQ ........................................................................................................... 98
Opóźnione zapytania LINQ . .................................................................................................... 102
Użycie metod asynchronicznych . ................................................................................................... 103
Użycie słów kluczowych async i await . ................................................................................... 105
Podsumowanie .................................................................................................................................. 106
6
SPIS TREŚCI
Rozdział 5.
Praca z silnikiem Razor .........................................................................................107
Utworzenie przykładowego projektu . ............................................................................................ 107
Definiowanie modelu ................................................................................................................ 108
Definiowanie kontrolera . .......................................................................................................... 108
Tworzenie widoku ..................................................................................................................... 109
Korzystanie z obiektów modelu . .................................................................................................... 109
Praca z układami ............................................................................................................................... 111
Tworzenie układu ...................................................................................................................... 112
Stosowanie układu ..................................................................................................................... 113
Użycie pliku ViewStart .............................................................................................................. 114
Użycie układów współdzielonych . .......................................................................................... 115
Użycie wyrażeń Razor ...................................................................................................................... 118
Wstawianie wartości danych . ................................................................................................... 119
Przypisanie wartości atrybutu . ................................................................................................. 121
Użycie konstrukcji warunkowych . .......................................................................................... 123
Wyświetlanie zawartości tablic i kolekcji . .............................................................................. 125
Praca z przestrzenią nazw ......................................................................................................... 127
Podsumowanie .................................................................................................................................. 128
Rozdział 6.
Ważne narzędzia wspierające MVC . ....................................................................129
Tworzenie przykładowego projektu . ............................................................................................. 130
Utworzenie klas modelu ........................................................................................................... 130
Dodanie kontrolera ................................................................................................................... 132
Dodanie widoku ......................................................................................................................... 132
Użycie Ninject ................................................................................................................................... 133
Zrozumienie problemu ............................................................................................................. 133
Dodawanie Ninject do projektu Visual Studio . ..................................................................... 135
Zaczynamy korzystać z Ninject . .............................................................................................. 136
Konfiguracja wstrzykiwania zależności na platformie MVC . ............................................. 137
Tworzenie łańcucha zależności . ............................................................................................... 140
Definiowanie wartości właściwości i parametrów konstruktora . ....................................... 142
Użycie łączenia warunkowego . ................................................................................................ 143
Ustawienie obiektu zakresu . ..................................................................................................... 144
Testy jednostkowe w Visual Studio . ............................................................................................... 147
Tworzenie projektu testów jednostkowych . .......................................................................... 147
Tworzenie testów jednostkowych . .......................................................................................... 148
Uruchamianie testów (nieudane) . ........................................................................................... 152
Implementacja funkcji .............................................................................................................. 152
Testowanie i poprawianie kodu . .............................................................................................. 153
Użycie Moq ........................................................................................................................................ 155
Zrozumienie problemu ............................................................................................................. 155
Dodawanie Moq do projektu Visual Studio . ......................................................................... 157
Dodanie obiektu imitacyjnego do testu jednostkowego . ..................................................... 157
Tworzenie bardziej skomplikowanych obiektów Mock . ..................................................... 160
Podsumowanie .................................................................................................................................. 162
7
SPIS TREŚCI
Rozdział 7.
SportsStore — kompletna aplikacja . ...................................................................163
Zaczynamy ......................................................................................................................................... 164
Tworzenie rozwiązania i projektów w Visual Studio . .......................................................... 164
Instalacja pakietów narzędziowych . ........................................................................................ 166
Dodawanie odwołań między projektami . .............................................................................. 166
Konfigurowanie kontenera DI . ................................................................................................ 167
Uruchamiamy aplikację ............................................................................................................ 168
Tworzenie modelu domeny ............................................................................................................ 168
Tworzenie abstrakcyjnego repozytorium . .............................................................................. 169
Tworzenie imitacji repozytorium . ........................................................................................... 169
Wyświetlanie listy produktów . ....................................................................................................... 171
Dodawanie kontrolera . ............................................................................................................. 171
Dodawanie układu, pliku ViewStart i widoku . ...................................................................... 172
Konfigurowanie domyślnej trasy . ............................................................................................ 173
Uruchamianie aplikacji ............................................................................................................. 174
Przygotowanie bazy danych ............................................................................................................ 175
Tworzenie bazy danych ............................................................................................................ 176
Definiowanie schematu bazy danych . .................................................................................... 177
Dodawanie danych do bazy ...................................................................................................... 179
Tworzenie kontekstu Entity Framework . ............................................................................... 180
Tworzenie repozytorium produktów . .................................................................................... 182
Dodanie stronicowania .................................................................................................................... 184
Wyświetlanie łączy stron .......................................................................................................... 185
Ulepszanie adresów URL . ......................................................................................................... 193
Dodawanie stylu ................................................................................................................................ 194
Instalacja pakietu Bootstrap . .................................................................................................... 194
Zastosowanie w aplikacji stylów Bootstrap . ........................................................................... 195
Tworzenie widoku częściowego . ............................................................................................. 196
Podsumowanie .................................................................................................................................. 199
Rozdział 8.
SportsStore — nawigacja .....................................................................................201
Dodawanie kontrolek nawigacji . .................................................................................................... 201
Filtrowanie listy produktów . .................................................................................................... 201
Ulepszanie schematu URL ........................................................................................................ 205
Budowanie menu nawigacji po kategoriach . ......................................................................... 208
Poprawianie licznika stron . ...................................................................................................... 213
Budowanie koszyka na zakupy . ...................................................................................................... 216
Definiowanie encji koszyka . ..................................................................................................... 217
Tworzenie przycisków koszyka . .............................................................................................. 221
Implementowanie kontrolera koszyka . .................................................................................. 222
Wyświetlanie zawartości koszyka . ........................................................................................... 223
Podsumowanie .................................................................................................................................. 226
Rozdział 9.
SportsStore — ukończenie koszyka na zakupy . ...................................................227
Użycie dołączania danych ............................................................................................................... 227
Tworzenie własnego łącznika modelu . ................................................................................... 227
Kończenie budowania koszyka . ...................................................................................................... 231
Usuwanie produktów z koszyka . ............................................................................................. 232
Dodawanie podsumowania koszyka . ...................................................................................... 233
8
SPIS TREŚCI
Składanie zamówień ......................................................................................................................... 236
Rozszerzanie modelu domeny . ................................................................................................ 236
Dodawanie procesu składania zamówienia . .......................................................................... 236
Implementowanie mechanizmu przetwarzania zamówień . ................................................ 242
Rejestrowanie implementacji . .................................................................................................. 244
Zakończenie pracy nad kontrolerem koszyka . ...................................................................... 246
Wyświetlanie informacji o błędach systemu kontroli poprawności . ................................. 249
Wyświetlanie strony podsumowania . ..................................................................................... 251
Podsumowanie .................................................................................................................................. 252
Rozdział 10. SportsStore — wersja mobilna ...............................................................................253
Kontekst programowania sieciowego dla urządzeń mobilnych . ............................................... 253
Odstąpienie od działania (lub jego podjęcie na minimalnym możliwym poziomie) ....... 254
Użycie układu responsywnego . ...................................................................................................... 255
Utworzenie responsywnego nagłówka . .................................................................................. 256
Tworzenie responsywnej listy produktów . ............................................................................ 260
Utworzenie zawartości specjalnie dla urządzeń mobilnych . ...................................................... 267
Utworzenie układu dla urządzeń mobilnych . ........................................................................ 268
Utworzenie widoków dla urządzeń mobilnych . .................................................................... 269
Podsumowanie .................................................................................................................................. 272
Rozdział 11. SportsStore — administracja ................................................................................275
Dodajemy zarządzanie katalogiem . ................................................................................................ 275
Tworzenie kontrolera CRUD . .................................................................................................. 276
Tworzenie nowego pliku układu . ............................................................................................ 277
Implementowanie widoku listy . .............................................................................................. 278
Edycja produktów ...................................................................................................................... 282
Tworzenie nowych produktów . ............................................................................................... 295
Usuwanie produktów ................................................................................................................ 298
Podsumowanie .................................................................................................................................. 301
Rozdział 12. SportsStore — bezpieczeństwo i ostatnie usprawnienia . ....................................303
Zabezpieczanie kontrolera administracyjnego . ............................................................................ 303
Zdefiniowanie prostej polityki bezpieczeństwa . .................................................................... 303
Realizacja uwierzytelniania z użyciem filtrów . ...................................................................... 305
Tworzenie dostawcy uwierzytelniania . ................................................................................... 306
Tworzenie kontrolera AccountController . ............................................................................ 308
Tworzenie widoku ..................................................................................................................... 309
Przesyłanie zdjęć ............................................................................................................................... 312
Rozszerzanie bazy danych ........................................................................................................ 312
Rozszerzanie modelu domeny . ................................................................................................ 313
Tworzenie interfejsu użytkownika do przesyłania plików . ................................................. 314
Zapisywanie zdjęć do bazy danych . ......................................................................................... 316
Implementowanie metody akcji GetImage . ........................................................................... 317
Wyświetlanie zdjęć produktów . ............................................................................................... 321
Podsumowanie .................................................................................................................................. 322
9
SPIS TREŚCI
Rozdział 13. Wdrażanie aplikacji ..............................................................................................323
Przygotowanie do użycia Windows Azure . .................................................................................. 324
Tworzenie witryny internetowej i bazy danych . ................................................................... 324
Przygotowanie bazy danych do zdalnej administracji . ........................................................ 325
Tworzenie schematu bazy danych . ......................................................................................... 326
Wdrażanie aplikacji .......................................................................................................................... 328
Podsumowanie .................................................................................................................................. 332
Rozdział 14. Przegląd projektu MVC .........................................................................................333
Korzystanie z projektów MVC z Visual Studio . ........................................................................... 333
Tworzenie projektu ................................................................................................................... 334
Przedstawienie konwencji MVC . ............................................................................................ 337
Debugowanie aplikacji MVC .......................................................................................................... 338
Tworzenie przykładowego projektu . ...................................................................................... 338
Uruchamianie debugera Visual Studio . .................................................................................. 341
Przerywanie pracy aplikacji przez debuger Visual Studio . .................................................. 342
Użycie opcji Edit and Continue . .............................................................................................. 347
Użycie funkcji połączonych przeglądarek . .................................................................................... 350
Podsumowanie .................................................................................................................................. 351
Rozdział 15. Routing URL ..........................................................................................................353
Utworzenie przykładowego projektu . ............................................................................................ 353
Utworzenie przykładowych kontrolerów . ............................................................................ 355
Utworzenie widoku ................................................................................................................... 356
Ustawienie początkowego adresu URL i przetestowanie aplikacji . .................................... 356
Wprowadzenie do wzorców URL . ................................................................................................. 357
Tworzenie i rejestrowanie prostej trasy . ........................................................................................ 358
Użycie prostej trasy .................................................................................................................. 363
Definiowanie wartości domyślnych . .............................................................................................. 363
Użycie statycznych segmentów adresu URL . ............................................................................... 366
Definiowanie własnych zmiennych segmentów . ......................................................................... 370
Użycie własnych zmiennych jako parametrów metod akcji . .............................................. 372
Definiowanie opcjonalnych segmentów URL . ...................................................................... 373
Definiowanie tras o zmiennej długości . ................................................................................. 375
Definiowanie priorytetów kontrolerów na podstawie przestrzeni nazw . ......................... 377
Ograniczenia tras .............................................................................................................................. 380
Ograniczanie trasy z użyciem wyrażeń regularnych . ........................................................... 380
Ograniczanie trasy do zbioru wartości . .................................................................................. 381
Ograniczanie tras z użyciem metod HTTP . ........................................................................... 381
Użycie ograniczeń dotyczących typu i wartości . ................................................................... 383
Definiowanie własnych ograniczeń . ....................................................................................... 385
Użycie atrybutów routingu ............................................................................................................. 387
Włączanie i stosowanie atrybutów routingu . ........................................................................ 387
Tworzenie tras za pomocą zmiennych segmentu . ................................................................ 389
Zastosowanie ograniczeń trasy . ............................................................................................... 390
Użycie prefiksu trasy ................................................................................................................. 392
Podsumowanie .................................................................................................................................. 393
10
SPIS TREŚCI
Rozdział 16. Zaawansowane funkcje routingu . .......................................................................395
Utworzenie przykładowego projektu . ............................................................................................ 396
Uproszczenie tras ....................................................................................................................... 396
Dodanie pakietu optymalizacyjnego . ...................................................................................... 396
Uaktualnienie projektu testów jednostkowych . .................................................................... 397
Generowanie wychodzących adresów URL w widokach . .............................................................. 397
Użycie systemu routingu do wygenerowania wychodzącego adresu URL . ............................ 397
Użycie innych kontrolerów . ..................................................................................................... 400
Przekazywanie dodatkowych parametrów . ............................................................................ 401
Definiowanie atrybutów HTML . ............................................................................................. 403
Generowanie w pełni kwalifikowanych adresów URL w łączach . ...................................... 404
Generowanie adresów URL (nie łączy) . ................................................................................. 405
Generowanie wychodzących adresów URL w metodach akcji . .......................................... 406
Generowanie adresu URL na podstawie wybranej trasy . .................................................... 407
Dostosowanie systemu routingu . ................................................................................................... 408
Tworzenie własnej implementacji RouteBase . ...................................................................... 408
Tworzenie własnego obiektu obsługi trasy . ........................................................................... 412
Korzystanie z obszarów ................................................................................................................... 414
Tworzenie obszaru ..................................................................................................................... 414
Wypełnianie obszaru ................................................................................................................. 416
Rozwiązywanie problemów z niejednoznacznością kontrolerów . ..................................... 417
Tworzenie obszarów za pomocą atrybutów . ......................................................................... 418
Generowanie łączy do akcji z obszarów . ................................................................................ 419
Routing żądań dla plików dyskowych . .......................................................................................... 420
Konfiguracja serwera aplikacji . ................................................................................................ 421
Definiowanie tras dla plików na dysku . .................................................................................. 422
Pomijanie systemu routingu . .......................................................................................................... 424
Najlepsze praktyki schematu adresów URL . ................................................................................ 424
Twórz jasne i przyjazne dla człowieka adresy URL . ............................................................. 425
GET oraz POST — wybierz właściwie . ................................................................................... 426
Podsumowanie .................................................................................................................................. 426
Rozdział 17. Kontrolery i akcje ..................................................................................................427
Utworzenie przykładowego projektu . ............................................................................................ 428
Ustawienie początkowego adresu URL . ................................................................................. 428
Wprowadzenie do kontrolerów . ..................................................................................................... 428
Tworzenie kontrolera z użyciem interfejsu IController . ..................................................... 428
Tworzenie kontrolera przez dziedziczenie po klasie Controller . ........................................ 430
Odczytywanie danych wejściowych . .............................................................................................. 432
Pobieranie danych z obiektów kontekstu . .............................................................................. 432
Użycie parametrów metod akcji . ............................................................................................. 433
Tworzenie danych wyjściowych . .................................................................................................... 435
Wyniki akcji ................................................................................................................................ 436
Zwracanie kodu HTML przez generowanie widoku . ........................................................... 440
Przekazywanie danych z metody akcji do widoku . ............................................................... 443
Wykonywanie przekierowań . .................................................................................................. 447
Zwracanie błędów i kodów HTTP . ......................................................................................... 452
Podsumowanie .................................................................................................................................. 453
11
SPIS TREŚCI
Rozdział 18. Filtry . ....................................................................................................................455
Utworzenie przykładowego projektu . ............................................................................................ 456
Ustawienie początkowego adresu URL i przetestowanie aplikacji . .................................... 458
Użycie filtrów .................................................................................................................................... 458
Wprowadzenie do podstawowych typów filtrów . ................................................................. 459
Dołączanie filtrów do kontrolerów i metod akcji . ................................................................ 460
Użycie filtrów autoryzacji . ............................................................................................................... 461
Użycie własnego filtra autoryzacji . .......................................................................................... 462
Użycie wbudowanego filtra autoryzacji . ................................................................................ 463
Użycie filtrów uwierzytelniania . ..................................................................................................... 464
Interfejs IAuthenticationFilter . ................................................................................................ 464
Implementacja sprawdzenia uwierzytelniania . ..................................................................... 466
Połączenie filtrów uwierzytelniania i autoryzacji . ................................................................ 468
Obsługa ostatniego uwierzytelnienia w żądaniu . .................................................................. 469
Użycie filtrów wyjątków .................................................................................................................. 470
Tworzenie filtra wyjątku ........................................................................................................... 470
Użycie filtra wyjątków ............................................................................................................... 471
Użycie widoku w celu reakcji na wyjątek . .............................................................................. 474
Użycie wbudowanego filtra wyjątków . ................................................................................... 476
Użycie filtrów akcji ........................................................................................................................... 478
Implementacja metody OnActionExecuting . ........................................................................ 479
Implementacja metody OnActionExecuted . ......................................................................... 481
Używanie filtra wyniku .................................................................................................................... 482
Użycie wbudowanych klas filtrów akcji i wyniku . ................................................................ 483
Użycie innych funkcji filtrów . ......................................................................................................... 485
Filtrowanie bez użycia atrybutów . ........................................................................................... 485
Użycie filtrów globalnych ......................................................................................................... 487
Określanie kolejności wykonywania filtrów . ......................................................................... 489
Nadpisywanie filtrów ................................................................................................................ 491
Podsumowanie .................................................................................................................................. 494
Rozdział 19. Rozszerzanie kontrolerów .....................................................................................495
Utworzenie przykładowego projektu . ............................................................................................ 496
Ustawienie początkowego adresu URL . ................................................................................. 498
Tworzenie własnej fabryki kontrolerów . ....................................................................................... 498
Przygotowanie kontrolera zapasowego . ................................................................................. 500
Utworzenie klasy kontrolera . ................................................................................................... 500
Implementacja innych metod interfejsu . ............................................................................... 501
Rejestrowanie własnej fabryki kontrolerów . .......................................................................... 501
Wykorzystanie wbudowanej fabryki kontrolerów . ...................................................................... 502
Nadawanie priorytetów przestrzeniom nazw . ....................................................................... 502
Dostosowywanie sposobu tworzenia kontrolerów w DefaultControllerFactory . ............ 504
Tworzenie własnego obiektu wywołującego akcje . ...................................................................... 506
Użycie wbudowanego obiektu wywołującego akcje . ................................................................... 508
Użycie własnych nazw akcji . .................................................................................................... 508
Selekcja metod akcji ................................................................................................................... 509
12
SPIS TREŚCI
Poprawianie wydajności z użyciem specjalizowanych kontrolerów . ........................................ 515
Użycie kontrolerów bezstanowych . ........................................................................................ 515
Użycie kontrolerów asynchronicznych . ................................................................................. 517
Podsumowanie .................................................................................................................................. 521
Rozdział 20. Widoki ..................................................................................................................523
Tworzenie własnego silnika widoku . ............................................................................................. 523
Tworzenie przykładowego projektu . ...................................................................................... 526
Tworzenie własnej implementacji IView . .............................................................................. 527
Tworzenie implementacji IViewEngine . ................................................................................ 528
Rejestrowanie własnego silnika widoku . ................................................................................ 529
Testowanie silnika widoku . ...................................................................................................... 529
Korzystanie z silnika Razor ............................................................................................................. 531
Tworzenie przykładowego projektu . ...................................................................................... 531
Sposób generowania widoków przez Razor . .......................................................................... 532
Konfigurowanie wyszukiwania lokalizacji widoków . ........................................................... 533
Dodawanie dynamicznych treści do widoku Razor . ................................................................... 536
Zastosowanie sekcji układu . ..................................................................................................... 536
Użycie widoków częściowych . ................................................................................................. 541
Użycie akcji potomnych . ........................................................................................................... 544
Podsumowanie .................................................................................................................................. 546
Rozdział 21. Metody pomocnicze .............................................................................................547
Tworzenie przykładowego projektu ................................................................................................ 548
Ustawienie początkowego adresu URL . ................................................................................. 549
Przetestowanie aplikacji ............................................................................................................ 549
Tworzenie własnej metody pomocniczej . ..................................................................................... 549
Tworzenie wewnętrznej metody pomocniczej HTML . ........................................................ 549
Tworzenie zewnętrznej metody pomocniczej HTML . ......................................................... 551
Zarządzanie kodowaniem ciągów tekstowych w metodzie pomocniczej . ........................ 554
Użycie wbudowanych metod pomocniczych . .............................................................................. 559
Przygotowania do obsługi formularzy . ................................................................................... 559
Określenie trasy używanej przez formularz . .......................................................................... 565
Użycie metod pomocniczych do wprowadzania danych . .................................................... 567
Tworzenie znaczników select . .................................................................................................. 571
Podsumowanie .................................................................................................................................. 573
Rozdział 22. Szablonowe metody pomocnicze . .......................................................................575
Przygotowanie przykładowego projektu . ...................................................................................... 576
Używanie szablonowych metod pomocniczych . .......................................................................... 578
Generowanie etykiety i wyświetlanie elementów . ................................................................. 581
Użycie szablonowych metod pomocniczych dla całego modelu . ....................................... 583
Użycie metadanych modelu ............................................................................................................ 586
Użycie metadanych do sterowania edycją i widocznością . .................................................. 586
Użycie metadanych dla etykiet . ............................................................................................... 589
Użycie metadanych wartości danych . ..................................................................................... 590
Użycie metadanych do wybierania szablonu wyświetlania . ................................................ 591
Dodawanie metadanych do klasy zaprzyjaźnionej . .............................................................. 593
Korzystanie z parametrów typów złożonych . ........................................................................ 595
13
SPIS TREŚCI
Dostosowywanie systemu szablonowych metod pomocniczych . .............................................. 596
Tworzenie własnego szablonu edytora . .................................................................................. 596
Tworzenie szablonu ogólnego . ................................................................................................ 597
Zastępowanie szablonów wbudowanych . .............................................................................. 599
Podsumowanie .................................................................................................................................. 599
Rozdział 23. Metody pomocnicze URL i Ajax .............................................................................601
Przygotowanie przykładowego projektu . ...................................................................................... 602
Definiowanie dodatkowych stylów CSS . ................................................................................ 603
Instalacja pakietów NuGet . ...................................................................................................... 603
Tworzenie podstawowych łączy i adresów URL . ......................................................................... 603
Nieprzeszkadzający Ajax ................................................................................................................. 605
Tworzenie widoku formularza synchronicznego . ................................................................ 606
Włączanie i wyłączanie nieprzeszkadzających wywołań Ajax . ........................................... 607
Utworzenie nieprzeszkadzających formularzy Ajax . ................................................................... 608
Przygotowanie kontrolera ........................................................................................................ 608
Tworzenie formularza Ajax ...................................................................................................... 610
Sposób działania nieprzeszkadzających wywołań Ajax . ...................................................... 612
Ustawianie opcji Ajax ....................................................................................................................... 612
Zapewnienie kontrolowanej degradacji . ................................................................................ 612
Informowanie użytkownika o realizowanym żądaniu Ajax . ............................................... 614
Wyświetlanie pytania przed wysłaniem żądania . .................................................................. 615
Tworzenie łączy Ajax ....................................................................................................................... 616
Zapewnienie kontrolowanej degradacji dla łączy . ................................................................ 618
Korzystanie z funkcji wywołania zwrotnego w technologii Ajax . ............................................. 618
Wykorzystanie JSON ........................................................................................................................ 621
Dodanie obsługi JSON do kontrolera . .................................................................................... 621
Przetwarzanie JSON w przeglądarce . ...................................................................................... 622
Przygotowanie danych do kodowania . ................................................................................... 624
Wykrywanie żądań Ajax w metodach akcji . .......................................................................... 626
Podsumowanie .................................................................................................................................. 628
Rozdział 24. Dołączanie modelu ...............................................................................................629
Przygotowanie przykładowego projektu . ...................................................................................... 630
Użycie dołączania modelu . .............................................................................................................. 632
Użycie domyślnego łącznika modelu . ............................................................................................ 633
Dołączanie typów prostych ...................................................................................................... 634
Dołączanie typów złożonych . ................................................................................................... 636
Dołączanie tablic i kolekcji ....................................................................................................... 643
Ręczne wywoływanie dołączania modelu . .................................................................................... 648
Obsługa błędów dołączania modelu . ...................................................................................... 650
Dostosowanie systemu dołączania modelu . ................................................................................. 650
Tworzenie własnego dostawcy wartości . ................................................................................ 651
Tworzenie własnego łącznika modelu . ................................................................................... 653
Rejestracja własnego łącznika modelu . ................................................................................... 655
Podsumowanie .................................................................................................................................. 656
14
SPIS TREŚCI
Rozdział 25. Kontrola poprawności modelu .............................................................................657
Utworzenie przykładowego projektu . ............................................................................................ 658
Utworzenie układu .................................................................................................................... 659
Utworzenie widoków ................................................................................................................ 660
Jawna kontrola poprawności modelu . ........................................................................................... 661
Wyświetlenie użytkownikowi błędów podczas kontroli poprawności . ............................. 662
Wyświetlanie komunikatów kontroli poprawności . ................................................................... 664
Wyświetlanie komunikatów kontroli poprawności poziomu właściwości . ........................... 667
Użycie alternatywnych technik kontroli poprawności . ............................................................... 668
Kontrola poprawności w łączniku modelu . ........................................................................... 668
Definiowanie zasad poprawności za pomocą metadanych . ................................................ 670
Definiowanie modeli automatycznie przeprowadzających kontrolę . ................................ 675
Użycie kontroli poprawności po stronie klienta . ......................................................................... 677
Aktywowanie i wyłączanie kontroli poprawności po stronie klienta . ............................... 678
Użycie kontroli poprawności po stronie klienta . .................................................................. 679
Jak działa kontrola poprawności po stronie klienta? . ........................................................... 680
Wykonywanie zdalnej kontroli poprawności . .............................................................................. 681
Podsumowanie .................................................................................................................................. 684
Rozdział 26. Paczki ...................................................................................................................685
Utworzenie przykładowego projektu . ............................................................................................ 685
Dodanie pakietów NuGet ......................................................................................................... 685
Utworzenie modelu i kontrolera . ............................................................................................ 686
Utworzenie układu i widoku . ................................................................................................... 687
Profilowanie wczytywania skryptów i arkuszy stylów . ............................................................... 689
Używanie paczek stylów i skryptów . .............................................................................................. 691
Dodanie pakietu NuGet ............................................................................................................ 691
Definiowanie paczki .................................................................................................................. 692
Stosowanie paczek ..................................................................................................................... 694
Optymalizacja plików JavaScript i CSS . .................................................................................. 695
Podsumowanie .................................................................................................................................. 697
Rozdział 27. Web API i aplikacje w postaci pojedynczej strony . ..............................................699
Aplikacja w postaci pojedynczej strony . ........................................................................................ 700
Utworzenie przykładowego projektu . ............................................................................................ 700
Tworzenie modelu ..................................................................................................................... 701
Dodanie pakietów NuGet ......................................................................................................... 702
Tworzenie kontrolera Home . ................................................................................................... 703
Dodanie układu i widoków ...................................................................................................... 703
Ustawienie początkowego adresu URL i przetestowanie aplikacji . ................................... 705
Zrozumienie Web API ..................................................................................................................... 706
Tworzenie kontrolera Web API . ............................................................................................. 707
Testowanie kontrolera API . ..................................................................................................... 707
Jak działa kontroler API? ................................................................................................................. 709
Jak wybierana jest akcja kontrolera API? . .............................................................................. 710
Mapowanie metod HTTP na metody akcji . ........................................................................... 711
15
SPIS TREŚCI
Użycie Knockout do utworzenia aplikacji typu SPA . .................................................................. 712
Dodanie bibliotek JavaScript do układu . ................................................................................ 712
Implementacja podsumowania . ............................................................................................... 713
Implementacja funkcji tworzenia rezerwacji . ........................................................................ 719
Ukończenie aplikacji ........................................................................................................................ 722
Uproszczenie kontrolera Home . .............................................................................................. 722
Zarządzanie wyświetlaniem zawartości . ................................................................................. 723
Podsumowanie .................................................................................................................................. 725
Skorowidz .............................................................................................................727
16
O autorze
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.
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.
ROZDZIAŁ 1.

ASP.NET MVC
w szerszym kontekście
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 5.
Krótka historia programowania witryn WWW
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. Dzięki temu armia programistów
aplikacji Windows uzyskała możliwość bezbolesnego przejścia do nowego świata aplikacji sieciowych.
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 1.1. Stos technologii ASP.NET Web Forms
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.
 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ć wielkości rzędu kilkuset 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 wydłużeniem
czasu udzielenia odpowiedzi i wymagać większej przepustowości łącza dla serwera.
 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. We wczesnych wersjach ASP.NET wynikowy kod HTML
zwykle nie był zgodny ze standardami sieciowymi, 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 nowszych wydaniach platformy Web Forms, 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 Web Forms 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.
20
ROZDZIAŁ 1.  ASP.NET MVC W SZERSZYM KONTEKŚCIE
Platforma Web Forms nie jest zła. Firma Microsoft włożyła wiele wysiłku w poprawę jej zgodności
ze standardami sieciowymi, uproszczenie procesu tworzenia aplikacji, a nawet przeniesienia pewnych funkcji
z ASP.NET MVC. Platforma Web Forms doskonale się sprawdza, gdy zachodzi konieczność szybkiego
otrzymania wyniku — skomplikowaną aplikację sieciową można przygotować dosłownie w jeden dzień.
Jednak jeśli nie zachowasz ostrożności podczas programowania, to przekonasz się, że utworzona aplikacja jest
trudna do przetestowania i konserwacji.
 Uwaga Dokładne omówienie platformy ASP.NET Web Forms znajdziesz w innej mojej książce, zatytułowanej Pro
ASP.NET 4.5 in C#, wydanej przez Apress. W wymienionej książce zamieściłem pełne omówienie platformy Web
Forms i pokazałem najlepsze praktyki pozwalające na unikanie najpoważniejszych błędów.
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.
Standardy sieciowe oraz REST
W ostatnich latach zwiększył 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. Nowoczesne platformy sieciowe nie mogą pozwolić sobie na ignorowanie 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 AngularJS, 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 pozycje zatytułowane HTML5. Przewodnik encyklopedyczny i AngularJS. Profesjonalne
techniki, 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 metody PUT i adresu http://www.przyklad.pl/Produkty/Kosiarka lub usunąć
dane klienta za pomocą metody 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.
21
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Programowanie zwinne i sterowane testami
W ostatnich latach 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 samodzielnie. 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.
 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 pozwalających na tworzenie atrap
(Moq i Rhino Mock), kontenerów inwersji kontroli (Niniect i AutoFac), serwerów ciągłej integracji (Cruise Control
i TeamCity), bibliotek mapowania obiektowo-relacyjnego (NHibernate i Subsonic) i wiele innych. 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 omówiona w rozdziale 3.).
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.
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
22
ROZDZIAŁ 1.  ASP.NET MVC W SZERSZYM KONTEKŚCIE
językiem programowania po stronie serwera. Służy jako język przechowywania i pobierania danych z wielu
nierelacyjnych baz danych, w tym CouchDB i Mongo; jest ponadto wykorzystywany jako język ogólnego
przeznaczenia dla platform serwerowych, takich jak Node.js. Framework Node.js jest dostępny od roku 2009
i bardzo szybko zdobył powszechną akceptację. Jego najważniejszymi cechami są:
 Użycie JavaScript — programiści muszą korzystać z tylko jednego języka. Dotyczy to nie tylko kodu klienta
i logiki serwera, ale także logiki dostępu do danych, realizowanego poprzez 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).
Node.js pozostaje technologią niszową. Zaskakujący może być fakt, że największym wkładem tej technologii
do programowania aplikacji sieciowych jest dostarczenie spójnego silnika JavaScript, za pomocą którego można
tworzyć narzędzia programistyczne. Działanie wielu frameworków JavaScript po stronie klienta, na przykład
AngularJS, jest wspomagane przez użycie Node.js.
Podczas wdrażania aplikacji sieciowych niezbyt często wykorzystuje się Node.js. 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 19.). 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.
Najważniejsze zalety ASP.NET MVC
W październiku 2007 roku firma Microsoft zaprezentowała całkiem nową platformę MVC, zbudowaną na
podstawie 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.
 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.
23
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rozszerzalność
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.
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 14.
Ś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 UI lub Bootstrap CSS. Platforma
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 w Visual Studio wraz z innymi popularnymi
bibliotekami, takimi jak Bootstrap. Knockout i Modernizr.
 Wskazówka W tej książce nie zamierzam dokładnie omawiać wymienionych bibliotek JavaScript, ponieważ nie
stanowią rdzenia platformy MVC i działają w przeglądarkach internetowych. Programowanie po stronie klienta
pod kątem aplikacji frameworka MVC to ważny temat — więcej informacji o tym znajdziesz w mojej książce Pro
ASP.NET MVC 5 Client wydanej przez Apress. Istnieją pewne biblioteki zapewniające obsługę kluczowych funkcji,
takich jak kontrola poprawności i obsługa żądań Ajax — ich omówienie znajdziesz w II części książki. Informacje
o bibliotece Knockout przedstawiłem w rozdziale 27., natomiast z Bootstrap CSS korzystam w całej książce
(choć bez dokładnego omawiania tej biblioteki).
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 i jednocześnie
pozwala na zmniejszenie kosztu działania popularnej aplikacji sieciowej.
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.
Ł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.
24
ROZDZIAŁ 1.  ASP.NET MVC W SZERSZYM KONTEKŚCIE
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łasnymi rozwiązaniami firmy Microsoft,
które przedstawię w rozdziale 6. 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ść poruszania się po
witrynie przez bezpośrednie 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, katalogó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 routingu adresów URL, co standardowo pozwala na tworzenie 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 routingu można znaleźć
w rozdziałach 15. i 16.
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ść tworzenia 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 uwierzytelnianie,
członkostwo, role, profile oraz internacjonalizacja — pozwalają na zmniejszenie ilości kodu do napisania
i utrzymania w każdej aplikacji i są efektywne zarówno na platformie MVC, jak i w klasycznych projektach
Web Forms. Platforma ASP.NET udostępnia bogaty zestaw narzędzi, za pomocą którego można tworzyć
aplikacje sieciowe ASP.NET MVC.
25
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
 Uwaga W książce omówione zostaną najczęściej używane funkcje platformy ASP.NET powiązane z programowaniem
MVC, ale samej platformie można poświęcić oddzielną książkę. Dokładne przedstawienie bogatych funkcji oferowanych
przez platformę ASP.NET znajdziesz w innej mojej książce, Pro ASP.NET MVC 5 Platform, wydanej przez Apress.
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 5 jest zbudowana na bazie .NET 4.5.1, 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 tworzenia kodu pozwala na
tworzenie czytelniejszego kodu w porównaniu z wcześniejszymi platformami. Nie przejmuj się, jeżeli nie
znasz najnowszych funkcji języka C#, ponieważ w rozdziale 4. przedstawię wprowadzenie do najważniejszych
funkcji C# niezbędnych podczas programowania na platformie MVC.
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 przejrzeć ten kod (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://aspnetwebstack.codeplex.com/.
Co powinienem wiedzieć?
Aby jak najwięcej skorzystać z tej książki, powinieneś mieć opanowane podstawy programowania sieciowego,
a także znać technologie HTML, CSS i — przynajmniej ogólnie — język C#. Nie przejmuj się, jeżeli nie znasz
wszystkich szczegółów dotyczących programowania po stronie klienta. W książce nacisk położono na
programowanie po stronie serwera, więc możesz się skoncentrować na interesujących Cię aspektach
prezentowanych przykładów. W rozdziale 4. znajduje się wprowadzenie do najużyteczniejszych funkcji C#
w aspekcie programowania na platformie MVC. Wspomniane wprowadzenie okaże się użyteczne,
jeżeli do najnowszych wersji .NET przechodzisz z wcześniejszych wydań.
26
ROZDZIAŁ 1.  ASP.NET MVC W SZERSZYM KONTEKŚCIE
Jaka jest struktura książki?
Książka została podzielona na dwie części, w których omówiono powiązane ze sobą tematy.
Część I. Wprowadzenie do ASP.NET MVC 5
Tę książkę rozpocznę od umieszczenia ASP.NET MVC w szerszym kontekście. Przedstawię zalety wzorca MVC,
a także sposób, w jaki platforma ASP.NET MVC wpisuje się w nowoczesne podejście do programowania
sieciowego. Ponadto poznasz narzędzia i funkcje języka C# niezbędne w programowaniu MVC.
W kolejnym rozdziale przejdę do utworzenia prostej aplikacji sieciowej. To pozwoli na przedstawienie
idei najważniejszych komponentów, elementów konstrukcyjnych oraz współpracy między nimi. Jednak większość
tej części książki została poświęcona na omówienie budowy projektu o nazwie SportsStore. Na jego przykładzie
pokażę praktyczny proces przygotowania aplikacji, od jej powstania aż po wdrożenie, a tym samym poznasz
najważniejsze funkcje frameworka ASP.NET MVC.
Część II. Szczegółowe omówienie platformy ASP.NET MVC
W części II książki przejdę do omówienia wewnętrznego sposobu działania funkcji platformy MVC używanych
podczas prac nad aplikacją SportsStore. Dowiesz się, jak działają poszczególne funkcje, poznasz odgrywane
przez nie role na platformie MVC, a także zobaczysz dostępne opcje zarówno konfiguracyjne, jak i pozwalające na
dostosowanie działania danej funkcji do własnych potrzeb. Po przedstawieniu ogólnego kontekstu w części
pierwszej, w drugiej przejdziemy od razu do szczegółów.
Co nowego w ASP.NET MVC 5?
Wersja 5. platformy ASP.NET MVC to względnie niewielkie uaktualnienie, a większość zmian tak naprawdę
dotyczy sposobu tworzenia projektów ASP.NET i zarządzania nimi w Visual Studio. W tabeli 1.1
wymieniono nowe funkcje platformy MVC i wskazano rozdziały, w których przedstawiono więcej
informacji na temat poszczególnych funkcji.
Tabela 1.1. Nowe funkcje w MVC 5
Funkcja
Opis
Rozdział
Filtry
uwierzytelniania
Nowy rodzaj filtru, który może być używany wraz z różnymi
rodzajami uwierzytelniania w ramach tego samego kontrolera.
18.
Nadpisywanie
filtru
Nowy rodzaj filtru stosowanego w metodzie akcji, aby uniemożliwić
działanie filtrów zdefiniowanych globalnie lub w kontrolerze.
18.
Routing atrybutu
Zestaw atrybutów pozwalających na definiowanie tras URL w klasie
kontrolera.
15. i 16.
Framework ASP.NET w wersji 4.5.1, na którym oparto platformę MVC 5, również został usprawniony.
Najważniejsza zmiana polega na dodaniu API ASP.NET Identity zastępującego system członkostwa
przeznaczony do zarządzania danymi uwierzytelniającymi użytkowników. W tej książce nie znajdziesz
omówienia ASP.NET Identity, ale pokażę, jak uwierzytelnianie i autoryzacja są stosowane w aplikacjach
MVC za pomocą funkcji, takich jak filtry.
27
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
 Uwaga Dokładne omówienie ASP.NET Identity oraz wszystkich możliwości oferowanych przez platformę ASP.NET
znajdziesz w innej mojej książce, zatytułowanej Pro ASP.NET MVC 5 Platform, wydanej przez Apress. To oczywiście
nie oznacza, że musisz kupić kolejną moją książkę, aby dowiedzieć się czegoś więcej na tak ważny temat, jakim
jest zapewnienie bezpieczeństwa użytkownikom. Wydawnictwo Apress pozwoliło mi na bezpłatne umieszczenie
w mojej witrynie poświęconych bezpieczeństwu rozdziałów z wymienionej powyżej książki.
Nowe wydanie daje szansę na nie tylko na uzupełnienie książki o omówienie nowych funkcji, ale również
na wprowadzenie innych zmian w tekście. Skorzystałem z tej możliwości i rozbudowałem przykład
SportsStore w taki sposób, aby pokazać podstawy tworzenia aplikacji responsywnych oraz dla urządzeń
mobilnych. Ponadto na początku wszystkich rozdziałów zawierających dokładne omówienie funkcji MVC
umieściłem odnośniki pozwalające na łatwe odszukanie konkretnych przykładów. W książce znalazł się
także nowy rozdział poświęcony jednej z bibliotek open source dodanej przez Microsoft — Knockout, którą
w połączeniu z funkcją Web API można użyć do tworzenia tak zwanych aplikacji w postaci pojedynczej
strony (ang. Single Page Application).
Gdzie znajdę przykładowe fragmenty kodu?
Wszystkie przykłady przedstawione w książce możesz pobrać ze strony ftp://ftp.helion.pl/przyklady/asp5zp.zip.
Te materiały są dostępne bezpłatne, archiwum zawiera wszystkie projekty Visual Studio wraz z ich
zawartością. Wprawdzie nie musisz pobierać wspomnianych przykładów, ale najłatwiejszym sposobem na
eksperymentowanie z przykładami jest wycinanie ich fragmentów i wklejanie we własnych projektach.
Jakiego oprogramowania będę potrzebował?
Jedynym niezbędnym krokiem w procesie przygotowania stacji roboczej do tworzenia aplikacji
z użyciem platformy ASP.NET MVC 5 jest zainstalowanie Visual Studio 2013. 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 2013, ale w niniejszej książce będziemy używali
wydania całkowicie bezpłatnego: Visual Studio Express 2013 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.visualstudio.com/products/visual-studio-express-vs. Istnieje kilka różnych wersji programu
Visual Studio 2013 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. Wprawdzie wykorzystamy
kilka dodatkowych pakietów oprogramowania, ale zostaną one pobrane z poziomu samego Visual Studio.
Nie jest wymagane pobieranie i instalowanie oddzielnych programów. (Wspomniane pakiety są dostępne
bezpłatnie).
 Wskazówka W przykładach tworzonych na potrzeby tej książki użyty został system Windows 8.1, ale pozwalające
na tworzenie aplikacji ASP.NET MVC 5 narzędzie Visual Studio 2013 może działać także we wcześniejszych wersjach
Windows. Szczegółowe informacje na temat wymagań systemowych dla Visual Studio 2013 znajdziesz na podanej
wcześniej stronie.
28
ROZDZIAŁ 1.  ASP.NET MVC W SZERSZYM KONTEKŚCIE
Bootstrap
W rozdziale 10. użyjemy funkcji biblioteki Bootstrap CSS o nazwie Glyphicons Halflings. Jest to zestaw ikon,
które zwykle nie są udostępniane bezpłatnie. Jednak ten konkretny zestaw jest oferowany w ramach licencji
otwartej, co pozwoliło na jego dołączenie do biblioteki Bootstrap CSS. Jedynym wymaganiem jest podanie
(o ile to możliwe) adresu URL prowadzącego do witryny twórcy, co wydaje się rozsądnym rozwiązaniem.
Oto adres wspomnianej witryny: http://glyphicons.com/.
Podsumowanie
W tym rozdziale opisałem kontekst, w którym istnieje platforma MVC, a także porównałem ją z Web
Forms. Zaprezentowałem także zalety użycia platformy MVC, strukturę niniejszej książki oraz
oprogramowanie niezbędne do uruchamiania przykładowych fragmentów kodu.
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ą tworzyć łatwy w obsłudze kod wysokiej jakości.
W następnym rozdziale przedstawię platformę MVC w działaniu oraz proste mechanizmy pozwalające osiągnąć
opisane wcześniej korzyści.
29
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
30
ROZDZIAŁ 2.

Pierwsza aplikacja MVC
Najlepszym sposobem na docenienie środowiska programistycznego 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żę, 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 Visual Studio
Oprogramowanie Visual Studio Express zawiera wszystkie funkcje niezbędne do tworzenia, testowania
i wdrażania aplikacji ASP.NET MVC. Niektóre z nich pozostają ukryte aż do chwili ich wywołania. W celu
uzyskania dostępu do wszystkich funkcji wybierz opcję Ustawienia ekspertowe z menu Narzędzia/Ustawienia.
 Wskazówka Z pewnych powodów firma Microsoft zadecydowała, że nazwy menu najwyższego poziomu są
wyświetlane wielkimi literami. Oznacza to, że wspomniane wcześniej menu tak naprawdę nosi nazwę NARZĘDZIA.
Ponieważ uważam, że wielkie litery oznaczają krzyk, w książce zdecydowałem się na zapis tego rodzaju menu jako
Narzędzia.
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,
pokazany na rysunku 2.1.
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 2.1. Szablon projektu Aplikacja sieci Web platformy ASP.NET w Visual Studio
 Wskazówka Upewnij się, że w liście rozwijanej na górze okna wybrano framework .NET w wersji 4.5.1. To jest
najnowsza wersja .NET i jednocześnie wymagana przez pewne funkcje zaawansowane, które zostaną omówione
w książce.
Jako nazwy nowego projektu użyj PartyInvites i kliknij przycisk OK, aby kontynuować. Wyświetli się
kolejne okno dialogowe pokazane na rysunku 2.2. Pozwala ono na określenie zawartości początkowej dla
tworzonego projektu ASP.NET. To jest jeden z aspektów innowacyjności Microsoftu mającej zapewnić
lepszą integrację między poszczególnymi elementami ASP.NET oraz zaoferować spójny zestaw narzędzi
i szablonów.
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 Empty i zaznacz pole wyboru MVC w sekcji Dodaj foldery
i podstawowe odwołania dla:. W ten sposób zostanie utworzony prosty projekt MVC wraz z minimalną
ilością predefiniowanej treści — to będzie punkt wyjścia dla wszystkich przykładów przedstawionych w książce.
Kliknij przycisk OK, tworząc w ten sposób nowy projekt.
 Uwaga Inne szablony projektu mają za zadanie dostarczyć znacznie bardziej rozbudowane punkty wyjścia
dla aplikacji ASP.NET. Szczerze mówiąc, nie lubię tych szablonów, ponieważ zachęcają one programistów
do traktowania ważnych funkcji, na przykład uwierzytelniania, jak czarnych pudełek. Moim celem jest dostarczenie Ci
wiedzy wystarczającej do poznania i zarządzania wszystkimi aspektami aplikacji MVC. Dlatego też w większości
projektów w książce używam szablonu Empty. Wyjątkiem będzie rozdział 14., w którym pokażę zawartość, jaką
do nowego projektu dodaje szablon MVC.
Po utworzeniu projektu przez Visual Studio wyświetli się w oknie Eksplorator rozwiązania zestaw plików
i katalogów (patrz rysunek 2.3). Jest to domyślna struktura dla nowego projektu MVC 5, wkrótce poznasz
przeznaczenie poszczególnych plików i katalogów utworzonych przez Visual Studio.
32
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
Rysunek 2.2. Wybór początkowej konfiguracji projektu
Rysunek 2.3. Początkowa struktura plików i katalogów projektu ASP.NET MVC
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.4. Zaczęliśmy od szablonu pustego projektu i aplikacja
nie zawiera nic użytecznego do uruchomienia — zobaczymy zatem komunikat o błędzie 404.
Zatrzymaj teraz debugowanie przez zamknięcie okna przeglądarki wyświetlającego komunikat błędu
lub przez wybranie opcji Stop Debugging z menu Debuguj w Visual Studio.
Jak przed chwilą zobaczyłeś, Visual Studio uruchamia przeglądarkę internetową w celu wyświetlenia
projektu. Domyślną przeglądarką jest oczywiście Internet Explorer, ale z poziomu paska narzędzi możesz wybrać
używaną przeglądarkę internetową (rysunek 2.5). Jak widać na rysunku, w moim systemie jest zainstalowanych
kilka przeglądarek internetowych, co jest użyteczne podczas testowania tworzonych aplikacji sieciowych.
W książce będziemy używali przeglądarki Google Chrome, ponieważ jest ona zainstalowana w wielu
komputerach. To dobry wybór, sam korzystam z tej przeglądarki w trakcie pracy nad własnymi projektami.
Możesz również użyć przeglądarki Internet Explorer. Wprawdzie wcześniejsze wersje tej przeglądarki różnie
radziły sobie ze standardami sieciowymi, ale ostatnie wydania całkiem dobrze implementują standard HTML5.
33
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 2.4. Próba uruchomienia pustego projektu
Rysunek 2.5. Zmiana przeglądarki internetowej używanej przez Visual Studio
do wyświetlenia uruchomionej aplikacji
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.
 Wskazówka Nie musisz postępować zgodnie z tą konwencją MVC i większością innych, ale zalecam, 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.6).
Gdy wyświetli się okno dialogowe Dodaj szkielet, wtedy wybierz Kontroler MVC 5 — pusty (rysunek 2.7)
i kliknij przycisk Dodaj.
Na ekranie zostanie wyświetlone okno dialogowe Dodaj kontroler. Jako nazwę dla nowego kontrolera
podaj HomeControler i kliknij przycisk Dodaj. Z użytą tutaj nazwą wiąże się kilka konwencji: nazwy
nadawane kontrolerom powinny być opisowe i kończyć się ciągiem Controller, a kontroler domyślny nosi
nazwę Home.
34
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
Rysunek 2.6. Dodawanie kontrolera do projektu MVC
Rysunek 2.7. Wybór pustego kontrolera w oknie dialogowym Dodaj szkielet
35
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
 Wskazówka Jeżeli używałeś wcześniejszych wersji Visual Studio do tworzenia aplikacji MVC, zauważysz, że proces
jest nieco inny. Microsoft zmienił sposób, w jaki Visual Studio umieszcza w projekcie prekonfigurowane klasy i inne
komponenty.
Visual Studio utworzy w katalogu Controllers nowy plik C# o nazwie HomeController.cs i otworzy go do
edycji. Domyślny kod pliku klasy wygenerowany przez Visual Studio został przedstawiony na listingu 2.1.
Zauważ, że znajduje się w nim klasa o nazwie HomeController, która dziedziczy po klasie Controller
dostępnej w przestrzeni nazw System.Web.Mvc.Controller.
Listing 2.1. Domyślny kod umieszczony w klasie HomeController
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
namespace PartyInvites.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
}
Dobrym sposobem rozpoczęcia pracy z MVC jest wprowadzenie kilku prostych 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
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
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.8).
36
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
Rysunek 2.8. Dane wyjściowe wygenerowane przez metodę akcji kontrolera
 Wskazówka Zwróć uwagę, że Visual Studio przekierowało przeglądarkę internetową na port 49159. U siebie
w komputerze niemal na pewno zobaczysz inny numer portu w adresie URL, ponieważ Visual Studio losowo wybiera port
podczas tworzenia projektu. Jeżeli spojrzysz na obszar powiadomień na pasku zadań Windows, wtedy dostrzeżesz
ikonę IIS Express. To jest uproszczona wersja serwera IIS dołączona do Visual Studio i używana w celu obsługi
zawartości ASP.NET oraz usług w trakcie prac nad projektem ASP.NET. Wdrożenie projektu ASP.NET MVC w środowisku
produkcyjnym zostanie omówione w rozdziale 13.
Poznajemy trasy
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 mapowany na określony kontroler i daną akcję. Gdy Visual Studio tworzy
projekt MVC, dodaje na początek kilka domyślnych tras. 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:49159/,
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 tras 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ć trasy, 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 RouteConfig.cs, który znajduje się
w katalogu App_Start. W rozdziałach 16. i 17. dowiesz się więcej o zawartości wymienionego pliku.
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.
37
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 2.3. Modyfikowanie kontrolera w celu wygenerowania widoku
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
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.9.
Rysunek 2.9. Aplikacja MVC próbuje znaleźć domyślny widok
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.9, aplikacja MVC próbuje znaleźć w katalogu Views różne pliki
o takiej nazwie.
Najłatwiejszym sposobem utworzenia widoku jest kliknięcie prawym przyciskiem myszy metody akcji
w pliku kodu HomeController.cs (możesz kliknąć nazwę metody lub jej treść), a następnie wybranie opcji
Dodaj widok… z menu kontekstowego (patrz rysunek 2.10.). Spowoduje to otwarcie okna dialogowego
Dodaj widok.
38
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
Rysunek 2.10. Dodanie widoku dla metody akcji w Visual Studio
Visual Studio wyświetli okno dialogowe Dodawanie widoku, w którym można zdefiniować początkową
zawartość tworzonego pliku widoku. Jako nazwę widoku podaj Index (nazwa metoda akcji będzie powiązana
z tym widokiem — to kolejna konwencja). Wybierz szablon Empty (bez modelu) i usuń zaznaczenie opcji
Utwórz jako widok częściowy i Użyj strony układu, jak pokazano na rysunku 2.11. W tym momencie nie
przejmuj się znaczeniem wymienionych opcji, zostaną one dokładnie omówione w dalszych rozdziałach.
Kliknięcie przycisku Dodaj spowoduje utworzenie pliku nowego widoku.
Rysunek 2.11. Konfiguracja początkowej zawartości pliku widoku
Visual Studio w katalogu Views/Home utworzy plik o nazwie Index.cshtml. Jeżeli nie uzyskasz oczekiwanego
efektu, po prostu usuń plik i spróbuj ponownie utworzyć widok. Mamy tutaj do czynienia z kolejną konwencją
frameworka MVC — widoki są umieszczane w katalogu Views oraz poukładane w katalogach o nazwach
odpowiadających nazwom kontrolerów, z którymi są powiązane.
 Wskazówka Rozszerzenie pliku .cshtml wskazuje na widok C#, który będzie przetwarzany przez Razor. Wczesne
wersje MVC korzystały z silnika widoku ASPX; w ich przypadku pliki miały rozszerzenie .aspx.
Efektem wartości wybranych w oknie dialogowym Dodawanie widoku jest utworzenie przez Visual
Studio najprostszego z możliwych widoków, którego zawartość przedstawiono w listingu 2.4.
39
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 2.4. Początkowa zawartość pliku Index.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
<div>
</div>
</body>
</html>
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 odpowiedzialny za przetwarzanie
zawartości widoków i generowanie kodu HTML przekazywanego później przeglądarce internetowej.
To bardzo prosty przykład. Informujemy w ten sposób Razor, że nie będziemy korzystać ze strony układu
(temat układów zostanie omówiony w rozdziale 5.). Zignorujmy Razor na moment. Zmodyfikuj plik
Index.cshtml, dodając elementy zaznaczone pogrubieniem na listingu 2.5.
Listing 2.5. Modyfikowanie kodu HTML widoku
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Indeks</title>
</head>
<body>
<div>
Witaj, świecie (z widoku)
</div>
</body>
</html>
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.12.
40
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
Rysunek 2.12. 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 rozdziale 17.
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.6 pokazane
jest przekazywanie prostych danych dynamicznych w taki sposób w pliku HomeController.cs.
Listing 2.6. Ustawianie danych widoku w pliku HomeController.cs
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
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. 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
41
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
ViewBag.Greeting odwołujemy się ponownie w widoku, ale tym razem w celu pobrania jej wartości, co zostało
przedstawione na listingu 2.7. Zmiany należy wprowadzić w pliku Index.cshtml.
Listing 2.7. Pobieranie w pliku Index.cshtml danych z ViewBag
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Indeks</title>
</head>
<body>
<div>
@ViewBag.Greeting, świecie (z widoku)
</div>
</body>
</html>
Nowością w listingu 2.7 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, o ile nazwy użyte w kontrolerze i widoku będą takie same. Oczywiście, w ten sposób
można przekazywać z kontrolera do widoku wiele wartości przez przypisanie ich do więcej niż tylko jednej
właściwości. Gdy ponownie uruchomisz projekt, możesz zobaczyć swój pierwszy dynamiczny widok MVC,
pokazany na rysunku 2.13.
Rysunek 2.13. 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:
42
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
 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.8 do istniejącego widoku, a otrzymamy informacje
o przyjęciu.
Listing 2.8. Umieszczenie w pliku Index.cshtml informacji o przyjęciu
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Przyjęcie sylwestrowe</title>
</head>
<body>
<div>
@ViewBag.Greeting, świecie (z widoku)
<p>Zapraszamy na wspaniałe przyjęcie.<br />
(Do zrobienia: trzeba to ulepszyć, dodać zdjęcia i inne takie).
</p>
</div>
</body>
</html>
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.14).
Rysunek 2.14. 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.
43
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Nie musimy wymagać zbyt wiele od modelu aplikacji PartyInvites, ponieważ to jest bardzo prosta aplikacja
i 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.9.
 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.9. Klasa domeny GuestResponse zdefiniowana w pliku GuestResponse.cs
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śnię w punkcie
„Dodanie kontroli poprawności”, w dalszej części rozdziału.
Łą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.10.
Listing 2.10. Dodanie w pliku Index.cshtml łącza do formularza RSVP
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Przyjęcie sylwestrowe</title>
</head>
<body>
<div>
@ViewBag.Greeting, świecie (z widoku)
<p>Zapraszamy na wspaniałe przyjęcie.<br />
44
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
(Do zrobienia: trzeba to ulepszyć, dodać zdjęcia i inne takie).
</p>
@Html.ActionLink("Wyślij RSVP", "RsvpForm")
</div>
</body>
</html>
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 21. do 23. Dodane przez nas łącze jest pokazane na rysunku 2.15.
Rysunek 2.15. 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.
 Wskazówka 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.
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.11.
Listing 2.11. Dodanie nowej metody akcji do kontrolera zdefiniowanego w pliku HomeController.cs
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
namespace PartyInvites.Controllers
{
public class HomeController : Controller
{
public ViewResult Index()
{
45
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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 PartyInvites z menu Debuguj w Visual Studio.
Kliknij prawym przyciskiem myszy wewnątrz metody akcji RsvpForm i z menu kontekstowego wybierz Dodaj
widok…. W oknie dialogowym Dodaj widok upewnij się, że nazwa widoku to RsvpForm, i wybierz Empty
z rozwijanego menu Szablon. Następnie w rozwijanym menu Klasa modelu wybierz opcję GuestResponse.
Nie zaznaczaj żadnego pola wyboru w sekcji Opcje (rysunek 2.16).
Rysunek 2.16. Dodawanie nowego widoku do projektu
Kliknij przycisk Dodaj, aby utworzyć nowy widok. W katalogu Views/Home Visual Studio utworzy
nowy plik o nazwie RvspForm.cshtml i otworzy go do edycji. Domyślny kod wspomnianego pliku przedstawiono
na listingu 2.12. Jak widać, jest to szkielet pliku HTML z wyrażeniem Razor @model. Jak pokażę za moment,
jest to klucz do widoku ściśle określonego typu i oferowanych przez niego udogodnień.
46
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
Listing 2.12. Domyślny kod wygenerowany w pliku RsvpForm.cshtml
@model PartyInvites.Models.GuestResponse
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>RsvpForm</title>
</head>
<body>
<div>
</div>
</body>
</html>
 Wskazówka Opcje wybierane podczas tworzenia widoku mają wpływ na początkową zawartość pliku widoku,
ale na tym koniec. Zmianę rodzaju widoku ze zwykłego na widok ściśle określonego typu możesz wprowadzić
w edytorze kodu przez dodanie lub usunięcie dyrektywy @model.
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.13.
Listing 2.13. Tworzenie w pliku RsvpForm.cshtml widoku z formularzem
@model PartyInvites.Models.GuestResponse
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>RSVP</title>
</head>
<body>
@using (Html.BeginForm()) {
<p>Imię i nazwisko: @Html.TextBoxFor(x => x.Name) </p>
<p>Twój e-mail: @Html.TextBoxFor(x => x.Email)</p>
<p>Twój telefon: @Html.TextBoxFor(x => x.Phone)</p>
<p>
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ę")
</p>
47
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
<input type="submit" value="Wyślij RSVP" />
}
</body>
</html>
Dla każdej właściwości klasy modelu GuestResponse używany metody pomocniczej HTML generującej
odpowiednią kontrolkę <input>. Metody te pozwalają na określenie właściwości, do której odnosi się element
<input>, za pomocą wyrażenia lambda, takiego jak:
...
@Html.TextBoxFor(x => x.Phone)
...
Metoda pomocnicza HTML TextBoxFor generuje kod HTML elementu <input>, ustawia wartość jego
parametru type na text, a atrybuty id oraz name na Phone — nazwę wybranej właściwości klasy domeny:
<input id="Phone" name="Phone" type="text" value="" />
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.17).
Rysunek 2.17. IntelliSense w Visual Studio dla wyrażeń lambda w metodach pomocniczych HTML
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 ...
</form>
...
48
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
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).
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:
<form action="/Home/RsvpForm" method="post">
... tu zawartość formularza ...
</form>
Nie przejmuj się, jeżeli nie znasz mechanizmu usuwania obiektów w języku C#. Moim celem jest pokazanie,
jak można tworzyć formularze za pomocą metod pomocniczych HTML.
Zdefiniowanie początkowego adresu URL
Visual Studio stara się być jak najbardziej użyteczne dla programisty i dlatego powoduje, że przeglądarka
internetowa żąda adresów URL na podstawie aktualnie edytowanych widoków. To jest funkcja typu „chybiłtrafił”, ponieważ nie działa podczas edycji innego rodzaju plików. Ponadto w najbardziej skomplikowanych
aplikacjach sieciowych nie można tak po prostu przejść do dowolnego miejsca.
W celu zdefiniowania konkretnego adresu URL dla żądania wykonywanego przez przeglądarkę
internetową po uruchomieniu aplikacji wybierz z menu Projekt opcję Właściwości PartyInvites…, przejdź
do sekcji Sieć Web i zaznacz opcję Określ stronę w sekcji Uruchom akcję, jak pokazano na rysunku 2.18.
Nie musisz podawać wartości we wskazanym polu, Visual Studio zażąda domyślnego adresu URL projektu.
To będzie dyrektywa do metody akcji Index w kontrolerze Home. (W rozdziałach 15. i 16. dowiesz się, jak
używać systemu routingu adresów URL i zmieniać mapowanie domyślne).
Rysunek 2.18. Ustawienie domyślnego początkowego adresu URL w projekcie
Aby wyświetlić formularz z widoku RsvpForm, uruchom aplikację i kliknij łącze Wyślij RSVP. Wynik jest
pokazany na rysunku 2.19.
49
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 2.19. Widok RsvpForm
Obsługa formularzy
Nie poinformowałem jeszcze MVC, co należy zrobić z danymi formularza przesłanymi do serwera, dlatego
kliknięcie przycisku Wyślij RSVP usuwa 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.
 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żę, 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 przejdzie na stronę /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.14 przedstawione są zmiany, jakie należy zastosować w klasie HomeController.
Listing 2.14. Dodawanie w pliku HomeController.cs metody akcji obsługującej żądania POST
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
PartyInvites.Models;
namespace PartyInvites.Controllers
{
public class HomeController : Controller
{
50
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
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);
}
}
}
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.18.
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 24.
51
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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 wygenerowania widoku o nazwie Thanks
i przekazania do niego obiektu GuestResponse. Aby utworzyć wskazany widok, kliknij prawym przyciskiem
myszy wewnątrz dowolnej metody w HomeController i wybierz Dodaj widok… z menu kontekstowego.
W wyświetlonym oknie dialogowym Dodawanie widoku utwórz widok Thanks o ściśle określonym typie
używający klasy modelu GuestResponse i oparty na szablonie Empty. (Jeżeli potrzebujesz dokładnych
informacji o procedurze tworzenia widoku, znajdziesz je w punkcie „Dodawanie widoku ściśle określonego
typu”). Visual Studio utworzy widok w postaci pliku /Views/Home/Thanks.cshtml. Zmodyfikuj kod nowo
utworzonego pliku w taki sposób, aby jego zawartość odpowiadała przedstawionej na listingu 2.15. Kod,
który trzeba dodać, oznaczono pogrubioną czcionką.
Listing 2.15. Widok Thanks
@model PartyInvites.Models.GuestResponse
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Dziękujemy</title>
</head>
<body>
<div>
<h1>Dziękujemy, @Model.Name!</h1>
@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ę.
}
</div>
</body>
</html>
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śnię ją w rozdziale 5.
Teraz, po utworzeniu widoku Thanks, mamy działający przykład obsługi formularza w aplikacji ASP.NET
MVC. 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.20 (choć może być inny, jeżeli nie
nazywasz się Janek i nie możesz przyjść na przyjęcie).
52
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
Rysunek 2.20. Wygenerowany widok Thanks
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. 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. W ten sposób reguły dotyczące kontroli poprawności są wyrażane
za pomocą standardowych w C# funkcji atrybutów. Na listingu 2.16 przedstawiony jest sposób zastosowania tych
atrybutów w klasie modelu GuestResponse.
Listing 2.16. 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.
53
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
 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
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. To jest przykład pokazujący, jak platforma ASP.NET MVC elegancko łączy
funkcje C# z HTML i HTTP.
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.17 pokazuję, w jaki sposób należy zastosować w obsługującej
żądania POST metodzie akcji RsvpForm klasy kontrolera Home.
Listing 2.17. Sprawdzanie w pliku HomeController.cs 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.18).
Listing 2.18. Użycie metody pomocniczej Html.ValidationSummary w pliku RsvpForm.cshtml
@model PartyInvites.Models.GuestResponse
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>RsvpForm</title>
</head>
<body>
<div>
54
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
@using (Html.BeginForm())
{
@Html.ValidationSummary()
<p>Imię i nazwisko: @Html.TextBoxFor(x => x.Name) </p>
<p>Twój e-mail: @Html.TextBoxFor(x => x.Email)</p>
<p>Twój telefon: @Html.TextBoxFor(x => x.Phone)</p>
<p>
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ę")
</p>
<input type="submit" value="Wyślij RSVP" />
}
</div>
</body>
</html>
Jeżeli nie wystąpiły błędy, metoda Html.ValidationSummary tworzy w formularzu ukryty element listy
— jest to rodzaj miejsca zarezerwowanego w formularzu. 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.21.
Rysunek 2.21. 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.
55
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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.
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:
<input data-val="true" data-val-required="Proszę podać imię." id="Name" name="Name"
type="text" value="" />
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):
<input class="input-validation-error" data-val="true" data-val-required="Proszę podać imię."
id="Name" name="Name" type="text" value="" />
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ę Styles.css, jak pokazano na rysunku 2.22.
Rysunek 2.22. Utworzenie nowego arkusza stylów
Po kliknięciu przycisku Dodaj Visual Studio utworzy plik Content/Styles.css. Zmodyfikuj jego zawartość
tak, aby odpowiadała przedstawionej na listingu 2.19.
56
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
Listing 2.19. Zawartość pliku arkusza stylów Styles.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.20. Elementy link do widoku dodajesz w taki sam sposób jak do zwykłych
statycznych plików HTML. W rozdziale 26. poznasz funkcję paczek (ang. bundle) pozwalającą na konsolidację
skryptów JavaScript i arkuszy stylów CSS w celu ich dostarczania przeglądarce internetowej za pomocą
pojedynczego żądania HTTP.
Listing 2.20. Dodanie elementu link do widoku RsvpForm
@model PartyInvites.Models.GuestResponse
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" type="text/css" href="~/Content/Styles.css" />
<title>RsvpForm</title>
</head>
<body>
<div>
@using (Html.BeginForm())
{
@Html.ValidationSummary()
<p>Imię i nazwisko: @Html.TextBoxFor(x => x.Name) </p>
<p>Twój e-mail: @Html.TextBoxFor(x => x.Email)</p>
<p>Twój telefon: @Html.TextBoxFor(x => x.Phone)</p>
<p>
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ę")
57
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
</p>
<input type="submit" value="Wyślij RSVP" />
}
</div>
</body>
</html>
 Wskazówka Pliki JavaScript i CSS możesz przeciągnąć z okna Eksploratora rozwiązania i upuścić w oknie
edytora kodu. Visual Studio utworzy elementy <script> i <link> dla przeciągniętych plików.
 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/Styles.css") lub
@Href.Content("~/Content/Styles.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.23).
Rysunek 2.23. Automatyczne wyróżnianie błędów kontroli poprawności
Nadanie stylu zawartości
Podstawowa funkcjonalność aplikacji została już przygotowana — za wyjątkiem wysyłania wiadomości e-mail,
czym się wkrótce zajmiemy — choć sam wygląd aplikacji pozostawia wiele do życzenia. Wprawdzie w książce
koncentruję się na programowaniu po stronie serwera, ale firma Microsoft zaadaptowała wiele bibliotek
typu open source i dołączyła je w niektórych szablonach projektów Visual Studio.
Mimo że nie jestem fanem tych szablonów, to jednak lubię niektóre z użytych w nich bibliotek.
Nowością w ASP.NET MVC 5 jest Bootstrap, czyli elegancka biblioteka CSS, pierwotnie opracowana
przez programistów serwisu Twitter i dość szeroko rozpowszechniona.
Oczywiście wcale nie musisz używać szablonów projektów Visual Studio, aby mieć możliwość
wykorzystania bibliotek takich jak Bootstrap. Odpowiednie pliki możesz pobrać bezpośrednio z witryny
internetowej danego projektu lub użyć menedżera pakietów NuGet. Wymieniony menedżer pakietów jest
zintegrowany z Visual Studio i oferuje dostęp do pakietów oprogramowania, które można automatycznie
pobrać i zainstalować. Jedną z najużyteczniejszych funkcji NuGet jest zarządzanie zależnościami pakietów.
Dlatego też, jeżeli spróbujesz zainstalować na przykład Bootstrap, menedżer NuGet pobierze i zainstaluje
także bibliotekę jQuery, na której została oparta biblioteka Bootstrap.
58
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
Użycie NuGet do instalacji Bootstrap
W celu instalacji pakietu Bootstrap wybierz opcję Menedżer pakietów NuGet/Konsola menedżera pakietów
z menu Narzędzia w Visual Studio. Na ekranie zostanie wyświetlony wiersz poleceń NuGet. Teraz wpisz
poniższe polecenie i naciśnij klawisz Enter:
Install-Package -version 3.0.0 bootstrap
Polecenie Install-Package nakazuje menedżerowi pakietów NuGet pobranie pakietu oraz wszystkich jego
zależności, a następnie dodanie ich do projektu. Nazwa interesującego nas pakietu to bootstrap. Nazwy
pakietów można sprawdzać w witrynie internetowej NuGet (http://www.nuget.org/) lub za pomocą interfejsu
użytkownika menedżera pakietów NuGet w Visual Studio (wybierz opcję Narzędzia/Menedżera pakietów
NuGet/Zarządzaj pakietami NuGet dla rozwiązania…).
W poleceniu zastosowaliśmy argument -version, aby wskazać, że ma zostać zainstalowana biblioteka
Bootstrap w wersji 3. To najnowsza stabilna wersja dostępna w chwili powstawania książki. Bez parametru
-version menedżer NuGet pobierze aktualnie najnowszą wersję pakietu. Chcę mieć pewność, że będziesz
mógł dokładnie odtworzyć przykłady omawiane w książce — instalacja konkretnej wersji biblioteki
pomaga w zapewnieniu spójności.
NuGet pobierze wszystkie pliki wymagane przez biblioteki Bootstrap i jQuery. Arkusze stylów CSS
zostaną umieszczone w katalogu Content. Ponadto zostanie utworzony katalog o nazwie Scripts (to standardowe
miejsce w MVC dla plików JavaScript), w którym znajdą się skrypty dla Bootstrap i jQuery. (Oprócz tego
tworzony jest również katalog fonts, to wymóg pewnych funkcji typograficznych biblioteki Bootstrap
oczekujących umieszczenia plików w konkretnych katalogach).
 Uwaga Bibliotekę Bootstrap zastosowałem w tym rozdziale, aby pokazać, że kod HTML wygenerowany przez
framework MVC może być używany wraz z popularnymi bibliotekami CSS i JavaScript. Nie chcę się odrywać od
zagadnień programowania po stronie serwera, więc jeśli jesteś zainteresowany aspektami programowania po
stronie klienta podczas pracy z frameworkiem MVC, zapoznaj się z inną moją książką, zatytułowaną Pro ASP.NET
MVC Client, wydaną przez Apress.
Nadanie stylu widokowi Index
Podstawowe funkcje Bootstrap są przypisywane za pomocą klas elementom odpowiadającym selektorom
CSS zdefiniowanym w plikach dodanych do katalogu Content. Dokładne informacje o klasach definiowanych
przez Bootstrap znajdziesz w witrynie internetowej biblioteki (http://getbootstrap.com/). Przykład zastosowania
pewnych podstawowych stylów w pliku widoku Index.cshtml przedstawiono na listingu 2.21.
Listing 2.21. Wykorzystanie biblioteki Bootstrap w pliku Index.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<title>Przyjęcie sylwestrowe</title>
<style>
.btn a { color: white; text-decoration: none}
body { background-color: #F1F1F1; }
</style>
59
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
</head>
<body>
<div class="text-center">
<h2>Zapraszamy na wspaniałe przyjęcie!</h2>
<h3>Ty też zostałeś zaproszony</h3>
<div class="btn btn-success">
@Html.ActionLink("Wyślij RSVP", "RsvpForm")
</div>
</div>
</body>
</html>
W pliku zostały dodane elementy link pozwalające na dołączenie plików bootstrap.css i bootstrap-theme.css
z katalogu Content. Wymienione pliki Bootstap są wymagane do zastosowania podstawowych stylów CSS
oferowanych przez bibliotekę. W katalogu Scripts znajduje się odpowiadającym im plik JavaScript, ale w tym
rozdziale nie będziemy z niego korzystać. Ponadto zdefiniowany został element style odpowiedzialny za
ustalenie koloru tła dla elementu body oraz tekstu dla łączy.
 Wskazówka Prawdopodobnie zauważyłeś, że każdemu plikowi Bootstrap w katalogu Content towarzyszy drugi,
z prefiksem min, na przykład bootstrap.css i bootstrap.min.css. Dość powszechną praktyką jest minimalizacja
plików JavaScript i CSS podczas wdrażania aplikacji w środowisku produkcyjnym. Wspomniana minimalizacja
to proces usuwania wszystkich znaków odstępu, a w przypadku JavaScript również zastępowania nazw funkcji
i zmiennych ich krótszymi odpowiednikami. Celem minimalizacji jest redukcja przepustowości wymaganej do
dostarczenia zawartości do przeglądarki internetowej. W rozdziale 26. omówię funkcje, jakie ASP.NET oferuje
w celu automatycznego zarządzania tym procesem. W tym — oraz w większości rozdziałów w książce — będą
używane zwykłe pliki. To jest normalna praktyka podczas tworzenia i testowania aplikacji.
Mając zaimportowane style biblioteki Bootstrap oraz zdefiniowane własne, można przystąpić do
przypisywania ich poszczególnym elementom. To jest prosty przykład i dlatego musimy użyć jedynie
trzech klas Bootstrap CSS: text-center, btn i btn-success.
Klasa text-center powoduje wyśrodkowanie zawartości elementu i jego elementów potomnych. Klasa btn
nadaje styl elementom <button>, <input > i <a>, tworząc z nich elegancki przycisk. Z kolei btn-success określa
zakres kolorów, jakie będą zastosowane w przycisku. Kolor przycisku zależy od użytego motywu, tutaj
wykorzystujemy domyślny (zdefiniowany w pliku bootstrap-theme.css), ale w internecie można znaleźć
niezliczone ilości jego zamienników. Efekt wprowadzonych zmian pokazano na rysunku 2.24.
Rysunek 2.24. Widok Index po zastosowaniu stylów
Na podstawie powyższego przykładu powinno być oczywiste, że nie mam zdolności graficznych. Tak
naprawdę jako dziecko byłem usprawiedliwiany na lekcjach plastyki z powodu absolutnego braku zdolności
manualnych. To było skutkiem poświęcania większej ilości czasu na zajęcia z matematyki, a tym samym
moje umiejętności plastyczne nie wykraczają poza umiejętności przeciętnego dziesięciolatka. Podczas pracy
nad rzeczywistym projektem przygotowanie oprawy graficznej i stylów dla zawartości zlecam
60
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
profesjonalistom. W omawianym przykładzie zająłem się tym samodzielnie, co oznacza wykorzystanie
biblioteki Bootstrap z maksymalną powściągliwością i spójnością, na jaką mnie stać.
Nadanie stylu widokowi RsvpForm
Biblioteka Bootstrap definiuje klasy, które mogą być używane w celu nadania stylu formularzom.
Nie zamierzam tutaj zagłębiać się w szczegóły — przykład zastosowania tego rodzaju klas jest
przedstawiony na listingu 2.22.
Listing 2.22. Zastosowanie klas Bootstrap w pliku RsvpForm.cshtml
@model PartyInvites.Models.GuestResponse
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<meta name="viewport" content="width=device-width" />
<link href="~/Content/Styles.css" rel="stylesheet" />
<title>RsvpForm</title>
</head>
<body>
<div class="panel panel-success">
<div class="panel-heading text-center"><h4>RSVP</h4></div>
<div class="panel-body">
@using (Html.BeginForm()) {
@Html.ValidationSummary()
<div class="form-group">
<label>Imię i nazwisko:</label>
@Html.TextBoxFor(x => x.Name, new { @class = "form-control"})
</div>
<div class="form-group">
<label>Twój e-mail:</label>
@Html.TextBoxFor(x => x.Email, new { @class = "form-control"})
</div>
<div class="form-group">
<label>Twój telefon:</label>
@Html.TextBoxFor(x => x.Phone, new { @class = "form-control"})
</div>
<div class="form-group">
<label>Czy przyjdziesz na przyjęcie?</label>
@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ę", new { @class = "form-control" })
</div>
<div class="text-center">
<input class="btn btn-success" type="submit" value="Wyślij RSVP" />
</div>
}
61
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
</div>
</div>
</body>
</html>
W omawianym przykładzie klasy Bootstrap tworzą panel wraz z nagłówkiem w celu przygotowania
struktury dla strony. Aby nadać styl formularzowi, użyłem klasy form-group, która jest przeznaczona do
stosowania stylu dla elementu <div> zawierającego <label> oraz powiązany z nim element <input> lub
<select>.
Wymienione elementy są tworzone za pomocą metod pomocniczych HTML, co oznacza, że nie są to
statycznie definiowane elementy, którym można przypisać wymaganą klasę form-control. Na szczęście metody
pomocnicze pobierają opcjonalny argument w postaci obiektu pozwalającego na wskazanie atrybutów
w tworzonych elementach, na przykład:
...
@Html.TextBoxFor(x => x.Name, new { @class = "form-control"})
...
Obiekt został utworzony za pomocą oferowanej przez C# funkcji typu anonimowego, która zostanie
omówiona w rozdziale 4. Tutaj obiekt wskazuje, że generowany przez metodę pomocniczą element TextBoxFor
powinien mieć atrybut class o wartości form-control. Właściwości zdefiniowane przez wymieniony obiekt są
używane w celu określenia nazw atrybutów dodawanych do elementu HTML. Ponieważ słowo class jest
zarezerwowane w C#, zostało poprzedzone znakiem @. To standardowa funkcja C# pozwalająca na użycie
słów kluczowych w wyrażeniach. Wynik wprowadzonych zmian pokazano na rysunku 2.25.
Rysunek 2.25. Widok RsvpForm po zastosowaniu stylów
Nadanie stylu widokowi Thanks
Ostatni widok, któremu musimy nadać styl, to Thanks.cshtml, a zmiany konieczne do wprowadzenia
przedstawiono na listingu 2.23. Zwróć uwagę, że dodany kod znaczników jest podobny do użytego
w widoku Index.cshtml. W celu ułatwienia sobie zadania zarządzania aplikacją dobrym rozwiązaniem
jest unikanie powielania kodu i znaczników, gdy tylko to możliwe. W rozdziale 5. poznasz tak zwane
układy Razor, natomiast w rozdziale 20. widoki częściowe — oba wymienione rozwiązania mogą być
wykorzystane w celu zmniejszenia stopnia powielania kodu znaczników.
62
ROZDZIAŁ 2.  PIERWSZA APLIKACJA MVC
Listing 2.23. Zastosowanie stylów Bootstrap w pliku Thanks.cshtml
@model PartyInvites.Models.GuestResponse
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<meta name="viewport" content="width=device-width" />
<title>Dziękujemy</title>
<style>
body { background-color: #F1F1F1; }
</style>
</head>
<body>
<div class="text-center">
<h1>Dziękujemy, @Model.Name!</h1>
<div class="lead">
@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ę.
}
</div>
</div>
</body>
</html>
Klasa lead powoduje zastosowanie jednego ze stylów typograficznych biblioteki Bootstrap, a efekt
wprowadzonych zmian pokazano na rysunku 2.26.
Rysunek 2.26. Widok Thanks po zastosowaniu stylów
Kończymy przykład
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.
To byłaby technika charakteryzująca się największą spójnością ze wzorcem MVC. 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. Chcemy, aby wiadomość e-mail została wysłana w czasie generowania widoku Thanks. Na listingu 2.24
pokazane są wymagane zmiany.
63
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 2.24. Użycie metody pomocniczej WebMail w pliku Thanks.cshtml
...
<body>
@{
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) {
@:<b>Przepraszamy - nie możemy wysłać wiadomości RSVP.</b>
}
}
<div class="text-center">
<h1>Dziękujemy, @Model.Name!</h1>
<div class="lead">
@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ę.
}
</div>
</div>
</body>
</html>
 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.
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 — stanowią one podstawę
efektywnego programowania na platformie ASP.NET MVC.
64
ROZDZIAŁ 3.

Wzorzec MVC
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 mechanizmu 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 kod HTML jest oddzielony od reszty
aplikacji, dzięki czemu jego 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.
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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 pokazałem 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 podczas konserwacji
aplikacji. Jeżeli potrzebujemy wykonać operacje 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.
66
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, który jest komponentem
odpowiedzialnym za przetwarzanie widoku w celu wygenerowania odpowiedzi przekazywanej przeglądarce
internetowej. 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 (usprawniony w MVC 4 i pozostawiony bez zmian w MVC 5), który korzysta
z całkowicie innej składni (opisanej w rozdziale 5.).
 Wskazówka 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.
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, podczas gdy pozostałe są zupełnie odmienne.
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 (ang. smart user interface).
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
67
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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ą
aplikację, której schemat jest pokazany na rysunku 3.2. Kod obsługujący interfejs użytkownika oraz kod obsługujący
logikę biznesową 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
Wzorzec smart UI jest idealny dla prostych projektów, ponieważ pozwala na bardzo szybkie osiągnięcie
dobrych wyników. (To przeciwieństwo wzorca MVC. Jak się przekonasz w rozdziale 7., zanim osiągniesz wyniki,
najpierw musisz poczynić odpowiednie przygotowania). Oprócz tego smart UI doskonale nadaje się do
prototypowania interfejsu użytkownika — te narzędzia wizualne są naprawdę dobre, choć zawsze uważałem,
że wspomniane narzędzia w Visual Studio pozostają niewygodne i nieprzewidywalne. 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.
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.
To nie jest powszechnie przyjmowany punkt widzenia, a jedynie nadmierne uproszczenie.
Bezrefleksyjne odrzucenie wzorca smart UI można uznać za błąd. Nie wszystko jest w nim złe, istnieją tam
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.
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).
68
ROZDZIAŁ 3.  WZORZEC MVC
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 konserwacji, ale z jej wykorzystaniem wiążą się dwa problemy. Pierwszy wynika
z faktu, że interfejs 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 (ang. data access layer, DAL). Wzorzec ten pokazano na rysunku 3.4.
Rysunek 3.4. Wzorzec architektury trójwarstwowej
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 konserwacji 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. Związane z nimi
pojęcia będziesz napotykał w większości dyskusji dotyczących wzorców stosowanych podczas tworzenia
oprogramowania.
69
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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.
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 firmie Microsoft,
w zespole pracującym nad technologią, która jest stosowana teraz w Windows Presentation Foundation (WPF).
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 WPF, która w sposób dwukierunkowy łączy
właściwości widoku (czyli listy rozwijane lub efekty kliknięcia przycisku) z właściwościami udostępnianymi
przez model widoku.
 Wskazówka 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.
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 tworzymy 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.5, nie istnieje bezpośrednia zależność pomiędzy PasswordResetHelper
a MyEmailSender.
70
ROZDZIAŁ 3.  WZORZEC MVC
Rysunek 3.5. Użycie interfejsów do rozdzielania komponentów
Przez wprowadzenie interfejsu IEmailSender zapewniamy, że nie będzie występowała bezpośrednia
zależność pomiędzy PasswordResetHelper i MyEmailSender. Możemy wymienić MyEmailSender na innego dostawcę
poczty elektronicznej, a nawet użyć imitacji do testowania. To nie będzie wymagało wprowadzenia jakichkolwiek
zmian w PasswordResetHelper. (Temat implementacji imitacji będzie poruszony w dalszej części rozdziału,
a ponadto powrócimy do niego jeszcze w rozdziale 6.).
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 poniżej:
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();
}
}
To podważa cel, jakim jest zastąpienie MyEmailSender bez konieczności zmiany PasswordReset, i jednocześnie
oznacza, że jesteśmy dopiero 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. Tak naprawdę tylko
pogorszyliśmy sprawę, ponieważ teraz klasa PasswordResetHelper zależy od IEmailSender oraz MyEmailSender
(rysunek 3.6).
Rysunek 3.6. 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.
Podczas omawiania wymienionego wzorca być może będziesz się zastanawiał, skąd bierze się zachwyt związany
z DI. Możesz mi jednak wierzyć, że DI jest jednym z głównych elementów efektywnego programowania
z użyciem MVC i może wprowadzić wiele zamieszania.
71
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Zrywanie i deklarowanie zależności
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, na przykład w następujący sposób:
public class PasswordResetHelper {
private IEmailSender emailSender;
public PasswordResetHelper(IEmailSender emailSenderParam) {
emailSender = emailSenderParam;
}
public void ResetPassword() {
//...wywołanie metod interfejsu w celu skonfigurowania szczegółów wiadomości e-mail...
mySender.SendEmail();
}
}
Możemy teraz powiedzieć, że konstruktor klasy PasswordResetHelper deklaruje zależność od IEmailSender.
Oznacza to, że nie będzie mógł zostać utworzony i użyty, zanim nie otrzyma obiektu implementującego
interfejs IEmailSender. Podczas deklarowania zależności klasa PasswordResetHelper nie ma żadnej wiedzy
o MyEmailSender i jedynie opiera się na interfejsie IEmailSender. W skrócie, PasswordResetHelper „nie wie”
(i nie interesuje się tym), jak został zaimplementowany interfejs IEmailSender.
Wstrzykiwanie zależności
Drugą częścią wzorca DI jest wstrzyknięcie zależności zadeklarowanych przez klasę PasswordResetHelper
podczas tworzenia jej egzemplarzy, stąd bierze się nazwa wstrzykiwanie zależności.
To oznacza konieczność określenia, która klasa implementująca interfejs IEmailSender zostanie użyta.
Następnie trzeba utworzyć egzemplarz tej klasy i przekazać jej obiekt będący argumentem dla konstruktora
PasswordResetHelper.
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.5.
Użycie kontenera wstrzykiwania zależności
Rozwiązaliśmy już nasz problem z zależnościami. Jednak pozostał jeden problem. W jaki sposób utworzyć
konkretną implementację interfejsu bez tworzenia zależności w innym miejscu aplikacji? Jak się okazuje,
w aplikacji nadal znajdują się polecenia podobne do poniższych:
72
ROZDZIAŁ 3.  WZORZEC MVC
...
IEmailSender sender = new MyEmailSender();
helper = new PasswordResetHelper(sender);
...
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.
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, że egzemplarz klasy MyEmailSender powinien być
tworzony w przypadku konieczności użycia implementacji IEmailSender.
Jeżeli w aplikacji będziemy potrzebowali obiektu PasswordResetHelper, wykorzystamy kontener DI do
jego utworzenia. Kontener DI wie, że klasa PasswordResetHelper ma zadeklarowaną zależność od interfejsu
IEmailSender, wie też o konieczności użycia klasy MyEmailSender jako implementacji wymienionego interfejsu.
Kontener DI łączy ze sobą te informacje, tworzy obiekt MyEmailSender, a następnie używa go jako argumentu
podczas tworzenia obiektu PasswordResetHelper. Ostatni z wymienionych obiektów może zostać użyty
w aplikacji.
 Uwaga Trzeba w tym miejscu koniecznie wspomnieć, że od teraz obiekty w aplikacji nie są już tworzone za pomocą
słowa kluczowego new. Zamiast tego należy przejść do kontenera DI i zażądać potrzebnego obiektu. Jeżeli dopiero
rozpoczynasz pracę z DI, to przyzwyczajenie się do takiego rozwiązania może zabrać trochę czasu. Jak się wkrótce
przekonasz, platforma ASP.NET MVC oferuje pewne funkcje ułatwiające ten proces.
Nie musimy tworzyć samodzielnie kontenera DI. Istnieje kilka świetnych implementacji dostępnych bezpłatnie
na zasadach open source. Jedna z nich, którą bardzo lubię i stosuję we własnych projektach, ma nazwę
Ninject; informacje na jej temat można znaleźć na stronie www.ninject.org. Wprowadzenie do Ninject
znajduje się w rozdziale 6. — dowiesz się tam, jak zainstalować odpowiedni pakiet za pomocą menedżera
NuGet.
 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, taki jak na przykład Ninject,
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.
73
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Opracowanie własnego kontenera DI stanowi doskonały sposób poznania, jak C# i .NET obsługują
typy oraz mechanizm refleksji. To będzie dobre ćwiczenie na deszczowy weekend. Jednak nie próbuj użyć
tego kodu w rzeczywistych projektach. Przygotowanie solidnego, niezawodnego i charakteryzującego się dużą
wydajnością kontenera DI jest trudnym zadaniem. Dlatego też w projektach należy stosować sprawdzone
i przetestowane pakiety. Osobiście lubię Ninject, choć dostępnych jest wiele innych kontenerów i na pewno
znajdziesz taki, który będzie odpowiadał Twojemu stylowi tworzenia kodu.
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 (ang. test-driven development, TDD), 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.
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ć przedmioty testów. Projekt ten zostanie utworzony przy dodaniu pierwszego testu jednostkowego lub
będzie utworzony automatycznie przy tworzeniu projektu z użyciem szablonu MVC. Wspomniany test 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 przeznaczonych do przeprowadzania testów.
Stosowanie testów jednostkowych
Możliwość przeprowadzania testów jednostkowych stanowi jedną z największych zalet pracy na platformie MVC.
To jednak nie jest rozwiązanie odpowiednie dla każdego i nie zamierzam udawać, że jest inaczej. Jeżeli wcześniej
nie spotkałeś się z testami jednostkowymi, to zachęcam Cię do ich wypróbowania i samodzielnego przekonania się,
czy takie podejście sprawdza się w Twojej pracy.
Osobiście lubię testy jednostkowe i stosuję je we własnych projektach, ale nie we wszystkich projektach i nie
tak spójnie, jak mógłbyś oczekiwać. Koncentruję się na opracowywaniu testów jednostkowych dla funkcji, których
utworzenie może być trudne, a same funkcje mogą stać się źródłem błędów podczas pracy nad projektem. W takich
przypadkach testy jednostkowe pomagają w przygotowaniu struktury i znalezieniu najlepszego sposobu implementacji
wymaganej funkcjonalności. Przekonałem się, że kiedy myślę o tym, co powinno być przetestowane, to łatwiej
wychwytuję potencjalne problemy — i to najczęściej zanim zacznę zmagać się z rzeczywistymi błędami i usterkami.
Mając to na uwadze, pamiętaj, że testy jednostkowe to nie religia. Tylko Ty wiesz, ile i jakiego rodzaju testy
jednostkowe trzeba przeprowadzić. Jeżeli nie uznajesz testów jednostkowych za użyteczne lub masz inną metodologię,
która sprawdza się doskonale podczas pracy, wtedy nie próbuj na siłę stosować testów jednostkowych tylko dlatego,
że takie podejście stało się popularne. (Z drugiej strony, jeśli nie masz lepszej metodologii i w ogóle nie przeprowadzasz
74
ROZDZIAŁ 3.  WZORZEC MVC
testów, wtedy prawdopodobnie zrzucasz na użytkowników wyszukiwanie błędów w kodzie i można Cię uznać za
kiepskiego programistę. Nie musisz stosować testów jednostkowych, ale naprawdę powinieneś przeprowadzać
jakiekolwiek formy testowania tworzonego kodu).
 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 testów
i sposobów ich wykorzystania.
W celu rozpoczęcia pracy utworzyłem pokazaną na listingu 3.1 klasę dla przykładowej aplikacji. Nazwa klasy
to AdminController. Definiuje ona metodę ChangeLoginName pozwalającą użytkownikom na zmianę hasła.
Listing 3.1. Definicja klasy AdminController
using System.Web.Mvc;
namespace TestingDemo {
public class AdminController : Controller {
private IUserRepository repository;
public AdminController(IUserRepository repo) {
repository = repo;
}
public ActionResult ChangeLoginName(string oldName, string newName) {
User user = repository.FetchByLoginName(oldName);
user.LoginName = newName;
repository.SubmitChanges();
// wygenerowanie pewnego widoku, aby wyświetlić wynik
return View();
}
}
}
 Wskazówka Tę klasę utworzyłem w celach demonstracyjnych w nowym projekcie Visual Studio o nazwie
TestingDemo. Nie musisz od początku tworzyć przykładów omawianych w tym podrozdziale, ponieważ
wymieniony projekt znajduje się w materiałach dołączonych do książki i dostępnych na stronie
ftp://ftp.helion.pl/przyklady/asp5zp.zip.
Kontroler jest oparty na pewnych klasach modelu oraz na interfejsie, o czym możesz się przekonać, analizując
listing 3.2. Warto przypomnieć ponownie, że ten kod nie pochodzi z rzeczywistego projektu. Zastosowałem tutaj
uproszczone klasy, aby łatwiej pokazać omawiane zagadnienia. Nie sugeruję więc, że powinieneś tworzyć klasę
User posiadającą na przykład tylko jedną właściwość.
Listing 3.2. Klasy modelu i interfejs, na którym oparta jest klasa AdminController
namespace TestingDemo {
public class User {
public string LoginName { get; set; }
}
75
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
public interface IUserRepository {
void Add(User newUser);
User FetchByLoginName(string loginName);
void SubmitChanges();
}
public class DefaultUserRepository : IUserRepository {
public void Add(User newUser) {
// miejsce na implementację
}
public User FetchByLoginName(string loginName) {
// miejsce na implementację
return new User() { LoginName = loginName };
}
public void SubmitChanges() {
// miejsce na implementację
}
}
}
W przykładowej aplikacji klasa User przedstawia użytkownika. Tworzenie użytkowników, zarządzanie nimi
i ich przechowywanie odbywa się za pomocą repozytorium, którego funkcjonalność jest zdefiniowana przez
interfejs IUserRepository. Wymieniona klasa to częściowa implementacja tego interfejsu w klasie
DefaultUserRepository.
Moim celem jest utworzenie testu jednostkowego przeznaczonego do przetestowania funkcjonalności
oferowanej przez metodę ChangeLoginName zdefiniowaną w klasie AdminController, jak przedstawiono
na listingu 3.3.
Listing 3.3. Przykładowy test dla metody AdminController.ChangeLoginName
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace TestingDemo.Tests {
[TestClass]
public class AdminControllerTest {
[TestMethod]
public void CanChangeLoginName() {
// przygotowanie (skonfigurowanie scenariusza)
User user = new User() { LoginName = "Bogdan" };
FakeRepository repositoryParam = new FakeRepository();
repositoryParam.Add(user);
AdminController target = new AdminController(repositoryParam);
string oldLoginParam = user.LoginName;
string newLoginParam = "Janek";
// działanie (wykonanie operacji)
target.ChangeLoginName(oldLoginParam, newLoginParam);
// asercje (weryfikacja wyniku)
Assert.AreEqual(newLoginParam,user.LoginName);
Assert.IsTrue(repositoryParam.DidSubmitChanges);
76
ROZDZIAŁ 3.  WZORZEC MVC
}
}
class FakeRepository : IUserRepository {
public List<User> Users = new List<User>();
public bool DidSubmitChanges = false;
public void Add(User user) {
Users.Add(user);
}
public User FetchByLoginName(string loginName) {
return User.First(m => m.LoginName == loginName);
}
public void SubmitChanges() {
DidSubmitChanges = true;
}
}
}
Przedmiotem 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 przedmioty 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 przedmiotu testu korzysta ze specyficznej dla testu implementacji interfejsu IUserRepository, która
symuluje określone warunki — w tym przypadku, gdy w repozytorium znajduje się tylko jeden użytkownik,
Bogdan. Tworzenie imitacji repozytorium oraz obiektu User 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. Metoda Assert jest
dostarczana przez zestaw testów Visual Studio i pozwala na sprawdzenie konkretnych danych wyjściowych.
Uruchomimy test za pomocą menu Test w Visual Studio i otrzymamy obraz stanu realizacji testów (rysunek 3.7).
Rysunek 3.7. Widok stanu realizacji testów jednostkowych
Jeżeli testy zostaną wykonane bez zgłoszenia żadnego nieobsłużonego wyjątku i wszystkie instrukcje Assert
zostaną wykonane bez problemów, w oknie Eksplorator testów 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.
77
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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. Jednym z możliwych do zastosowania usprawnień jest między
innymi wyeliminowanie specyficznych dla testu klas imitacji, takich jak FakeRepository, 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ę.
 Utwórz test, który sprawdzi poprawność działania nowej funkcji.
 Uruchom test i sprawdź, czy pojawia się czerwone światło.
 Utwórz 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. Podejście z użyciem TDD odwraca
kolejność etapów w tradycyjnym procesie tworzenia oprogramowania. Pracę zaczynasz od przygotowania testów
dla xperfekcyjnej implementacji funkcji, wiedząc, że ich wykonanie teraz zakończy się niepowodzeniem. Następnie
przystępujesz do implementacji danej funkcji, tworząc kolejne aspekty jej zachowania w celu zaliczenia jednego
lub większej liczby testów.
Ten cykl 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.
Podejście z użyciem TDD wydaje się na początku nieco dziwne, ale jest niezwykle inspirujące. Utworzenie
testów jako pierwszych powoduje, że zastanawiasz się nad perfekcyjną implementacją, zanim ograniczysz
sobie możliwości przez techniki używane do tworzenia kodu.
Wadą TDD jest konieczność zachowania ścisłej dyscypliny. Gdy zbliża się ostateczny termin zakończenia
pracy nad projektem, coraz częściej pojawia się pokusa odrzucenia TDD i po prostu rozpoczęcia tworzenia
kodu, ewentualnie celowego odrzucania problematycznych testów (z czym już wielokrotnie się spotkałem),
aby kod jawił się w lepszej kondycji niż ta, w której faktycznie się znajduje. Z tego powodu podejście TDD
powinno być stosowane w uznanych i dojrzałych zespołach charakteryzujących się ogólnie większym
poziomem umiejętności i dyscypliny, a także w zespołach, w których liderzy mogą wymuszać stosowanie
dobrych praktyk, nawet w obliczu ograniczeń czasowych.
 Wskazówka Przykład użycia podejścia TDD poznasz w rozdziale 6., w którym będę omawiał wbudowane w Visual
Studio narzędzia przeznaczone do testowania.
78
ROZDZIAŁ 3.  WZORZEC MVC
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, wtedy najczęściej nie zostanie on prawidłowo wykonany.
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, ponieważ chcę się skoncentrować
na platformie ASP.NET MVC. Każda aplikacja może odnieść korzyści z przeprowadzania testów jednostkowych.
Na platformie ASP.NET MVC nie istnieją żadne specjalne funkcje przeznaczone do wspierania tego rodzaju
testów. 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,
z którymi mogłeś się już wcześniej spotkać. Omówiłem znaczenie modelu domeny, a następnie wprowadziłem
też DI, pozwalający na rozdzielenie komponentów w celu zapewnienia jasnego podziału pomiędzy częściami
naszej aplikacji. Zademonstrowałem prosty przykład testów jednostkowych i wyjaśniłem, jak oddzielić luźno
sprzężone komponenty oraz jak DI ułatwia testowanie jednostkowe. W następnym rozdziale przedstawię
podstawowe funkcje języka C# używane podczas tworzenia aplikacji na platformie ASP.NET MVC.
79
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
80
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 niniejszym rozdziale przedstawię krótko te cechy języka C#, które dobry programista MVC musi
znać, oraz te używane w przykładach zaprezentowanych w książce.
Przedstawię tutaj jedynie krótkie omówienie poszczególnych cech. 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. W tabeli 4.1 znajdziesz podsumowanie
materiału omówionego w rozdziale.
Tabela 4.1. Podsumowanie materiału omówionego w rozdziale
Temat
Rozwiązanie
Listing (nr)
Uproszczenie właściwości C#
Użycie automatycznie
implementowanych właściwości
Od 1. do 7.
Utworzenie obiektu i ustawienie jego
właściwości w jednym kroku
Użycie inicjalizatorów kolekcji lub
obiektu
Od 8. do 10.
Dodanie funkcjonalności do klasy,
której nie można modyfikować
Użycie metody rozszerzającej
Od 11. do 18.
Uproszczenie użycia delegatów
Użycie wyrażenia lambda
Od 19. do 23.
Użycie niejawnych typów
Użycie słowa kluczowego var
24.
Utworzenie obiektu bez definiowania typu
Użycie typu anonimowego
25. i 26.
Wykonywanie zapytań do obiektów kolekcji,
jakby były bazą danych.
Użycie LINQ.
Od 27. do 31.
Uproszczenie użycia metod asynchronicznych.
Użycie słów kluczowych async i await.
32. i 33.
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) opartego na szablonie Empty. Nowemu projektowi nadaj
nazwę LanguageFeatures i zaznacz pole wyboru MVC, jak to zrobiliśmy podczas tworzenia projektu
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
w rozdziale 2. Omawiane tutaj funkcje nie są stosowane wyłącznie w aplikacjach MVC, ale narzędzie Visual
Studio Express 2013 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ż utwórz plik
HomeController.cs w katalogu Controllers. W tym celu prawym przyciskiem myszy kliknij katalog Controllers
w Eksploratorze rozwiązania, a następnie z menu kontekstowego wybierz opcję Dodaj/Kontroler….
W wyświetlonym oknie dialogowym Dodaj szkielet wybierz opcję Kontroler MVC 5 — pusty i kliknij
przycisk Dodaj. W oknie dialogowym Dodaj kontroler podaj nazwę HomeController i kliknij przycisk Dodaj.
Visual Studio utworzy plik klasy kontrolera, którego początkowa zawartość została pokazana na listingu 4.1.
Listing 4.1. Początkowy kod kontrolera HomeController
using System;
using System.Web.Mvc;
using LanguageFeatures.Models;
namespace LanguageFeatures.Controllers
{
public class HomeController : Controller
{
public string Index()
{
return "Przejście do adresu URL pokazującego przykład";
}
}
}
Dla każdego przykładu utworzymy metody akcji. W przypadku metody akcji Index wartością zwrotną
jest prosty ciąg tekstowy, co pozwala na zachowanie prostoty projektu.
 Ostrzeżenie W tym momencie klasa HomeController nie będzie mogła być skompilowana, ponieważ importuje
przestrzeń nazw LanguageFeatures.Models. Wymieniona przestrzeń nazw zostanie utworzona dopiero po
dodaniu klasy do katalogu Models, czym się zajmiemy już za chwilę, w pierwszej części przykładu.
Aby mieć możliwość wyświetlania wyników działania metod akcji, konieczne jest kliknięcie prawym
przyciskiem myszy metody Index, wybranie opcji Dodaj widok… i nadanie nowemu widokowi nazwy Result.
Kod wymienionego pliku widoku został przedstawiony na listingu 4.2. (Nie ma znaczenia, jakie opcje wybierzesz
w oknie dialogowym Dodawanie widoku, ponieważ początkową zawartość pliku zastąpisz kodem
przedstawionym na listingu).
Listing 4.2. Kod w pliku widoku Result.cshtml
@model String
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Result</title>
</head>
<body>
82
ROZDZIAŁ 4.  NAJWAŻNIEJSZE CECHY JĘZYKA
<div>
@Model
</div>
</body>
</html>
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.
Dodanie podzespołu System.Net.Http
W dalszej części rozdziału przedstawię przykład oparty na podzespole System.Net.Http, który nie jest
domyślnie dodawany do projektów ASP.NET MVC. Z menu Projekt w Visual Studio wybierz opcję Dodaj
odwołanie, co spowoduje wyświetlenie okna dialogowego Menedżer odwołań. Upewnij się o wybraniu sekcji
Zestawy w kolumnie po lewej stronie, a następnie odszukaj element System.Net.Http, jak pokazano na
rysunku 4.1.
Rysunek 4.1. Dodanie podzespołu do projektu
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. Wymieniona klasa jest zdefiniowana w pliku Product.cs.
Listing 4.3. Definiowanie właściwości w pliku Product.cs
namespace LanguageFeatures.Models {
public class Product {
private string name;
public string Name {
get { return name; }
set { name = value; }
}
}
}
83
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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 używana 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. Przykład użycia właściwości w pliku HomeController.cs
using System;
using System.Web.Mvc;
using LanguageFeatures.Models;
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 20.
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
84
ROZDZIAŁ 4.  NAJWAŻNIEJSZE CECHY JĘZYKA
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 w pliku Product.cs
namespace LanguageFeatures.Models {
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 w pliku Product.cs automatycznie implementowanych właściwości
namespace LanguageFeatures.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;}
}
}
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ć.
85
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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 w pliku Product.cs
namespace LanguageFeatures.Models {
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 łączenia w postaci pojedynczej właściwości 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 w pliku HomeController.cs
using System;
using System.Web.Mvc;
using LanguageFeatures.Models;
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();
86
ROZDZIAŁ 4.  NAJWAŻNIEJSZE CECHY JĘZYKA
// 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 wygenerować wynik, musimy przejść przez trzy etapy: utworzenie obiektu,
ustawienie wartości parametrów, a następnie wywołanie metody View, co pozwala na wyświetlenie wyniku
w widoku. Na szczęście możemy użyć funkcji inicjalizatora obiektów, która pozwala na utworzenie oraz
wypełnienie egzemplarza Product w jednym kroku (listing 4.9).
Listing 4.9. Użycie w pliku HomeController.cs 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));
}
...
Nawias klamrowy ({}) 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).
Listing 4.10. Inicjowanie kolekcji i tablic w pliku HomeController.cs
using
using
using
using
System;
System.Collections.Generic;
System.Web.Mvc;
LanguageFeatures.Models;
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" };
87
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
List<int> intList = new List<int> { 10, 20, 30, 40 };
Dictionary<string, int> myDict = new Dictionary<string, int> {
{ "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. 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. Wspomniana klasa została
zdefiniowana w pliku ShoppingCart.cs.
Listing 4.11. Klasa ShoppingCart zdefiniowana w pliku ShoppingCart.cs
using System.Collections.Generic;
namespace LanguageFeatures.Models {
public class ShoppingCart {
public List<Product> Products { get; set; }
}
}
ShoppingCart jest klasą działającą w charakterze opakowania dla kolekcji List obiektów Products
(w tym przykładzie taka prosta klasa jest wystarczająca). Załóżmy, że musimy określić całkowitą wartość
obiektów Products zawartych 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ć potrzebną
funkcjonalność. Na listingu 4.12 przedstawiono klasę MyExtensionMethods, którą również trzeba dodać
do katalogu Models. Wspomniana klasa została zdefiniowana w pliku MyExtensionMethods.cs.
Listing 4.12. Definiowanie metody rozszerzającej w pliku MyExtensionMethods.cs
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;
}
}
}
88
ROZDZIAŁ 4.  NAJWAŻNIEJSZE CECHY JĘZYKA
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 w pliku HomeController.cs
using
using
using
using
System;
System.Collections.Generic;
System.Web.Mvc;
LanguageFeatures.Models;
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<Product> {
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
decimal cartTotal = cart.TotalPrices();
return View("Result",
(object)String.Format("Razem: {0:c}", cartTotal));
}
}
}
 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 powyższym listingu kluczowym poleceniem jest:
...
decimal cartTotal = cart.TotalPrices();
...
89
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
W kodzie z listingu 4.13 tworzymy obiekt klasy ShoppingCart i wypełniamy go obiektami Products. 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 metody akcji UseExtension
możesz zobaczyć po uruchomieniu aplikacji i przejściu na stronę pod adresem URL /Home/UseExtension:
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<Product>.
Listing 4.14. Implementowanie interfejsu w klasie ShoppingCart
using System.Collections;
using System.Collections.Generic;
namespace LanguageFeatures.Models {
public class ShoppingCart : IEnumerable<Product> {
public List<Product> Products { get; set; }
public IEnumerator<Product> GetEnumerator() {
return Products.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
}
}
Możemy teraz zmienić naszą metodę rozszerzającą, aby operowała na IEnumerable<Product>, jak pokazano
na listingu 4.15.
Listing 4.15. Metoda rozszerzająca, która w pliku MyExtensionMethods.cs operuje na interfejsie
using System.Collections.Generic;
namespace LanguageFeatures.Models {
public static class MyExtensionMethods {
public static decimal TotalPrices(this IEnumerable<Product> productEnum) {
decimal total = 0;
foreach (Product prod in productEnum) {
total += prod.Price;
}
return total;
}
}
}
90
ROZDZIAŁ 4.  NAJWAŻNIEJSZE CECHY JĘZYKA
Typ pierwszego parametru zmieniliśmy na IEnumerable<Product>, co oznacza, że pętla foreach w treści
metody działa bezpośrednio na obiekcie Product. Przejście na interfejs oznacza, że możemy obliczyć całkowitą
wartość obiektów Products znajdujących się w dowolnej kolekcji IEnumerable<Product>, do których zalicza
się obiekt ShoppingCart, ale także tablice obiektów Product (listing 4.16).
Listing 4.16. Stosowanie w pliku HomeController.cs metody rozszerzającej dla różnych implementacji tego
samego interfejsu
using
using
using
using
System;
System.Collections.Generic;
System.Web.Mvc;
LanguageFeatures.Models;
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<Product> products = new ShoppingCart {
Products = new List<Product> {
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}
}
};
// 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<T> 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.
91
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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<T>, które zwracają również wartość IEnumerable<T>, 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 w pliku MyExtensionMethods.cs
using System.Collections.Generic;
namespace LanguageFeatures.Models {
public static class MyExtensionMethods {
public static decimal TotalPrices(this IEnumerable<Product> productEnum) {
decimal total = 0;
foreach (Product prod in productEnum) {
total += prod.Price;
}
return total;
}
public static IEnumerable<Product> FilterByCategory(
this IEnumerable<Product> 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<Product>, 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
System;
System.Collections.Generic;
System.Web.Mvc;
LanguageFeatures.Models;
namespace LanguageFeatures.Controllers {
92
ROZDZIAŁ 4.  NAJWAŻNIEJSZE CECHY JĘZYKA
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<Product> products = new ShoppingCart {
Products = new List<Product> {
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));
}
}
}
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 w pliku MyExtensionMethods.cs
using System;
using System.Collections.Generic;
namespace LanguageFeatures.Models {
public static class MyExtensionMethods {
public static decimal TotalPrices(this IEnumerable<Product> productEnum) {
decimal total = 0;
foreach (Product prod in productEnum) {
total += prod.Price;
}
return total;
}
93
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
public static IEnumerable<Product> FilterByCategory(
this IEnumerable<Product> productEnum, string categoryParam) {
foreach (Product prod in productEnum) {
if (prod.Category == categoryParam) {
yield return prod;
}
}
}
public static IEnumerable<Product> Filter(
this IEnumerable<Product> productEnum,
Func<Product, bool> 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 w kontrolerze Home.
Listing 4.20. Użycie w pliku HomeController.cs filtrującej metody rozszerzającej z parametrem Func
...
public ViewResult UseFilterExtensionMethod() {
// tworzenie i wypełnianie ShoppingCart
IEnumerable<Product> products = new ShoppingCart {
Products = new List<Product> {
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<Product, bool> 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));
}
...
94
ROZDZIAŁ 4.  NAJWAŻNIEJSZE CECHY JĘZYKA
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 w pliku HomeController.cs
...
public ViewResult UseFilterExtensionMethod() {
// tworzenie i wypełnianie ShoppingCart
IEnumerable<Product> products = new ShoppingCart {
Products = new List<Product> {
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<Product, bool> 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));
}
...
Wyrażenie lambda jest zaznaczone czcionką pogrubioną. Parametr jest definiowany bez specyfikowania
typu, który zostanie ustalony automatycznie. 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 w pliku HomeController.cs
...
public ViewResult UseFilterExtensionMethod() {
IEnumerable<Product> products = new ShoppingCart {
Products = new List<Product> {
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));
}
...
95
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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 w pliku HomeController.cs
...
public ViewResult UseFilterExtensionMethod() {
IEnumerable<Product> products = new ShoppingCart {
Products = new List<Product> {
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;
}
return View("Result", (object)String.Format("Razem: {0}", total));
}
...
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.
96
ROZDZIAŁ 4.  NAJWAŻNIEJSZE CECHY JĘZYKA
Automatyczna inferencja typów
Słowo kluczowe var z języka C# pozwala na zdefiniowanie zmiennej lokalnej bez wyraźnego określania
typu zmiennej, jak pokazano na listingu 4.24. Nazywa się to inferencją typu lub niejawnym typowaniem.
Listing 4.24. Użycie inferencji 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 ustalił go na podstawie
kodu. Jak pokazałem w zamieszczonym powyżej kodzie, kompilator pozwala na korzystanie ze
składników inferowanej klasy — w tym przypadku Product.
Użycie typów anonimowych
Łącząc inicjalizatory obiektów z inferencją 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.
Listing 4.25. Tworzenie typu anonimowego
...
var myAnonType = new {
Name = "MVC",
Category = "Wzorzec"
};
...
W przykładzie tym myAnonType jest obiektem typu anonimowego. 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 obiekty typu
anonimowego, 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 obiektów typu anonimowego, jak pokazano
na listingu 4.26, w którym przedstawiono metodę akcji CreateAnonArray dodaną do kontrolera Home.
Listing 4.26. Tworzenie w pliku HomeController.cs tablicy obiektów typu anonimowego
using
using
using
using
using
System;
System.Collections.Generic;
System.Text;
System.Web.Mvc;
LanguageFeatures.Models;
namespace LanguageFeatures.Controllers {
public class HomeController : Controller {
public string Index() {
return "Przejście do adresu URL pokazującego przykład";
}
97
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
// … 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
obiektów typu anonimowego. 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 w pliku HomeController.cs 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<decimal>.Default.Compare(item1.Price, item2.Price);
});
98
ROZDZIAŁ 4.  NAJWAŻNIEJSZE CECHY JĘZYKA
// 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 w pliku HomeController.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Text;
System.Web.Mvc;
LanguageFeatures.Models;
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 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 foundProducts = from match in products
orderby match.Price descending
select new { match.Name, match.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());
}
}
}
99
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Jest to znacznie elegantsze rozwiązanie. Składnia przypominająca SQL jest zaznaczona czcionką pogrubioną.
Sortujemy obiekty Product w kolejności malejącej i za pomocą słowa kluczowego select zwracamy typ anonimowy
zawierający po prostu właściwości Name i Price. Ten styl korzystania z LINQ jest nazywany składnią zapytania
i większość programistów po rozpoczęciu pracy z LINQ uznaje go za najwygodniejszy. Niestety, przy użyciu tej
metody zapytanie zwraca jeden obiekt typu anonimowego 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 w pliku HomeController.cs 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 foundProducts = 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());
}
...
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#.
W przypadku zaawansowanych zapytań LINQ konieczne jest skorzystanie z metod rozszerzających. Każda
z metod rozszerzających LINQ użytych na listingu 4.29 jest stosowana do IEnumerable<T> i zwraca
IEnumerable<T>, 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.
 Wskazówka Zwróć uwagę, że nie musieliśmy nawet określać nazw właściwości w typie anonimowym. C# ustalił
je na podstawie właściwości wybranych w metodzie Select.
100
ROZDZIAŁ 4.  NAJWAŻNIEJSZE CECHY JĘZYKA
W tabeli 4.2 zebrane są najużyteczniejsze metody rozszerzające LINQ. Zapytania LINQ wykorzystuję
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.2 operują na IEnumerable<T>.
Tabela 4.2. 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
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
101
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Opóźnione zapytania LINQ
Na pewno zauważyłeś, że w tabeli 4.2 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<T> nie zaczną być przeglądane
(listing 4.30). Na listingu pokazano prostą zmianę wprowadzoną w metodzie akcji FindProducts.
Listing 4.30. Użycie w pliku HomeController.cs 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 foundProducts = 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
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 i otrzymania wyników odzwierciedlających bieżący stan
ź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.
102
ROZDZIAŁ 4.  NAJWAŻNIEJSZE CECHY JĘZYKA
Listing 4.31. Natychmiast wykonywane zapytanie LINQ w pliku HomeController.cs
...
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.
Użycie metod asynchronicznych
Jednym z największych dodatków do języka C# na platformie .NET 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 (plik MyAsyncMethods.cs) dodanej do katalogu Models.
Listing 4.32. Prosty przykład metody asynchronicznej w pliku MyAsyncMethods.cs
using System.Net.Http;
using System.Threading.Tasks;
namespace LanguageFeatures.Models {
public class MyAsyncMethods {
public static Task<long?> GetPageLength() {
HttpClient client = new HttpClient();
var httpTask = client.GetAsync("http://apress.com");
103
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
// w trakcie oczekiwania na zakończenie działania żądania HTTP
// można przeprowadzić inne operacje
return httpTask.ContinueWith((Task<HttpResponseMessage> antecedent) => {
return antecedent.Result.Content.Headers.ContentLength;
});
}
}
}
 Ostrzeżenie Ten przykład wymaga podzespołu System.Net.Http, który dodaliśmy do projektu na początku
rozdziału.
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<HttpResponseMessage>. W ten
sposób platforma informuje, że żądanie zostanie wykonane w tle, a wynikiem wykonania wspomnianego
żądania będzie obiekt HttpResponseMessage.
 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 19.
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<HttpResponseMessage> 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<HttpResponseMessage>, 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<long?>, np.:
...
public static Task<long?> GetPageLength() {
...
104
ROZDZIAŁ 4.  NAJWAŻNIEJSZE CECHY JĘZYKA
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<long?> 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
return httpMessage.Content.Headers.ContentLength;
}
}
}
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<long?>. 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<long?>, 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.
105
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
 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 19., 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 19., 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.
106
ROZDZIAŁ 5.

Praca z silnikiem Razor
Silnik widoku przetwarza zawartość ASP.NET, szukając specjalnych poleceń, najczęściej odpowiedzialnych
za dynamiczne umieszczanie treści w danych wyjściowych wysyłanych do przeglądarki internetowej. Razor
to nazwa silnika widoku na platformie MVC. W wersji 5. platformy MVC silnik widoku nie uległ zmianie.
Jeżeli znasz składnię stosowaną we wcześniejszych wersjach, możesz pominąć ten rozdział.
W tym rozdziale przedstawię krótki przewodnik po składni Razora, dzięki czemu będziesz mógł
rozpoznać jego wyrażenia, 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 w kontekście innych funkcji platformy MVC. W tabeli 5.1 znajdziesz podsumowanie materiału
omówionego w rozdziale.
Tabela 5.1. Podsumowanie materiału omówionego w rozdziale
Temat
Rozwiązanie
Listing (nr)
Zdefiniowanie i uzyskanie dostępu do typu
modelu
Użycie wyrażeń @model i @Model
Od 1. do 4. i 15.
Ograniczenie stopnia powielania kodu
w widokach
Użycie pliku układu
Od 5. do 7.
i od 10. do 12.
Określenie układu domyślnego
Użycie pliku ViewStart
8. i 9.
Przekazanie wartości danych z kontrolera
do widoku
Przekazanie obiektu modelu widoku
lub ViewBag
13. i 14.
Wygenerowanie odmiennej zawartości
w zależności od wartości danych
Użycie konstrukcji warunkowych
silnika Razor
16. i 17.
Wymienienie elementów tablicy lub kolekcji
Użycie wyrażenia @foreach
18. i 19.
Dodanie przestrzeni nazw do widoku
Użycie wyrażenia @using
20.
Utworzenie przykładowego 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. Następnie wybierz szablon projektu Empty. Projektowi
nadaj nazwę Razor.
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Definiowanie modelu
Skorzystamy tu z bardzo prostego modelu domeny, który będzie zawierał jedną klasę domeny o nazwie Product.
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 {
public
public
public
public
public
int ProductID { get; set; }
string Name { get; set; }
string Description { get; set; }
decimal Price { get; set; }
string Category { set; get; }
}
}
Definiowanie kontrolera
Będziemy stosować konwencję platformy MVC i jako punkt wyjścia dla aplikacji zdefiniujemy kontroler
o nazwie HomeController. Kliknij prawym przyciskiem myszy katalog Controllers w projekcie i wybierz
Dodaj, a następnie Kontroler… z menu kontekstowego. Wybierz Kontroler MVC 5 — pusty, podaj nazwę
HomeController i kliknij przycisk Dodaj. Po kliknięciu drugiego przycisku Dodaj Visual Studio utworzy
plik HomeController.cs w katalogu Controllers. Umieść w nim kod z listingu 5.2.
Listing 5.2. Zawartość pliku HomeController.cs
using System.Web.Mvc;
using Razor.Models;
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);
}
}
}
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.
108
ROZDZIAŁ 5.  PRACA Z SILNIKIEM RAZOR
Tworzenie widoku
Aby utworzyć widok, kliknij prawym przyciskiem myszy metodę Index w klasie HomeController, a następnie
wybierz Dodaj widok… z menu kontekstowego. Upewnij się, że nazwa widoku to Index, zmień szablon na Empty
oraz wskaż Product jako klasę modelu. (Jeżeli nie widzisz klasy Product na liście rozwijanej, skompiluj projekt
i ponownie spróbuj utworzyć widok). Usuń zaznaczenia z pól wyboru i kliknij Dodaj, aby utworzyć widok,
który powinien pojawić się w katalogu Views/Home jako Index.cshtml. Początkowa zawartość pliku nowego
widoku została przedstawiona na listingu 5.3.
Listing 5.3. Zawartość pliku Index.cshtml
@model Razor.Models.Product
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
<div>
</div>
</body>
</html>
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
...
Polecenia Razor zaczynają się od znaku @. W tym przypadku polecenie @model oznacza zadeklarowanie
typu obiektu modelu, 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 obiektu modelu widoku za pomocą właściwości @Model (listing 5.4).
Na listingu pokazano prostą zmianę wprowadzoną w omawianym widoku.
109
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 5.4. Odwołanie do obiektu modelu w pliku Index.cshtml
@model Razor.Models.Product
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
<div>
@Model.Name
</div>
</body>
</html>
 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.1.
Rysunek 5.1. 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.2. 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.3 — 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.
110
ROZDZIAŁ 5.  PRACA Z SILNIKIEM RAZOR
Rysunek 5.2. Visual Studio podpowiada nazwy, które można wprowadzić w wyrażeniu @Model
Rysunek 5.3. 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 20., 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 20.,
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.
111
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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 5
(Razor) jak pokazano na rysunku 5.4.
Rysunek 5.4. Utworzenie nowego układu
Jako nazwę dla tworzonego pliku podaj _BasicLayout.cshtml (zwróć uwagę, że pierwszy znak w nazwie to
podkreślenie) 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 _BasicLayout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>
112
ROZDZIAŁ 5.  PRACA Z SILNIKIEM RAZOR
 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 <title>.
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, co pozwala na zademonstrowanie sposobu jego działania.
Listing 5.6. Dodanie elementów do układu w pliku _BasicLayout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
</head>
<body>
<h1>Informacje o produkcie</h1>
<div style="padding: 20px; border: solid medium black; font-size: 20pt">
@RenderBody()
</div>
<h2>Odwiedź witrynę <a href="http://helion.pl">Helion</a></h2>
</body>
</html>
Dodano kilka elementów oraz zastosowano pewne style CSS względem elementu <div> 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 w pliku Index.cshtml do wskazania układu
@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 <title> 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 <title>.
113
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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, co jest idealnym
rozwiązaniem. Nie tylko otrzymujemy prostszy kod znaczników, ale również unikamy powielania najczęściej
występujących elementów we wszystkich widokach. Aby zobaczyć układ w działaniu, po prostu uruchom
omawianą aplikację. Wynik pokazano na rysunku 5.5.
Rysunek 5.5. Efekt zastosowania prostego układu w widoku
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
(ponownie zwróć uwagę na znak podkreślenia na początku nazwy), a następnie umieść w nim kod
przedstawiony na listingu 5.8.
Listing 5.8. Zawartość pliku _ViewStart.cshtml
@{
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 Index.cshtml, aby używał pliku ViewStart
@model Razor.Models.Product
@{
ViewBag.Title = "Nazwa produktu";
}
Nazwa produktu: @Model.Name
114
ROZDZIAŁ 5.  PRACA Z SILNIKIEM RAZOR
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 pliku HomeController.cs
using System.Web.Mvc;
using Razor.Models;
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 ustaw opcje jak pokazano na rysunku 5.6: jako nazwę
widoku podaj NameAndPrice, wybierz szablon Empty, natomiast jako klasę modelu wskaż Product
(Razor.Models).
115
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 5.6. Utworzenie widoku korzystającego z układu
Zwróć uwagę na komunikat znajdujący się pod polem wyboru Użyj strony układu. 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.7) pozwalające na wybór pliku układu.
Rysunek 5.7. 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.8, nazwa pliku układu została umieszczona w polu tekstowym.
116
ROZDZIAŁ 5.  PRACA Z SILNIKIEM RAZOR
Rysunek 5.8. Wybór pliku układu podczas tworzenia nowego widoku
Po kliknięciu przycisku Dodaj nastąpi utworzenie pliku /Views/Home/NameAndPrice.cshtml.
Zawartość wymienionego pliku przedstawiono na listingu 5.11.
Listing 5.11. Zawartość pliku NameAndPrice.cshtml
@model Razor.Models.Product
@{
ViewBag.Title = "NameAndPrice";
Layout = "~/Views/_BasicLayout.cshtml";
}
<h2>NameAndPrice</h2>
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 pliku NameAndPrice.cshtml
@model Razor.Models.Product
@{
ViewBag.Title = "NameAndPrice";
Layout = "~/Views/_BasicLayout.cshtml";
}
<h2>NameAndPrice</h2>
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.9. 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).
117
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 5.9. Treść z pliku układu zastosowana w widoku NameAndPrice
 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.2.
Tabela 5.2. 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:
118
ROZDZIAŁ 5.  PRACA Z SILNIKIEM RAZOR
...
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.
Przetwarzanie kontra formatowanie danych
Bardzo ważne jest rozróżnianie między przetwarzaniem a formatowaniem danych. Widoki formatują dane i dlatego
w powyższym przykładzie widokowi przekazaliśmy obiekt Product zamiast właściwości obiektu w ciągu tekstowym.
Przetwarzanie danych — łączenie z wyborem obiektów danych do wyświetlenia — to zadanie kontrolera, który
będzie wywoływany dla modelu oraz pobierze i zmodyfikuje wymagane dane. Czasami trudno określić granicę
między przetwarzaniem i formatowaniem danych. Warto wówczas pamiętać o zachowaniu ostrożności i stosowaniu
w widokach i kontrolerach jedynie najprostszych wyrażeń Razor.
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 w pliku HomeController.cs
using System.Web.Mvc;
using Razor.Models;
namespace Razor.Controllers
{
public class HomeController : Controller
{
Product myProduct = new Product
{
ProductID = 1,
119
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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 w katalogu Views/Home tworzymy ś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.
Listing 5.14. Zawartość pliku widoku DemoExpression.cshtml
@model Razor.Models.Product
@{
ViewBag.Title = "DemoExpression";
}
<table>
<thead>
<tr><th>Właściwość</th><th>Wartość</th></tr>
</thead>
<tbody>
<tr><td>Nazwa</td><td>@Model.Name</td></tr>
<tr><td>Cena</td><td>@Model.Price</td></tr>
<tr><td>Ilość w magazynie</td><td>@ViewBag.ProductCount</td></tr>
</tbody>
</table>
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.10 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.
120
ROZDZIAŁ 5.  PRACA Z SILNIKIEM RAZOR
Rysunek 5.10. 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.
Listing 5.15. Użycie wyrażenia Razor w celu przypisania wartości atrybutu w pliku DemoExpression.cshtml
@model Razor.Models.Product
@{
ViewBag.Title = "DemoExpression";
Layout = "~/Views/_BasicLayout.cshtml";
}
<table>
<thead>
<tr><th>Właściwość</th><th>Wartość</th></tr>
</thead>
<tbody>
<tr><td>Nazwa</td><td>@Model.Name</td></tr>
<tr><td>Cena</td><td>@Model.Price</td></tr>
<tr><td>Ilość w magazynie</td><td>@ViewBag.ProductCount</td></tr>
</tbody>
</table>
<div data-discount="@ViewBag.ApplyDiscount" data-express="@ViewBag.ExpressShip"
data-supplier="@ViewBag.Supplier">
Element posiada atrybuty danych
</div>
Rabat:<input type="checkbox" checked="@ViewBag.ApplyDiscount" />
121
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Express:<input type="checkbox" checked="@ViewBag.ExpressShip" />
Dostawca:<input type="checkbox" checked="@ViewBag.Supplier" />
Użyliśmy prostych wyrażeń Razor w celu przypisania wartości pewnych atrybutów data w elemencie
<div>.
 Wskazówka 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.:
...
<div data-discount="False" data-express="True" data-supplier="">
Element posiada atrybuty danych
</div>
...
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: <input type="checkbox" />
Express: <input type="checkbox" checked="checked" />
Dostawca: <input type="checkbox" />
...
Na platformie ASP.NET MVC silnik Razor potrafi wykryć sposób użycia atrybutu takiego jak checked,
w którym obecność atrybutu, a nie wartość, zmienia konfigurację elementu (w specyfikacji HTML to będzie
atrybut boolowski). 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.11.
Rysunek 5.11. Efekt usunięcia atrybutów, których obecność konfiguruje element
122
ROZDZIAŁ 5.  PRACA Z SILNIKIEM RAZOR
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 w pliku DemoExpression.cshtml
@model Razor.Models.Product
@{
ViewBag.Title = "DemoExpression";
Layout = "~/Views/_BasicLayout.cshtml";
}
<table>
<thead>
<tr><th>Właściwość</th><th>Wartość</th></tr>
</thead>
<tbody>
<tr><td>Nazwa</td><td>@Model.Name</td></tr>
<tr><td>Cena</td><td>@Model.Price</td></tr>
<tr>
<td>Ilość w magazynie</td>
<td>
@switch ((int)ViewBag.ProductCount) {
case 0:
@: Brak
break;
case 1:
<b>Mało (@ViewBag.ProductCount)</b>
break;
default:
@ViewBag.ProductCount
break;
}
</td>
</tr>
</tbody>
</table>
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:
123
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
...
<b>Mało (@ViewBag.ProductCount)</b>
...
lub tak:
...
@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.12.
Rysunek 5.12. Użycie polecenia warunkowego switch w widoku 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 zdefiniowanym w pliku DemoExpression.cshtml
@model Razor.Models.Product
@{
ViewBag.Title = "DemoExpression";
Layout = "~/Views/_BasicLayout.cshtml";
}
<table>
<thead>
124
ROZDZIAŁ 5.  PRACA Z SILNIKIEM RAZOR
<tr><th>Właściwość</th><th>Wartość</th></tr>
</thead>
<tbody>
<tr><td>Nazwa</td><td>@Model.Name</td></tr>
<tr><td>Cena</td><td>@Model.Price</td></tr>
<tr>
<td>Ilość w magazynie</td>
<td>
@if (ViewBag.ProductCount == 0) {
@: Brak
} else if (ViewBag.ProductCount == 1) {
<b>Mało (@ViewBag.ProductCount)</b>
} else {
@ViewBag.ProductCount
}
</td>
</tr>
</tbody>
</table>
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 20., w którym dokładnie przyjrzymy się widokom.
Wyświetlanie zawartości tablic i kolekcji
Tworząc aplikacje w technologii ASP.NET MVC, często będziesz spotykał się z koniecznością wyświetlenia
zawartoś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 zdefiniowana w pliku HomeController.cs
using System.Web.Mvc;
using Razor.Models;
namespace Razor.Controllers
{
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
{
Product[] array
new Product
new Product
DemoArray()
= {
{Name = "Kajak", Price = 275M},
{Name = "Kamizelka ratunkowa", Price = 48.95M},
125
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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. (Nie wiem,
dlaczego zdecydowano się na takie rozwiązanie, skoro Razor bez problemów obsługuje tablice). Dlatego też,
aby utworzyć widok dla wymienionej metody akcji przekazującej tablicę, najlepszym rozwiązaniem jest
utworzenie widoku bez modelu, a następnie ręczne dodanie wyrażenia @model już po utworzeniu pliku.
Na listingu 5.19 przedstawiono zawartość pliku widoku DemoArray.cshtml, który został utworzony
w katalogu Views/Home, a następnie zmodyfikowany.
Listing 5.19. Zawartość pliku widoku DemoArray.cshtml
@model Razor.Models.Product[]
@{
ViewBag.Title = "DemoArray";
Layout = "~/Views/_BasicLayout.cshtml";
}
@if (Model.Length > 0) {
<table>
<thead><tr><th>Produkt</th><th>Cena</th></tr></thead>
<tbody>
@foreach (Razor.Models.Product p in Model) {
<tr>
<td>@p.Name</td>
<td>@p.Price zł</td>
</tr>
}
</tbody>
</table>
} else {
<h2>Brak danych produktu</h2>
}
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 <h2> 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.13.
126
ROZDZIAŁ 5.  PRACA Z SILNIKIEM RAZOR
Rysunek 5.13. Wygenerowanie elementów za pomocą konstrukcji pętli
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 w pliku DemoArray.cshtml
@using Razor.Models
@model Product[]
@{
ViewBag.Title = "DemoArray";
Layout = "~/Views/_BasicLayout.cshtml";
}
@if (Model.Length > 0) {
<table>
<thead><tr><th>Produkt</th><th>Cena</th></tr></thead>
<tbody>
@foreach (Product p in Model) {
<tr>
<td>@p.Name</td>
<td>@p.Price zł</td>
</tr>
}
127
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
</tbody>
</table>
} else {
<h2>Brak danych produktu</h2>
}
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 20. 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. Wspomniane narzędzia pozwalają na maksymalne
wykorzystanie możliwości oferowanych przez projekty.
128
ROZDZIAŁ 6.

Ważne narzędzia wspierające MVC
W niniejszym rozdziale przedstawię trzy narzędzia, które powinny się znaleźć w arsenale każdego programisty
MVC: kontener wstrzykiwania zależności (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. 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 frameworkiem
Rhino Mocks, który można uznać za dobrą alternatywę dla Moq.
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. W tabeli 6.1 znajdziesz podsumowanie materiału omówionego w rozdziale.
 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ę.
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Tabela 6.1. Podsumowanie materiału omówionego w rozdziale
Temat
Rozwiązanie
Listing (nr)
Rozdzielenie klas
Zastosowanie interfejsów i zadeklarowanie
zależności od nich w konstruktorach klas
Od 1. do 9.
i od 13. do
16.
Automatyczne rozwiązywanie
zależności wyrażonych
za pomocą interfejsów
Użycie Ninject lub innego kontenera
wstrzykiwania zależności
10.
Utworzenie implementacji interfejsu
11. i 12.
Integracja kontenera Ninject
w aplikacji MVC
IDependencyResolver, który wywołuje jądro Ninject
i rejestruje je jako mechanizm rozwiązywania
zależności przez wywołanie metody
System.Web.Mvc.DependencyResolver.SetResolver
Wstrzyknięcie do nowo
utworzonych obiektów wartości
właściwości i konstruktora
Użycie metod WithPropertyValue
i WithConstructorArgument
Od 17. do 20.
Dynamiczny wybór klasy
implementacji dla interfejsu
Użycie warunkowego dołączania Ninject
21. i 22.
Kontrola cyklu życiowego
obiektów tworzonych przez
Ninject
Ustawienie zakresu obiektu
Od 23. do 25.
Utworzenie testów
jednostkowych
Dodanie projektu testów jednostkowych
do rozwiązania i udekorowanie pliku klasy
atrybutami TestClass i TestMethod
26. i 27. oraz
29. i 30.
Sprawdzenie oczekiwanych
danych wyjściowych testu
jednostkowego
Użycie klasy Assert
28.
Skoncentrowanie testu
jednostkowego na pojedynczej
funkcji komponentu
Izolacja testu docelowego za pomocą obiektów
imitacyjnych
Od 31. do 34.
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 i następnie
wybierz szablon projektu Empty i zaznacz pole wyboru MVC, aby wygenerować podstawowy projekt MVC.
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.
130
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Listing 6.1. Zawartość pliku Product.cs
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. Zawartość pliku LinqValueCalculator.cs
using System.Collections.Generic;
using System.Linq;
namespace EssentialTools.Models
{
public class LinqValueCalculator
{
public decimal ValueProducts(IEnumerable<Product> 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. Zawartość pliku ShoppingCart.cs
using System.Collections.Generic;
namespace EssentialTools.Models
{
public class ShoppingCart
{
private LinqValueCalculator calc;
public ShoppingCart(LinqValueCalculator calcParam)
{
calc = calcParam;
}
public IEnumerable<Product> Products { get; set; }
public decimal CalculateProductTotal()
{
return calc.ValueProducts(Products);
}
}
}
131
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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, który jest powiązany
z metodą akcji (tutaj jest to widok zdefiniowany w pliku Views/Home/Index.cshtml).
Listing 6.4. Zawartość pliku HomeController.cs
using System.Linq;
using System.Web.Mvc;
using EssentialTools.Models;
namespace EssentialTools.Controllers
{
public class HomeController : Controller
{
private 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);
}
}
}
Dodanie widoku
Ostatnim dodatkiem do projektu jest widok o nazwie Index. Nie ma znaczenia, jakie opcje wybierzesz podczas
jego tworzenia, o ile zawartość pliku Index.cshtml będzie odpowiadała przedstawionej na listingu 6.5.
Listing 6.5. Zawartość pliku Index.cshtml
@model decimal
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
132
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
<title>Wartość</title>
</head>
<body>
<div>
Wartość całkowita wynosi @Model zł
</div>
</body>
</html>
Ten widok używa 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. Wprawdzie to bardzo prosty projekt, ale wystarczający do przedstawienia różnych narzędzi
i technik, które zostaną omówione w rozdziale.
Rysunek 6.1. Testowanie przykładowej aplikacji
Użycie Ninject
W rozdziale 3. przedstawiłem temat wstrzykiwania zależności (DI). Dla przypomnienia — chcemy zapewnić
odrębność komponentów aplikacji MVC przez użycie interfejsów oraz kontenera DI. Wspomniany kontener
tworzy egzemplarze obiektów przez utworzenie implementacji interfejsów, od których są zależne obiekty,
a następnie wstrzykuje je do konstruktora.
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. Nie przejmuj się, jeżeli nie polubisz się z Ninject —
podstawowe zasady są takie same dla wszystkich kontenerów DI. Dostępnych jest wiele kontenerów DI
do użycia, spośród których możesz wybrać ulubiony.
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 (na przykład w celu przeprowadzenia testów), zamiast jedynie zastąpić
jedną klasę inną klasą.
133
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
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 klasy IValueCalculator.cs
do katalogu Models i utworzyć interfejs przedstawiony na listingu 6.6.
Listing 6.6. Zawartość pliku IValueCalculator.cs
using System.Collections.Generic;
namespace EssentialTools.Models
{
public interface IValueCalculator
{
decimal ValueProducts(IEnumerable<Product> products);
}
}
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<Product> 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.
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<Product> Products { get; set; }
public decimal CalculateProductTotal()
{
134
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
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,
jak przedstawiono na listingu 6.9.
Listing 6.9. Zastosowanie interfejsu w kontrolerze HomeController
...
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.
Oznacza to wskazanie Ninject, że LinqValueCalculator to implementacja interfejsu IValueCalculator,
której chcemy użyć. Musimy więc uaktualnić klasę HomeController, aby obiekty były pobierane za pomocą
Ninject, a nie przez użycie słowa kluczowego new.
Dodawanie Ninject do projektu Visual Studio
Najłatwiejszym sposobem dodania Ninject do projektu MVC jest użycie wbudowanego w Visual Studio
menedżera pakietów NuGet, co znacznie ułatwia instalację i aktualizację wielu różnych pakietów. W rozdziale
2. użyliśmy NuGet do instalacji biblioteki Bootstrap, ale katalog dostępnych pakietów jest ogromny i zawiera
również Ninject.
W Visual Studio wybierz opcję menu Narzędzia/Menedżer pakietów NuGet/Konsola menedżera pakietów.
To spowoduje przejście do wiersza poleceń menedżera pakietów NuGet, w którym należy wydać poniższe
polecenia:
Install-Package Ninject -version 3.0.1.10
Install-Package Ninject.Web.Common -version 3.0.0.7
Install-Package Ninject.MVC3 -Version 3.0.0.6
Pierwsze polecenie powoduje instalację podstawowego pakietu, natomiast pozostałe instalują rozszerzenia,
dzięki którym Ninject jeszcze lepiej współpracuje z aplikacjami ASP.NET (wkrótce to wyjaśnię). Nie przejmuj
się odniesieniem do MVC3 w nazwie ostatniego pakietu, działa on doskonale na platformie MVC 5.
W poleceniach zastosowaliśmy argument -version, aby zainstalować wskazane wersje pakietów. To są
najnowsze wersje dostępne w czasie powstawania książki. Powinieneś użyć argumentu version, aby mieć
pewność, że będziesz mógł dokładnie odtworzyć przykłady omawiane w książce. W rzeczywistych projektach
możesz pominąć ten argument i tym samym zainstalować najnowsze (prawdopodobnie znacznie nowsze)
wersje pakietów.
135
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Zaczynamy korzystać z Ninject
W celu uzyskania podstawowej funkcjonalności Ninject konieczne jest wykonanie trzech kroków — wszystkie
zostały przedstawione na listingu 6.10. W wymienionym listingu zaprezentowano zmiany, które trzeba
wprowadzić w kontrolerze Home.
Listing 6.10. Dodanie podstawowej funkcjonalności Ninject do metody akcji Index w pliku HomeController.cs
using System.Web.Mvc;
using EssentialTools.Models;
using Ninject;
namespace EssentialTools.Controllers
{
public class HomeController : Controller
{
private 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<IValueCalculator>().To<LinqValueCalculator>();
IValueCalculator calc = ninjectKernel.Get<IValueCalculator>();
ShoppingCart cart = new ShoppingCart(calc) { Products = products };
decimal totalValue = cart.CalculateProductTotal();
return View(totalValue);
}
}
}
 Wskazówka W tym podrozdziale będę dokładnie omawiał kolejne kroki. Zrozumienie wstrzykiwania zależności
wymaga nieco czasu i chcę mieć pewność, że nie pominę niczego, co mogłoby Ci pomóc w poznaniu DI.
Pierwszym krokiem jest przygotowanie Ninject do użycia. W tym celu tworzymy egzemplarz obiektu
kernel Ninject, pozwalającego na rozwiązywanie zależności i tworzenie nowych obiektów. Kiedy potrzebny
jest nowy obiekt, do jego utworzenia będziemy używać Ninject, a nie słowa kluczowego new. Poniższe
polecenie w listingu 6.10 tworzy egzemplarz obiektu kernel:
...
IKernel ninjectKernel = new StandardKernel();
...
136
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Konieczne jest utworzenie implementacji interfejsu Ninject.IKernel, co też czynimy poprzez utworzenie
nowego egzemplarza klasy StandardKernel. Wprawdzie Ninject można rozbudować i dostosować do użycia
różnych rodzajów obiektu kernel, ale w tym rozdziale potrzebujemy jedynie wbudowanego StandardKernel.
(Tak naprawdę z Ninject korzystam od lat i jeszcze nigdy nie musiałem użyć obiektu innego niż StandardKernel).
Teraz możemy przejść do kroku drugiego, czyli konfiguracji obiektu Ninject, aby wskazać obiekty
implementacji przeznaczone do użycia z interfejsami, z którymi będziemy pracować. Poniżej przedstawiono
polecenie, które na listingu 6.10 jest odpowiedzialne za wykonanie kroku drugiego:
...
ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
...
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<IValueCalculator>();
...
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 punkcie 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ę, a tym samym
wyprowadzenie konfiguracji z kontrolera.
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, na przykład podczas tworzenia egzemplarzy kontrolerów.
Aby skonfigurować mechanizm rozwiązywania zależności, do omawianego projektu dodaj nowy katalog
o nazwie Infrastructure. Ten katalog jest przeznaczony na klasy, które nie pasują do innych katalogów
aplikacji MVC. Następnie umieść w nim nowy plik o nazwie NinjectDependencyResolver.cs. W nowo
dodanym pliku powinien znaleźć się kod przedstawiony na listingu 6.11.
Listing 6.11. Kod, który należy umieścić w pliku NinjectDependencyResolver.cs
using
using
using
using
using
System;
System.Collections.Generic;
System.Web.Mvc;
Ninject;
EssentialTools.Models;
137
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
namespace EssentialTools.Infrastructure
{
public class NinjectDependencyResolver : IDependencyResolver
{
private IKernel kernel;
public NinjectDependencyResolver(IKernel kernelParam)
{
kernel = kernelParam;
AddBindings();
}
public object GetService(Type serviceType)
{
return kernel.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
return kernel.GetAll(serviceType);
}
private void AddBindings()
{
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
}
}
}
Klasa NinjectDependencyResolver implementuje interfejs IDependencyResolver, będący częścią przestrzeni
nazw System.Mvc i używany przez platformę MVC do pobierania niezbędnych obiektów. Platforma MVC będzie
wywoływała metody GetService lub GetServices, gdy będzie potrzebowała egzemplarza klasy do obsługi żądania
przychodzącego. Zadaniem mechanizmu rozwiązywania zależności jest utworzenie egzemplarza — to zadanie jest
wykonywane poprzez wywołanie metod Ninject TryGet i GetAll. Metoda TryGet działa podobnie jak użyta
wcześniej metoda Get, ale jeśli nie znajdzie odpowiedniego skojarzenia, wówczas zwraca wartość null, zamiast
zgłaszać wyjątek. Metoda GetAll obsługuje wiele skojarzeń dla pojedynczego typu, który jest używany
w przypadku dostępności kilku różnych dostawców usług.
Utworzona tutaj klasa mechanizmu rozwiązywania zależności jest również miejscem, w którym
przeprowadzamy konfigurację skojarzeń Ninject. W metodzie AddBindings użyto metod Bind i To do
zdefiniowania związku pomiędzy interfejsem IValueCalculator oraz klasą LinqValueCalculator.
Rejestracja mechanizmu rozwiązywania zależności
Nie wystarczy po prostu przygotować implementację interfejsu IDependencyResolver — platformę MVC
musisz poinformować o tym, że chcesz używać własnego mechanizmu rozwiązywania zależności. Dodane
za pomocą menedżera NuGet pakiety Ninject tworzą w katalogu App_Start plik NinjectWebCommon.cs
definiujący metody i wywoływany automatycznie w trakcie uruchamiania aplikacji, aby tym samym
zapewnić integrację z cyklem życiowym żądania ASP.NET. (To ma na celu zapewnienie obsługi funkcji
zakresów, która zostanie omówiona w dalszej części rozdziału). W metodzie RegisterServices klasy
NinjectWebCommon dodajemy polecenie tworzące egzemplarz klasy NinjectDepenedencyResolver. Metoda
statyczna SetResolver, zdefiniowana przez klasę System.Web.Mvc.DependencyResolver, jest używana do
rejestracji mechanizmu rozwiązywania zależności na platformie MVC, jak pokazano na listingu 6.12.
Nie przejmuj się, jeśli to wszystko nie jest jeszcze dla Ciebie jasne. Efektem działania pokazanego polecenia
jest utworzenie pomostu między Ninject i platformą MVC w celu obsługi DI.
138
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Listing 6.12. Rejestracja mechanizmu rozwiązywania zależności w pliku NinjectWebCommon.cs
...
private static void RegisterServices(IKernel kernel) {
System.Web.Mvc.DependencyResolver.SetResolver(new
EssentialTools.Infrastructure.NinjectDependencyResolver(kernel));
}
...
Refaktoring kontrolera Home
Ostatnim krokiem jest refaktoring kontrolera Home, co pozwoli na wykorzystanie funkcji skonfigurowanych
w poprzednich punktach. Zmiany wprowadzone w kontrolerze zostały przedstawione na listingu 6.13.
Listing 6.13. Refaktoring kontrolera HomeController
using System.Web.Mvc;
using EssentialTools.Models;
namespace EssentialTools.Controllers
{
public class HomeController : Controller
{
private IValueCalculator calc;
private 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 HomeController(IValueCalculator calcParam)
{
calc = calcParam;
}
public ActionResult Index()
{
ShoppingCart cart = new ShoppingCart(calc) { Products = products };
decimal totalValue = cart.CalculateProductTotal();
return View(totalValue);
}
}
}
Podstawowa zmiana polega na dodaniu konstruktora klasy akceptującego implementację interfejsu
IValueCalculator. Ponadto zmieniliśmy klasę HomeController, aby zadeklarować zależność. Podczas tworzenia
egzemplarza kontrolera Ninject dostarcza obiekt implementujący interfejs IValueCalculator, co odbywa się za
pomocą konfiguracji przygotowanej w klasie NinjectDependencyResolver na listingu 6.10.
Kolejna zmiana polega na usunięciu z kontrolera wszystkich fragmentów kodu związanego z Ninject
i klasą LinqValueCalculator — w ten sposób wreszcie udaje się usunąć ścisłe powiązanie klas HomeController
i LinqValueCalculator.
Po uruchomieniu aplikacji otrzymasz wynik pokazany na rysunku 6.2. Otrzymany wynik jest oczywiście
taki sam jak w przypadku utworzenia egzemplarza klasy LinqValueCalculator bezpośrednio w kontrolerze.
139
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 6.2. Efekt uruchomienia omawianej aplikacji
W ten sposób utworzyliśmy przykład wstrzyknięcia konstruktora, co jest jedną z postaci wstrzykiwania
zależności. Poniżej omówiono procesy zachodzące po uruchomieniu przykładowej aplikacji, gdy przeglądarka
internetowa wykonuje żądanie do głównego adresu URL aplikacji.
1. Platforma MVC otrzymała żądanie i określiła, że dotyczy ono kontrolera Home (sposób, w jaki
platforma MVC to określiła, zostanie przedstawiony w rozdziale 17.).
2. Platforma MVC poprosiła przygotowaną przez nas klasę mechanizmu rozwiązywania zależności
o utworzenie nowego egzemplarza klasy HomeController, podając, że klasa używa parametru Type
metody GetService.
3. Mechanizm rozwiązywania zależności prosi Ninject o utworzenie nowej klasy HomeController
i przekazuje obiekt Type metodzie TryGet.
4. Ninject analizuje konstruktora klasy HomeController i odkrywa, że wymagana jest implementacja
interfejsu IValueCalculator, do którego Ninject posiada skojarzenie.
5. Ninject tworzy egzemplarz klasy LinqValueCalculator i używa jej do utworzenia nowego egzemplarza
klasy HomeController.
6. Ninject przekazuje nowo utworzony egzemplarz HomeController do mechanizmu rozwiązywania
zależności, który z kolei zwraca klasę platformie MVC. Następnie platforma MVC używa egzemplarza
kontrolera do obsługi żądania.
Proces przebiegł nieco ociężale, ponieważ koncepcja DI może wydawać się zagmatwana, kiedy używasz jej
po raz pierwszy. Jedną z zalet przedstawionego podejścia jest to, że dowolny kontroler może zadeklarować
w konstruktorze wymóg użycia interfejsu IValueCalculator. W takim przypadku zostanie użyta biblioteka
Ninject.
W tak przygotowanym rozwiązaniu najlepsze jest to, że jeśli będziesz chciał zastąpić klasę
LinqValueCalculator inną implementacją, to konieczne będzie zmodyfikowanie jedynie klasy mechanizmu
rozwiązywania zależności. Wymieniona klasa to po prostu jedyne miejsce, w którym trzeba wskazać
implementację używaną do obsługi żądań dotyczących interfejsu IValueCalculator.
Tworzenie łańcucha zależności
Gdy Ninject tworzy dany typ, analizuje powiązania pomiędzy tym typem a innymi. Ponadto sprawdza
te zależności, aby przekonać się, czy opierają się na innych typach — innymi słowy, czy deklarują własne
zależności. Jeżeli istnieją dodatkowe zależności, Ninject automatycznie je rozwiązuje i tworzy egzemplarze
wszystkich wymaganych klas. W ten sposób porusza się wzdłuż łańcucha zależności i ostatecznie tworzy
egzemplarz żądanego typu.
Aby zademonstrować tę funkcję, do katalogu Models w projekcie dodamy nowy plik o nazwie Discount.cs
i zdefiniujemy nowy interfejs oraz implementującą go klasę (listing 6.14).
Listing 6.14. Zawartość pliku Discount.cs
namespace EssentialTools.Models {
public interface IDiscountHelper {
decimal ApplyDiscount(decimal totalParam);
}
140
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
public class DefaultDiscountHelper : IDiscountHelper {
public decimal ApplyDiscount(decimal totalParam) {
return (totalParam - (10m / 100m * totalParam));
}
}
}
Interfejs IDiscountHelper definiuje metodę ApplyDiscount, która pozwala zastosować rabat do podanej
wartości decimal. Klasa DefaultDiscountHelper implementuje interfejs i wylicza rabat o stałej wielkości
10 procent. Zmodyfikowana klasa LinqValueCalculator użyje interfejsu IDiscountHelper podczas
przeprowadzania obliczeń (listing 6.15).
Listing 6.15. Dodawanie zależności w klasie LinqValueCalculator
using System.Collections.Generic;
using System.Linq;
namespace EssentialTools.Models {
public class LinqValueCalculator : IValueCalculator {
private IDiscountHelper discounter;
public LinqValueCalculator(IDiscountHelper discountParam) {
discounter = discountParam;
}
public decimal ValueProducts(IEnumerable<Product> products) {
return discounter.ApplyDiscount(products.Sum(p => p.Price));
}
}
}
Nowo dodany konstruktor klasy deklaruje zależność od interfejsu IDiscountHelper. Implementacja
obiektu otrzymywanego przez konstruktor jest przypisywana do właściwości używanej następnie w metodzie
ValueProducts do zastosowania rabatu do całkowitej wartości przetwarzanych obiektów Product.
Podobnie jak wykonaliśmy to w odniesieniu do IValueCalculator, za pomocą Ninject kojarzymy interfejs
IDiscountHelper z klasą implementacji, co jest pokazane na listingu 6.16.
Listing 6.16. Kojarzenie kolejnego interfejsu z implementacją w pliku NinjectDependencyRessolver.cs
...
private void AddBindings()
{
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>();
}
...
W ten sposób utworzyliśmy łańcuch zależności. Kontroler HomeController jest zależny od interfejsu
IValueCalculator, a tę zależność Ninject rozwiązuje za pomocą klasy LinqValueCalculator. Z kolei klasa
LinqValueCalculator ma zależność w postaci interfejsu IDiscountHelper. Tę zależność Ninject rozwiązuje
za pomocą klasy DefaultDiscountHelper.
Ninject bezproblemowo rozwiązuje zależności zdefiniowane w łańcuchu, tworząc przy tym obiekty
wymagane do rozwiązania wszystkich zależności. W omawianym przykładzie ostatecznie następuje
utworzenie egzemplarza klasy HomeController przeznaczonej do obsługi żądań HTTP.
141
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Definiowanie wartości właściwości i parametrów konstruktora
Obiekty tworzone przez Ninject można konfigurować, dostarczając wartości właściwości na etapie
łączenia interfejsu z implementacją. Zmienimy teraz klasę DefaultDiscountHelper w taki sposób,
aby udostępniała wygodną właściwość DiscountSize pozwalającą określić wielkość rabatu — jest ona
zamieszczona na listingu 6.17.
Listing 6.17. Dodawanie właściwości w pliku Discount.cs
namespace EssentialTools.Models
{
public interface IDiscountHelper {
decimal ApplyDiscount(decimal totalParam);
}
public class DefaultDiscountHelper : IDiscountHelper {
public decimal DiscountSize { get; set; }
public decimal ApplyDiscount(decimal totalParam) {
return (totalParam - (DiscountSize / 100m * totalParam));
}
}
}
Wskazując Ninject klasę dla interfejsu, możemy użyć metody WithPropertyValue do ustawiania wartości
właściwości DiscountSize w obiekcie DefaultDiscountHelper. Na listingu 6.18 przedstawiono odpowiednie
zmiany wprowadzone w metodzie AddBindings klasy NinjectDeendencyResolver. Zwróć uwagę na podanie
ciągu tekstowego wskazującego nazwę właściwości, której wartość będzie ustawiana.
Listing 6.18. Użycie metody Ninject WithPropertyValue w pliku NinjectDependencyResolver.cs
...
private void AddBindings() {
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
kernel.Bind<IDiscountHelper>()
.To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 50M);
...
Nie musimy modyfikować innych skojarzeń ani zmieniać sposobu użycia metody Get w celu uzyskania
egzemplarza klasy ShoppingCart. Wartość właściwości jest ustawiana po utworzeniu klasy DefaultDiscountHelper,
co powoduje zmniejszenie wartości produktów o połowę. Wynik otrzymany po tej zmianie został pokazany
na rysunku 6.3.
Rysunek 6.3. Efekt zastosowania rabatu za pomocą właściwości podczas rozwiązywania łańcucha zależności
Jeżeli mamy więcej niż jedną wartość właściwości do ustawienia, możemy tworzyć łańcuch wywołań metody
WithPropertyValue. W ten sam sposób możemy potraktować parametry konstruktora. Na listingu 6.19 pokazana
jest zmieniona klasa DefaultDiscounterHelper, w której wielkość rabatu możemy przekazywać poprzez
parametr konstruktora.
142
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Listing 6.19. Użycie parametru konstruktora w pliku Discount.cs
namespace EssentialTools.Models {
public interface IDiscountHelper {
decimal ApplyDiscount(decimal totalParam);
}
public class DefaultDiscountHelper : IDiscountHelper {
private decimal discountSize;
public DefaultDiscountHelper(decimal discountParam) {
discountSize = discountParam;
}
public decimal ApplyDiscount(decimal totalParam) {
return (totalParam - (discountSize / 100m * totalParam));
}
}
}
Aby klasa ta mogła być użyta przez Ninject, określamy wartość parametru konstruktora za pomocą metody
WithConstructorArgument w metodzie AddBindings (listing 6.20).
Listing 6.20. Podanie w pliku NinjectDependencyResolver.cs parametru konstruktora
...
private void AddBindings() {
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
kernel.Bind<IDiscountHelper>()
.To< DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);
}
...
Również w tym przypadku możemy łączyć ze sobą te wywołania metod, dostarczając wiele wartości
i dopasowując zależności. Ninject użyje ich tam, gdzie będą potrzebne, i utworzy odpowiednie obiekty.
 Wskazówka Zwróć uwagę, że nie zmieniliśmy po prostu wywołania WithPropertyValue na WithConstructorArgument.
Zmieniona została także nazwa elementu składowego, aby odpowiadała stosowanej w języku C# konwencji nazw
parametrów.
Użycie łączenia warunkowego
Ninject zapewnia obsługę warunkowego łączenia metod i tym samym pozwala na wskazanie klas, które powinny
być używane w celu udzielenia odpowiedzi na żądania poszczególnych interfejsów. Aby zademonstrować tę
funkcję, do katalogu Models projektu dodamy nowy plik o nazwie FlexibleDiscountHelper.cs, którego kod
przedstawiono na listingu 6.21.
Listing 6.21. Kod, który należy umieścić w pliku FlexibleDiscountHelper.cs
namespace EssentialTools.Models
{
public class FlexibleDiscountHelper : IDiscountHelper
{
public decimal ApplyDiscount(decimal totalParam)
{
decimal discount = totalParam > 100 ? 70 : 25;
143
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
return (totalParam - (discount / 100m * totalParam));
}
}
}
Klasa FlexibleDiscountHelper powoduje stosowanie różnych rabatów na podstawie wartości całkowitej
zamówienia. Skoro mamy możliwość wyboru klasy implementującej interfejs IDiscountHelper, kolejnym
krokiem jest modyfikacja metody AddBindings w klasie NinjectDependencyResolver i wskazanie bibliotece
Ninject, kiedy mają być używane klasy FlexibleDiscountHelper i DefaultDiscountHelper, co przedstawiono na
listingu 6.22.
Listing 6.22. Użycie warunkowego dołączania w pliku NinjectDependencyResolver.cs
...
private void AddBindings()
{
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>()
.WithConstructorArgument("discountParam", 50M);
kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>()
.WhenInjectedInto<LinqValueCalculator>();
}
...
W nowym dowiązaniu zdefiniowaliśmy, że klasa FlexibleDiscountHelper powinna być tworzona
jako implementacja interfejsu IDiscountHelper, gdy Ninject będzie wstrzykiwać implementację do obiektu
LinqValueCalculator. Zwróć uwagę na fakt, że początkowe dowiązanie IDiscountHelper pozostało bez zmian.
Ninject próbuje znaleźć najlepsze dopasowanie, więc jeżeli kryterium warunku nie będzie spełnione, użyte
będzie domyślne powiązanie dla tej samej klasy bądź interfejsu. Ninject obsługuje wiele różnych metod
dołączania, najbardziej użyteczne z nich zostały opisane w tabeli 6.2.
Tabela 6.2. Metody dołączania warunkowego w Ninject
Metoda
Efekt
When(predykat)
Dołączanie jest wykonywane, jeżeli predykat — wyrażenie lambda — ma
wartość true.
WhenClassHas<T>()
Dołączanie jest używane, gdy klasa, do której jest wstrzykiwana zależność,
jest oznaczona atrybutem typu zdefiniowanego przez T.
WhenInjectedInto<T>()
Dołączenie jest używane, gdy klasa, do której jest wstrzykiwana zależność,
jest typu T.
Ustawienie obiektu zakresu
Ostatnia funkcja Ninject pomaga w dostosowaniu cyklu życiowego obiektów tworzonych przez Ninject
do wymagań aplikacji. Domyślnie Ninject w trakcie każdego żądania obiektu utworzy nowe egzemplarze
obiektów niezbędnych do rozwiązania wszystkich zależności.
Aby zademonstrować to, co się stanie, zmodyfikujemy konstruktor klasy LinqValueCalculator.
Wprowadzona modyfikacja (patrz listing 6.23) powoduje wyświetlenie komunikatu w oknie Dane wyjściowe
w Visual Studio za każdym razem, gdy tworzony jest nowy egzemplarz.
Listing 6.23. Modyfikacja konstruktora w pliku LinqValueCalculator.cs
using System.Collections.Generic;
using System.Linq;
144
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
namespace EssentialTools.Models {
public class LinqValueCalculator : IValueCalculator {
private IDiscountHelper discounter;
private static int counter = 0;
public LinqValueCalculator(IDiscountHelper discountParam) {
discounter = discountParam;
System.Diagnostics.Debug.WriteLine(
string.Format("Utworzono egzemplarz {0}", ++counter));
}
public decimal ValueProducts(IEnumerable<Product> products) {
return discounter.ApplyDiscount(products.Sum(p => p.Price));
}
}
}
Klasa System.Diagnostics.Debug zawiera wiele metod, które można wykorzystać do wyświetlania
komunikatów podczas działania aplikacji. Uważam je za użyteczne, gdy trzeba analizować sposób działania
kodu. Kiedy zaczynałem karierę programisty, narzędzia przeznaczone do usuwania błędów w kodzie nie były
jeszcze tak zaawansowane i użyteczne jak teraz. Dlatego też podczas usuwania błędów nadal korzystam
z najprostszych technik.
Na listingu 6.24 znajduje się zmodyfikowana wersja kontrolera Home, który teraz domaga się od Ninject
dwóch implementacji interfejsu IValueCalculator.
Listing 6.24. Użycie w pliku HomeController.cs wielu egzemplarzy klasy kalkulatora
...
public HomeController(IValueCalculator calcParam,
calc = calcParam;
}
...
IValueCalculator calc2 ) {
Nie przeprowadzamy żadnych użytecznych operacji na obiekcie dostarczanym przez Ninject — w powyższym
kodzie są po prostu żądane dwie implementacje interfejsu. Jeżeli uruchomisz aplikację i spojrzysz na okno Dane
wyjściowe w Visual Studio, wówczas zobaczysz komunikaty potwierdzające utworzenie przez Ninject dwóch
egzemplarzy klasy LinqValueCalculator:
Utworzono egzemplarz 1
Utworzono egzemplarz 2
Wprawdzie egzemplarze klasy LinqValueCalculator mogą być bez problemów wielokrotnie tworzone,
ale taka możliwość nie istnieje dla wszystkich klas. W przypadku niektórych zachodzi potrzeba współdzielenia
pojedynczego egzemplarza w całej aplikacji, z kolei w innych trzeba tworzyć nowy egzemplarz dla każdego żądania
HTTP otrzymywanego przez platformę ASP.NET. Ninject pozwala na kontrolę cyklu życiowego tworzonych
obiektów, wykorzystując w tym celu funkcjonalność o nazwie zakresu. Jest ona wyrażona za pomocą wywołania
metody podczas konfiguracji wiązania między interfejsem i jego typem implementacji. Na listingu 6.25 możesz
zobaczyć, w jaki sposób zastosowałem najużyteczniejszy zakres dla aplikacji MVC: zakres żądania do klasy
LinqValueCalculator w NinjectDependencyResolver.
Listing 6.25. Użycie zakresu żądania w pliku NinjectDependencyResolver.cs
using
using
using
using
System;
System.Collections.Generic;
System.Web.Mvc;
EssentialTools.Models;
145
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
using Ninject;
using Ninject.Web.Common;
namespace EssentialTools.Infrastructure {
public class NinjectDependencyResolver : IDependencyResolver {
private IKernel kernel;
public NinjectDependencyResolver(IKernel kernelParam) {
kernel = kernelParam;
AddBindings();
}
public object GetService(Type serviceType) {
return kernel.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType) {
return kernel.GetAll(serviceType);
}
private void AddBindings() {
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>().InRequestScope();
kernel.Bind<IDiscountHelper>()
.To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);
kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>()
.WhenInjectedInto<LinqValueCalculator>();
}
}
}
Metoda rozszerzająca InRequestScope (znajduje się w przestrzeni nazw Ninject.Web.Common) wskazuje
Ninject, że ma być tworzony tylko jeden egzemplarz klasy LinqValueCalculator dla każdego żądania HTTP
otrzymywanego przez ASP.NET. Poszczególne żądania będą otrzymywały własny obiekt, ale wiele zależności
rozwiązywanych w ramach tego samego żądania będzie rozwiązywanych za pomocą pojedynczego egzemplarza
klasy. Efekt wprowadzonej zmiany możesz zobaczyć, uruchamiając aplikację i przyglądając się komunikatom
wyświetlanym w oknie Dane wyjściowe w Visual Studio. Jak się przekonasz, teraz Ninject tworzy tylko jeden
egzemplarz klasy LinqValueCalculator. Po odświeżeniu strony w przeglądarce internetowej (ale bez ponownego
uruchomienia aplikacji) zobaczysz, że Ninject tworzy drugi obiekt. Ninject oferuje wiele różnych obiektów
zakresu, te najużyteczniejsze wymieniono w tabeli 6.3.
Tabela 6.3. Metody zakresu w Ninject
Metoda
Efekt
InTransientScope()
Dokładnie taki sam, jak w przypadku niepodawania zakresu i tworzenia
nowego obiektu dla każdej rozwiązywanej zależności.
InSingletonScope()
Utworzenie pojedynczego egzemplarza, który będzie współdzielony w aplikacji.
Ninject utworzy egzemplarz po użyciu metody InSingletonScope lub po jego
dostarczeniu za pomocą metody ToConstant.
ToConstant(obiekt)
InThreadScope()
Utworzenie pojedynczego egzemplarza, który będzie używany do rozwiązywania
zależności dla obiektów żądanych przez pojedynczy wątek.
InRequestScope()
Utworzenie pojedynczego egzemplarza, który będzie używany do rozwiązywania
zależności dla obiektów pobieranych przez pojedyncze żądanie HTTP.
146
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Testy jednostkowe w Visual Studio
W niniejszej książce przy testowaniu jednostkowym skorzystamy z mechanizmów wbudowanych w Visual
Studio, chociaż istnieje także wiele innych pakietów przeznaczonych do tego celu. Najpopularniejszym
pakietem testowania jednostkowego dla .NET jest prawdopodobnie NUnit. Wszystkie takie pakiety są bardzo
zbliżone w działaniu, a powodem, dla którego wybrałem obsługę z Visual Studio, jest integracja z resztą IDE.
Aby zaprezentować wbudowaną w Visual Studio obsługę testów jednostkowych, do omawianego
wcześniej projektu dodamy nową implementację interfejsu IDiscountHelper. W katalogu Models projektu
utwórz nowy plik o nazwie MinimumDiscountHelper.cs i umieść w nim kod przedstawiony na listingu 6.26.
Listing 6.26. Kod w pliku MinimumDiscountHelper.cs
using System;
namespace EssentialTools.Models
{
public class MinimumDiscountHelper : IDiscountHelper
{
public decimal ApplyDiscount(decimal totalParam)
{
throw new NotImplementedException();
}
}
}
Naszym celem jest utworzenie implementacji MinimumDiscountHelper, która będzie spełniać następujące
warunki:
 jeżeli wartość całkowita produktów będzie wyższa niż 100 zł, rabat wyniesie 10%;
 jeżeli wartość całkowita produktów będzie wyższa niż 10 zł, ale niższa niż 100 zł, rabat wyniesie 5%;
 jeżeli wartość całkowita produktów będzie niższa niż 10 zł, rabat nie zostanie naliczony;
 w przypadku ujemnej wartości całkowitej produktów nastąpi zgłoszenie wyjątku
ArgumentOutOfRangeException.
Klasa MinimumDiscountHelper jeszcze nie implementuje żadnego z wymienionych powyżej zachowań.
Zastosujemy podejście TDD (ang. Test Driven Development) do utworzenia testów jednostkowych
i dopiero później zaimplementujemy kod, zgodnie z opisem przedstawionym w rozdziale 3.
Tworzenie projektu testów jednostkowych
Pierwszym krokiem jest utworzenie projektu testów jednostkowych. W tym celu kliknij prawym przyciskiem
myszy element główny w oknie Eksplorator rozwiązania (w omawianej aplikacji będzie zatytułowany
Rozwiązanie 'EssentialTools'), a następnie wybierz opcję Dodaj/Nowy projekt… z menu kontekstowego.
 Wskazówka Projekt testów jednostkowych możesz utworzyć także w chwili tworzenia zwykłego projektu aplikacji
ASP.NET MVC. W oknie dialogowym tworzenia projektu znajduje się pole wyboru Dodaj testy jednostkowe.
Na ekranie zostanie wyświetlone okno dialogowe tworzenia nowego projektu. Z szablonów C# wybierz
grupę Test, natomiast w środkowym panelu Projekt testu jednostkowego, jak pokazano na rysunku 6.4.
147
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 6.4. Utworzenie projektu testów jednostkowych
Tworzonemu projektowi nadaj nazwę EssentialTools.Tests i kliknij przycisk OK zatwierdzający jego
utworzenie. Nowy projekt zostanie dodany do bieżącego rozwiązania Visual Studio i znajdzie się obok
projektu aplikacji MVC.
Do projektu testów jednostkowych trzeba dodać odwołanie, aby było możliwe jego wykorzystanie
do przeprowadzania testów w klasach projektu MVC. W oknie eksploratora rozwiązania kliknij prawym
przyciskiem myszy katalog Odwołania w projekcie EssentialTools.Tests, a następnie wybierz opcję Dodaj
odwołanie… z menu kontekstowego. W lewym panelu kliknij Rozwiązanie, a w środkowym zaznacz pole
wyboru obok nazwy projektu EssentialTools, jak pokazano na rysunku 6.5.
Rysunek 6.5. Dodanie odwołania do projektu MVC
Tworzenie testów jednostkowych
Testy jednostkowe umieścimy w pliku UnitTest1.cs w projekcie EssentialTools.Tests. Płatne wersje Visual
Studio są wyposażone w przydatne funkcje automatycznego generowania metod testowych dla klas.
Wprawdzie wspomniane funkcje są niedostępne w wersjach Express, ale nadal można tworzyć użyteczne
testy. Aby rozpocząć pracę, wprowadź kilka zmian w pliku UnitTest1.cs, które przedstawiono na listingu 6.27.
148
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Listing 6.27. Metody testowe dodane do pliku UnitTest1.cs
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EssentialTools.Models;
namespace EssentialTools.Tests
{
[TestClass]
public class UnitTest1
{
private IDiscountHelper getTestObject()
{
return new MinimumDiscountHelper();
}
[TestMethod]
public void Discount_Above_100()
{
// przygotowanie
IDiscountHelper target = getTestObject();
decimal total = 200;
// działanie
var discountedTotal = target.ApplyDiscount(total);
// asercje
Assert.AreEqual(total * 0.9M, discountedTotal);
}
}
}
W ten sposób dodaliśmy pojedynczy test jednostkowy. Klasa zawierająca testy została oznaczona
atrybutem TestClass, natomiast poszczególne testy są metodami oznaczonymi atrybutem TestMethod. Nie
wszystkie metody w klasie testów jednostkowych muszą być testami. Aby to zademonstrować, w klasie jest
zdefiniowana metoda getTestObject, która zostanie użyta do przygotowania testów. Ponieważ wymieniona
metoda nie posiada przypisanego atrybutu TestMethod, Visual Studio nie traktuje jej jako testu jednostkowego.
 Wskazówka Zwróć uwagę na konieczność użycia polecenia using w celu zaimportowania do klasy testowej
przestrzeni nazw EssentialTools.Models. Klasy testowe są zwykłymi klasami C# i nie mają szczególnej wiedzy
o projekcie MVC. To dzięki atrybutom TestClass i TestMethod mogą wykonywać swoje działania.
Należy zwrócić uwagę, że w metodzie testu jednostkowego wykorzystujemy omówiony w rozdziale 3. wzorzec
przygotowanie/działanie/asercje (ang. arrange/act/assert — A/A/A). Istnieje kilka konwencji nazywania testów
jednostkowych; zalecam po prostu nadawanie nazw, które jasno określają, co jest sprawdzane przez dany
test. W omawianym przykładzie metodę testową nazwałem Discount_Above_100, co wydaje się wystarczająco
jasne. Jeżeli jednak nie lubisz takiego stylu, to możesz użyć dowolnego innego, który jest dla Ciebie (i Twojego
zespołu) zrozumiały.
W metodzie testowej przeprowadzamy konfigurację poprzez wywołanie metody getTestObject, która tworzy
egzemplarz obiektu przeznaczonego do testowania — w omawianym przypadku będzie to obiekt klasy
MinimumDiscountHelper. Definiujemy także wartość total, która będzie poddawana testom. Ten krok można
nazwać sekcją przygotowań do testu jednostkowego.
W sekcji działania testu następuje wywołanie metody MinimumDiscountHelper.ApplyDiscount i przypisanie
otrzymanego wyniku zmiennej discountedTotal. Na koniec, w sekcji asercji testu, używamy metody
Assert.AreEqual() do sprawdzenia, czy wartość otrzymana z metody ApplyDiscount wynosi 90% wartości początkowej.
149
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Klasa Assert ma dużą liczbę metod statycznych, które można wykorzystać w testach. Wymieniona klasa
znajduje się w przestrzeni nazw Microsoft.VisualStudio.TestTools.UnitTesting wraz innymi klasami
użytecznymi podczas konfiguracji i przeprowadzania testów. Więcej informacji na temat wspomnianych klas
i przestrzeni nazw znajdziesz na stron ie http://msdn.microsoft.com/en-us/library/ms182530.aspx.
Klasa Assert będzie jedną z najczęściej przez nas używanych, a jej najważniejsze metody wymieniono
w tabeli 6.4.
Każda z tych metod statycznych klasy Assert pozwala sprawdzić pewien aspekt testu jednostkowego.
Jeżeli asercja jest nieudana, zgłaszany jest wyjątek. Aby test jednostkowy został zaliczony, wszystkie asercje
muszą zakończyć się powodzeniem.
Każda z tych metod jest przeciążona i posiada wersję z dodatkowym parametrem typu string. Ten ciąg
tekstowy jest dołączany jako element komunikatu w przypadku nieudanej asercji. Metody AreEqual oraz
AreNotEqual mają więcej przeciążonych wersji, pozwalających na porównywanie różnych typów. Na przykład
istnieje wersja umożliwiająca porównywanie ciągów tekstowych bez uwzględniania wielkości liter.
 Wskazówka Jeden z elementów przestrzeni nazw Microsoft.VisualStudio.TestTools.UnitTesting, o którym
szczególnie warto wspomnieć, to atrybut ExceptionExpected. Jest to asercja, która udaje się, jeżeli test jednostkowy
zgłasza wyjątek typu zdefiniowanego za pomocą parametru ExceptionType. Jest to przyjemny sposób na
upewnienie się, że został zgłoszony wyjątek, bez konieczności stosowania bloków try ... catch w teście.
Tabela 6.4. Metody statyczne klasy Assert
Metoda
Opis
AreEqual<T>(T, T)
AreEqual<T>(T, T, string)
Sprawdza, czy dwa obiekty typu T mają taką samą wartość.
AreNotEqual<T>(T, T)
AreNotEqual<T>(T, T, string)
Sprawdza, czy dwa obiekty typu T mają różną wartość.
AreSame<T>(T, T)
AreSame<T>(T, T, string)
Sprawdza, czy dwie zmienne odwołują się do tego samego
obiektu.
AreNotSame<T>(T, T)
AreNotSame<T>(T, T, string)
Sprawdza, czy dwie zmienne odwołują się do różnych
obiektów.
Fail()
Fail(string)
Powoduje, że asercja jest fałszywa — nie są sprawdzane
żadne warunki.
Inconclusive()
Inconclusive(string)
Wskazuje, że wynik testu jednostkowego nie może być
jednoznacznie określony.
IsTrue(bool)
IsTrue(bool, string)
Sprawdza, czy wartość bool jest równa true — najczęściej
wykorzystywana do sprawdzania wartości wyrażeń
zwracających wynik bool.
IsFalse(bool)
IsFalse(bool, string)
Sprawdza, czy wartość bool jest równa false.
IsNull(object)
IsNull(object, string)
Sprawdza, czy zmienna nie ma przypisanej referencji
do obiektu.
IsNotNull(object)
IsNotNull(object, string)
Sprawdza, czy zmienna ma przypisaną referencję do obiektu.
IsInstanceOfType(object, Type)
IsInstanceOfType(object, Type, string)
Sprawdza, czy obiekt ma podany typ lub typ dziedziczący
po nim.
IsNotInstanceOfType(object, Type)
IsNotInstanceOfType(object, Type, string)
Sprawdza, czy obiekt nie ma podanego typu.
150
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Skoro wiesz już, jak zaimplementować pojedynczy test jednostkowy, teraz dodamy do projektu kolejne testy
odpowiedzialne za sprawdzenie poprawności zachowania pozostałych funkcji MinimumDiscountHelper. Dodane
testy przedstawiono na listingu 6.28. Warto w tym miejscu nadmienić, że dodane testy jednostkowe są na tyle
krótkie i proste (ogólnie rzecz biorąc, to cecha charakterystyczna testów jednostkowych), że nie będziemy ich tutaj
szczegółowo omawiać.
Listing 6.28. Zdefiniowanie pozostałych testów jednostkowych w pliku UnitTest1.cs
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EssentialTools.Models;
namespace EssentialTools.Tests
{
[TestClass]
public class UnitTest1
{
private IDiscountHelper getTestObject()
{
return new MinimumDiscountHelper();
}
[TestMethod]
public void Discount_Above_100()
{
// przygotowanie
IDiscountHelper target = getTestObject();
decimal total = 200;
// działanie
var discountedTotal = target.ApplyDiscount(total);
// asercje
Assert.AreEqual(total * 0.9M, discountedTotal);
}
[TestMethod]
public void Discount_Between_10_And_100() {
// przygotowanie
IDiscountHelper target = getTestObject();
// działanie
decimal TenDollarDiscount = target.ApplyDiscount(10);
decimal HundredDollarDiscount = target.ApplyDiscount(100);
decimal FiftyDollarDiscount = target.ApplyDiscount(50);
// asercje
Assert.AreEqual(5, TenDollarDiscount, "rabat w wysokości 10 zł jest nieprawidłowy");
Assert.AreEqual(95, HundredDollarDiscount, "rabat w wysokości 100 zł jest nieprawidłowy");
Assert.AreEqual(45, FiftyDollarDiscount, "rabat w wysokości 50 zł jest nieprawidłowy");
}
[TestMethod]
public void Discount_Less_Than_10()
{
// przygotowanie
IDiscountHelper target = getTestObject();
// działanie
decimal discount5 = target.ApplyDiscount(5);
decimal discount0 = target.ApplyDiscount(0);
// asercje
Assert.AreEqual(5, discount5);
151
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
}
Assert.AreEqual(0, discount0);
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Discount_Negative_Total()
{
// przygotowanie
IDiscountHelper target = getTestObject();
// działanie
target.ApplyDiscount(-1);
}
}
}
Uruchamianie testów (nieudane)
Visual Studio oferuje użyteczne okno Eksplorator testów przeznaczone do zarządzania testami i ich przeprowadzania.
Aby wyświetlić to okno, wybierz opcję menu Test/Okna/Eksplorator testów, a następnie kliknij opcję Uruchom
wszystkie znajdującą się w lewym górnym rogu. Otrzymasz wynik podobny do pokazanego na rysunku 6.6.
Rysunek 6.6. Uruchomienie testów jednostkowych w projekcie
W lewym panelu okna eksploratora testów znajduje się lista wszystkich zdefiniowanych testów.
Oczywiście wykonanie wszystkich testów zakończyło się niepowodzeniem, ponieważ jeszcze nie
zaimplementowaliśmy testowanej metody. Możesz kliknąć dowolny test, a w prawym panelu okna
eksploratora testów zostaną wyświetlone informacje o przyczynach jego niepowodzenia. Okno Eksplorator
testów oferuje wiele różnych sposobów wyboru i filtrowania testów jednostkowych. Jednak w przypadku
omawianego tutaj prostego projektu możemy po prostu uruchomić wszystkie testy kliknięciem opcji
Uruchom wszystkie.
Implementacja funkcji
Teraz możemy zacząć implementować funkcję, mając pewność, że będziemy w stanie sprawdzić jakość gotowego
kodu. Po tych wszystkich przygotowaniach implementacja metody MinimumDiscountHelper jest całkiem prosta,
jak widać na listingu 6.29.
152
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Listing 6.29. Implementacja klasy MinimumDiscountHelper
using System;
namespace EssentialTools.Models
{
public class MinimumDiscountHelper : IDiscountHelper
{
public decimal ApplyDiscount(decimal totalParam)
{
if (totalParam < 0)
{
throw new ArgumentOutOfRangeException();
}
else if (totalParam > 100)
{
return totalParam * 0.9M;
}
else if (totalParam > 10 && totalParam <= 100)
{
return totalParam - 5;
}
else
{
return totalParam;
}
}
}
}
Testowanie i poprawianie kodu
W przedstawionym powyżej kodzie celowo pozostawiliśmy błąd, aby pokazać, jak działa iteracyjne przeprowadzanie
testów jednostkowych w Visual Studio. Efekt pozostawienia błędu w kodzie zobaczysz po kliknięciu przycisku
Uruchom wszystkie w oknie eksploratora testów. Po przeprowadzeniu testów otrzymasz efekt podobny do
pokazanego na rysunku 6.7.
Rysunek 6.7. Efekt implementacji funkcji zawierającej błąd
Visual Studio zawsze stara się umieszczać najużyteczniejsze informacje na górze okna eksploratora testów.
W omawianym przypadku oznacza to, że testy zakończone niepowodzeniem są wyświetlane przed tymi,
których wykonanie zakończyło się sukcesem.
Jak możesz się przekonać, trzy testy jednostkowe zostały zakończone powodzeniem, ale w metodzie
Discount_Between_10_And_100 wykryto problem. Po kliknięciu nieudanego testu widać wyraźnie, że oczekiwaną
wartością było 5, natomiast otrzymaną 10.
153
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Na tym etapie powracamy do kodu, aby znaleźć błędy w implementacji — tutaj jest to błędnie naliczany
rabat w przypadku wartości całkowitej produktów mieszczącej się w zakresie od 10 do 100. Problem znajduje się
w poniższym poleceniu klasy MinimumDiscountHelper:
...
} else if (totalParam > 10 && totalParam <= 100) {
...
Przedstawione polecenie powoduje określenie zachowania dla wartości z przedziału od 10 zł do 100 zł
włącznie, podczas gdy zastosowana implementacja wyłącza wartości graniczne i sprawdza jedynie, czy
testowana wartość jest większa niż 10 zł, wykluczając tym samym wartość wynoszącą dokładnie 10 zł.
Rozwiązanie jest proste i zostało przedstawione na listingu 6.30 — wystarczy dodać znak równości do
polecenia, aby w ten sposób zmienić efekt działania polecenia if.
Listing 6.30. Poprawienie kodu klasy MinimumDiscountHelper
using System;
namespace EssentialTools.Models
{
public class MinimumDiscountHelper : IDiscountHelper
{
public decimal ApplyDiscount(decimal totalParam)
{
if (totalParam < 0)
{
throw new ArgumentOutOfRangeException();
}
else if (totalParam > 100)
{
return totalParam * 0.9M;
}
else if (totalParam >= 10 && totalParam <= 100)
{
return totalParam - 5;
}
else
{
return totalParam;
}
}
}
}
Po naciśnięciu opcji Uruchom wszystkie w oknie eksploratora testów — i tym samym po ponownym
przeprowadzeniu testów — możemy się upewnić, że problem został usunięty. Wszystkie testy zostały
wykonane z powodzeniem, co pokazano na rysunku 6.8.
Rysunek 6.8. Zaliczenie wszystkich testów jednostkowych
154
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Przedstawiłem krótkie wprowadzenie do testowania jednostkowego. Omawianie testów jednostkowych
będę kontynuować w kolejnych rozdziałach. Zwróć uwagę, że Visual Studio sprawdza się doskonale
w obsłudze testów jednostkowych. Zachęcam Cię do zapoznania się z dokumentacją dotyczącą testów
jednostkowych dostępną na MSDN, na stronie http://msdn.microsoft.com/en-us/library/dd264975.aspx.
Użycie Moq
Jednym z powodów pozwalających na zachowanie prostoty testów w poprzednim podrozdziale był fakt, że
testowana przez nas pojedyncza klasa nie pozostawała w zależności od innych klas lub funkcji. Oczywiście tego
rodzaju obiekty istnieją w rzeczywistych projektach, ale często będziesz musiał testować obiekty, które
nie mogą funkcjonować w zupełnej izolacji. W takich sytuacjach musisz mieć możliwość skoncentrowania się
na interesującej Cię klasie lub metodzie, tak aby jednocześnie nie przeprowadzić testów klas zależnych.
Jedno z użytecznych podejść polega na zastosowaniu obiektów imitujących, które symulują funkcjonalność
rzeczywistych obiektów istniejących w projekcie, ale w bardzo specyficzny i kontrolowany sposób. Obiekty
imitujące pozwalają na zawężenie testów i przeprowadzenie analizy jedynie interesującej Cię funkcjonalności.
Płatne wersje Visual Studio oferują obsługę tworzenia obiektów imitujących, ale ja osobiście preferuję
użycie biblioteki o nazwie Moq. Jest to prosta i łatwa w użyciu biblioteka przeznaczona do pracy ze wszystkimi
wydaniami Visual Studio, także bezpłatnymi.
Zrozumienie problemu
Zanim przejdziemy do używania biblioteki Moq, w pierwszej kolejności zaprezentuję problem, który
powinniśmy rozwiązać. W tym punkcie przeprowadzimy test jednostkowy klasy LinqValueCalculator
zdefiniowanej w katalogu Models omawianego projektu. Dla przypomnienia, na listingu 6.31 przedstawiono
definicję klasy LinqValueCalculator.
Listing 6.31. Zawartość pliku LinqValueCalculator.cs
using System.Collections.Generic;
using System.Linq;
namespace EssentialTools.Models {
public class LinqValueCalculator : IValueCalculator {
private IDiscountHelper discounter;
private static int counter = 0;
public LinqValueCalculator(IDiscountHelper discounterParam) {
discounter = discounterParam;
System.Diagnostics.Debug.WriteLine(
string.Format("Instance {0} created", ++counter));
}
public decimal ValueProducts(IEnumerable<Product> products) {
return discounter.ApplyDiscount(products.Sum(p => p.Price));
}
}
}
Aby przetestować tę klasę, do projektu testowego dodajemy nową klasę testu jednostkowego. W tym celu
wystarczy kliknąć prawym przyciskiem myszy projekt testowy w oknie eksploratora rozwiązania i wybrać
opcję Dodaj/Test jednostki… z menu kontekstowego. Jeżeli w menu Dodaj nie znajduje się opcja Test jednostki…,
wówczas wybierz opcję Nowy element…, a następnie Podstawowy test jednostki. Domyślnie Visual Studio
utworzy plik o nazwie UnitTest2.cs (wprowadzone w nim zmiany przedstawiono na listingu 6.32).
155
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 6.32. Dodanie zdefiniowanego w pliku UnitTest2.cs testu jednostkowego dla klasy ShoppingCart
using
using
using
using
System;
Microsoft.VisualStudio.TestTools.UnitTesting;
EssentialTools.Models;
System.Linq;
namespace EssentialTools.Tests
{
[TestClass]
public class UnitTest2
{
private 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}
};
[TestMethod]
public void Sum_Products_Correctly()
{
// przygotowanie
var discounter = new MinimumDiscountHelper();
var target = new LinqValueCalculator(discounter);
var goalTotal = products.Sum(e => e.Price);
// działanie
var result = target.ValueProducts(products);
// wynik
Assert.AreEqual(goalTotal, result);
}
}
}
Problem polega na tym, że poprawne działanie klasy LinqValueCalculator zależy od implementacji
interfejsu IDiscountHelper. W omawianym przykładzie użyliśmy klasy MinimumDiscountHelper, co prowadzi
do powstania dwóch różnych problemów.
Pierwszy polega na tym, że przygotowane testy jednostkowe są skomplikowane i kruche. Aby przygotować
działający test jednostkowy, trzeba koniecznie wziąć pod uwagę logikę rabatów znajdującą się w implementacji
IDiscountHelper i na jej podstawie określić wartość oczekiwaną z metody ValueProducts. Kruchość testu
jednostkowego bierze się z faktu, że wykonanie testu zakończy się niepowodzeniem w przypadku zmiany
implementacji logiki rabatów, nawet jeśli klasa LinqValueCalculator będzie działała prawidłowo.
Drugi, poważniejszy problem wynika z tego, że zakres testu jednostkowego został rozszerzony i mimowolnie
obejmuje klasę MinimumDiscountHelper. Kiedy działanie testu jednostkowego zakończy się niepowodzeniem,
nie będziesz wiedział, w której klasie powstał problem (LinqValueCalculator czy MinimumDiscountHelper).
Testy jednostkowe sprawdzają się najlepiej wtedy, gdy są proste i skoncentrowane na pojedynczej
funkcjonalności. Na obecnym etapie żaden z wymienionych warunków nie został spełniony. W kolejnych
punktach pokażę, jak dodać i zastosować bibliotekę Moq w projekcie MVC, co pozwoli na uniknięcie
wymienionych problemów.
156
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Dodawanie Moq do projektu Visual Studio
Podobnie jak w przypadku przedstawionej we wcześniejszej części rozdziału biblioteki Ninject, najłatwiejszym
sposobem dodania Moq do projektu MVC jest użycie zintegrowanej z Visual Studio obsługi pakietów NuGet.
Przejdź do konsoli menedżera NuGet, a następnie wydaj poniższe polecenie:
Install-Package Moq -version 4.1.1309.1617 -projectname EssentialTools.Tests
Argument projectname pozwala na wskazanie NuGet, że pakiet Moq ma zostać zainstalowany
w projekcie testów jednostkowych, a nie w głównej aplikacji.
Dodanie obiektu imitacyjnego do testu jednostkowego
Dodanie obiektu imitacyjnego do testu jednostkowego oznacza poinformowanie biblioteki Moq, z jakiego
typu obiektem chcesz pracować, konfigurację zachowania obiektu, a następnie jego zastosowanie względem
testowanego komponentu. Sposób dodania obiektu imitacyjnego do naszego testu jednostkowego dla klasy
LinqValueCalculator przedstawiono na listingu 6.33.
Listing 6.33. Użycie obiektu imitacyjnego w teście jednostkowym w pliku UnitTest2.cs
using
using
using
using
EssentialTools.Models;
Microsoft.VisualStudio.TestTools.UnitTesting;
System.Linq;
Moq;
namespace EssentialTools.Tests
{
[TestClass]
public class UnitTest2
{
private 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}
};
[TestMethod]
public void Sum_Products_Correctly()
{
// przygotowanie
Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();
mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>()))
.Returns<decimal>(total => total);
var target = new LinqValueCalculator(mock.Object);
// działanie
var result = target.ValueProducts(products);
// wynik
Assert.AreEqual(products.Sum(e => e.Price), result);
}
}
}
157
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Kiedy po raz pierwszy zetkniesz się ze składnią Moq, zapewne wyda Ci się nieco dziwna. Dlatego też poniżej
znajdziesz omówienie poszczególnych etapów procesu.
 Wskazówka Pamiętaj, że dostępnych jest wiele różnych bibliotek imitacyjnych. Jeżeli więc nie lubisz sposobu
działania Moq (choć tak naprawdę Moq to bardzo łatwa w użyciu biblioteka), istnieje duże prawdopodobieństwo,
że znajdziesz dla siebie inne rozwiązanie. Musisz mieć świadomość, że podręczniki niektórych innych popularnych
bibliotek składają się z setek stron.
Tworzenie obiektu imitacji
Pierwszym krokiem jest poinformowanie biblioteki Moq o typie obiektu, z którym chcesz pracować. Moq
w ogromnym stopniu opiera się na ogólnych typach parametrów. Możesz się o tym przekonać, analizując
sposób, w jaki informujemy Moq o chęci utworzenia implementacji IDiscountHelper:
...
Mock<IDiscountHelper> mock = new Mock<IDiscountHelper()>();
...
Tworzony jest ściśle określonego typu Mock<IDiscountHelper> obiekt, który informuje bibliotekę Moq
o obsługiwanym przez nią typie. W omawianym przykładzie jest to interfejs IDiscountHelper dla testu
jednostkowego, ale może to być inny dowolny typ, który chcesz odizolować, aby skoncentrować się na
testach jednostkowych.
Wybór metody
Oprócz utworzenia obiektu Mock o ściśle określonym typie, konieczne jest jeszcze zdefiniowanie jego
zachowania — to jest serce procesu imitacji. Pozwala na zagwarantowanie zdefiniowania w obiekcie imitacji
zachowania bazowego. Wspomniany obiekt będzie wykorzystywany do przetestowania funkcjonalności
obiektu docelowego w teście jednostkowym. Poniższe polecenie z testu jednostkowego przedstawia żądaną
przez nas konfigurację zachowania:
...
mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);
...
Użyta została metoda Setup w celu dodania metody do obiektu imitacyjnego. Moq korzysta z LINQ oraz
wyrażeń lambda. Gdy wywołujemy metodę Setup, Moq przekazuje nam interfejs, którego implementacji
zażądaliśmy. Jest on sprytnie opakowany przy użyciu zaawansowanych mechanizmów LINQ. Pozwalają one
wybrać metodę do konfiguracji lub weryfikacji za pomocą wyrażeń lambda; nie będę tu o nich pisać. Na
potrzeby naszego testu jednostkowego chcemy zdefiniować zachowanie metody ApplyDiscount, która jest
jedyną metodą interfejsu IDiscountHelper, a przy tym metodą potrzebną do przetestowania klasy
LinqValueCalculator.
Konieczne jest również poinformowanie Moq, które wartości parametrów nas interesują. Do tego celu
używana jest klasa It, co zostało pokazane w poniższym wierszu kodu:
...
mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);
...
W klasie It jest zdefiniowanych kilka metod posiadających ogólne typy parametrów. W tym przypadku
wywołaliśmy metodę IsAny z użyciem decimal jako typu ogólnego. Informujemy w ten sposób Moq,
że definiowane zachowanie ma zostać zastosowane, gdy metoda ApplyDiscount zostanie wywołana
z dowolną wartością dziesiętną. W tabeli 6.5 wymienione są statyczne metody klasy It.
158
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
Tabela 6.5. Statyczne metody klasy It
Metoda
Opis
Is<T>(predykat)
Określa wartości typu T, które powodują, że predykat zwróci wartość true
(przykład z listingu 6.34).
IsAny<T>()
Określa dowolną wartość typu T.
IsInRange<T>(min,
max, rodzaj)
Dopasowuje wartości, jeżeli parametr jest pomiędzy zdefiniowanymi wartościami
i typem T. Ostatnim parametrem jest wartość z typu wyliczeniowego Range i może
być Inclusive lub Exclusive.
IsRegex(wyrażenie)
Dopasowuje parametr w postaci ciągu tekstowego, jeżeli pasuje on do podanego
wyrażenia regularnego.
W dalszej części rozdziału zademonstruję bardziej skomplikowany przykład używający innych metod klasy
It. W tej chwili jednak pozostaniemy przy metodzie IsAny<decimal>, która pozwala na udzielanie odpowiedzi
na wartość dziesiętną.
Zwracanie wyniku
W powyższym przykładzie do wywołania Setup dołączona jest metoda Returns, co pozwala określić zwracaną
wartość. Typ wyniku można określić za pomocą parametru, natomiast sam wynik — za pomocą wyrażenia
lambda. Takie rozwiązanie pokazano w poniższym wierszu kodu:
...
mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);
...
Wywołując metodę Returns z parametrem typu decimal (np. Returns<decimal>), informujemy Moq, że
wartością zwrotną będzie decimal. Do naszego wyrażenia lambda Moq przekazuje wartość typu otrzymaną
w metodzie ApplyDiscount — w omawianym przykładzie tworzymy w ten sposób metodę przekazującą,
w której wartość zwrotna jest przekazywana metodzie ApplyDiscount bez przeprowadzania na niej
jakichkolwiek operacji. To jest najprostszy rodzaj metody imitacyjnej, wkrótce poznasz nieco bardziej
skomplikowane przykłady.
Użycie obiektu Mock
Ostatnim krokiem jest użycie obiektu imitacyjnego w teście jednostkowym, co następuje w wyniku odczytania
wartości właściwości Object obiektu Mock<IDiscountHelper>:
...
var target = new LinqValueCalculator(mock.Object);
...
Podsumowując, w omawianym przykładzie właściwość Object zwraca implementację interfejsu
IDiscountHelper, podczas gdy metoda ApplyDiscount zwraca wartość przekazanego jej parametru decimal.
To bardzo ułatwia przeprowadzanie testu jednostkowego, ponieważ możemy zsumować ceny testowych
obiektów Product i sprawdzić, czy tę samą wartość otrzymamy z obiektu LinqValueCalculator:
...
Assert.AreEqual(products.Sum(e => e.Price), result);
...
Zaletą użycia biblioteki Moq w przedstawiony sposób jest to, że nasz test jednostkowy sprawdza jedynie
zachowanie obiektu LinqValueCalculator i nie jest zależny od żadnych rzeczywistych implementacji interfejsu
IDiscountHelper w katalogu Models. Jeżeli więc test jednostkowy zakończy się niepowodzeniem, to wiadomo,
159
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
że problem występuje albo w implementacji LinqValueCalculator, albo w sposobie konfiguracji obiektu
imitacyjnego. Rozwiązanie problemu w którymkolwiek z wymienionych miejsc jest prostsze i łatwiejsze niż
zmaganie się z łańcuchem rzeczywistych obiektów i interakcji pomiędzy nimi.
Tworzenie bardziej skomplikowanych obiektów Mock
W poprzednim punkcie przedstawiono bardzo prosty obiekt imitacyjny. Jednak prawdziwa moc biblioteki
Moq drzemie w możliwości szybkiego tworzenia skomplikowanych zachowań w celu testowania różnych
sytuacji. Na listingu 6.34 dodano nowy test jednostkowy do pliku UnitTest2.cs. Test imituje znacznie bardziej
skomplikowaną implementację interfejsu IDiscountHelper. W rzeczywistości biblioteka Moq została użyta
do modelowania zachowania klasy MinimumDiscountHelper.
Listing 6.34. Imitacja zachowania klasy MinimumDiscountHelper w pliku UnitTest2.cs
using
using
using
using
EssentialTools.Models;
Microsoft.VisualStudio.TestTools.UnitTesting;
Moq;
System.Linq;
namespace EssentialTools.Tests
{
[TestClass]
public class UnitTest2
{
private 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}
};
[TestMethod]
public void Sum_Products_Correctly()
{
// przygotowanie
Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();
mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);
var target = new LinqValueCalculator(mock.Object);
// działanie
var result = target.ValueProducts(products);
// wynik
Assert.AreEqual(products.Sum(e => e.Price), result);
}
private Product[] createProduct(decimal value)
{
return new[] { new Product { Price = value } };
}
[TestMethod]
[ExpectedException(typeof(System.ArgumentOutOfRangeException))]
public void Pass_Through_Variable_Discounts()
{
// przygotowanie
Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();
mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>()))
160
ROZDZIAŁ 6.  WAŻNE NARZĘDZIA WSPIERAJĄCE MVC
.Returns<decimal>(total => total);
mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v == 0)))
.Throws<System.ArgumentOutOfRangeException>();
mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v > 100)))
.Returns<decimal>(total => (total * 0.9M));
mock.Setup(m => m.ApplyDiscount(It.IsInRange<decimal>(10, 100,
Range.Inclusive))).Returns<decimal>(total => total - 5);
var target = new LinqValueCalculator(mock.Object);
// działanie
decimal FiveDollarDiscount = target.ValueProducts(createProduct(5));
decimal TenDollarDiscount = target.ValueProducts(createProduct(10));
decimal FiftyDollarDiscount = target.ValueProducts(createProduct(50));
decimal HundredDollarDiscount = target.ValueProducts(createProduct(100));
decimal FiveHundredDollarDiscount = target.ValueProducts(createProduct(500));
// asercje
Assert.AreEqual(5, FiveDollarDiscount, "Niepowodzenie 5 zł ");
Assert.AreEqual(5, TenDollarDiscount, "Niepowodzenie 10 zł");
Assert.AreEqual(45, FiftyDollarDiscount, "Niepowodzenie 50 zł");
Assert.AreEqual(95, HundredDollarDiscount, "Niepowodzenie 100 zł");
Assert.AreEqual(450, FiveHundredDollarDiscount, "Niepowodzenie 500 zł");
target.ValueProducts(createProduct(0));
}
}
}
W terminologii testów zastąpienie oczekiwanego zachowania inną klasą modelu można uznać za dziwne
rozwiązanie, ale jednocześnie to doskonała prezentacja pewnych różnych sposobów wykorzystania
możliwości Moq.
Jak możesz się przekonać, zdefiniowano cztery różne zachowania dla metody ApplyDiscount na podstawie
otrzymanej wartości parametru. Najprostsze zachowanie to przechwycenie wszystkiego, które powoduje
zwrot wartości dla dowolnej wartości decimal:
...
mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);
...
Takie samo zachowanie zostało użyte w poprzednim przykładzie. Zastosowałem je tutaj, ponieważ
kolejność wywoływania metody Setup ma wpływ na zachowanie obiektu imitacyjnego. Moq sprawdza
zachowania w odwrotnej kolejności, a więc ostatnie wywołanie metody Setup jest analizowane jako pierwsze.
Oznacza to konieczność zachowania ostrożności i utworzenie zachowań obiektu imitacyjnego w kolejności
od najbardziej ogólnego do najbardziej szczegółowego. Warunek It.IsAny<decimal> jest najbardziej ogólnym
warunkiem zdefiniowanym w omawianym przykładzie i tym samym będzie zastosowany jako pierwszy.
Jeżeli odwrócimy kolejność wywołań metody Setup, to zachowanie przechwyci wszystkie wywołania metody
ApplyDiscount, a tym samym spowoduje wygenerowanie przez obiekt imitacyjny nieprawidłowych wyników.
Imitacja określonych wartości (i zgłaszanie wyjątku)
W przypadku drugiego wywołania metody Setup została użyta metoda It.Is:
...
mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v == 0)))
.Throws<System.ArgumentOutOfRangeException>();
...
161
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Predykat przekazany metodzie Is zwraca wartość true, jeśli wartością przekazaną metodzie ApplyDiscount
jest 0. Zamiast zwrócić wynik, wykorzystano metodę Throws, która powoduje, że biblioteka Moq zgłosi nowy
egzemplarz wyjątku wskazany w parametrze.
Metodę Is użyto ponadto w celu przechwycenia wartości większych niż 100, np.:
...
mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v > 100)))
.Returns<decimal>(total => (total * 0.9M));
...
Zastosowanie metody It.Is jest najbardziej elastycznym sposobem zdefiniowania określonego
zachowania dla różnych wartości parametrów, ponieważ pozwala na użycie predykatu zwracającego wartości
true i false. Tę metodę wykorzystuję najczęściej podczas tworzenia skomplikowanych obiektów imitacyjnych.
Imitacja zakresu wartości
Ostatni przykład użycia obiektu It dotyczy metody IsInRange, która pozwala na przechwycenie zakresu
wartości parametru:
...
mock.Setup(m => m.ApplyDiscount(It.IsInRange<decimal>(10, 100, Range.Inclusive)))
.Returns<decimal>(total => total - 5);
...
Powyższy przykład umieściłem dla porządku, we własnych projektach mam tendencję do używania
metody Is i predykatu, co daje taki sam wynik:
...
mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v >= 10 && v <= 100)))
.Returns<decimal>(total => total - 5);
...
Efekt jest taki sam, ale podejście z użyciem predykatu zapewnia większą elastyczność. Moq oferuje
ogromną ilość niezwykle użytecznych funkcji. Sposób wykorzystania wielu z nich możesz poznać po
przeczytaniu krótkiego wprowadzenia do Moq, które znajdziesz na stronie http://code.google.com/p/
moq/wiki/QuickStart.
Podsumowanie
W rozdziale tym przedstawiłem trzy narzędzia, które uznałem za niezbędne do efektywnego programowania
MVC — Ninject, narzędzia obsługi testów w Visual Studio oraz Moq. Każde z tych narzędzi ma wiele
odpowiedników, zarówno komercyjnych, jak i open source. Jeżeli nie przyzwyczaisz się do proponowanych
przeze mnie narzędzi, nie będziesz cierpiał na brak możliwości wyboru innego rozwiązania.
Możesz uznać, że nie lubisz TDD lub testowania jednostkowego albo że wystarczy Ci ręczne przeprowadzanie
DI i samodzielne tworzenie imitacji. To oczywiście Twoja decyzja. Jednak uważam, że korzystanie z tych
wszystkich trzech narzędzi w cyklu programowania ma znaczące zalety. Jeżeli masz opory przed ich przyswojeniem
sobie, ponieważ nigdy z nich nie korzystałeś, zalecam dać im szansę — przynajmniej na czas lektury tej książki.
162
ROZDZIAŁ 7.

SportsStore
— kompletna aplikacja
W poprzednich rozdziałach zbudowaliśmy już pierwsze proste aplikacje MVC. Zapoznaliśmy się
z wzorcem MVC. Przypomnieliśmy najważniejsze funkcje C# oraz poznaliśmy narzędzia wykorzystywane
przez dobrych programistów MVC. Teraz czas połączyć to wszystko i zbudować kompletną i realistyczną
aplikację handlu elektronicznego.
Nasza aplikacja, SportsStore, będzie realizowała klasyczny projekt sklepów internetowych: będzie ona
zawierać katalog produktów, który można przeglądać według kategorii, koszyk, do którego użytkownik może
dodawać produkty i usuwać je, jak również ekran realizujący funkcje kasy, gdzie można też wprowadzić
informacje dotyczące wysyłki. Utworzymy ponadto moduł administracyjny, który będzie realizował funkcje
tworzenia, przeglądania, aktualizacji i usuwania (CRUD) pozwalające na zarządzanie katalogiem — będzie
on chroniony, dzięki czemu tylko zalogowani administratorzy będą mogli wprowadzać zmiany.
Budowana aplikacja nie będzie tylko powierzchowną demonstracją. Zamierzamy zbudować solidną
i realistyczną aplikację, która korzysta z zalecanych obecnie najlepszych praktyk. Ponieważ chcę się
skoncentrować na platformie MVC, konieczne okazało się uproszczenie integracji z systemami zewnętrznymi
(na przykład bazą danych) oraz całkowite pominięcie innych (na przykład przetwarzanie płatności za
dokonane zakupy).
Zauważysz, że dosyć powoli będziemy budować potrzebne nam poziomy infrastruktury. Oczywiście,
mógłbyś znacznie szybciej uzyskać początkowe funkcje przy użyciu Web Forms, przeciągając kontrolki na
formularz i wiążąc je bezpośrednio z bazą danych. Jednak początkowa inwestycja w aplikację MVC zwraca się
nieco później, ponieważ aplikacja ta jest łatwa w utrzymaniu, jest rozszerzalna, uporządkowana i świetnie
obsługuje testy jednostkowe.
Testy jednostkowe
Sporo napisałem na temat łatwości testowania jednostkowego w MVC oraz na temat mojego przekonania,
że testowanie jednostkowe jest ważną częścią procesu tworzenia aplikacji. Przekonanie to będzie się przejawiać
w całej książce, ponieważ będę opisywać szczegóły technik stosowanych w testach jednostkowych, powiązanych
z kluczowymi funkcjami MVC.
Wiem jednak, że nie jest to powszechne przeświadczenie. Jeżeli nie chcesz pisać testów jednostkowych,
jest to Twoja decyzja. Zatem gdy będę pisać wyłącznie o testowaniu jednostkowym lub TDD, tekst będzie umieszczony
w tego rodzaju ramce. Jeżeli nie jesteś zainteresowany tym tematem, po prostu pomiń ją — nie wpłynie to na samą
aplikację SportsStore. Nie musisz wykonywać żadnej formy testowania automatycznego, aby skorzystać z większości
udogodnień ASP.NET MVC. Oczywiście obsługa testów jednostkowych to jeden z kluczowych powodów, dla których
platforma MVC zyskuje coraz większą popularność.
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Większości funkcji MVC, z jakich będziemy korzystać, poświęciłem osobne rozdziały w dalszej części
książki. Zamiast powielać potrzebne informacje, przedstawiam tyle, ile jest niezbędne w danym momencie,
i wskażę rozdział zawierający dokładny opis.
Opisuję wszystkie kroki niezbędne przy budowaniu aplikacji, dzięki czemu będziesz widział, jak łączą się
ze sobą poszczególne elementy MVC. Szczególnie powinieneś zwrócić uwagę na tworzenie widoków. Jeżeli
nie będziesz się ściśle stosował do przedstawianych poleceń, aplikacja może się dziwnie zachowywać.
Zaczynamy
Jeżeli planujesz tworzyć aplikację SportsStore równolegle z lekturą, powinieneś mieć zainstalowane oprogramowanie
Visual Studio. Aplikacja ta jest również dostępna w pliku archiwum kodu źródłowego pod adresem
ftp://ftp.helion.pl/przyklady/asp5zp.zip. Nie musisz oczywiście przeglądać tego kodu. Starałem się, aby ekrany
i listingi kodu były możliwie czytelne, dzięki czemu możesz czytać tę książkę w pociągu lub w kawiarni.
Tworzenie rozwiązania i projektów w Visual Studio
Będziemy potrzebować rozwiązania Visual Studio z trzema projektami. Jeden projekt będzie zawierał nasz
model domeny, drugi aplikację MVC, a trzeci testy jednostkowe. Na początek za pomocą szablonu Puste
rozwiązanie utworzymy nowe rozwiązanie; znajduje się on w sekcji Inne typy projektów/Rozwiązania Visual
Studio okna Nowy projekt (rysunek 7.1). Nazwij rozwiązanie SportsStore i kliknij OK.
Rysunek 7.1. Tworzenie nowego rozwiązania w Visual Studio
164
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
Rozwiązanie Visual Studio to kontener przeznaczony dla jednego lub więcej projektów. W tworzonej tutaj
aplikacji potrzebujemy trzech projektów. Informacje na temat poszczególnych projektów są zamieszczone
w tabeli 7.1. Aby utworzyć każdy z tych projektów, kliknij rozwiązanie SportsStore w oknie Eksplorator
rozwiązania, wybierz Dodaj/Nowy projekt…, a następnie szablon wymieniony w tabeli.
Tabela 7.1. Trzy projekty SportsStore
Nazwa projektu
Typ projektu
Przeznaczenie
SportsStore.Domain
Biblioteka klas
Zawiera encje i logikę związaną
z domeną biznesową, konfigurację
zapisu w bazie danych poprzez
repozytoria zbudowane z użyciem
Entity Framework.
SportsStore.WebUI
Aplikacja sieci Web platformy ASP.NET
MVC (wybierz Empty, gdy zostaniesz
poproszony o wybór szablonu projektu,
i zaznacz pole wyboru MVC)
Przechowuje kontrolery i widoki;
zawiera UI dla aplikacji SportsStore.
SportsStore.UnitTests
Projekt testów jednostkowych
Przechowuje testy jednostkowe
dla pozostałych dwóch projektów.
Zawsze używam opcji Empty szablonu Aplikacja sieci Web platformy ASP.NET MVC. Pozostałe opcje
powodują dodanie do projektu konfiguracji początkowej obejmującej między innymi biblioteki JavaScript,
arkusze stylów CSS oraz klasy C# przeznaczone do skonfigurowania funkcji, takich jak zapewnienie
bezpieczeństwa i routing. Wprawdzie wymienione elementy z natury nie są złe, a niektóre biblioteki open
source ostatnio dołączane przez Microsoft do nowych projektów są doskonałe. Jednak wszystkie niezbędne
komponenty i konfigurację można później dodać ręcznie do projektu, co pomaga w lepszym zrozumieniu
sposobu działania platformy MVC.
Po utworzeniu wszystkich trzech projektów okno Eksplorator rozwiązania powinno wyglądać tak jak na
rysunku 7.2. Usunąłem plik Class1.cs w projekcie SportsStore.Domain, ponieważ nie będziemy go używać.
Rysunek 7.2. Projekty w oknie Eksplorator rozwiązania
165
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Aby debugowanie było łatwiejsze, kliknij prawym przyciskiem myszy projekt SportsStore.WebUI i wybierz
opcję Ustaw jako projekt startowy z menu kontekstowego (jego nazwa zostanie pogrubiona). Dzięki temu, gdy
wybierzemy Start Debugging lub Start without Debugging z menu Debuguj, zostanie uruchomiony właśnie
ten projekt.
Kiedy uruchomisz debuger, Visual Studio próbuje poruszać się po poszczególnych plikach widoku, jeśli
były edytowane. Dlatego też prawym przyciskiem myszy kliknij projekt SportsStore.WebUI w oknie Eksplorator
rozwiązania, a następnie wybierz opcję Właściwości z menu kontekstowego. Kliknij kartę Sieć Web, wyświetlając
tym samym właściwości dotyczące sieci, i zaznacz opcję Określ stronę. Nie ma konieczności podawania
jakiejkolwiek wartości w tym polu tekstowym. Zaznaczenie wymienionej opcji jest wystarczające, aby Visual
Studio przestało próbować odgadywać adres URL, który chciał wyświetlić użytkownik. Po uruchomieniu
debugera przeglądarka internetowa będzie wykonywać żądania do głównego adresu URL aplikacji.
Instalacja pakietów narzędziowych
W tym rozdziale będziemy używać Ninject i Moq. Z menu Narzędzia wybierz więc opcję Menedżer
pakietów NuGet/Konsola menedżera pakietów, co spowoduje wyświetlenie przez Visual Studio okna wiersza
poleceń menedżera NuGet. Następnie wydaj poniższe polecenia:
Install-Package
Install-Package
Install-Package
Install-Package
Install-Package
Install-Package
Install-Package
Install-Package
Install-Package
Install-Package
Ninject -version 3.0.1.10 -projectname SportsStore.WebUI
Ninject.Web.Common -version 3.0.0.7 -projectname SportsStore.WebUI
Ninject.MVC3 -Version 3.0.0.6 -projectname SportsStore.WebUI
Ninject -version 3.0.1.10 -projectname SportsStore.UnitTests
Ninject.Web.Common -version 3.0.0.7 -projectname SportsStore.UnitTests
Ninject.MVC3 -Version 3.0.0.6 -projectname SportsStore.UnitTests
Moq -version 4.1.1309.1617 -projectname SportsStore.WebUI
Moq -version 4.1.1309.1617 -projectname SportsStore.UnitTests
Microsoft.Aspnet.Mvc -version 5.0.0 -projectname SportsStore.Domain
Microsoft.Aspnet.Mvc -version 5.0.0 -projectname SportsStore.UnitTests
To całkiem sporo poleceń NuGet do wydania, ponieważ staram się dokładnie wybierać pakiety, które
NuGet instaluje w moich projektach. Podobnie jak we wcześniejszych rozdziałach, także tutaj podaję konkretne
wersje pakietów do pobrania i instalacji.
Dodawanie odwołań między projektami
Konieczne jest zdefiniowanie odwołań między projektami i pewnymi podzespołami Microsoftu. Najłatwiejszym
sposobem dodania bibliotek jest kliknięcie prawym przyciskiem myszy każdego projektu, a następnie wybranie
opcji Dodaj odwołanie… z menu kontekstowego. Kolejnym krokiem jest dodanie wymienionych w tabeli 7.2
odwołań z sekcji Zestawy/Framework, Zestawy/Rozszerzenia lub Rozwiązanie.
 Ostrzeżenie Poświęć nieco czasu na prawidłową konfigurację zależności. Jeżeli nie będziesz miał odpowiednich
bibliotek i odwołań do projektu, podczas próby kompilacji projektu wystąpią problemy.
Tabela 7.2. Wymagane zależności projektów
Nazwa projektu
Zależność od projektu
Odwołania Microsoft
SportsStore.Domain
Brak
System.ComponentModel.DataAnnotations
SportsStore.WebUI
SportsStore.Domain
Brak
SportsStore.UnitTests
SportsStore.DomainSportsStore.WebUI
System.Web
Microsoft.CSharp
166
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
Konfigurowanie kontenera DI
W rozdziale 6. pokazałem Ci, jak używać Ninject w celu utworzenia własnego mechanizmu rozwiązywania
zależności używanego przez platformę MVC do tworzenia obiektów w aplikacji. W tym miejscu zamierzam
powtórzyć ten proces. Na początek w projekcie SportsStore.WebUI utwórz nowy katalog o nazwie Infrastructure,
a następnie dodaj do niego plik klasy o nazwie NinjectDependencyResolver.cs, który powinien zawierać kod
z listingu 7.1.
Listing 7.1. Zawartość pliku NinjectDependencyResolver.cs
using
using
using
using
System;
System.Collections.Generic;
System.Web.Mvc;
Ninject;
namespace SportsStore.WebUI.Infrastructure {
public class NinjectDependencyResolver : IDependencyResolver {
private IKernel kernel;
public NinjectDependencyResolver(IKernel kernelParam) {
kernel = kernelParam;
AddBindings();
}
public object GetService(Type serviceType) {
return kernel.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType) {
return kernel.GetAll(serviceType);
}
private void AddBindings() {
// tu umieść dodatkowe powiązania
}
}
}
Jak pewnie pamiętasz z lektury rozdziału 6., kolejnym krokiem jest utworzenie pomostu między klasą
NinjectDependencyResolver i oferowaną przez platformę MVC obsługą mechanizmu wstrzykiwania
zależności. Wspomniana konfiguracja odbywa się w pliku App_Start/NinjectWebCommon.cs, który jest
jednym z plików dodanych do projektu przez pakiety Ninject zainstalowane za pomocą NuGet. Konfigurację
przedstawiono na listingu 7.2.
Listing 7.2. Integracja Ninject z aplikacją przeprowadzana za pomocą pliku NinjectWebCommon.cs
...
private static void RegisterServices(IKernel kernel) {
System.Web.Mvc.DependencyResolver.SetResolver(new
SportsStore.WebUI.Infrastructure.NinjectDependencyResolver(kernel));
}
...
167
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Uruchamiamy aplikację
Jeżeli wybierzesz opcję Start Debugging z menu Debuguj, wyświetli się strona z informacją o błędzie,
widoczna na rysunku 7.3. Dzieje się tak, ponieważ zażądałeś wyświetlenia adresu URL skojarzonego
z nieistniejącym kontrolerem.
Rysunek 7.3. Strona z informacją o błędzie
Tworzenie modelu domeny
Wszystkie projekty MVC zaczynają się od modelu domeny (tak naprawdę wszystko na platformie obraca się
wokół modelu domeny). Ponieważ tworzymy aplikację handlu elektronicznego, najbardziej oczywistą encją
domeny jest produkt. Wewnątrz projektu SportsStore.Domain utwórz folder o nazwie Entities, a następnie
dodaj nowy plik klasy C# o nazwie Product.cs. Oczekiwana struktura jest pokazana na rysunku 7.4.
Rysunek 7.4. Tworzenie klasy Product
Znasz już zawartość klasy Product, ponieważ jest ona taka sama jak w klasie używanej w poprzednich
rozdziałach. Zawiera oczywiste, potrzebne nam właściwości. Zmień plik klasy Product.cs w sposób
pokazany na listingu 7.3.
168
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
Listing 7.3. Zawartość pliku Product.cs
namespace SportsStore.Domain.Entities
{
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 { get; set; }
}
}
Korzystamy z konwencji definiowania modelu domeny w osobnym projekcie Visual Studio, dlatego klasa
ta musi być oznaczona jako public. Nie musisz stosować tej konwencji, ale uważam, że pomaga ona oddzielić
model od kontrolerów, co jest niezwykle użyteczne w ogromnych i skomplikowanych projektach.
Tworzenie abstrakcyjnego repozytorium
Wiemy, że potrzebny będzie mechanizm pozwalający na pobieranie encji Product z bazy danych. Zgodnie
z informacjami przedstawionymi w rozdziale 3., model zawiera logikę przeznaczoną do przechowywania
i pobierania danych z trwałego magazynu danych. Jednak nawet w modelu warto zachować separację między
poszczególnymi encjami modelu danych oraz logiką odpowiedzialną za przechowywanie i pobieranie danych.
Dlatego też wykorzystamy tak zwany wzorzec repozytorium. Nie musimy się teraz przejmować, w jaki sposób
cały silnik dostępu do danych będzie realizował swoje zadanie, wystarczy, że zdefiniujemy dla niego interfejs.
Utwórz nowy katalog w projekcie SportsStore.Domain, nazwij go Abstract i utwórz w nim nowy plik
interfejsu o nazwie IProductRepository.cs, którego zawartość jest zamieszczona na listingu 7.4. Nowy interfejs
można dodać, klikając prawym przyciskiem myszy folder Abstract, następnie Dodaj/Nowy element… i szablon
Interfejs.
Listing 7.4. Zawartość pliku IProductRepository.cs
using System.Collections.Generic;
using SportsStore.Domain.Entities;
namespace SportsStore.Domain.Abstract
{
public interface IProductRepository
{
IEnumerable<Product> Products { get; }
}
}
W interfejsie tym wykorzystany jest interfejs IEnumerable<T>, który pozwala na pozyskanie sekwencji
obiektów Product bez konieczności określania sposobu przechowywania i pobierania danych. Klasa używająca
interfejsu IProductRepository może uzyskać obiekty Product bez potrzeby znajomości jakichkolwiek szczegółów
ich pochodzenia czy sposobu dostarczenia. Na tym właśnie polega wzorzec repozytorium. Wrócimy do tego
interfejsu w dalszych etapach tworzenia aplikacji, dodając do niego kolejne metody.
Tworzenie imitacji repozytorium
Mamy już zdefiniowany abstrakcyjny interfejs, więc możemy zaimplementować mechanizm trwałego
magazynu danych i dołączyć go do bazy danych. Jednak wcześniej chciałbym dodać inne komponenty
aplikacji. Aby można było rozpocząć prace nad dalszymi częściami aplikacji, utworzymy imitację implementacji
interfejsu IProductRepository, który będzie używany aż do chwili, gdy powrócimy do tematu magazynu danych.
169
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Zdefiniujemy imitację repozytorium oraz dołączymy ją do interfejsu IProductRepository w metodzie
AddBindings klasy NinjectDependencyResolver projektu SportsStore.WebUI, jak pokazano na listingu 7.5.
Listing 7.5. Dodawanie w pliku NinjectDependencyResolver.cs imitacji implementacji IProductRepository
using
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web.Mvc;
Moq;
Ninject;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Infrastructure {
public class NinjectDependencyResolver : IDependencyResolver {
private IKernel kernel;
public NinjectDependencyResolver(IKernel kernelParam) {
kernel = kernelParam;
AddBindings();
}
public object GetService(Type serviceType) {
return kernel.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType) {
return kernel.GetAll(serviceType);
}
private void AddBindings() {
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new List<Product> {
new Product { Name = "Piłka nożna", Price = 25 },
new Product { Name = "Deska surfingowa", Price = 179 },
new Product { Name = "Buty do biegania", Price = 95 }
});
kernel.Bind<IProductRepository>().ToConstant(mock.Object);
}
}
}
Konieczne było dodanie kilku przestrzeni nazw do pliku, ale sam proces tworzenia imitacji repozytorium
opiera się na takich samych technikach Moq jak przedstawione w rozdziale 6. Ninject ma zwrócić ten sam
obiekt imitujący, gdy żądanie będzie dotyczyło implementacji interfejsu IProductRepository, dlatego użyliśmy
metody ToConstant w następujący sposób:
...
kernel.Bind<IProductRepository>().ToConstant(mock.Object);
...
Zamiast za każdym razem tworzyć nowy egzemplarz obiektu implementacji, Ninject zawsze stara się
obsłużyć przy pomocy obiektu imitacyjnego żądania dotyczące interfejsu IProductRepository.
170
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
Wyświetlanie listy produktów
Moglibyśmy spędzić cały dzień na dodawaniu funkcji i zachowań do modelu domeny, nie korzystając wcale
z projektu interfejsu użytkownika. Uważam jednak, że jest to nudne, więc zmienimy kierunek i zaczniemy
korzystać z platformy MVC. Będziemy dodawać funkcje do modelu i repozytorium, gdy będziemy ich potrzebować.
W podrozdziale tym utworzymy kontroler i metody akcji pozwalające wyświetlić dane produktu
z repozytorium. Na razie będą one korzystały z imitacji repozytorium, ale problemem tym zajmiemy się nieco
później. Utworzymy również początkową konfigurację routingu, dzięki czemu MVC będzie w stanie przekazywać
żądania do tworzonych przez nas kontrolerów.
Dodawanie kontrolera
Kliknij prawym przyciskiem myszy katalog Controllers w oknie Eksplorator rozwiązania (w projekcie
SportsStore.WebUI) i wybierz Dodaj/Kontroler…. Zmień nazwę tego kontrolera na ProductController i upewnij
się, że w sekcji Szablon wybrana jest opcja Kontroler MVC 5 - pusty. Gdy Visual Studio otworzy ten plik do
edycji, możesz usunąć domyślną metodę akcji, aby plik wyglądał jak na listingu 7.6.
Listing 7.6. Początkowa zawartość pliku ProductController.cs
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Controllers {
public class ProductController : Controller {
private IProductRepository repository;
public ProductController(IProductRepository productRepository) {
this.repository = productRepository;
}
}
}
Jak możesz zauważyć, poza usunięciem metody akcji Index dodaliśmy konstruktor deklarujący zależność
od interfejsu IProductRepository. Pozwala to na wstrzyknięcie przez Ninject do tworzonego obiektu kontrolera
zależności od repozytorium produktów podczas tworzenia klasy kontrolera. Zaimportowaliśmy także
przestrzeń nazw SportsStore.Domain, aby móc odwoływać się do klas modelu i repozytorium bez konieczności
podawania pełnych nazw. Następnie dodajemy metodę akcji o nazwie List, która wygeneruje widok
zawierający pełną listę produktów (listing 7.7).
Listing 7.7. Dodawanie metody akcji w pliku ProductController.cs
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Controllers {
public class ProductController : Controller {
171
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
private IProductRepository repository;
public ProductController(IProductRepository productRepository) {
this.repository = productRepository;
}
public ViewResult List() {
return View(repository.Products);
}
}
}
Wywołanie w ten sposób metody View (czyli bez podawania nazwy widoku) informuje platformę, że powinna
wygenerować domyślny szablon widoku dla metody akcji. Przez przekazanie listy obiektów Product do metody
View informujemy platformę, że wypełniliśmy obiekt Model w widoku o ściśle określonym typie.
Dodawanie układu, pliku ViewStart i widoku
Teraz musimy dodać domyślny widok dla metody akcji List. Kliknij prawym przyciskiem myszy metodę List
w klasie ProductController i wybierz Dodaj widok… z menu kontekstowego. Nazwij widok List, wskaż
szablon Empty oraz wybierz Product jako klasę dla modelu (rysunek 7.5). Upewnij się o zaznaczeniu opcji
Użyj strony układu. Kliknięcie przycisku Dodaj spowoduje utworzenie widoku.
Rysunek 7.5. Dodawanie widoku Views/Product/List.cshtml
Po kliknięciu przycisku Dodaj Visual Studio utworzy nie tylko plik List.cshtml, ale również pliki
_ViewStart.cshtml i Shared/_Layout.cshtml. Wprawdzie to użyteczna funkcja, ale powinieneś pamiętać,
że domyślnie tworzony plik _Layout.cshtml zawiera niepotrzebny nam kod. Zmodyfikuj więc zawartość
wymienionego pliku, aby odpowiadała kodowi przedstawionemu na listingu 7.8.
Listing 7.8. Zawartość pliku _Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewBag.Title</title>
</head>
<body>
<div>
172
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
@RenderBody()
</div>
</body>
</html>
Wygenerowanie danych widoku
Wprawdzie zdefiniowaliśmy typ widoku jako klasę Product, ale tak naprawdę chcemy pracować z obiektami
IEnumerable<Product>, które kontroler Product pobiera z repozytorium i przekazuje do widoku. Na listingu 7.9
przedstawiono zmodyfikowaną wersję pliku List.cshtml. Dodano w nim wyrażenie @model, a także pewien
kod HTML i wyrażenia silnika Razor odpowiedzialne za wyświetlanie szczegółowych informacji o produktach.
Listing 7.9. Zmodyfikowana zawartość pliku List.cshtml
@using SportsStore.Domain.Entities
@model IEnumerable<Product>
@{
ViewBag.Title = "Produkty";
}
@foreach (var p in Model) {
<div >
<h3>@p.Name</h3>
@p.Description
<h4>@p.Price.ToString("c")</h4>
</div>
}
Zmieniliśmy tytuł strony i utworzyliśmy prostą listę. Zwróć uwagę, że nie musimy korzystać z elementów
Razor text i @:. Każdy wiersz kodu jest dyrektywą Razor lub zaczyna się od znacznika HTML.
 Wskazówka W przedstawionym tu widoku do konwersji właściwości Price na postać ciągu tekstowego
wykorzystana jest metoda formatująca .ToString("c"), która zwraca wartość numeryczną jako zapis waluty
zgodny z ustawieniami regionalnymi serwera. Jeżeli serwer jest skonfigurowany na przykład jako pl-PL, to wywołanie
(1002.3).ToString("c") zwróci 1 002,30 zł, a jeżeli jako en-US, to zwróci $1,002.30. Możesz zmienić
ustawienie regionalne przez dodanie w pliku Web.config do sekcji <system.web> następującego elementu:
<globalization culture="pl-PL" uiCulture="pl-PL" />.
Konfigurowanie domyślnej trasy
Musimy teraz poinformować platformę MVC, że żądania dotyczące katalogu głównego witryny
(http://nasza_witryna/) powinny być przekazane do metody akcji List z klasy ProductController.
Możemy to zrobić przez edycję zawartości metody RegisterRoutes z pliku App_Start/RouteConfig.cs w sposób
pokazany na listingu 7.10.
Listing 7.10. Zdefiniowanie w pliku RouteConfig.cs trasy domyślnej
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
173
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
namespace SportsStore.WebUI {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Product", action = "List", id =
UrlParameter.Optional }
);
}
}
}
Zmiany są zaznaczone pogrubioną czcionką — zmień Home na Product oraz Index na List, jak pokazano
na listingu. Funkcje routingu na platformie ASP.NET przedstawimy dokładniej w rozdziałach 15. i 16. Na razie
wystarczy wiedzieć, że zmiana ta powoduje kierowanie żądań domyślnego adresu URL do zdefiniowanej
przez nas metody akcji (List w kontrolerze Product).
 Wskazówka Zwróć uwagę, że wartością właściwości controller na listingu 7.10 jest Product, a nie nazwa klasy
ProductController. Jest to obowiązkowy schemat nazewnictwa ASP.NET MVC, w którym klasy kontrolerów zawsze
kończą się na Controller; przy odwołaniu do klasy pomijamy tę część nazwy.
Uruchamianie aplikacji
Podstawowe mechanizmy są gotowe. Mamy kontroler z metodą akcji, która jest wywoływana przez
platformę MVC w momencie zażądania domyślnego adresu URL. Ta metoda akcji korzysta z imitacji
implementacji naszego repozytorium, która generuje przykładowe dane testowe. Dane testowe są
przekazywane przez kontroler do widoku skojarzonego z metodą akcji, a widok tworzy prostą listę z danymi
o każdym produkcie. Jeżeli uruchomisz aplikację, powinieneś zobaczyć wynik zamieszczony na rysunku 7.6.
Jeżeli nie otrzymasz wyniku pokazanego na rysunku, to upewnij się, że podany został główny adres URL
aplikacji, a nie żadna inna metoda akcji.
Rysunek 7.6. Podstawowe funkcje aplikacji
174
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
Wzorzec używany przy tworzeniu tej aplikacji jest typowy dla platformy ASP.NET MVC. Inwestujemy
relatywnie dużo czasu na skonfigurowanie wszystkich elementów, ale za to bardzo szybko powstają
podstawowe funkcje aplikacji.
Ułatwienie debugowania
Gdy uruchamiasz projekt za pomocą opcji w menu Debuguj, Visual Studio otwiera nowe okno przeglądarki w celu
wyświetlenia aplikacji, co jednak może zabrać kilka sekund. Istnieją pewne alternatywy pozwalające na przyśpieszenie
tej operacji.
Jeżeli edytujesz pliki widoków, a nie klas, wówczas zmiany w Visual Studio możesz wprowadzać przy
uruchomionym debugerze. Odśwież okno przeglądarki internetowej, gdy chcesz zobaczyć efekt wprowadzonych
zmian. ASP.NET ponownie skompiluje widoki i natychmiast wyświetli zmiany. Natomiast przy uruchomionym
debugerze nie można edytować plików klas lub wprowadzać jakichkolwiek zmian w projekcie, korzystając z okna
Eksploratora rozwiązania. Dlatego też wymieniona technika jest najbardziej użyteczna podczas dopracowywania
kodu HTML generowanego przez aplikację.
W Visual Studio 2013 wprowadzono nową funkcję o nazwie połączone przeglądarki, która umożliwia otwieranie
wielu okien przeglądarek internetowych i odświeżanie ich z poziomu paska menu Visual Studio. Tę funkcję
zademonstruję w rozdziale 14.
Ostatnią alternatywą jest pozostawienie otwartej aplikacji w oddzielnym oknie przeglądarki. W tym celu,
przy założeniu, że uruchomiłeś choć raz debuger, znajdź ikonę IIS Express w zasobniku systemowym, kliknij ją
prawym przyciskiem myszy, a następnie z menu wybierz adres URL aplikacji. Po wprowadzeniu zmian wystarczy
skompilować rozwiązanie w Visual Studio przez naciśnięcie klawisza F6 lub wybranie z menu Kompilacja/Kompiluj
rozwiązanie, a następnie przejść do okna przeglądarki i odświeżyć stronę.
Przygotowanie bazy danych
Możemy już wyświetlić prosty widok zawierający dane naszych produktów, ale nadal są to dane testowe zwracane
przez imitację IProductRepository. Zanim zbudujemy rzeczywiste repozytorium, musimy skonfigurować bazę
danych i wypełnić ją danymi.
Jako bazy danych użyjemy SQL Server. Będziemy z niej korzystać za pośrednictwem Entity Framework (EF),
czyli opracowanej przez Microsoft platformy ORM dla .NET. Platforma ORM pozwala nam pracować
na tabelach, kolumnach i wierszach relacyjnej bazy danych z użyciem zwykłych obiektów C#. W rozdziale 6.
wspomniałem, że LINQ może pracować na różnych źródłach danych, z których jednym jest Entity Framework.
Pokażę teraz, w jaki sposób ułatwia to pracę.
 Uwaga Jest to kolejny obszar, w którym możesz wybierać z wielu narzędzi i technologii. Można korzystać nie tylko
z wielu relacyjnych baz danych, ale również z repozytoriów obiektów, magazynów dokumentów oraz kilku
egzotycznych odpowiedników. Dla .NET dostępnych jest też wiele platform ORM, z których każda przyjmuje
nieco inne podejście — któraś z nich może pasować do Twojego projektu.
Entity Framework wybrałem z kilku powodów. Pierwszym jest łatwość konfiguracji i wykorzystywania
tej platformy. Drugim jest pierwszorzędna integracja z LINQ, a ja lubię używać LINQ. Trzeci powód jest
taki, że platforma ta jest obecnie całkiem dobra — we wcześniejszych wersjach występowały wprawdzie
problemy, ale bieżąca jest bardzo elegancka i ma duże możliwości.
175
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Tworzenie bazy danych
Jedną z użytecznych funkcji w Visual Studio i SQL Server jest LocalDB, czyli pozbawiona funkcji administracyjnych
implementacja podstawowych funkcji SQL Server przeznaczonych specjalnie na potrzeby programistów. Dzięki
LocalDB można pominąć proces konfiguracji bazy danych podczas budowy projektu, a następnie dodać pełny
egzemplarz SQL Server w trakcie wdrażania projektu. Większość aplikacji ASP.NET MVC jest wdrażana
w środowiskach obsługiwanych przez profesjonalnych administratorów. Dzięki wspomnianej funkcji LocalDB
zadanie konfiguracji bazy danych pozostaje w rękach administratorów baz danych, natomiast programiści
zajmują się tworzeniem kodu. Funkcja LocalDB jest instalowana automatycznie wraz z wydaniem Visual
Studio 2013 Express, ale jeśli chcesz, odpowiedni komponent możesz pobrać bezpośrednio ze strony
http://www.microsoft.com/pl-pl/server-cloud/products/sql-server/.
Pierwszym krokiem jest utworzenie w Visual Studio połączenia z bazą danych. Otwórz okno Eksplorator
serwera przez wybranie opcji o tej samej nazwie z menu Wyświetl. Następnie kliknij przycisk Łączenie z bazą
danych, który wygląda jak wtyczka sieciowa wraz z zielonym plusem.
Na ekranie zostanie wyświetlone okno dialogowe Wybierz źródło danych. W tym oknie wybierz Microsoft
SQL Server, jak pokazano na rysunku 7.7, i kliknij przycisk Kontynuuj. (Visual Studio pamięta dokonany
wybór i jeśli będziesz tworzył połączenie z bazą danych w innym projekcie, to nie zobaczysz już tego okna).
Rysunek 7.7. Wybór źródła danych
Na ekranie zostanie teraz wyświetlone okno dialogowe Dodaj połączenie. Jako nazwę serwera podaj
(localdb)\v11.0 — jest to nazwa specjalna, wskazująca że chcesz użyć funkcji LocalDB. Upewnij się o wybraniu
opcji Użyj uwierzytelnienia systemu Windows. Jako nazwę nowej bazy danych podaj SportsStore, jak pokazano
na rysunku 7.8.
 Wskazówka Jeżeli nie zostało wyświetlone okno wyboru źródła danych, wówczas możesz kliknąć przycisk Zmień…
widoczny w prawym górnym rogu okna dialogowego Dodaj połączenie.
Po kliknięciu przycisku OK pojawi się okno dialogowe z pytaniem o utworzenie nowej bazy danych.
Kliknij przycisk Tak, a nowa baza danych zostanie wyświetlona w sekcji Połączenia danych okna Eksplorator
serwera. Możesz teraz rozwinąć nowy element i wyświetlić tym samym zawartość nowo utworzonej bazy danych
(zobacz rysunek 7.9). Powinieneś otrzymać wynik podobny do pokazanego na rysunku 7.9, choć nazwa
połączenia z bazą danych może być inna, ponieważ będzie zawierała lokalną nazwę komputera PC
(w omawianym przypadku jest to po prostu pc).
176
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
Rysunek 7.8. Konfiguracja bazy danych SportsStore
Rysunek 7.9. Baza danych LocalDB wyświetlona w oknie eksploratora serwera
Definiowanie schematu bazy danych
Jak wyjaśniłem na początku rozdziału, moim celem podczas budowy aplikacji SportsStore jest skoncentrowanie się
na procesie budowy aplikacji na platformie ASP.NET MVC. Oznacza to, że pozostałe komponenty
wykorzystywane przez aplikację pozostaną jak najprostsze. Nie chcę się w tym miejscu zagłębiać w temat
projektowania baz danych, a także szczegółowo omawiać Entity Framework. To wykracza poza zakres tematu
pobierania i wstawiania danych za pomocą budowanej tutaj aplikacji. Wspomniane zagadnienia są obszerne
i nie dotyczą platformy ASP.NET lub MVC.
177
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Mając to wszystko na uwadze, w przykładowej aplikacji użyjemy tylko jednej tabeli w naszej bazie danych.
Warto pamiętać, że w rzeczywistych aplikacjach typu e-commerce nie stosuje się takiej struktury danych. Dla
nas najważniejsze jest poznanie wzorca repozytorium i sposobu jego użycia do przechowywania i pobierania
danych, a nie zajmowanie się strukturą bazy danych.
W celu utworzenia tabeli bazy danych w oknie Eksploratora serwera rozwiń dodaną bazę i kliknij prawym
przyciskiem myszy węzeł Tabele, a następnie z menu kontekstowego wybierz opcję Dodaj nową tabelę
(rysunek 7.10).
Rysunek 7.10. Tworzenie nowej tabeli
Na ekranie zostanie wyświetlone narzędzie pozwalające na graficzne tworzenie nowej tabeli. Wprawdzie
możesz użyć wspomnianego narzędzia graficznego, ale w omawianym przykładzie wykorzystamy okno T-SQL,
ponieważ umożliwia ono bardziej zwięzłe i lepsze opisanie specyfikacji wymaganej przez nas tabeli.
Wprowadź polecenie SQL przedstawione na listingu 7.11, a następnie kliknij przycisk Update znajdujący
się w lewym górnym rogu okna tworzenia nowej tabeli.
Listing 7.11. Polecenie SQL tworzące tabelę w bazie danych SportsStore
CREATE TABLE Products
(
[ProductID] INT NOT NULL PRIMARY KEY IDENTITY,
[Name] NVARCHAR(100) NOT NULL,
[Description] NVARCHAR(500) NOT NULL,
[Category] NVARCHAR(50) NOT NULL,
[Price] DECIMAL(16, 2) NOT NULL
)
Powyższe polecenie powoduje utworzenie tabeli o nazwie Products, której kolumny odpowiadają
poszczególnym właściwościom zdefiniowanym wcześniej w klasie modelu o nazwie Product.
 Wskazówka Ustawienie właściwości IDENTITY dla ProductID powoduje, że gdy będą dodawane dane do tej
tabeli, SQL Server będzie generował unikatową wartość klucza głównego. Przy korzystaniu z bazy danych w aplikacji
sieciowej dosyć trudne może być generowanie unikatowych kluczy głównych, ponieważ żądania są realizowane
równolegle. Włączenie tej funkcji powoduje, że możemy zapisywać nowe wiersze tabeli, a SQL Server będzie
generował dla nas unikatowe wartości.
Po kliknięciu przycisku Update na ekranie zostanie wyświetlone okno dialogowe (rysunek 7.11) zawierające
podsumowanie efektów wykonania danego polecenia.
178
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
Rysunek 7.11. Podsumowanie efektów wykonania danego polecenia SQL
Kliknij przycisk Update Database w celu rzeczywistego wykonania polecenia SQL i utworzenia tabeli
Products w bazie danych. Efekt działania polecenia możesz zobaczyć w oknie Eksplorator serwera po
kliknięciu w nim przycisku Odśwież. Sekcja Tabele będzie zawierała nową tabelę Products oraz
szczegółowe informacje o poszczególnych kolumnach.
 Wskazówka Po uaktualnieniu bazy danych możesz zamknąć okno dbo.Products. Visual Studio zaoferuje możliwość
zapisania skryptu SQL użytego do utworzenia bazy danych. W omawianym przykładzie nie ma potrzeby zapisywania
skryptu, ale wspomniana funkcja może być naprawdę użyteczna w rzeczywistych projektach, gdy będzie występowała
potrzeba przeprowadzenia konfiguracji wielu baz danych.
Dodawanie danych do bazy
Wprowadzimy teraz nieco danych do bazy, abyśmy mieli na czym pracować do momentu, w którym dodamy
funkcje administrowania katalogiem w rozdziale 11.
W oknie Eksplorator serwera rozwiń węzeł Tabele w bazie danych SportsStore, kliknij prawym przyciskiem
myszy tabelę Products i wybierz opcję Pokaż dane tabeli. Wpisz dane pokazane na rysunku 7.12. Można
przechodzić pomiędzy wierszami przy użyciu klawisza Tab. Naciśnięcie klawisza Tab na końcu każdego
rekordu powoduje przejście do kolejnego rekordu i uaktualnienie bazy danych.
 Uwaga Kolumnę ProductID należy pozostawić pustą. Jest to kolumna identyfikatora, więc SQL Server wygeneruje
unikatowe wartości w momencie przejścia do następnego wiersza.
Informacje szczegółowe dotyczące produktów wymieniono w tabeli 7.3, na wypadek gdybyś miał
trudności w ich odczytaniu z rysunku. Tak naprawdę nie ma żadnego znaczenia, czy w bazie danych
wprowadzisz dokładnie takie same dane jak pokazano w książce. Jednak jeżeli użyjesz innych, wtedy
na kolejnych etapach tworzenia aplikacji SportsStore będziesz otrzymywał inne wyniki niż w książce.
179
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 7.12. Dodawanie danych do tabeli Products
Tabela 7.3. Dane przeznaczone do umieszczenia w tabeli produktów
Nazwa
Opis
Kategoria
Cena
Kajak
Łódka przeznaczona dla jednej osoby
Sporty wodne
275
Kamizelka ratunkowa
Chroni i dodaje uroku
Sporty wodne
48,95
Piłka
Zatwierdzone przez FIFA rozmiar i waga
Piłka nożna
19,5
Flagi narożne
Nadadzą twojemu boisku profesjonalny wygląd
Piłka nożna
34,95
Stadion
Składany stadion na 35 000 osób
Piłka nożna
79500,00
Czapka
Zwiększa efektywność mózgu o 75%
Szachy
16
Niestabilne krzesło
Zmniejsza szanse przeciwnika
Szachy
29,95
Ludzka szachownica
Przyjemna gra dla całej rodziny!
Szachy
75
Błyszczący król
Figura pokryta złotem i wysadzana diamentami
Szachy
1200
Tworzenie kontekstu Entity Framework
Najnowsze wersje platformy Entity Framework zawierają użyteczną funkcję o nazwie code-first (zacznij od kodu).
Pozwala ona zdefiniować klasy w modelu, a następnie wygenerować bazę danych na podstawie tych klas.
Jest to doskonałe w przypadku projektów powstających od zera, ale jest ich niewiele. Zamiast tego skorzystamy
z odmiany tego procesu i skojarzymy nasze klasy modelu z istniejącą bazą danych. Z menu Narzędzia wybierz
więc opcję Menedżer pakietów NuGet/Konsola menedżera pakietów, co spowoduje wyświetlenie przez Visual
Studio okna wiersza poleceń menedżera NuGet. Następnie wydaj poniższe polecenia:
Install-Package EntityFramework -projectname SportsStore.Domain
Install-Package EntityFramework -projectname SportsStore.WebUI
 Wskazówka W Konsoli menedżera pakietów mogą pojawić się błędy informujące o braku możliwości
wygenerowania tzw. binding redirects. Te komunikaty można bezpiecznie zignorować.
Wymienione polecenia powodują dodanie pakietu Entity Framework do rozwiązania. Ten sam pakiet
trzeba zainstalować w projektach Domain i WebUI. Utworzymy klasy uzyskujące dostęp do bazy danych
w projektach Domain i WebUI.
180
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
Następnym krokiem jest utworzenie klasy kontekstu, która skojarzy nasz prosty model z bazą danych.
W projekcie SportsStore.Domain utwórz nowy katalog o nazwie Concrete, a następnie dodaj w nim plik klasy
EFDbContext.cs i zmień jej zawartość tak, jak pokazano na listingu 7.12.
Listing 7.12. Zawartość pliku EfDbContext.cs
using SportsStore.Domain.Entities;
using System.Data.Entity;
namespace SportsStore.Domain.Concrete {
public class EFDbContext : DbContext {
public DbSet<Product> Products { get; set; }
}
}
Aby skorzystać z opcji code-first, należy utworzyć klasę dziedziczącą po System.Data.Entity.DbContext.
W klasie tej definiujemy właściwości dla każdej tabeli, z której chcemy korzystać.
Nazwa właściwości definiuje nazwę tabeli, a typ parametru wyniku DbSet określa model, który powinien
być użyty przez Entity Framework do reprezentacji wierszy tabeli. W naszym przypadku nazwą właściwości jest
Products, a typem parametru Product. Oczekujemy więc, że typ Product zostanie zastosowany do reprezentowania
rekordów tabeli Products.
Musimy poinformować Entity Framework, w jaki sposób należy podłączyć się do bazy danych, więc w pliku
Web.config, znajdującym się w projekcie SportsStore.WebUI, należy dodać ciąg tekstowy połączenia o takiej
samej nazwie jak klasa kontekstu (listing 7.13).
 Wskazówka Zwróć uwagę na zamianę projektów. Model i logika repozytorium znajdują się w projekcie
SportsStore.Domain, natomiast informacje o połączeniu z bazą danych zostały umieszczone w pliku Web.config
projektu SportsStore.WebUI.
Listing 7.13. Dodawanie w pliku Web.config definicji połączenia z bazą danych
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<connectionStrings>
<add name="EFDbContext" connectionString="Data Source=(localdb)\v11.0;Initial
Catalog=SportsStore;Integrated Security=True"
providerName="System.Data.SqlClient"/>
</connectionStrings>
<appSettings>
<add key="webpages:Version" value="3.0.0.0" />
<add key="webpages:Enabled" value="false" />
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
</appSettings>
<system.web>
<compilation debug="true" targetFramework="4.5.1" />
<httpRuntime targetFramework="4.5.1" />
</system.web>
</configuration>
181
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
 Ostrzeżenie Wartość atrybutu connectionString musiała zostać podzielona na kilka wierszy, aby zmieściła się
na stronie książki. Bardzo ważne jest jednak, aby w pliku Web.config cały ciąg tekstowy połączenia znajdował się
w pojedynczym wierszu.
W sekcji connectionsString pliku Web.config będzie znajdował się jeszcze jeden element <add>. Wymieniony
element jest domyślnie tworzony przez Visual Studio, możesz go zignorować lub nawet usunąć z pliku.
Tworzenie repozytorium produktów
Mamy już wszystko, co jest potrzebne do rzeczywistego zaimplementowania klasy IProcuctRepository. W projekcie
SportsStore.Domain utwórz klasę EFProductRepository w katalogu Concrete. Umieść w pliku klasy kod
zamieszczony na listingu 7.14.
Listing 7.14. Zawartość pliku EFProductRepository.cs
using System.Collections.Generic;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
namespace SportsStore.Domain.Concrete {
public class EFProductRepository : IProductRepository {
private EFDbContext context = new EFDbContext();
public IEnumerable<Product> Products {
get { return context.Products; }
}
}
}
Jest to nasza klasa repozytorium. Implementuje ona interfejs IProductRepository i korzysta z obiektu
EFDbContext do pobierania danych z bazy za pomocą Entity Framework. Sposób korzystania z Entity Framework
(i jego prostotę) omówię przy okazji dodawania kolejnych funkcji do repozytorium.
W celu użycia nowej klasy repozytorium trzeba jeszcze zamienić powiązania Ninject dla imitacji
repozytorium na rzeczywiste. Zmień klasę NinjectControllerFactory z projektu SportsStore.WebUI
w taki sposób, aby metoda AddBindings wyglądała jak na listingu 7.15.
Listing 7.15. Dodawanie w pliku NinjectDependencyResolver.cs powiązania z rzeczywistym repozytorium
using
using
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web.Mvc;
Moq;
Ninject;
SportsStore.Domain.Entities;
SportsStore.Domain.Concrete;
SportsStore.Domain.Abstract;
namespace SportsStore.WebUI.Infrastructure {
public class NinjectDependencyResolver : IDependencyResolver{
private IKernel kernel;
public NinjectDependencyResolver(IKernel kernelParam) {
kernel = kernelParam;
182
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
AddBindings();
}
public object GetService(Type serviceType) {
return kernel.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType) {
return kernel.GetAll(serviceType);
}
private void AddBindings() {
kernel.Bind<IProductRepository>().To<EFProductRepository>();
}
}
}
Nowe powiązanie jest zaznaczone czcionką pogrubioną. Informuje ono Ninject, że chcemy tworzyć
egzemplarze klasy EFProductRepository w odpowiedzi na żądania udostępnienia interfejsu IProductRepository.
Pozostało nam ponownie uruchomić aplikację. Wynik jest pokazany na rysunku 7.13, na którym możemy
zobaczyć, że dane produktów są pobierane z bazy danych, a nie z imitacji repozytorium.
Rysunek 7.13. Wynik implementacji rzeczywistego repozytorium
 Wskazówka Jeżeli podczas uruchamiania projektu nastąpi zgłoszenie wyjątku System.ArgumentException,
oznacza to, że informacje szczegółowe o połączeniu z bazą danych zostały w pliku Web.config podzielone na dwa
wiersze. Więcej informacji na ten temat znajdziesz w poprzednim punkcie.
Ten sposób przedstawienia Entity Framework bazie danych SQL Server jako serii obiektów modelu jest
proste i łatwe, a ponadto pozwala nam skoncentrować się na platformie MVC. Oczywiście pominąłem tutaj
wiele informacji szczegółowych dotyczących sposobu działania platformy Entity Framework oraz ogromną
liczbę dostępnych opcji konfiguracyjnych. Bardzo lubię platformę Entity Framework i zachęcam Cię do
poświęcenia nieco czasu na jej dokładniejsze poznanie. Dobrym punktem do rozpoczęcia poznawania
platformy jest poświęcona Entity Framework strona firmy Microsoft, którą znajdziesz pod adresem
http://msdn.microsoft.com/pl-PL/data/ef.
183
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Dodanie stronicowania
Jak widać na rysunku 7.13, wszystkie dane produktów pobrane z bazy danych są wyświetlane na jednej
stronie. W tym podrozdziale dodamy obsługę stronicowania, dzięki czemu będziemy mogli wyświetlić
określoną liczbę produktów na stronie, a użytkownik będzie mógł przechodzić pomiędzy stronami, aby przejrzeć
cały katalog. Aby zapewnić tę funkcję, dodamy parametr metody List w kontrolerze Product (listing 7.16).
Listing 7.16. Dodawanie stronicowania w metodzie List kontrolera Product
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Controllers {
public class ProductController : Controller {
private IProductRepository repository;
public int PageSize = 4;
public ProductController(IProductRepository productRepository) {
this.repository = productRepository;
}
public ViewResult List(int page = 1) {
return View(repository.Products
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize));
}
}
}
Pole PageSize pozwala zdefiniować, że chcemy widzieć na stronie cztery produkty. Nieco później
zrealizujemy lepszy mechanizm stronicowania. Do metody List dodaliśmy parametr opcjonalny. Dzięki temu,
gdy wywołamy metodę bez parametru (List()), nasze wywołanie będzie traktowane tak, jakbyśmy podali
wartość określoną w definicji parametru (List(1)). W efekcie metoda akcji powoduje wyświetlenie pierwszej
strony produktów, gdy platforma MVC wywołuje tę metodę bez argumentu. W metodzie List pobieramy
obiekty Product, układamy je w kolejności klucza podstawowego, pomijamy produkty znajdujące się przed
naszą stroną, a następnie odczytujemy tyle produktów, ile jest zdefiniowanych w polu PageSize.
Test jednostkowy — stronicowanie
Aby przetestować funkcję stronicowania, utworzymy imitację repozytorium, wstrzykniemy ją do konstruktora klasy
ProductController, a następnie wywołamy metodę List dla określonej strony. Następnie porównamy obiekty
Product, jakie otrzymamy, z tymi, których oczekiwaliśmy. Informacje na temat konfigurowania testów jednostkowych
znajdziesz w rozdziale 6. Poniżej znajduje się test, jaki utworzyłem w pliku UnitTest1.cs w projekcie
SportsStore.UnitTests.
using
using
using
using
184
System.Collections.Generic;
System.Linq;
Microsoft.VisualStudio.TestTools.UnitTesting;
Moq;
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Controllers;
namespace SportsStore.UnitTests {
[TestClass]
public class UnitTest1 {
[TestMethod]
public void Can_Paginate() {
// przygotowanie
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"},
new Product {ProductID = 3, Name = "P3"},
new Product {ProductID = 4, Name = "P4"},
new Product {ProductID = 5, Name = "P5"}
});
ProductController controller = new ProductController(mock.Object);
controller.PageSize = 3;
// działanie
IEnumerable<Product> result =
(IEnumerable<Product>)controller.List(2).Model;
// asercje
Product[] prodArray = result.ToArray();
Assert.IsTrue(prodArray.Length == 2);
Assert.AreEqual(prodArray[0].Name, "P4");
Assert.AreEqual(prodArray[1].Name, "P5");
}
}
}
Zwróć uwagę, jak łatwo dostać się do danych zwróconych z metody kontrolera. Skorzystaliśmy z właściwości
Model w celu pobrania kolekcji IEnumerable<Product>, wygenerowanej przez metodę List. Po tej operacji możemy
sprawdzić, czy mamy oczekiwane dane. W tym przypadku za pomocą metody LINQ o nazwie ToArray
skonwertowaliśmy kolekcję na tablicę i sprawdziliśmy jej wielkość i wartości poszczególnych obiektów.
Wyświetlanie łączy stron
Jeżeli uruchomimy aplikację, zauważymy tylko cztery produkty na jednej stronie. Jeżeli chcemy zobaczyć inną
stronę, możemy dodać do adresu URL parametr:
http://localhost:49159/?page=2
Prawdopodobnie będziesz musiał zmienić numer portu w tym adresie URL, aby pasował do tego, na którym
działa Twój serwer ASP.NET. Z wykorzystaniem tego typu ciągów zapytania można przechodzić pomiędzy
stronami katalogu produktów.
Oczywiście, tylko my wiemy o tym. Klienci nie będą wiedzieć, jakich parametrów ciągu zapytania powinni
użyć, a nawet jeżeli udałoby się ich o tym poinformować, nie byliby zadowoleni z takiego sposobu nawigacji.
Niezbędne jest wygenerowanie łączy stron na dole każdej listy produktów, dzięki którym użytkownicy będą mogli
przechodzić pomiędzy stronami. W tym celu zaimplementujemy metodę pomocniczą HTML wielokrotnego
użytku, podobną do Html.TextBoxFor i Html.BeginForm, z których korzystaliśmy w rozdziale 2. Nasza metoda
wygeneruje znaczniki HTML dla potrzebnych łączy nawigacji.
185
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Dodawanie modelu widoku
Aby wywołać metodę pomocniczą HTML, będziemy musieli przekazać informacje o liczbie dostępnych stron,
bieżącej stronie oraz całkowitej liczbie produktów w repozytorium. Najprostszym sposobem zrealizowania tego
zadania jest utworzenie modelu widoku, o którym wspomnieliśmy krótko w rozdziale 3. Dodaj klasę PagingInfo
do katalogu Models w projekcie SportsStore.WebUI i umieść w niej kod z listingu 7.17.
Listing 7.17. Zawartość pliku PagingInfo.cs
using System;
namespace SportsStore.WebUI.Models {
public class PagingInfo {
public int TotalItems { get; set; }
public int ItemsPerPage { get; set; }
public int CurrentPage { get; set; }
public int TotalPages {
get { return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage); }
}
}
}
Model widoku nie wchodzi w skład modelu domeny. Jest to tylko klasa ułatwiająca przekazywanie danych
pomiędzy kontrolerem i widokiem. Aby to podkreślić, umieściliśmy ją w projekcie SportsStore.WebUI,
aby oddzielić ją od klas modelu domeny.
Dodanie metody pomocniczej HTML
Po utworzeniu modelu widoku możemy zaimplementować metodę pomocniczą HTML, którą nazwiemy
PageLinks. W projekcie SportsStore.WebUI utwórz katalog o nazwie HtmlHelpers, a następnie dodaj nowy
plik klasy o nazwie PagingHelpers.cs. Kod tej klasy jest przedstawiony na listingu 7.18.
Listing 7.18. Zawartość pliku klasy PagingHelpers.cs
using
using
using
using
System;
System.Text;
System.Web.Mvc;
SportsStore.WebUI.Models;
namespace SportsStore.WebUI.HtmlHelpers {
public static class PagingHelpers {
public static MvcHtmlString PageLinks(this HtmlHelper html,
PagingInfo pagingInfo,
Func<int, string> pageUrl) {
StringBuilder result = new StringBuilder();
for (int i = 1; i <= pagingInfo.TotalPages; i++) {
TagBuilder tag = new TagBuilder("a");
tag.MergeAttribute("href", pageUrl(i));
tag.InnerHtml = i.ToString();
if (i == pagingInfo.CurrentPage) {
tag.AddCssClass("selected");
tag.AddCssClass("btn-primary");
}
tag.AddCssClass("btn btn-default");
result.Append(tag.ToString());
186
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
}
return MvcHtmlString.Create(result.ToString());
}
}
}
Metoda rozszerzająca PageLinks generuje HTML dla zbioru łączy stron, korzystając z informacji przekazanych
w obiekcie PagingInfo. Parametr Func pozwala na przekazanie delegata, który może być użyty do generowania
łączy do innych stron.
Test jednostkowy — tworzenie łączy stron
Aby przetestować metodę pomocniczą PageLinks, wywołamy ją z danymi testowymi i porównamy
wyniki z oczekiwanym kodem HTML. Metoda testowa jest następująca:
using
using
using
using
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web.Mvc;
Microsoft.VisualStudio.TestTools.UnitTesting;
Moq;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
SportsStore.WebUI.Controllers;
SportsStore.WebUI.Models;
SportsStore.WebUI.HtmlHelpers;
namespace SportsStore.UnitTests {
[TestClass]
public class UnitTest1 {
[TestMethod]
public void Can_Paginate() {
// …polecenie usunięte w celu zachowania zwięzłości…
}
[TestMethod]
public void Can_Generate_Page_Links() {
// przygotowanie — definiowanie metody pomocniczej HTML — potrzebujemy tego,
// aby użyć metody rozszerzającej
HtmlHelper myHelper = null;
// przygotowanie — tworzenie danych PagingInfo
PagingInfo pagingInfo = new PagingInfo {
CurrentPage = 2,
TotalItems = 28,
ItemsPerPage = 10
};
// przygotowanie — konfigurowanie delegatu z użyciem wyrażenia lambda
Func<int, string> pageUrlDelegate = i => "Strona" + i;
// działanie
MvcHtmlString result = myHelper.PageLinks(pagingInfo, pageUrlDelegate);
187
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
// asercje
Assert.AreEqual(@"<a class=""btn btn-default"" href=""Strona1"">1</a>"
+ @"<a class="" btn btn-default btn-primary selected"" href=""Strona2"">2</a>"
+ @"<a class=""btn btn-default"" href=""Strona3"">3</a>", result.ToString());
}
}
}
Test ten weryfikuje wynik działania metody pomocniczej z użyciem literałów znakowych zawierających
cudzysłowy. C# radzi sobie bez problemów z takimi literałami, jeżeli tylko będziemy pamiętać o poprzedzeniu
ciągu znakiem @ i użyciu podwójnych znaków cudzysłowu ("") w miejsce pojedynczych. Trzeba również pamiętać,
aby nie łamać literału w na kilka wierszy, ponieważ w takim przypadku porównanie się nie powiedzie. W przykładzie
tym literał jest zawinięty na dwa wiersze, gdyż szerokość drukowanej strony jest za mała. Nie dodaliśmy znaku
nowego wiersza; w takim przypadku test byłby nieudany.
Należy pamiętać, że metoda rozszerzająca jest dostępna tylko wtedy, gdy zawierająca ją przestrzeń nazw
znajduje się w zasięgu. W pliku kodu dodanie przestrzeni nazw odbywa się za pomocą polecenia using, ale
w widoku Razor konieczne jest zmodyfikowanie konfiguracji w pliku Web.config lub użycie @using w samym
widoku. W projekcie Razor MVC znajdują się dwa pliki Web.config, co jest nieco mylące — główny plik,
znajdujący się w głównym katalogu projektu aplikacji, oraz specyficzny dla widoków, znajdujący się
w katalogu Views. Powinniśmy zmienić plik Views/Web.config w sposób pokazany na listingu 7.19.
Listing 7.19. Dodawanie przestrzeni nazw metody pomocniczej HTML do pliku Views/Web.config
...
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc,
Version=5.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="System.Web.WebUI" />
<add namespace="SportsStore.WebUI.HtmlHelpers"/>
</namespaces>
</pages>
</system.web.webPages.razor>
...
Każda przestrzeń nazw, do której chcemy się odwołać w widoku Razor, musi być zadeklarowana albo
w pliku web.config, albo w samym widoku za pomocą instrukcji @using.
Dodawanie danych modelu widoku
Nie jesteśmy w pełni gotowi do użycia naszej metody pomocniczej. Musimy jeszcze przekazać obiekt klasy
PagingInfo do widoku. Możemy zrealizować to za pomocą mechanizmu View Bag, ale lepszym rozwiązaniem
jest opakowanie wszystkich danych wysyłanych z kontrolera do widoku pojedynczą klasą modelu widoku. W tym
celu dodaj nowy plik klasy, o nazwie ProductsListViewModel.cs, do katalogu Models w projekcie
SportsStore.WebUI. Kod tej klasy jest przedstawiony na listingu 7.20.
188
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
Listing 7.20. Zawartość pliku ProductsListViewModel.cs
using System.Collections.Generic;
using SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Models {
public class ProductsListViewModel {
public IEnumerable<Product> Products { get; set; }
public PagingInfo PagingInfo { get; set; }
}
}
Teraz możemy zaktualizować metodę List w klasie ProductController, aby korzystała z klasy
ProductsListViewModel do przekazania danych wyświetlanych produktów oraz informacji o stronicowaniu
(listing 7.21).
Listing 7.21. Aktualizacja metody List w pliku ProductController.cs
using
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers
{
public class ProductController : Controller
{
private IProductRepository repository;
public int PageSize = 4;
public ProductController(IProductRepository productRepository)
{
this.repository = productRepository;
}
public ViewResult List(int page = 1) {
ProductsListViewModel model = new ProductsListViewModel {
Products = repository.Products
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo {
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = repository.Products.Count()
}
};
return View(model);
}
}
}
Zmiany te powodują przekazanie obiektu ProductsListViewModel jako danych modelu dla widoku.
189
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Test jednostkowy — dane stronicowania w widoku modelu
Musimy upewnić się, że kontroler przesyła do widoku prawidłowe dane stronicowania. Poniżej zamieszczony jest
test jednostkowy dodany do projektu testowego:
...
[TestMethod]
public void Can_Send_Pagination_View_Model() {
// przygotowanie
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"},
new Product {ProductID = 3, Name = "P3"},
new Product {ProductID = 4, Name = "P4"},
new Product {ProductID = 5, Name = "P5"}
});
// przygotowanie
ProductController controller = new ProductController(mock.Object);
controller.PageSize = 3;
// działanie
ProductsListViewModel result = (ProductsListViewModel)controller.List(2).Model;
// asercje
PagingInfo pageInfo = result.PagingInfo;
Assert.AreEqual(pageInfo.CurrentPage, 2);
Assert.AreEqual(pageInfo.ItemsPerPage, 3);
Assert.AreEqual(pageInfo.TotalItems, 5);
Assert.AreEqual(pageInfo.TotalPages, 2);
}
...
Musimy również zmienić nasz wcześniejszy test stronicowania znajdujący się w metodzie Can_Paginate.
Zakłada on, że metoda akcji List zwraca ViewResult, którego właściwość Model jest sekwencją obiektów
Product, ale dane te umieściliśmy w innym typie modelu widoku. Zmieniony test wygląda następująco:
...
[TestMethod]
public void Can_Paginate() {
// przygotowanie
// — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"},
new Product {ProductID = 3, Name = "P3"},
new Product {ProductID = 4, Name = "P4"},
new Product {ProductID = 5, Name = "P5"}
});
ProductController controller = new ProductController(mock.Object);
controller.PageSize = 3;
190
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
// działanie
ProductsListViewModel result = (ProductsListViewModel)controller.List(2).Model;
// asercje
Product[] prodArray = result.Products.ToArray();
Assert.IsTrue(prodArray.Length == 2);
Assert.AreEqual(prodArray[0].Name, "P4");
Assert.AreEqual(prodArray[1].Name, "P5");
}
Zwykle tworzę wspólną metodę konfiguracji testu, aby uniknąć duplikacji kodu w tego typu metodach
testowych. Jednak tu zamieszczam testy jednostkowe w osobnych ramkach; musimy tworzyć testy samodzielne.
Teraz widok oczekuje sekwencji obiektów Product, więc aby obsłużyć nowy typ modelu, musimy jeszcze
zmienić plik List.cshtml, jak pokazano na listingu 7.22.
Listing 7.22. Zaktualizowany widok List.cshtml
@model SportsStore.WebUI.Models.ProductsListViewModel
@{
ViewBag.Title = "Produkty";
}
@foreach (var p in Model.Products) {
<div>
<h3>@p.Name</h3>
@p.Description
<h4>@p.Price.ToString("c")</h4>
</div>
}
Zmieniliśmy dyrektywę @model, aby poinformować Razor, że teraz pracujemy na innym typie danych. Musimy
również zmodyfikować pętlę foreach, ponieważ źródłem danych jest właściwość Products w danych modelu.
Wyświetlanie łączy stron
Mamy już wszystko przygotowane, aby dodać łącza stron do widoku List. Utworzyliśmy model widoku, który
zawiera dane stronicowania, zaktualizowaliśmy kontroler, aby dane te zostały przekazane do widoku, a następnie
zmieniliśmy dyrektywę @model, aby pasowała do nowego typu modelu widoku. Pozostało nam wywołać metodę
pomocniczą HTML z widoku, co pokazano na listingu 7.23.
Listing 7.23. Wywołanie w pliku List.cshtml metody pomocniczej HTML
@model SportsStore.WebUI.Models.ProductsListViewModel
@{
ViewBag.Title = "Produkty";
}
@foreach (var p in Model.Products) {
<div>
<h3>@p.Name</h3>
@p.Description
<h4>@p.Price.ToString("c")</h4>
</div>
}
191
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
<div>
@Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new {page = x}))
</div>
Po uruchomieniu aplikacji możemy zobaczyć łącza stron na dole strony (rysunek 7.14). Styl strony jest nadal
bardzo prosty, ale zmienimy to pod koniec rozdziału. Na tym etapie ważniejsze jest, że łącza te pozwalają na
przechodzenie pomiędzy stronami w katalogu i przeglądanie dostępnych produktów.
Rysunek 7.14. Wyświetlanie łączy nawigacji między stronami
Dlaczego po prostu nie użyjemy GridView?
Jeżeli korzystałeś wcześniej z ASP.NET, możesz uznać, że włożyliśmy sporo pracy, a uzyskaliśmy mało imponujące
wyniki. Poświęciliśmy wiele miejsca, by uzyskać tylko listę stron. W przypadku Web Forms moglibyśmy uzyskać
to samo przy użyciu gotowej kontrolki GridView lub ListView z ASP.NET Web Forms, dołączając ją bezpośrednio
do naszej tabeli Products.
To, co uzyskaliśmy do tej pory, bardzo różni się jednak od przeciągnięcia GridView na formularz. Po pierwsze,
budujemy tę aplikację na bazie solidnej architektury, która wymaga odpowiedniej separacji zadań. W przeciwieństwie do
najprostszego wariantu użycia ListView nie mamy bezpośredniego powiązania interfejsu użytkownika z bazą
danych — podejście to daje wynik najszybciej, ale z czasem sprawia bardzo dużo problemów. Po drugie, tworzyliśmy
testy jednostkowe, które pozwalają nam kontrolować działanie naszej aplikacji w sposób, który jest niemal
niemożliwy w przypadku użycia skomplikowanych kontrolek z Web Forms. Na koniec należy pamiętać, że spora
część tego rozdziału została poświęcona tworzeniu bazowej infrastruktury, na podstawie której będzie budowana
aplikacja. Musimy tylko raz zdefiniować repozytorium, a potem będziemy mogli szybko budować i testować nowe
funkcje, co pokażę w kolejnych rozdziałach.
192
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
Wymienione punkty w żaden sposób nie pomniejszają faktu, że w przypadku Web Forms podobne wyniki
można uzyskać po wykonaniu znacznie mniejszej ilości pracy. Jednak, jak wyjaśniłem w rozdziale 3., szybki wynik
w Web Forms wiąże się z kosztem, który może być wysoki w ogromnych i skomplikowanych projektach.
Ulepszanie adresów URL
Nasze łącza stron działają, ale nadal korzystają z ciągu zapytania do przekazywania danych do serwera w następujący
sposób:
http://localhost/?page=2
Możemy zrobić to lepiej, tworząc schemat oparty na wzorcu składanych adresów URL. Składany adres URL
to taki, który ma sens dla użytkownika, tak jak poniższy:
http://localhost/Strona2
Na szczęście MVC pozwala bardzo łatwo zmieniać schemat adresów URL, ponieważ wykorzystuje funkcje
routingu ASP.NET. Wystarczy dodać nowe trasy do metody RegisterRoutes w pliku RouteConfig.cs,
który znajduje się w katalogu App_Start projektu SportsStore.WebUI. Odpowiednie zmiany konieczne do
wprowadzenia przedstawiono na listingu 7.24.
Listing 7.24. Dodawanie nowej trasy w pliku RouteConfig.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace SportsStore.WebUI
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: null,
url: "Strona{page}",
defaults: new { Controller = "Product", action = "List" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Product", action = "List", id = UrlParameter.Optional }
);
}
}
}
193
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Bardzo ważne jest, aby nowa trasa została dodana przed trasą domyślną (Default) zdefiniowaną w pliku. Jak
pokażę w rozdziale 15., trasy są przetwarzane w kolejności definiowania, a nasza nowa trasa musi mieć większy
priorytet niż istniejąca.
To jedyne, co musimy zrobić w celu zmiany schematu URL dla stronicowania produktów. Platforma MVC
jest ściśle zintegrowana z funkcjami routingu, więc taka zmiana jest automatycznie stosowana na stronie wynikowej,
ponieważ metoda Url.Action korzysta z nowych danych (właśnie tej metody użyliśmy w widoku List.cshtml
do wygenerowania łączy stron). Nie przejmuj się, jeżeli nie wiesz, jak działa routing — wyjaśnię to szczegółowo
w rozdziałach 15. i 16.
Jeżeli uruchomisz aplikację i przejdziesz do kolejnej strony, zobaczysz nowy schemat URL w działaniu
(rysunek 7.15).
Rysunek 7.15. Nowy schemat URL wyświetlany w przeglądarce
Dodawanie stylu
Do tej pory zbudowaliśmy całkiem niezłą infrastrukturę i nasza aplikacja zaczyna nabierać kształtu, ale nie
zwracaliśmy uwagi na projekt graficzny. Wprawdzie książka nie jest poświęcona CSS ani projektowaniu dla WWW,
ale w tym podrozdziale zajmiemy się szatą graficzną aplikacji SportsStore, gdyż teraz jej słaby wygląd przesłania
techniczną doskonałość programu. Mam zamiar zaimplementować klasyczny, dwukolumnowy układ
z nagłówkiem (rysunek 7.16).
Rysunek 7.16. Cel projektowy dla aplikacji SportsStore
Instalacja pakietu Bootstrap
W celu nadania stylów CSS w aplikacji wykorzystamy framework Bootstrap. Aby zainstalować pakiet
Bootstrap, z menu Narzędzia wybierz opcję Menedżer pakietów NuGet/Konsola menedżera pakietów,
co spowoduje wyświetlenie przez Visual Studio okna wiersza poleceń menedżera NuGet.
Następnie wydaj poniższe polecenie:
Install-Package -version 3.0.0 bootstrap –projectname SportsStore.WebUI
To jest dokładnie to samo proste polecenie NuGet, którego używałeś już w rozdziale 2. Jedyna różnica
polega na dodaniu argumentu projectname w celu zagwarantowania, że pliki zostaną umieszczone we
właściwym projekcie.
194
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
 Uwaga W tym miejscu przypomnę, że wprawdzie użyjemy Bootstrap w aplikacji, ale nie zamierzam dokładnie
omawiać możliwości oferowanych przez ten pakiet. Dokładne omówienie Bootstrap oraz innych działających po
stronie klienta bibliotek, których Microsoft pozwala używać na platformie MVC, znajdziesz w innej mojej książce,
zatytułowanej Pro ASP.NET MVC 5 Client, wydanej przez Apress.
Zastosowanie w aplikacji stylów Bootstrap
W rozdziale 5. wyjaśniłem, jak działają strony układu Razor oraz jak można je stosować. Podczas tworzenia
widoku List.cshtml dla kontrolera Product poprosiłem o zaznaczenie opcji użycia strony układu i jednocześnie
o pozostawienie pustego pola tekstowego przeznaczonego do wskazania konkretnej strony. W efekcie
używany jest układ zdefiniowany w pliku Views/_ViewStart.cshtml, który Visual Studio tworzy
automatycznie wraz z widokiem. Zawartość wymienionego pliku przedstawiono na listingu 7.25.
Listing 7.25. Zawartość pliku _ViewStart.cshtml
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
Wartość właściwości Layout wskazuje, że widoki będą używać układu zdefiniowanego w pliku
Views/_ViewStart.cshtml, o ile wyraźnie nie wskażemy alternatywy. Zawartość pliku _Layout.cshtml
zmieniliśmy wcześniej w rozdziale i usunęliśmy kod dodany w nim przez Visual Studio. Na listingu 7.26 możesz
zobaczyć przywróconą zawartość pliku _Layout.cshtml, w którym znalazły się polecenia importujące pliki
Bootstrap CSS oraz zastosowano pewne style CSS.
Listing 7.26. Zastosowanie stylów Bootstrap CSS w pliku _Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<title>@ViewBag.Title</title>
</head>
<body>
<div class="navbar navbar-inverse" role="navigation">
<a class="navbar-brand" href="#">Sklep sportowy</a>
</div>
<div class="row panel">
<div id="categories" class="col-xs-3">
Później umieścimy tu coś użytecznego.
</div>
<div class="col-xs-8">
@RenderBody()
</div>
</div>
</body>
</html>
Za pomocą elementów <link> do układu dodaliśmy pliki bootstrap.css i bootstrap-theme.css, a ponadto
zastosowaliśmy różne klasy Bootstrap, tworząc tym samym prosty układ graficzny. Teraz trzeba zmodyfikować
jeszcze plik List.cshtml — odpowiednie zmiany przedstawiono na listingu 7.27.
195
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 7.27. Użycie Bootstrap w celu nadania stylów w pliku List.cshtml
@model SportsStore.WebUI.Models.ProductsListViewModel
@{
ViewBag.Title = "Produkty";
}
@foreach (var p in Model.Products) {
<div class="well">
<h3>
<strong>@p.Name</strong>
<span class="pull-right label label-primary">@p.Price.ToString("c")</span>
</h3>
<span class="lead"> @p.Description</span>
</div>
}
<div class="btn-group pull-right">
@Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x }))
</div>
Problem z nadawaniem stylów elementom
Elementy HTML wygenerowane przez aplikację MVC pochodzą z wielu różnych źródeł (treść statyczna, wyrażenia
Razor, metody pomocnicze HTML itd.). Dlatego też w projekcie stosowane są klasy stylów o różnych nazwach.
Jeżeli uważasz to za nieco uciążliwe, nie jesteś odosobniony. Mieszanie stylów CSS w generowanych elementach
nie jest dobrym pomysłem i jest sprzeczne ze stosowaną na platformie MVC ideą separacji zadań. Sytuację możesz nieco
poprawić przez przypisanie elementom klas innych niż Bootstrap na podstawie ich roli w aplikacji, a następnie
użyć bibliotek, takich jak jQuery lub LESS, do mapowania między klasami niestandardowymi i Bootstrap.
W budowanej tutaj aplikacji chcę zachować prostotę i dlatego akceptuję osadzone klasy Bootstrap w całej
aplikacji, nawet jeśli ma to skomplikować proces zmiany stylów w przyszłości. Nie zdecydowałbym się na takie
podejście w rzeczywistej aplikacji. Doskonale jednak wiem, że ta aplikacja jest jedynie przykładowa i nie będzie
nigdy w fazie konserwacji.
Po uruchomieniu aplikacji zauważysz poprawę wyglądu — przynajmniej troszeczkę. Zmiany te są pokazane
na rysunku 7.17.
Tworzenie widoku częściowego
Końcowym zadaniem w tym rozdziale będzie refaktoring naszej aplikacji — uprościmy widok List.cshtml.
Utworzymy widok częściowy, to jest raczej fragment treści, który można dołączyć do innego widoku, a nie
szablon. Widoki częściowe znajdują się w osobnych plikach i można je wielokrotnie wykorzystywać w wielu
widokach, co pomaga zmniejszyć ilość powielonego kodu, szczególnie jeżeli używamy tych samych danych
w kilku miejscach aplikacji.
Aby dodać widok częściowy, kliknij prawym przyciskiem myszy katalog /Views/Shared w projekcie
SportsStore.WebUI i wybierz z menu podręcznego Dodaj/Widok…. Jako nazwę widoku wpisz ProductSummary.
Wskaż szablon Empty oraz wybierz klasę Product z listy rozwijanej Klasa modelu. Zaznacz opcję Utwórz jako
widok częściowy, jak pokazano na rysunku 7.18.
196
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
Rysunek 7.17. Aplikacja SportsStore po usprawnieniu projektu
Rysunek 7.18. Tworzenie widoku częściowego
Gdy klikniesz przycisk Dodaj, Visual Studio utworzy plik widoku częściowego ~/Views/Shared/
ProductSummary.cshtml. Widok częściowy jest bardzo podobny do zwykłego widoku poza tym, że generuje
fragment kodu HTML, a nie pełny dokument. Jeżeli otworzymy widok ProductSummary, zauważymy, że zawiera
tylko dyrektywę modelu widoku, ustawioną na naszą klasę modelu domeny, Product. Umieść w nim kod
pokazany na listingu 7.28.
Listing 7.28. Uaktualniony kod widoku częściowego w pliku ProductSummary.cs
@model SportsStore.Domain.Entities.Product
<div class="well">
<h3>
<strong>@Model.Name</strong>
<span class="pull-right label label-primary">@Model.Price.ToString("c")</span>
197
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
</h3>
<span class="lead">@Model.Description</span>
</div>
Teraz musimy zmodyfikować widok Views/Products/List.cshtml, aby korzystał z widoku częściowego.
Zmiany są zamieszczone na listingu 7.29.
Listing 7.29. Użycie widoku częściowego w List.cshtml
@model SportsStore.WebUI.Models.ProductsListViewModel
@{
ViewBag.Title = "Produkty";
}
@foreach (var p in Model.Products) {
@Html.Partial("ProductSummary", p);
}
<div class="pager">
@Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new {page = x}))
</div>
Kod, który wcześniej znajdował się w pętli foreach, w widoku List.cshtml, został przeniesiony do nowego
widoku częściowego. Ten widok częściowy wywołujemy przy użyciu metody pomocniczej Html.Partial. Jej
parametrami są nazwa widoku oraz obiekt modelu widoku. Korzystanie z widoków częściowych jest dobrą
praktyką, ale nie zmienia to wyglądu aplikacji. Po jej uruchomieniu zobaczysz, że wygląda ona identycznie
jak wcześniej, co widać na rysunku 7.19.
Rysunek 7.19. Użycie widoku częściowego
198
ROZDZIAŁ 7.  SPORTSSTORE — KOMPLETNA APLIKACJA
Podsumowanie
W tym rozdziale zbudowaliśmy większość podstawowej infrastruktury dla aplikacji SportsStore. Nie posiada
ona zbyt wielu funkcji, które można pokazać klientowi, ale „pod maską” mamy już początki modelu domeny
oraz repozytorium produktów obsługujące bazę SQL Server za pośrednictwem Entity Framework. Mamy jeden
kontroler, ProductContyroller, który pozwala wygenerować stronicowaną listę produktów, skonfigurowaliśmy
też kontener DI oraz przyjazny schemat adresów URL.
Jeżeli uważasz, że w tym rozdziale było zbyt dużo konfiguracji i za mało wyników, to w następnym znajdziesz
wyrównanie. Mamy zbudowane podstawowe elementy, więc możemy pójść dalej i dodać wszystkie funkcje
użytkownika — nawigację według kategorii, koszyk na zakupy i proces składania zamówienia.
199
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
200
ROZDZIAŁ 8.

SportsStore — nawigacja
W poprzednim rozdziale utworzyliśmy podstawową infrastrukturę, czyli szkielet aplikacji SportsStore. Teraz
użyjemy tej infrastruktury w celu dodania kluczowych funkcji aplikacji i pokażemy, że początkowy trud się opłacił.
Będziemy w stanie szybko i łatwo dodawać funkcje użytkownika. Przy okazji przedstawimy kilka dodatkowych
funkcji oferowanych przez platformę MVC.
Dodawanie kontrolek nawigacji
Aplikacja SportsStore będzie znacznie lepsza, jeżeli pozwolimy użytkownikom na przeglądanie produktów według
kategorii. Zrobimy to w trzech etapach:
 rozszerzenie modelu metody akcji List w klasie ProductController, aby możliwe było filtrowanie obiektów
Product w repozytorium,
 rozszerzenie schematu adresów URL i modyfikacja strategii routingu,
 utworzenie listy kategorii wyświetlanej w panelu bocznym witryny, wyróżnienie bieżącej kategorii produktu
i udostępnienie łączy do pozostałych.
Filtrowanie listy produktów
Zaczniemy od rozszerzania naszej klasy modelu widoku, ProductsListViewModel, którą dodaliśmy do projektu
SportsStore.WebUI w poprzednim rozdziale. Musimy przekazać bieżącą kategorię do widoku, dzięki czemu
będzie możliwe wygenerowanie panelu bocznego — to dobre zadanie na początek. Na listingu 8.1 zamieszczone
są zmiany wprowadzone w pliku ProductListViewModel.cshtml.
Listing 8.1. Rozszerzanie pliku klasy ProductsListViewModel.cs
using System.Collections.Generic;
using SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Models {
public class ProductsListViewModel {
}
}
public IEnumerable<Product> Products { get; set; }
public PagingInfo PagingInfo { get; set; }
public string CurrentCategory { get; set; }
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Dodaliśmy nową właściwość, CurrentCategory. Następnym krokiem będzie modyfikacja klasy
ProductController w taki sposób, aby metoda akcji List filtrowała obiekty Product według kategorii i korzystała
z nowej właściwości dodanej do modelu widoku w celu wskazania wybranej kategorii. Zmiany te są pokazane
na listingu 8.2.
Listing 8.2. Dodawanie obsługi kategorii do metody akcji List w pliku ProductController.cs
using
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers {
public class ProductController : Controller {
private IProductRepository repository;
public int PageSize = 4;
public ProductController(IProductRepository productRepository) {
this.repository = productRepository;
}
public ViewResult List(string category, int page = 1) {
ProductsListViewModel viewModel = new ProductsListViewModel {
Products = repository.Products
.Where(p => category == null || p.Category == category)
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo {
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = repository.Products.Count()
},
CurrentCategory = category
};
return View(viewModel);
}
}
}
Do metody tej wprowadziliśmy trzy zmiany. Dodaliśmy nowy parametr o nazwie category. Parametr ten
jest używany w drugiej zmianie, gdzie rozszerzamy nasze zapytanie LINQ — jeżeli wartość category jest różna
od null, wybierane są obiekty Product, których właściwość Category zawiera nazwę wybranej kategorii.
Ostatnią zmianą jest ustawienie wartości właściwości CurrentCategory, którą dodaliśmy do klasy
ProductListViewModel. Jednak zmiany te powodują, że wartość PagingInfo.TotalItems ma niewłaściwą
wartość — naprawimy to wkrótce.
202
ROZDZIAŁ 8.  SPORTSSTORE — NAWIGACJA
Test jednostkowy — aktualizowanie istniejących testów jednostkowych
Zmieniliśmy sygnaturę metody akcji List, co spowodowało, że istniejące testy jednostkowe przestały się kompilować.
Aby temu zaradzić, przekażemy null jako pierwszy parametr metody List w tych testach jednostkowych, które
działają z kontrolerem. Na przykład w teście Can_Paginate sekcja akcji testu jednostkowego będzie wyglądać
następująco:
...
[TestMethod]
public void Can_Paginate() {
// przygotowanie
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"},
new Product {ProductID = 3, Name = "P3"},
new Product {ProductID = 4, Name = "P4"},
new Product {ProductID = 5, Name = "P5"}
});
// utworzenie kontrolera i ustawienie 3-elementowej strony
ProductController controller = new ProductController(mock.Object);
controller.PageSize = 3;
// działanie
ProductsListViewModel result
= (ProductsListViewModel)controller.List(null, 2).Model;
}
...
// asercje
Product[] prodArray = result.Products.ToArray();
Assert.IsTrue(prodArray.Length == 2);
Assert.AreEqual(prodArray[0].Name, "P4");
Assert.AreEqual(prodArray[1].Name, "P5");
Ponieważ użyliśmy null, wszystkie obiekty Product zostaną pobrane z repozytorium, czyli uzyskamy taką
samą sytuację jak przed dodaniem nowego parametru. Musimy się jeszcze upewnić o wprowadzeniu tego
samego rodzaju zmiany w teście Can_Send_Pagination_View_Model:
...
[TestMethod]
public void Can_Send_Pagination_View_Model() {
// przygotowanie
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"},
new Product {ProductID = 3, Name = "P3"},
new Product {ProductID = 4, Name = "P4"},
new Product {ProductID = 5, Name = "P5"}
});
// utworzenie kontrolera i ustawienie 3-elementowej strony
ProductController controller = new ProductController(mock.Object);
controller.PageSize = 3;
// działanie
203
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
ProductsListViewModel result
= (ProductsListViewModel)controller.List(null, 2).Model;
// asercje
PagingInfo pageInfo = result.PagingInfo;
Assert.AreEqual(pageInfo.CurrentPage, 2);
Assert.AreEqual(pageInfo.ItemsPerPage, 3);
Assert.AreEqual(pageInfo.TotalItems, 5);
Assert.AreEqual(pageInfo.TotalPages, 2);
}
...
Gdy przywykniesz do przeprowadzania testów jednostkowych, zachowanie ich synchronizacji z kodem bardzo
szybko stanie się Twoją drugą naturą.
Nawet po wprowadzeniu tak małej zmiany możemy zobaczyć efekty filtrowania. Jeżeli uruchomisz aplikację
i wybierzesz kategorię za pomocą ciągu tekstowego zapytania (pamiętaj o zmianie numeru portu na przypisany
przez Visual Studio Twojemu projektowi), na przykład:
http://localhost:49159/?category=Szachy
zobaczysz jedynie produkty z kategorii Szachy, jak pokazano na rysunku 8.1.
Rysunek 8.1. Użycie ciągu tekstowego zapytania do filtrowania według kategorii
204
ROZDZIAŁ 8.  SPORTSSTORE — NAWIGACJA
Oczywiście użytkownicy nie będą chcieli poruszać się po kategoriach, używając do tego adresów URL, ale ten
przykład pokazuje, że małe zmiany mogą mieć duży wpływ na aplikację MVC po przygotowaniu dla niej
podstawowej struktury.
Test jednostkowy — filtrowanie według kategorii
Aby prawidłowo przetestować funkcję filtrowania według kategorii, potrzebujemy testu upewniającego nas, że będziemy
otrzymywać wyłącznie produkty z wybranej kategorii. Test ten jest następujący:
...
[TestMethod]
public void Can_Filter_Products() {
// przygotowanie
// — utworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1", Category = "Cat1"},
new Product {ProductID = 2, Name = "P2", Category = "Cat2"},
new Product {ProductID = 3, Name = "P3", Category = "Cat1"},
new Product {ProductID = 4, Name = "P4", Category = "Cat2"},
new Product {ProductID = 5, Name = "P5", Category = "Cat3"}
});
// przygotowanie — utworzenie kontrolera i ustawienie 3-elementowej strony
ProductController controller = new ProductController(mock.Object);
controller.PageSize = 3;
// działanie
Product[] result = ((ProductsListViewModel)controller.List("Cat2", 1).Model)
.Products.ToArray();
// asercje
Assert.AreEqual(result.Length, 2);
Assert.IsTrue(result[0].Name == "P2" && result[0].Category == "Cat2");
Assert.IsTrue(result[1].Name == "P4" && result[1].Category == "Cat2");
}
...
Test ten tworzy imitację repozytorium zawierającą obiekty Product należące do kilku kategorii. Jedna z kategorii
jest przekazywana do metody akcji, po czym sprawdzamy, czy w wyniku otrzymaliśmy właściwe obiekty we właściwej
kolejności.
Ulepszanie schematu URL
Nikt nie chce widzieć brzydkich adresów URL, takich jak /?category=Szachy. Ulepszymy nasz schemat routingu,
aby można było korzystać z adresów URL, które są dla nas (i naszych klientów) wygodniejsze. Aby zaimplementować
nowy schemat, zmień metodę RegisterRoutes w pliku App_Start/RouteConfig.cs w sposób pokazany
na listingu 8.3.
Listing 8.3. Nowy schemat URL zdefiniowany w pliku RouteConfig.cs
using System;
using System.Collections.Generic;
205
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
using
using
using
using
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace SportsStore.WebUI {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(null,
"",
new {
controller = "Product", action = "List",
category = (string)null, page = 1
}
);
routes.MapRoute(null,
"Strona{page}",
new { controller = "Product", action = "List", category = (string)null },
new { page = @"\d+" }
);
routes.MapRoute(null,
"{category}",
new { controller = "Product", action = "List", page = 1 }
);
routes.MapRoute(null,
"{category}/Strona{page}",
new { controller = "Product", action = "List" },
new { page = @"\d+" }
);
routes.MapRoute(null, "{controller}/{action}");
}
}
}
 Ostrzeżenie Ważne jest, aby dodać nowe trasy z listingu 8.3 w pokazanej kolejności. Trasy są stosowane
w kolejności definiowania, więc jeżeli zmienisz kolejność, możesz uzyskać dziwne efekty.
W tabeli 8.1 przedstawiony jest schemat URL realizowany przez te trasy. System routingu omówimy
dokładniej w rozdziałach 15. i 16.
System routingu ASP.NET jest używany przez MVC do obsługi żądań przychodzących od klientów, ale
również do generowania wychodzących adresów URL zgodnych z naszym schematem URL, które można
osadzić na stronach WWW. Tym sposobem możemy się upewnić, że wszystkie adresy URL w aplikacji są
spójne.
206
ROZDZIAŁ 8.  SPORTSSTORE — NAWIGACJA
Tabela 8.1. Podsumowanie tras
URL
Działanie
/
/Strona2
/Szachy
Wyświetla pierwszą stronę produktów ze wszystkich kategorii.
Wyświetla podaną stronę (w tym przypadku stronę drugą) produktów z wszystkich kategorii.
Wyświetla pierwszą stronę elementów z podanej kategorii (w tym przypadku kategorii
Szachy).
Wyświetla podaną stronę (w tym przypadku stronę drugą) produktów z podanej kategorii
(w tym przypadku kategorii Szachy).
/Szachy/Strona2
 Uwaga Sposób testowania jednostkowego konfiguracji routingu jest przedstawiony w rozdziale 15.
Metoda Url.Action jest najwygodniejszym sposobem generowania łączy wychodzących. W poprzednim
rozdziale używaliśmy tej metody pomocniczej w widoku List.cshtml do wyświetlenia łączy stron. Teraz, gdy
dodaliśmy obsługę filtrowania kategorii, musimy wrócić do tego miejsca i przekazać dodatkowe dane do metody
pomocniczej, co zostało pokazane na listingu 8.4.
Listing 8.4. Dodawanie danych o kategoriach do łączy stron generowanych w pliku List.cshtml
@model SportsStore.WebUI.Models.ProductsListViewModel
@{
ViewBag.Title = "Produkty";
}
@foreach (var p in Model.Products) {
Html.Partial("ProductSummary", p);
}
<div class=" btn-group pull-right">
@Html.PageLinks(Model.PagingInfo, x => Url.Action("List",
new {page = x, category = Model.CurrentCategory}))
</div>
Przed tą zmianą łącza generowane w kontrolce stronicowania wyglądały następująco:
http://<serwer>:<port>/Strona1
Jeżeli użytkownik kliknie tego typu łącze, zastosowany filtr kategorii zostanie utracony i wyświetli się strona
zawierająca produkty z wszystkich kategorii. Przez dodanie bieżącej kategorii, pobranej z modelu widoku,
wygenerowane zostaną następujące adresy URL:
http://<serwer>:<port>/Szachy/Strona1
Gdy użytkownik kliknie łącze tego typu, bieżąca kategoria będzie przekazana do metody akcji List i filtr
zostanie zachowany. Po wprowadzeniu tej zmiany możemy skorzystać z adresów URL takich jak /Szachy; na dole
strony zobaczymy łącza stron zawierające prawidłową kategorię.
207
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Budowanie menu nawigacji po kategoriach
Teraz musimy udostępnić klientom sposób wybrania kategorii inny niż jej wpisywanie w adresie URL. Musimy
wyświetlić listę wszystkich dostępnych kategorii oraz wskazać, która z nich jest wybrana, o ile jakakolwiek
została wybrana. Wraz z rozwojem aplikacji będziemy korzystać z tej listy kategorii w wielu kontrolerach,
więc potrzebujemy czegoś, co będzie niezależne i co będzie się nadawało do wielokrotnego użytku.
Platforma ASP.NET MVC posiada mechanizm akcji potomnych, które są doskonałe do tworzenia takich
elementów aplikacji, jak kontrolka nawigacji wielokrotnego użytku. Akcje potomne korzystają z metody
pomocniczej HTML o nazwie Html.Action, pozwalającej dołączyć wynik dowolnej metody akcji do bieżącego
widoku. W tym przypadku możemy utworzyć nowy kontroler (nazwiemy go NavController) z metodą akcji
(tutaj: Menu), który wygeneruje menu nawigacji. Następnie za pomocą metody pomocniczej Html.Action
wygenerowane dane wyjściowe zostaną umieszczone na stronie.
Podejście takie pozwoli nam korzystać z osobnego kontrolera, który będzie zawierał potrzebny nam kod,
który może być testowany identycznie jak każdy inny kontroler. Jest to naprawdę bardzo przyjemny sposób
tworzenia mniejszych segmentów aplikacji z zachowaniem ogólnego podejścia stosowanego na platformie MVC.
Tworzenie kontrolera nawigacji
Kliknij prawym przyciskiem myszy katalog Controllers w projekcie SportsStore.WebUI i wybierz Dodaj,
a następnie Kontroler… z menu kontekstowego. Nazwij nowy kontroler NavController, wybierz opcję Kontroler
MVC 5 - pusty z menu Szablon, a następnie kliknij Dodaj, aby utworzyć plik klasy NavController.cs. Usuń metodę
Index, utworzoną domyślnie przez Visual Studio i dodaj metodę akcji Menu zamieszczoną na listingu 8.5.
Listing 8.5. Dodanie metody akcji Menu do pliku NavController.cs
using System.Web.Mvc;
namespace SportsStore.WebUI.Controllers {
public class NavController : Controller {
public string Menu() {
return "Pozdrowienia z NavController";
}
}
}
Metoda ta zwraca wyłącznie komunikat, ale jest to wystarczające do integracji akcji potomnej z resztą aplikacji.
Chcemy, aby lista kategorii pojawiała się na wszystkich stronach, więc wygenerujemy akcję potomną w układzie,
a nie w konkretnym widoku. Otwórz plik Views/Shared/_Layout.cshtml i dodaj wywołanie metody pomocniczej
Html.Action, jak pokazano na listingu 8.6.
Listing 8.6. Dodawanie wywołania Html.Action w pliku _Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<title>@ViewBag.Title</title>
</head>
<body>
<div class="navbar navbar-inverse" role="navigation">
<a class="navbar-brand" href="#">Sklep sportowy</a>
</div>
<div class="row panel">
<div id="categories" class="col-xs-3">
@Html.Action("Menu", "Nav")
208
ROZDZIAŁ 8.  SPORTSSTORE — NAWIGACJA
</div>
<div class="col-xs-8">
@RenderBody()
</div>
</div>
</body>
</html>
Usunęliśmy tekst dodany w rozdziale 7., zastępując go wywołaniem metody Html.Action. Parametrami tej
metody są akcja, którą chcemy wywołać (Menu), oraz kontroler, którego chcemy użyć (Nav). Po uruchomieniu
aplikacji zobaczymy wynik metody akcji Menu dołączony do każdej strony, jak pokazano na rysunku 8.2.
Rysunek 8.2. Wyświetlanie wyniku z metody akcji Menu
Generowanie listy kategorii
Możemy teraz wrócić do kontrolera Nav i wygenerować rzeczywistą listę kategorii. Nie chcemy jednak generować
adresów URL kategorii w kontrolerze. Aby to zrobić, użyjemy metody pomocniczej w widoku. W metodzie
akcji Menu utworzymy listę kategorii w sposób pokazany na listingu 8.7.
Listing 8.7. Implementacja metody Menu w pliku NavController.cs
using
using
using
using
System.Collections.Generic;
System.Web.Mvc;
SportsStore.Domain.Abstract;
System.Linq;
namespace SportsStore.WebUI.Controllers {
public class NavController : Controller {
private IProductRepository repository;
public NavController(IProductRepository repo) {
repository = repo;
}
public PartialViewResult Menu() {
IEnumerable<string> categories = repository.Products
.Select(x => x.Category)
.Distinct()
.OrderBy(x => x);
return PartialView(categories);
}
}
}
209
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Pierwsza zmiana polega na dodaniu konstruktora akceptującego argument w postaci implementacji
IProductRepository — wymieniona implementacja będzie dostarczona przez Ninject w trakcie tworzenia
egzemplarzy klasy NavController. Druga zmiana została wprowadzona w metodzie akcji Menu, która teraz
używa zapytania LINQ do pobrania listy kategorii z repozytorium i przekazania ich widokowi. Ponieważ
pracujemy z widokiem częściowym w tym kontrolerze, wywołujemy metodę PartialView w metodzie akcji,
a wynikiem jest obiekt PartialViewResult.
Test jednostkowy — generowanie listy kategorii
Test jednostkowy metody generującej listę kategorii jest względnie prosty. Naszym celem jest utworzenie
posortowanej alfabetycznie listy, na której nie będzie duplikatów. Najprostszym sposobem na realizację tego
testu jest użycie danych zawierających powtarzające się kategorie, które nie są w odpowiedniej kolejności,
przekazanie ich do NavController i sprawdzenie, czy zostały prawidłowo ułożone. Wykorzystamy
następujący test jednostkowy:
...
[TestMethod]
public void Can_Create_Categories() {
// przygotowanie
// — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1", Category = "Jabłka"},
new Product {ProductID = 2, Name = "P2", Category = "Jabłka"},
new Product {ProductID = 3, Name = "P3", Category = "Śliwki"},
new Product {ProductID = 4, Name = "P4", Category = "Pomarańcze"},
});
// przygotowanie — utworzenie kontrolera
NavController target = new NavController(mock.Object);
// działanie — pobranie zbioru kategorii
string[] results = ((IEnumerable<string>)target.Menu().Model).ToArray();
// asercje
Assert.AreEqual(results.Length, 3);
Assert.AreEqual(results[0], "Jabłka");
Assert.AreEqual(results[1], "Pomarańcze");
Assert.AreEqual(results[2], "Śliwki");
}
Użyliśmy tu imitacji repozytorium zawierającej powtarzające się kategorie, które nie zostały zapisane
w odpowiedniej kolejności. Następnie sprawdziliśmy, czy zostały usunięte duplikaty i czy dane są uporządkowane
alfabetycznie.
Tworzenie widoku
W celu utworzenia widoku dla metody akcji Menu kliknij prawym przyciskiem myszy katalog Views/Nav,
a następnie z menu kontekstowego wybierz opcję Dodaj/Strona widoku MVC 5 (Razor)…. Jako nazwę podaj
Menu i kliknij przycisk OK, co spowoduje utworzenie pliku Menu.cshtml. Usuń kod wstawiany przez Visual
Studio w nowych widokach, a następnie zmień zawartość widoku, aby odpowiadała pokazanej na listingu 8.8.
210
ROZDZIAŁ 8.  SPORTSSTORE — NAWIGACJA
Listing 8.8. Zawartość pliku Menu.cshtml
@model IEnumerable<string>
@Html.ActionLink("Home", "List", "Product", null,
new { @class = "btn btn-block btn-default btn-lg" })
@foreach (var link in Model) {
@Html.RouteLink(link, new {
controller = "Product",
action = "List",
category = link,
page = 1
}, new {
@class = "btn btn-block btn-default btn-lg"
})
}
Na górze listy kategorii dodaliśmy łącze Home, które pozwala użytkownikowi na przejście na stronę
wyświetlającą wszystkie produkty bez filtra kategorii. Zrealizowaliśmy to za pomocą metody pomocniczej
ActionLink, która generuje łącze HTML z zastosowaniem skonfigurowanych wcześniej danych routingu.
Następnie przeglądamy nazwy kategorii i tworzymy łącza za pomocą metody RouteLink. Jest ona podobna
do ActionLink, ale pozwala na podanie pary nazwa-wartość, która będzie używana przy generowaniu adresu
URL na podstawie konfiguracji routingu. Jeżeli informacje na temat routingu nie są dla Ciebie jasne, nie przejmuj
się — wyjaśnimy wszystko dokładnie w rozdziałach 15. i 16.
Wygenerowane przez nas łącza są brzydkie, więc obu metodom pomocniczym (ActionLink i RouteLink)
dostarczamy obiekty zawierające wartości dla atrybutów tworzonych elementów. Wspomniane obiekty
definiują atrybut class (został poprzedzony znakiem @, ponieważ class to słowo zarezerwowane w C#)
i wskazują klasy Bootstrap nadające styl dużych przycisków.
Jeżeli uruchomisz aplikację, powinieneś zobaczyć łącza kategorii pokazane na rysunku 8.3. Gdy dana kategoria
zostanie kliknięta, lista elementów powinna się zaktualizować i zawierać wyłącznie pozycje z tej kategorii.
Rysunek 8.3. Łącza kategorii
211
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Test jednostkowy — raportowanie wybranej kategorii
Możemy sprawdzić, czy metoda Menu prawidłowo dodaje informacje na temat wybranej kategorii przez przypisanie
wartości właściwości ViewBag w teście jednostkowym; właściwość ta jest dostępna poprzez klasę ViewResult.
Test ten jest następujący:
...
[TestMethod]
public void Indicates_Selected_Category() {
// przygotowanie
// — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1", Category = "Jabłka"},
new Product {ProductID = 4, Name = "P2", Category = "Pomarańcze"},
});
// przygotowanie — utworzenie kontrolera
NavController target = new NavController(mock.Object);
// przygotowanie — definiowanie kategorii do wybrania
string categoryToSelect = "Jabłka";
// działanie
string result = target.Menu(categoryToSelect).ViewBag.SelectedCategory;
// asercje
Assert.AreEqual(categoryToSelect, result);
}
...
Ten test jednostkowy nie zostanie skompilowany, dopóki nie dodasz odwołania do podzespołu
Microsoft.CSharp, jak pokazano w poprzednim rozdziale.
Wyróżnianie bieżącej kategorii
Obecnie nie informujemy użytkowników, która kategoria jest przeglądana. Być może klient może wywnioskować
to z elementów na liście, ale lepiej w sposób jasny pokazać to w interfejsie. Możemy zrealizować to zadanie
przez utworzenie modelu widoku, który zawiera listę kategorii oraz wybraną kategorię — i zazwyczaj tak się to
robi. Jednak zamiast tego pokażemy mechanizm View Bag, wspomniany w rozdziale 2. Mechanizm ten
pozwala na przekazywanie danych z kontrolera do widoku bez użycia modelu. Na listingu 8.9 są
zamieszczone zmiany w metodzie akcji Menu kontrolera Nav.
Listing 8.9. Użycie mechanizmu View Bag w pliku NavController.cs
using
using
using
using
System.Collections.Generic;
System.Web.Mvc;
SportsStore.Domain.Abstract;
System.Linq;
namespace SportsStore.WebUI.Controllers {
public class NavController : Controller {
private IProductRepository repository;
212
ROZDZIAŁ 8.  SPORTSSTORE — NAWIGACJA
public NavController(IProductRepository repo) {
repository = repo;
}
public PartialViewResult Menu(string category = null) {
ViewBag.SelectedCategory = category;
IEnumerable<string> categories = repository.Products
.Select(x => x.Category)
.Distinct()
.OrderBy(x => x);
return PartialView(categories);
}
}
}
Do metody akcji Menu dodaliśmy parametr o nazwie category. Wartość tego parametru będzie przekazywana
automatycznie przez konfigurację routingu. Wewnątrz metody dynamicznie tworzymy właściwość
SelectedCategory w obiekcie ViewBag i przypisujemy do niej wartość parametru. W rozdziale 2. wyjaśniłem, że
ViewBag jest obiektem dynamicznym i że możemy tworzyć nowe właściwości przez przypisanie do nich wartości.
Teraz, gdy mamy informacje na temat wybranej kategorii, możemy zaktualizować widok i wykorzystać
ją, jak również dodać klasę CSS do łącza elementu reprezentującego wybraną kategorię. Na listingu 8.10 pokazane
są zmiany w pliku Menu.cshtml.
Listing 8.10. Wyróżnianie bieżącej kategorii zaimplementowane w pliku Menu.cshtml
@model IEnumerable<string>
@Html.ActionLink("Home", "List", "Product", null,
new { @class = "btn btn-block btn-default btn-lg" })
@foreach (var link in Model) {
@Html.RouteLink(link,
new {
controller = "Product",
action = "List",
category = link,
page = 1
}, new {
@class = "btn btn-block btn-default btn-lg"
+ (link == ViewBag.SelectedCategory ? " btn-primary" : "")
})
}
Zmiana jest prosta. Jeżeli wartość bieżąca link zostanie dopasowana do wartości SelectedCategory,
wówczas tworzony element dodajemy do innej klasy Bootstrap, która spowoduje wyróżnienie danego
przycisku. Po uruchomieniu aplikacji zobaczymy efekt wyróżnienia kategorii pokazany na rysunku 8.4.
Poprawianie licznika stron
Ostatnim elementem do wykonania jest poprawienie łączy stron, aby działały prawidłowo po wybraniu kategorii.
Obecnie liczba stron jest określana przez całkowitą liczbę produktów, a nie liczbę produktów w wybranej kategorii.
Powoduje to, że klient może kliknąć łącze do strony nr 2 w kategorii Szachy i otrzyma pustą stronę, ponieważ
nie ma wystarczająco dużo produktów, aby trafiły na drugą stronę. Ten sposób działania jest przedstawiony
na rysunku 8.5.
213
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 8.4. Wyróżnianie bieżącej kategorii
Rysunek 8.5. Wyświetlanie niewłaściwych łączy stron po wybraniu kategorii
Możemy to poprawić, modyfikując metodę akcji List w kontrolerze Product, aby przy generowaniu danych
dotyczących stronicowania były brane pod uwagę kategorię. Wymagane zmiany są zamieszczone na listingu 8.11.
Listing 8.11. Tworzenie w pliku ProductController.cs danych stronicowania uwzględniających kategorie
...
public ViewResult List(string category, int page = 1) {
ProductsListViewModel viewModel = new ProductsListViewModel {
214
ROZDZIAŁ 8.  SPORTSSTORE — NAWIGACJA
Products = repository.Products
.Where(p => category == null || true : p.Category == category)
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo {
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = category == null ?
repository.Products.Count() :
repository.Products.Where(e => e.Category == category).Count()
},
CurrentCategory = category
};
return View(viewModel);
}
...
Jeżeli zostanie wybrana kategoria, zwracamy liczbę elementów w tej kategorii; jeżeli nie, zwracamy całkowitą
liczbę produktów. Teraz, gdy przeglądamy kategorię, łącza na dole strony prawidłowo odzwierciedlają
liczbę produktów w kategorii, jak pokazano na rysunku 8.6.
Rysunek 8.6. Wyświetlanie liczby stron zależnej od kategorii
215
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Test jednostkowy — zliczanie produktów w kategoriach
Test pozwalający na wygenerowanie bieżącej liczby produktów dla różnych kategorii jest bardzo prosty — tworzymy
imitację repozytorium zawierającą znane dane w różnych kategoriach, a następnie wywołujemy metodę akcji List,
żądając kolejnych kategorii. Test ten jest następujący:
...
[TestMethod]
public void Generate_Category_Specific_Product_Count() {
// przygotowanie
// — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1", Category = "Cat1"},
new Product {ProductID = 2, Name = "P2", Category = "Cat2"},
new Product {ProductID = 3, Name = "P3", Category = "Cat1"},
new Product {ProductID = 4, Name = "P4", Category = "Cat2"},
new Product {ProductID = 5, Name = "P5", Category = "Cat3"}
});
// przygotowanie — tworzenie kontrolera i ustawienie 3-elementowej strony
ProductController target = new ProductController(mock.Object);
target.PageSize = 3;
// działanie — testowanie liczby produktów dla różnych kategorii
int res1 = ((ProductsListViewModel)target.List("Cat1").Model).PagingInfo.TotalItems;
int res2 = ((ProductsListViewModel)target.List("Cat2").Model).PagingInfo.TotalItems;
int res3 = ((ProductsListViewModel)target.List("Cat3").Model).PagingInfo.TotalItems;
int resAll = ((ProductsListViewModel)target.List(null).Model).PagingInfo.TotalItems;
// asercje
Assert.AreEqual(res1, 2);
Assert.AreEqual(res2, 2);
Assert.AreEqual(res3, 1);
Assert.AreEqual(resAll, 5);
}
...
Zwróć uwagę, że wywołamy również metodę List bez określania kategorii, aby upewnić się, że uzyskamy
całkowitą liczbę elementów.
Budowanie koszyka na zakupy
Nasza aplikacja ładnie się rozwija, ale nie będziemy mogli sprzedać żadnego produktu, jeżeli nie zaimplementujemy
koszyka na zakupy. W tym podrozdziale utworzymy funkcje koszyka na zakupy przedstawione na rysunku 8.7.
Jest on znany każdemu, kto dokonywał zakupów w sieci.
Przycisk Dodaj do koszyka będzie wyświetlany obok każdego produktu w katalogu. Kliknięcie tego przycisku
spowoduje wyświetlenie podsumowania wszystkich wybranych do tej pory produktów, jak również ich całkowitej
wartości. Użytkownik będzie mógł następnie kliknąć przycisk Kontynuuj zakupy, aby wrócić do katalogu
produktów, lub Zamówienie, aby dokończyć zamawianie towarów i zakończyć sesję zakupów.
216
ROZDZIAŁ 8.  SPORTSSTORE — NAWIGACJA
Rysunek 8.7. Podstawowe działanie koszyka na zakupy
Definiowanie encji koszyka
Ponieważ koszyk na zakupy jest częścią domeny biznesowej aplikacji, sensowne jest zdefiniowanie nowej klasy
w modelu domeny — Cart. Dodaj plik klasy Cart.cs do katalogu Entities w projekcie SportsStore.Domain i użyj
go do zdefiniowania klas przedstawionych na listingu 8.12.
Listing 8.12. Klasy Cart i CartLine zdefiniowane w pliku Cart.cs
using System.Collections.Generic;
using System.Linq;
namespace SportsStore.Domain.Entities {
public class Cart {
private List<CartLine> lineCollection = new List<CartLine>();
public void AddItem(Product product, int quantity) {
CartLine line = lineCollection
.Where(p => p.Product.ProductID == product.ProductID)
.FirstOrDefault();
if (line == null) {
lineCollection.Add(new CartLine { Product = product, Quantity = quantity });
} else {
line.Quantity += quantity;
}
}
public void RemoveLine(Product product) {
lineCollection.RemoveAll(l => l.Product.ProductID == product.ProductID);
}
public decimal ComputeTotalValue() {
return lineCollection.Sum(e => e.Product.Price * e.Quantity);
}
public void Clear() {
lineCollection.Clear();
}
public IEnumerable<CartLine> Lines {
get { return lineCollection; }
}
}
217
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
public class CartLine {
public Product Product { get; set; }
public int Quantity { get; set; }
}
}
Klasa Cart korzysta ze zdefiniowanej w tym samym pliku klasy CartLine do reprezentowania produktu
wybranego przez klienta oraz ilości tego produktu. Zdefiniowaliśmy metody pozwalające na dodanie elementu
do koszyka, usunięcie poprzednio dodanego elementu z koszyka, obliczenie całkowitej wartości towarów
w koszyku oraz wyzerowanie go przez usunięcie wszystkich towarów. Udostępniliśmy również właściwość dającą
dostęp do zawartości koszyka poprzez IEnumerble<CartLine>. Są to bardzo proste metody, zaimplementowane
w języku C# z niewielką pomocą LINQ.
Test jednostkowy — testowanie koszyka
Klasa Cart jest względnie prosta, ale zawiera wiele ważnych funkcji, które muszą działać prawidłowo. Źle
funkcjonujący koszyk podważy zaufanie do całej aplikacji SportsStore. Wydzieliliśmy więc poszczególne funkcje
i przetestowaliśmy je indywidualnie. W projekcie SportsStore.UnitTest został utworzony plik testów jednostkowych
o nazwie CartTests.cs przeznaczony na testy.
Pierwsza funkcja jest związana z dodawaniem elementu do koszyka. Jeżeli dany produkt jest dodawany do koszyka
po raz pierwszy, to chcemy, aby został dodany nowy obiekt CartLine. Oto test wraz z definicją klasy testu
jednostkowego:
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SportsStore.Domain.Entities;
namespace SportsStore.UnitTests {
[TestClass]
public class CartTests {
[TestMethod]
public void Can_Add_New_Lines() {
// przygotowanie — utworzenie produktów testowych
Product p1 = new Product { ProductID = 1, Name = "P1" };
Product p2 = new Product { ProductID = 2, Name = "P2" };
// przygotowanie — utworzenie nowego koszyka
Cart target = new Cart();
// działanie
target.AddItem(p1, 1);
target.AddItem(p2, 1);
CartLine[] results = target.Lines.ToArray();
// asercje
Assert.AreEqual(results.Length, 2);
Assert.AreEqual(results[0].Product, p1);
Assert.AreEqual(results[1].Product, p2);
}
}
}
Jednak jeżeli klient dodał już dany produkt do koszyka, chcemy zwiększyć ilość w odpowiednim obiekcie CartLine,
a nie tworzyć nowy. Test ten jest następujący:
218
ROZDZIAŁ 8.  SPORTSSTORE — NAWIGACJA
...
[TestMethod]
public void Can_Add_Quantity_For_Existing_Lines() {
// przygotowanie — tworzenie produktów testowych
Product p1 = new Product { ProductID = 1, Name = "P1" };
Product p2 = new Product { ProductID = 2, Name = "P2" };
// przygotowanie — utworzenie nowego koszyka
Cart target = new Cart();
// działanie
target.AddItem(p1,
target.AddItem(p2,
target.AddItem(p1,
CartLine[] results
1);
1);
10);
= target.Lines.OrderBy(c => c.Product.ProductID).ToArray();
// asercje
Assert.AreEqual(results.Length, 2);
Assert.AreEqual(results[0].Quantity, 11);
Assert.AreEqual(results[1].Quantity, 1);
}
...
Musimy również sprawdzić, czy użytkownik może zmienić zdanie i usunąć produkt z koszyka. Funkcja ta jest
implementowana poprzez metodę RemoveLine. Test ten jest następujący:
...
[TestMethod]
public void Can_Remove_Line() {
// przygotowanie — tworzenie produktów testowych
Product p1 = new Product { ProductID = 1, Name = "P1" };
Product p2 = new Product { ProductID = 2, Name = "P2" };
Product p3 = new Product { ProductID = 3, Name = "P3" };
// przygotowanie — utworzenie nowego koszyka
Cart target = new Cart();
// przygotowanie — dodanie kilku produktów do koszyka
target.AddItem(p1, 1);
target.AddItem(p2, 3);
target.AddItem(p3, 5);
target.AddItem(p2, 1);
// działanie
target.RemoveLine(p2);
// asercje
Assert.AreEqual(target.Lines.Where(c => c.Product == p2).Count(), 0);
Assert.AreEqual(target.Lines.Count(), 2);
}
...
219
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Następną funkcją, jaką chcemy przetestować, jest możliwość obliczenia całkowitej wartości towarów w koszyku.
Poniżej pokazany jest odpowiedni test:
...
[TestMethod]
public void Calculate_Cart_Total() {
// przygotowanie — tworzenie produktów testowych
Product p1 = new Product { ProductID = 1, Name = "P1", Price = 100M};
Product p2 = new Product { ProductID = 2, Name = "P2" , Price = 50M};
// przygotowanie — utworzenie nowego koszyka
Cart target = new Cart();
// działanie
target.AddItem(p1, 1);
target.AddItem(p2, 1);
target.AddItem(p1, 3);
decimal result = target.ComputeTotalValue();
// asercje
Assert.AreEqual(result, 450M);
}
...
Ostatni test jest bardzo prosty. Chcemy upewnić się, że zawartość koszyka jest prawidłowo usuwana przy operacji
czyszczenia. Test ten jest następujący:
...
[TestMethod]
public void Can_Clear_Contents() {
// przygotowanie — tworzenie produktów testowych
Product p1 = new Product { ProductID = 1, Name = "P1", Price = 100M };
Product p2 = new Product { ProductID = 2, Name = "P2", Price = 50M };
// przygotowanie — utworzenie nowego koszyka
Cart target = new Cart();
// przygotowanie — dodanie kilku produktów do koszyka
target.AddItem(p1, 1);
target.AddItem(p2, 1);
// działanie — czyszczenie koszyka
target.Clear();
// asercje
Assert.AreEqual(target.Lines.Count(), 0);
}
...
Czasami, tak jak w tym przypadku, kod wymagany do przetestowania działania klasy jest znacznie dłuższy
i bardziej skomplikowany niż kod samej klasy. Jednak nie powinieneś przez to zaprzestać tworzenia testów
jednostkowych. Usterki w prostych klasach, szczególnie tych, które odgrywają ważną rolę, tak jak Cart w naszym
przykładzie, mogą mieć bardzo negatywny wpływ na aplikację.
220
ROZDZIAŁ 8.  SPORTSSTORE — NAWIGACJA
Tworzenie przycisków koszyka
Musimy teraz zmienić widok Views/Shared/ProductSummary.cshtml, aby dodać przyciski do listy
produktów. Zmiany te są pokazane na listingu 8.13.
Listing 8.13. Dodawanie przycisków do widoku zdefiniowanego w pliku ProductSummary.cshtml
@model SportsStore.Domain.Entities.Product
<div class="well">
<h3>
<strong>@Model.Name</strong>
<span class="pull-right label label-primary">@Model.Price.ToString("c")</span>
</h3>
@using(Html.BeginForm("AddToCart", "Cart")) {
<div class="pull-right">
@Html.HiddenFor(x => x.ProductID)
@Html.Hidden("returnUrl", Request.Url.PathAndQuery)
<input type="submit" class="btn btn-success" value="Dodaj do koszyka" />
</div>
}
<span class="lead">@Model.Description</span>
</div>
Dodaliśmy tu blok kodu Razor, który tworzy mały formularz HTML dla każdego produktu z listy.
Gdy zostaną wysłane dane formularza, spowodują wywołanie metody akcji AddToCart w kontrolerze Cart
(zaimplementujemy go w następnym kroku).
 Uwaga Domyślnie metoda pomocnicza BeginForm tworzy formularz korzystający z metody HTTP POST. Można
to zmienić tak, aby używał metody GET, ale należy przy tym zachować ostrożność. Specyfikacja HTTP wymaga,
aby żądania GET były powtarzalne, czyli nie mogą powodować zmiany czegokolwiek, a dodawanie produktów
do koszyka w sposób oczywisty zmienia koszyk. Więcej na ten temat napiszę w rozdziale 16.; wyjaśnię przy tym,
co się może stać, gdy zignorujemy zasadę powtarzalności żądań GET.
Tworzenie wielu formularzy HTML na stronie
Użycie metody pomocniczej Html.BeginForm w każdej kontrolce danych produktu powoduje, że każdy przycisk
Dodaj do koszyka jest umieszczany w osobnym elemencie HTML form. Może to być zaskoczenie dla osób korzystających
z ASP.NET Web Forms, gdzie wymuszone jest ograniczenie do jednego formularza na stronie, jeżeli chcesz użyć
funkcji widoku stanu lub skomplikowanych kontrolek (które opierają się na widoku stanu). Ponieważ ASP.NET MVC
nie korzysta z widoku stanu, więc nie ogranicza liczby formularzy na stronie i można utworzyć ich dowolnie dużo.
Nie ma technicznego wymagania, aby tworzyć formularz dla każdego przycisku. Jednak każdy formularz
wysyła dane do tej samej metody kontrolera, ale z innym zbiorem parametrów — jest to zatem bardzo prosty
sposób na obsłużenie kliknięcia przycisku.
221
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Implementowanie kontrolera koszyka
Do obsługi kliknięć przycisków Dodaj do koszyka potrzebny jest nam kontroler. W projekcie SportsStore.WebUI
utwórz nowy kontroler o nazwie CartController i umieść w nim kod przedstawiony na listingu 8.14.
Listing 8.14. Zawartość pliku CartController.cs
using
using
using
using
System.Linq;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Controllers {
public class CartController : Controller {
private IProductRepository repository;
public CartController(IProductRepository repo) {
repository = repo;
}
public RedirectToRouteResult AddToCart(int productId, string returnUrl) {
Product product = repository.Products
.FirstOrDefault(p => p.ProductID == productId);
if (product != null) {
GetCart().AddItem(product, 1);
}
return RedirectToAction("Index", new { returnUrl });
}
public RedirectToRouteResult RemoveFromCart(int productId, string returnUrl) {
Product product = repository.Products
.FirstOrDefault(p => p.ProductID == productId);
if (product != null) {
GetCart().RemoveLine(product);
}
return RedirectToAction("Index", new { returnUrl });
}
private Cart GetCart() {
Cart cart = (Cart)Session["Cart"];
if (cart == null) {
cart = new Cart();
Session["Cart"] = cart;
}
return cart;
}
}
}
Warto wspomnieć o kilku punktach dotyczących tego kontrolera. Otóż do przechowywania i pobierania
obiektów Cart zastosowaliśmy mechanizm stanu sesji z ASP.NET. Jest to zadanie metody GetCart. ASP.NET
zapewnia mechanizm sesji wykorzystujący dane cookies bądź modyfikacje adresu URL do skojarzenia żądań
danego użytkownika, stanowiących jedną sesję przeglądania. Związany jest z tym mechanizm stanu sesji, który
pozwala kojarzyć dane z sesją. Idealnie nadaje się to dla naszej klasy Cart. Chcemy, aby każdy użytkownik miał
własny koszyk, który będzie zachowywany pomiędzy żądaniami. Dane skojarzone z sesją są usuwane po jej
222
ROZDZIAŁ 8.  SPORTSSTORE — NAWIGACJA
wygaśnięciu (zazwyczaj, gdy użytkownik nie wykonuje żadnych żądań przez pewien czas), dzięki czemu nie
musimy zajmować się zarządzaniem cyklem życia obiektów Cart. Aby dodać obiekt do stanu sesji, ustawiamy
wartość dla wybranego klucza w obiekcie Session:
...
Session["Cart"] = cart;
...
Aby odczytać obiekt, po prostu odczytujemy ten sam klucz w następujący sposób:
...
Cart cart = (Cart)Session["Cart"];
...
 Wskazówka Obiekty stanu sesji w domyślnej konfiguracji są przechowywane w pamięci serwera ASP.NET, ale można
skonfigurować kilka różnych strategii przechowywania, w tym użycie bazy danych SQL. Więcej informacji na ten
temat znajdziesz w innej mojej książce, zatytułowanej Pro ASP.NET MVC 5 Platform, wydanej przez Apress.
W przypadku metod AddToCart oraz RemoveFromCart korzystamy z nazw parametrów odpowiadających
elementom <input> w formularzach HTML użytych w widoku ProductSummary.cshtml. Pozwala to platformie
MVC skojarzyć przychodzące zmienne POST z tymi parametrami, dzięki czemu nie musimy ich przetwarzać
ręcznie.
Wyświetlanie zawartości koszyka
Warto jeszcze zwrócić uwagę, że w kontrolerze Cart metody AddToCart oraz RemoveFromCart wywołują metodę
RedirectToAction. Wskutek tego wysyłane jest polecenie przekierowania HTTP do przeglądarki klienta powodujące
wysłanie przez przeglądarkę żądania nowego adresu URL. W tym przypadku chcemy, aby przeglądarka użyła
żądania URL wywołującego metodę akcji Index w kontrolerze Cart.
Zaimplementujemy teraz metodę Index i wykorzystamy ją do wyświetlenia zawartości koszyka. Jeżeli wrócisz
do rysunku 8.7, zauważysz, że jest to trasa używana po kliknięciu przez użytkownika przycisku Dodaj do
koszyka.
Aby wyświetlić zawartość koszyka, musimy przekazać do widoku dwie dane — obiekt Cart oraz URL
do wyświetlenia, gdy użytkownik kliknie przycisk Kontynuuj zakupy. W tym celu utworzymy prosty model widoku.
W projekcie SportsStore.WebUI utwórz w katalogu Models nową klasę — CartIndexViewModel. Kod tej klasy
jest przedstawiony na listingu 8.15.
Listing 8.15. Zawartość pliku CartIndexViewModel.cs
using SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Models {
public class CartIndexViewModel {
public Cart Cart { get; set; }
public string ReturnUrl { get; set; }
}
}
Teraz, gdy mamy już model widoku, możemy zaimplementować metodę akcji Index w klasie kontrolera
Cart w sposób pokazany na listingu 8.16.
Listing 8.16. Metoda akcji Index zaimplementowana w pliku CartController.cs
using System.Linq;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
223
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers {
public class CartController : Controller {
private IProductRepository repository;
public CartController(IProductRepository repo) {
repository = repo;
}
public ViewResult Index(string returnUrl) {
return View(new CartIndexViewModel {
Cart = GetCart(),
ReturnUrl = returnUrl
});
}
// …inne metody akcji zostały pominięte w celu zachowania zwięzłości…
}
}
Ostatnim krokiem jest wyświetlenie nowego widoku z zawartością koszyka. Kliknij prawym przyciskiem
myszy metodę Index i wybierz Dodaj widok… z menu kontekstowego. Kliknij przycisk OK w celu utworzenia
pliku Index.cshtml, po czym zmień jego zawartość, aby odpowiadała tej z listingu 8.17.
Listing 8.17. Zawartość pliku Index.cshtml
@model SportsStore.WebUI.Models.CartIndexViewModel
@{
ViewBag.Title = "Sklep sportowy: Twój koszyk";
}
<h2>Twój koszyk</h2>
<table class="table">
<thead>
<tr>
<th>Ilość</th>
<th>Produkt</th>
<th class="text-right">Cena</th>
<th class="text-right">Wartość</th>
</tr>
</thead>
<tbody>
@foreach(var line in Model.Cart.Lines) {
<tr>
<td class="text-center">@line.Quantity</td>
<td class="text-left">@line.Product.Name</td>
<td class="text-right">@line.Product.Price.ToString("c")</td>
<td class="text-right">@((line.Quantity * line.Product.Price).ToString("c"))</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-right">Razem:</td>
<td class="text-right">
@Model.Cart.ComputeTotalValue().ToString("c")
224
ROZDZIAŁ 8.  SPORTSSTORE — NAWIGACJA
</td>
</tr>
</tfoot>
</table>
<div class="text-center">
<a class="btn btn-primary" href="@Model.ReturnUrl">Kontynuuj zakupy</a>
</div>
Pozycje koszyka są przeglądane i dla każdego produktu następuje dodanie wiersza do tabeli HTML
razem z wartością każdej z pozycji oraz całkowitą wartością koszyka. Klasy przypisywane elementom
odpowiadają stylom Bootstrap dla tabel i wyrównania tekstu. Mamy gotowe podstawowe funkcje koszyka
na zakupy. Po pierwsze, produkty są wyszczególnione wraz z przyciskiem pozwalającym na dodanie danego
produktu do koszyka, jak pokazano na rysunku 8.8.
Rysunek 8.8. Przycisk pozwalający na dodanie produktu do koszyka
225
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Po drugie, po kliknięciu przycisku Dodaj do koszyka odpowiedni produkt zostanie dodany do koszyka
i wyświetli się podsumowanie zawartości koszyka pokazane na rysunku 8.9. Możemy następnie kliknąć
przycisk Kontynuuj zakupy i wrócić do strony produktu, z której tu trafiliśmy — jest to bardzo sprytne
i przyjemne rozwiązanie.
Rysunek 8.9. Wyświetlanie zawartości koszyka na zakupy
Podsumowanie
W tym rozdziale zaczęliśmy dodawać do aplikacji SportsStore funkcje użytkownika. Po wprowadzonych
zmianach użytkownik może przeglądać produkty wedle kategorii, a także umieszczać produkty w koszyku
na zakupy. Przed nami jeszcze sporo pracy, którą będziemy kontynuować w kolejnym rozdziale.
226
ROZDZIAŁ 9.

SportsStore
— ukończenie koszyka na zakupy
W tym rozdziale będziemy kontynuowali budowę przykładowej aplikacji SportsStore. W poprzednim
rozdziale zaimplementowaliśmy w niej podstawową obsługę koszyka na zakupy, a teraz usprawnimy go
i dodamy pozostałe funkcje.
Użycie dołączania danych
Platforma MVC korzysta z systemu nazywanego dołączaniem modelu, który pozwala tworzyć obiekty C#
na podstawie żądań HTTP w celu przekazywania ich jako wartości parametrów metod akcji. W ten sposób
MVC przetwarza na przykład formularze. Platforma sprawdza parametry wywołanej metody akcji i korzysta
z łącznika modelu w celu uzyskania wartości przekazanych przez przeglądarkę internetową, a następnie
skonwertowania ich na typ parametru o tej samej nazwie przed przekazaniem metodzie akcji.
Łączniki modelu mogą tworzyć obiekty C# na podstawie dowolnych danych dostępnych w żądaniu.
Jest to jedna z najważniejszych funkcji platformy MVC. Utworzymy teraz własny łącznik modelu pozwalający
ulepszyć klasę CartController.
Bardzo lubię rozwiązanie oparte na funkcji stanu sesji zastosowanej w kontrolerze koszyka do przechowywania
i zarządzania obiektami Cart, które przygotowaliśmy w rozdziale 8., ale naprawdę nie podoba mi się sposób,
w jaki musieliśmy z niej skorzystać. Nie pasuje on do reszty modelu naszej aplikacji bazującego na parametrach
metod akcji. Nie możemy prawidłowo testować klasy CartController, chyba że zapewnimy imitację parametru
Session w klasie bazowej, a to oznacza imitowanie klasy Controller i całej masy innych elementów, którymi
nie chcemy się zajmować.
Aby rozwiązać ten problem, utworzymy własny łącznik modelu, który będzie pozyskiwał obiekty Cart
znajdujące się w danych sesji. Platforma MVC będzie następnie w stanie utworzyć obiekty Cart i przekazywać
je jako parametry do metod akcji w naszej klasie CartController. Mechanizm dołączania modelu jest bardzo
efektywny i elastyczny. Przedstawię go szczegółowo w rozdziale 24., ale ten przykład pozwoli nam rozpocząć.
Tworzenie własnego łącznika modelu
Własny łącznik modelu tworzymy przez zaimplementowanie interfejsu System.Web.Mvc.IModelBinder.
W projekcie SportsStore.WebUI w katalogu Infrastructure utwórz nowy podkatalog o nazwie Binders i utwórz
w nim plik klasy CartModelBinder.cs. Na listingu 9.1 przedstawiona jest implementacja tej klasy.
Listing 9.1. Zawartość pliku CartModelBinder.cs
using System.Web.Mvc;
using SportsStore.Domain.Entities;
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
namespace SportsStore.WebUI.Infrastructure.Binders {
public class CartModelBinder : IModelBinder {
private const string sessionKey = "Cart";
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext) {
// pobranie obiektu Cart z sesji
Cart cart = null;
if (controllerContext.HttpContext.Session != null) {
cart = (Cart)controllerContext.HttpContext.Session[sessionKey];
}
// utworzenie obiektu Cart, jeżeli nie został znaleziony w danych sesji
if (cart == null) {
cart = new Cart();
if (controllerContext.HttpContext.Session != null) {
controllerContext.HttpContext.Session[sessionKey] = cart;
}
}
// zwróć koszyk
return cart;
}
}
}
Interfejs IModelBinder definiuje jedną metodę: BindModel. Dwa dostarczane parametry pozwalają na tworzenie
obiektów modelu domeny. Parametr ControllerContext zapewnia dostęp do wszystkich danych z klasy
kontrolera, w tym informacje na temat żądania klienta. Parametr ModelBindingContext dostarcza danych na temat
modelu obiektów, jakie budujemy, oraz zapewnia narzędzia ułatwiające to zadanie.
Ze względu na nasze cele interesująca jest klasa ControllerContext. Posiada ona właściwość HttpContext,
która z kolei zawiera właściwość Session, pozwalającą nam odczytać i zmieniać dane sesji. Obiekt Cart uzyskujemy
przez odczyt wartości klucza z danych sesji, a jeżeli nie ma tam tego obiektu, tworzymy go.
Musimy teraz poinformować platformę MVC, aby przy tworzeniu obiektów Cart używana była nasza klasa
CartModelBinder. Wykonany to w metodzie Application_Start z pliku Global.asax w sposób pokazany
na listingu 9.2.
Listing 9.2. Rejestrowanie klasy CartModelBinder w pliku Global.asax.cs
using
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
SportsStore.Domain.Entities;
SportsStore.WebUI.Infrastructure.Binders;
namespace SportsStore.WebUI {
public class MvcApplication : System.Web.HttpApplication {
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());
}
}
}
228
ROZDZIAŁ 9.  SPORTSSTORE — UKOŃCZENIE KOSZYKA NA ZAKUPY
Teraz możemy zmienić klasę Cart i usunąć metodę GetCart. Od tego momentu korzystamy z naszego
łącznika modelu, który platforma MVC będzie stosować automatycznie. Zmiany są pokazane na listingu 9.3.
Listing 9.3. Wykorzystanie łącznika modelu w pliku CartController.cs
using
using
using
using
using
System.Linq;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers {
public class CartController : Controller {
private IProductRepository repository;
public CartController(IProductRepository repo) {
repository = repo;
}
public ViewResult Index(Cart cart, string returnUrl) {
return View(new CartIndexViewModel {
ReturnUrl = returnUrl,
Cart = cart
});
}
public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl) {
Product product = repository.Products
.FirstOrDefault(p => p.ProductID == productId);
if (product != null) {
cart.AddItem(product, 1);
}
return RedirectToAction("Index", new { returnUrl });
}
public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl) {
Product product = repository.Products
.FirstOrDefault(p => p.ProductID == productId);
if (product != null) {
cart.RemoveLine(product);
}
return RedirectToAction("Index", new { returnUrl });
}
}
}
Usunęliśmy metodę GetCart i dodaliśmy parametr Cart do każdej z metod akcji. Gdy platforma MVC
otrzyma żądanie, które wymaga wywołania naszej metody, na przykład AddToCart, zaczyna od sprawdzenia
parametrów metody akcji. Sprawdza ona listę dostępnych łączników i próbuje znaleźć takie, które zwracają
obiekty każdego z typów parametrów. Nasz łącznik zostanie poproszony o utworzenie obiektu Cart, co będzie
zrealizowane z użyciem mechanizmu stanu sesji. Dzięki naszemu oraz domyślnemu łącznikowi platforma
MVC jest w stanie utworzyć zestaw parametrów wymaganych do wywołania metody akcji. Możemy zatem
zrefaktoryzować kontroler w taki sposób, że nie będzie on posiadał żadnej wiedzy na temat sposobu
tworzenia obiektów Cart w momencie otrzymania żądania.
229
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Istnieje kilka zalet korzystania z tego typu niestandardowych łączników modelu. Po pierwsze, oddzielamy
logikę używaną do tworzenia obiektów Cart od kontrolera, co pozwala nam zmienić sposób przechowywania
obiektów Cart bez potrzeby modyfikowania kontrolera. Po drugie, dowolny kontroler korzystający z obiektów
Cart może je po prostu zadeklarować jako parametry metody akcji i użyć naszego łącznika modelu. Trzecia zaleta
jest według nas najważniejsza — możemy teraz tworzyć testy jednostkowe dla kontrolera koszyka bez konieczności
imitacji zbyt dużej ilości podstawowego kodu ASP.NET.
Test jednostkowy — kontroler koszyka
Możemy testować klasę CartController przez utworzenie obiektów Cart i przekazanie ich do metody akcji.
Chcemy przetestować trzy różne aspekty tego kontrolera:



jego akcja AddToCart powinna dodawać wybrany produkt do koszyka użytkownika,
po dodaniu produktu do koszyka powinniśmy być przekierowani do widoku Index,
adres URL, który użytkownik powinien wykorzystać do powrotu do tego katalogu, powinien być prawidłowo
przekazany do metody akcji Index.
Użyjemy następujących testów jednostkowych, które zostały dodane do pliku CartTest.cs w projekcie
SportsStore.UnitTests:
using
using
using
using
using
using
using
using
using
System;
Microsoft.VisualStudio.TestTools.UnitTesting;
SportsStore.Domain.Entities;
System.Linq;
Moq;
SportsStore.Domain.Abstract;
SportsStore.WebUI.Controllers;
System.Web.Mvc;
SportsStore.WebUI.Models;
namespace SportsStore.UnitTests {
[TestClass]
public class CartTests {
//…istniejące metody testowe zostały pominięte w celu zachowania zwięzłości…
[TestMethod]
public void Can_Add_To_Cart() {
// przygotowanie — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1", Category = "Jab"},
}.AsQueryable());
// przygotowanie — utworzenie koszyka
Cart cart = new Cart();
// przygotowanie — utworzenie kontrolera
CartController target = new CartController(mock.Object);
// działanie — dodanie produktu do koszyka
target.AddToCart(cart, 1, null);
230
ROZDZIAŁ 9.  SPORTSSTORE — UKOŃCZENIE KOSZYKA NA ZAKUPY
}
// asercje
Assert.AreEqual(cart.Lines.Count(), 1);
Assert.AreEqual(cart.Lines.ToArray()[0].Product.ProductID, 1);
[TestMethod]
public void Adding_Product_To_Cart_Goes_To_Cart_Screen() {
// przygotowanie — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1", Category = "Jabłka"},
}.AsQueryable());
// przygotowanie — utworzenie koszyka
Cart cart = new Cart();
// przygotowanie — utworzenie kontrolera
CartController target = new CartController(mock.Object);
// działanie — dodanie produktu do koszyka
RedirectToRouteResult result = target.AddToCart(cart, 2, "myUrl");
}
// asercje
Assert.AreEqual(result.RouteValues["action"], "Index");
Assert.AreEqual(result.RouteValues["returnUrl"], "myUrl");
[TestMethod]
public void Can_View_Cart_Contents() {
// przygotowanie — utworzenie koszyka
Cart cart = new Cart();
// przygotowanie — utworzenie kontrolera
CartController target = new CartController(null);
// działanie — wywołanie metody akcji Index
CartIndexViewModel result
= (CartIndexViewModel)target.Index(cart, "myUrl").ViewData.Model;
}
}
// asercje
Assert.AreSame(result.Cart, cart);
Assert.AreEqual(result.ReturnUrl, "myUrl");
}
Kończenie budowania koszyka
Teraz, po utworzeniu własnego łącznika modelu, czas na zakończenie budowy koszyka na zakupy przez
dodanie dwóch nowych funkcji koszyka. Pierwsza będzie pozwalała klientom na usuwanie towarów z koszyka.
Drugą funkcją będzie wyświetlenie podsumowania koszyka na górze strony.
231
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Usuwanie produktów z koszyka
Mamy już zdefiniowaną i przetestowaną metodę RemoveFromCart w kontrolerze, więc zapewnienie klientom
możliwości usuwania produktów jest kwestią udostępnienia tej metody w widoku, co zrealizujemy przez dodanie
przycisku Usuń w każdym wierszu podsumowania koszyka. Zmiany wprowadzone w pliku Views/Cart/Index.cshtml
są pokazane na listingu 9.4.
Listing 9.4. Dodawanie przycisku usuwania w pliku Index.cshtml
@model SportsStore.WebUI.Models.CartIndexViewModel
@{
}
ViewBag.Title = "Sklep sportowy: Twój koszyk";
<style>
#cartTable td { vertical-align: middle; }
</style>
<h2>Twój koszyk</h2>
<table id="cartTable" class="table">
<thead>
<tr>
<th>Ilość</th>
<th>Produkt</th>
<th class="text-right">Cena</th>
<th class="text-right">Wartość</th>
</tr>
</thead>
<tbody>
@foreach (var line in Model.Cart.Lines)
{
<tr>
<td class="text-center">@line.Quantity</td>
<td class="text-left">@line.Product.Name</td>
<td class="text-right">@line.Product.Price.ToString("c")</td>
<td class="text-right">@((line.Quantity * line.Product.Price).ToString("c"))</td>
<td>
@using (Html.BeginForm("RemoveFromCart", "Cart")) {
@Html.Hidden("ProductId", line.Product.ProductID)
@Html.HiddenFor(x => x.ReturnUrl)
<input class="btn btn-sm btn-warning" type="submit" value="Usuń" />
}
</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-right">Razem:</td>
<td class="text-right">
@Model.Cart.ComputeTotalValue().ToString("c")
</td>
</tr>
</tfoot>
</table>
<div class="text-center">
<a class="btn btn-primary" href="@Model.ReturnUrl">Kontynuuj zakupy</a>
</div>
232
ROZDZIAŁ 9.  SPORTSSTORE — UKOŃCZENIE KOSZYKA NA ZAKUPY
Do każdego wiersza tabeli zawierającego elementy <form> i <input> dodaliśmy nową kolumnę. Nadany
styl Bootstrap powoduje, że element <input> jest przyciskiem. Natomiast element <style> oraz atrybut id
w elemencie <table> gwarantują prawidłowe umieszczenie przycisku i zawartości pozostałych kolumn.
 Uwaga W kodzie zastosowaliśmy silnie typowaną metodę pomocniczą Html.HiddenFor do utworzenia pola
ukrytego dla właściwości modelu ReturnUrl. Konieczne okazało się użycie również korzystającej z literałów
znakowych metody Html.Hidden w celu utworzenia takiego samego pola dla ProductID. Jeżeli użyjemy
wywołania Html.HiddenFor(x => line.Product.ProductID), metoda utworzy pole ukryte o nazwie
line.Product.ProductID. Nazwa pola nie będzie pasowała do nazwy parametru metody akcji
CartController.RemoveFromCart, co uniemożliwi działanie domyślnego łącznika modelu i platforma MVC
nie będzie w stanie wywołać metody.
Możesz teraz sprawdzić, czy przyciski Usuń działają, uruchamiając aplikację i dodając kilka produktów do
koszyka. Pamiętaj, że koszyk posiada już funkcjonalność pozwalającą na usuwanie produktów przez kliknięcie
jednego z nowo dodanych przycisków, jak pokazano na rysunku 9.1.
Rysunek 9.1. Usuwanie towarów z koszyka na zakupy
Dodawanie podsumowania koszyka
Mamy już działający koszyk, ale nie jest on prawidłowo zintegrowany z interfejsem. Klienci mogą sprawdzić
zawartość koszyka tylko przez wejście do ekranu podsumowania. Wejście do ekranu podsumowania jest możliwe
wyłącznie przez dodanie nowego produktu do koszyka.
233
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Aby rozwiązać ten problem, dodamy kontrolkę ze skrótem do podsumowania koszyka, której kliknięcie
umożliwi wyświetlenie całej zawartości koszyka. Zrealizujemy to podobnie jak wtedy, gdy dodawaliśmy
kontrolkę nawigacji — jako akcję, której wynik będzie dodany do układu Razor. Na początek musimy dodać
do klasy CartController prostą metodę zamieszczoną na listingu 9.5.
Listing 9.5. Dodanie do pliku CartController.cs metody podsumowania do kontrolera koszyka
using
using
using
using
using
System.Linq;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers {
public class CartController : Controller {
private IProductRepository repository;
public CartController(IProductRepository repo) {
repository = repo;
}
//…istniejące metody zostały pominięte w celu zachowania zwięzłości…
public PartialViewResult Summary(Cart cart) {
return PartialView(cart);
}
}
}
Jak widać, metoda ta jest bardzo prosta. Powoduje ona wygenerowanie widoku i przekazanie do niego
bieżącego obiektu Cart (który zostanie uzyskany za pomocą naszego łącznika modelu). W celu utworzenia
widoku kliknij prawym przyciskiem myszy metodę Summary i wybierz Dodaj widok… z menu kontekstowego.
Jako nazwę widoku podaj Summary, a następnie kliknij przycisk OK, aby utworzyć plik
Views/Cart/Summary.cshtml. Umieść w nowym widoku zawartość listingu 9.6.
Listing 9.6. Zawartość pliku Summary.cshtml
@model SportsStore.Domain.Entities.Cart
<div class="navbar-right">
@Html.ActionLink("Zamów", "Index", "Cart",
new { returnUrl = Request.Url.PathAndQuery },
new { @class = "btn btn-default navbar-btn" })
</div>
<div class="navbar-text navbar-right">
<b>Twój koszyk:</b>
@Model.Lines.Sum(x => x.Quantity) sztuk,
@Model.ComputeTotalValue().ToString("c")
</div>
Jest to prosty widok pozwalający na wyświetlenie liczby produktów w koszyku, całkowitej wartości tych
produktów oraz łącza umożliwiającego użytkownikowi na przejście do koszyka (jak mogłeś się spodziewać,
elementom widoku przypisano klasy definiowane przez Bootstrap). Skoro mamy przygotowany widok
zwracany przez metodę akcji Summary, możemy teraz wywoływać metodę akcji Summary w pliku układu
_Layout.cshtml w celu wyświetlenia koszyka, jak pokazano na listingu 9.7.
234
ROZDZIAŁ 9.  SPORTSSTORE — UKOŃCZENIE KOSZYKA NA ZAKUPY
Listing 9.7. Dodanie widoku częściowego z podsumowaniem koszyka do pliku _Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<title>@ViewBag.Title</title>
</head>
<body>
<div class="navbar navbar-inverse" role="navigation">
<a class="navbar-brand" href="#">Sklep sportowy</a>
@Html.Action("Summary", "Cart")
</div>
<div class="row panel">
<div id="categories" class="col-xs-3">
@Html.Action("Menu", "Nav")
</div>
<div class="col-xs-8">
@RenderBody()
</div>
</div>
</body>
</html>
Teraz możemy zobaczyć podsumowanie koszyka po uruchomieniu aplikacji. Po dodaniu kolejnych produktów
do koszyka liczba i całkowita wartość będą się zwiększać, jak pokazano na rysunku 9.2.
Rysunek 9.2. Kontrolka podsumowania koszyka
Gdy wprowadzimy tę poprawkę, nasi klienci będą wiedzieli, co znajduje się w ich koszyku, jak również
zapewnimy im oczywisty sposób na przejście do etapu kończenia zakupów. Kolejny raz pokazaliśmy, jak łatwo
można użyć metody pomocniczej Html.Action do wstawienia w innym widoku odpowiednich danych wyjściowych
metody akcji. Jest to świetna technika pozwalająca dzielić funkcje aplikacji na osobne bloki do wielokrotnego
wykorzystania.
235
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Składanie zamówień
Doszliśmy do ostatniej funkcji niezbędnej użytkownikowi aplikacji SportsStore — możliwości złożenia zamówienia.
W kolejnych punktach rozszerzymy model domeny w celu zapewnienia rejestracji danych do wysyłki wpisywanych
przez użytkownika oraz dodania funkcji pozwalających na przetwarzanie tych danych.
Rozszerzanie modelu domeny
Dodaj plik klasy ShippingDetails.cs do katalogu Entities w projekcie SportsStore.Domain. Klasa ta będzie używana
do reprezentowania danych do wysyłki podawanych przez klienta. Jej zawartość jest pokazana na listingu 9.8.
Listing 9.8. Zawartość pliku ShippingDetails.cs
using System.ComponentModel.DataAnnotations;
namespace SportsStore.Domain.Entities {
public class ShippingDetails
{
[Required(ErrorMessage = "Proszę podać nazwisko.")]
public string Name { get; set; }
[Required(ErrorMessage = "Proszę podać pierwszy wiersz adresu.")]
public string Line1 { get; set; }
public string Line2 { get; set; }
public string Line3 { get; set; }
[Required(ErrorMessage = "Proszę podać nazwę miasta.")]
public string City { get; set; }
[Required(ErrorMessage = "Proszę podać nazwę województwa")]
public string State { get; set; }
public string Zip { get; set; }
[Required(ErrorMessage = "Proszę podać nazwę kraju.")]
public string Country { get; set; }
public bool GiftWrap { get; set; }
}
}
Jak możesz zauważyć, na listingu 9.8 wykorzystaliśmy atrybuty kontroli poprawności z przestrzeni nazw
System.ComponentModel.DataAnnotations, podobnie jak w przykładzie z rozdziału 2. Więcej informacji
na temat kontroli poprawności można znaleźć w rozdziale 25.
 Uwaga Klasa ShippingDetails nie zawiera żadnych funkcji, więc nie ma tu nic, co można sensownie sprawdzić
za pomocą testów jednostkowych.
Dodawanie procesu składania zamówienia
Naszym celem jest osiągnięcie punktu, w którym użytkownicy będą w stanie podać dane do wysyłki i złożyć
zamówienie. Na początek dodamy przycisk Złóż zamówienie do widoku podsumowania koszyka. Na listingu 9.9
zamieszczona jest zmiana, jaką trzeba wprowadzić do pliku Views/Cart/Index.cshtml.
236
ROZDZIAŁ 9.  SPORTSSTORE — UKOŃCZENIE KOSZYKA NA ZAKUPY
Listing 9.9. Dodanie w pliku Index.cshtml przycisku Złóż zamówienie
...
<div class="text-center">
<a class="btn btn-primary" href="@Model.ReturnUrl">Kontynnuj zakupy</a>
@Html.ActionLink("Złóż zamówienie", "Checkout", null, new { @class = "btn btn-primary"})
</div>
...
Ta jedna zmiana powoduje wygenerowanie łącza, po którego kliknięciu wywoływana jest metoda Checkout
z kontrolera koszyka. Wygląd tego przycisku jest przedstawiony na rysunku 9.3.
Rysunek 9.3. Przycisk zamówienia
Jak możemy oczekiwać, konieczne jest zdefiniowanie w klasie CartController metody Checkout. Jest ona
zamieszczona na listingu 9.10.
Listing 9.10. Metoda akcji Checkout zdefiniowana w pliku CartController.cs
using
using
using
using
using
System.Linq;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers {
public class CartController : Controller {
private IProductRepository repository;
public CartController(IProductRepository repo) {
repository = repo;
}
//…istniejące metody zostały pominięte w celu zachowania zwięzłości…
public ViewResult Checkout() {
return View(new ShippingDetails());
}
}
}
237
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Metoda Checkout zwraca domyślny widok oraz przekazuje nowy obiekt ShippingDetails jako model widoku.
Aby utworzyć odpowiedni widok, kliknij prawym przyciskiem myszy metodę Checkout i wybierz Dodaj widok…
z menu kontekstowego. Jako nazwę widoku podaj Checkout i kliknij przycisk OK. Visual Studio utworzy plik
Views/Cart/Checkout.cshtml, którego zawartość należy dopasować do przedstawionej na listingu 9.11.
Listing 9.11. Zawartość pliku Checkout.cshtml
@model SportsStore.Domain.Entities.ShippingDetails
@{
ViewBag.Title = "Sklep sportowy: Wysyłka";
}
<h2>Wysyłka</h2>
<p>Proszę podać swoje dane, a towar zostanie natychmiast wysłany!</p>
@using (Html.BeginForm()) {
<h3>Wysyłka dla</h3>
<div class="form-group">
<label>Nazwisko:</label>
@Html.TextBoxFor(x => x.Name, new {@class = "form-control"})
</div>
<h3>Adres</h3>
<div class="form-group">
<label>Wiersz 1:</label>
@Html.TextBoxFor(x => x.Line1, new {@class = "form-control"})
</div>
<div class="form-group">
<label>Wiersz 2:</label>
@Html.TextBoxFor(x => x.Line2, new {@class = "form-control"})
</div>
<div class="form-group">
<label>Wiersz 3:</label>
@Html.TextBoxFor(x => x.Line3, new {@class = "form-control"})
</div>
<div class="form-group">
<label>Miasto:</label>
@Html.TextBoxFor(x => x.City, new {@class = "form-control"})
</div>
<div class="form-group">
<label>Województwo:</label>
@Html.TextBoxFor(x => x.State, new {@class = "form-control"})
</div>
<div class="form-group">
<label>Kod pocztowy:</label>
@Html.TextBoxFor(x => x.Zip, new {@class = "form-control"})
</div>
<div class="form-group">
<label>Kraj:</label>
@Html.TextBoxFor(x => x.Country, new {@class = "form-control"})
</div>
<h3>Opcje</h3>
<div class="checkbox">
<label>
@Html.EditorFor(x => x.GiftWrap)
Zapakowanie jako prezent
238
ROZDZIAŁ 9.  SPORTSSTORE — UKOŃCZENIE KOSZYKA NA ZAKUPY
</label>
</div>
<div class="text-center">
<input class="btn btn-primary" type="submit" value="Zakończ zamówienie" />
</div>
}
Dla każdej właściwości w modelu utworzyliśmy elementy <label> i <input> sformatowane za pomocą
stylów Bootstrap. Aby sprawdzić, jak wygląda utworzony widok, uruchom aplikację, kliknij przycisk Zamów
na górze strony, a następnie kliknij przycisk Złóż zamówienie. Wyświetlony widok pokazano na rysunku 9.4.
(Do tego widoku możesz przejść bezpośrednio, podając adres URL /Cart/Checkout).
Rysunek 9.4. Formularz szczegółów wysyłki
Problem związany z tym widokiem to duża ilość powtarzającego się kodu znaczników. Platforma
MVC oferuje pewne metody pomocnicze HTML, które mogą pomóc w zmniejszeniu poziomu powielania kodu.
Jednak ich wadą jest trudność w nadaniu struktury i stylów dla zawartości w oczekiwany przez nas sposób.
239
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Zamiast tego wykorzystamy więc użyteczną funkcję pobrania metadanych z obiektu modelu widoku, a następnie
ich połączenia z wyrażeniami C# i Razor. Wprowadzone zmiany przedstawiono na listingu 9.12.
Listing 9.12. Zmniejszenie poziomu powielenia kodu w pliku Checkout.cshtml
@model SportsStore.Domain.Entities.ShippingDetails
@{
}
ViewBag.Title = "Sklep sportowy: Wysyłka";
<h2>Wysyłka</h2>
<p>Proszę podać swoje dane, a towar zostanie natychmiast wysłany!</p>
@using (Html.BeginForm())
{
<h3>Wysyłka dla</h3>
<div class="form-group">
<label>Nazwisko:</label>
@Html.TextBoxFor(x => x.Name, new { @class = "form-control" })
</div>
<h3>Adres</h3>
foreach (var property in ViewData.ModelMetadata.Properties) {
if (property.PropertyName != "Name" && property.PropertyName != "GiftWrap") {
<div class="form-group">
<label>@(property.DisplayName ?? property.PropertyName)</label>
@Html.TextBox(property.PropertyName, null, new {@class = "form-control"})
</div>
}
}
<h3>Opcje</h3>
<div class="checkbox">
<label>
@Html.EditorFor(x => x.GiftWrap)
Zapakuj jako prezent
</label>
</div>
}
<div class="text-center">
<input class="btn btn-primary" type="submit" value="Zakończ zamówienie" />
</div>
Wartością zwrotną statycznej właściwości ViewData.ModelMetadata jest obiekt System.Web.Mvc.ModelMetaData
dostarczający widokowi informacje o typie modelu. Właściwość Properties użyta w pętli foreach zwraca
kolekcję obiektów ModelMetaData, z których każdy przedstawia właściwość zdefiniowaną przez typ modelu.
W kodzie zastosowaliśmy właściwość PropertyName, aby mieć pewność, że nie zostanie wygenerowana
zawartość dla właściwości Name i GiftWrap (nimi zajmujemy się w innej części widoku), a wygenerowany
będzie zbiór elementów wraz z klasami Bootstrap dla wszystkich pozostałych właściwości.
 Wskazówka Słowa kluczowe for i if zostały użyte w zakresie wyrażenia Razor (czyli wyrażenia @using tworzącego
formularz) i dlatego nie trzeba poprzedzać ich znakiem @. Tak naprawdę po zastosowaniu wymienionego prefiksu
Razor zgłosi błąd. Nabycie umiejętności określenia, kiedy znaki @ są wymagane przez Razor, może zabrać trochę czasu,
ale dla większości programistów staje się to później drugą naturą. Jeżeli (podobnie jak ja) na początku masz z tym
problemy, wówczas komunikat błędu Razor wyświetlony w przeglądarce internetowej dostarczy dokładnych
informacji dotyczących sposobu usunięcia błędu.
240
ROZDZIAŁ 9.  SPORTSSTORE — UKOŃCZENIE KOSZYKA NA ZAKUPY
Na tym jednak nie koniec. Jeżeli uruchomisz aplikację i spojrzysz na dane wyjściowe wygenerowane
przez widok, to zobaczysz, że etykiety nie są całkiem prawidłowe, jak pokazano na rysunku 9.5.
Rysunek 9.5. Problem związany z generowaniem etykiet dla nazw właściwości
Problem polega na tym, że nazwy właściwości nie zawsze są odpowiednie do użycia jako etykiety.
Dlatego też podczas generowania elementów formularza warto sprawdzić dostępność wartości
DisplayName, na przykład następująco:
...
<label>@(property.DisplayName ?? property.PropertyName)</label>
...
Aby wykorzystać zalety właściwości DisplayName, konieczne jest użycie atrybutu Display w klasie
modelu, jak przedstawiono na listingu 9.13.
Listing 9.13. Użycie atrybutu Display w pliku ShippingDetails.cshtml
using System.ComponentModel.DataAnnotations;
namespace SportsStore.Domain.Entities
{
public class ShippingDetails
{
[Required(ErrorMessage = "Proszę podać nazwisko.")]
public string Name { get; set; }
[Required(ErrorMessage = "Proszę podać pierwszy wiersz adresu.")]
[Display(Name="Wiersz 1")]
public string Line1 { get; set; }
[Display(Name="Wiersz 2")]
public string Line2 { get; set; }
[Display(Name="Wiersz 3")]
public string Line3 { get; set; }
[Required(ErrorMessage = "Proszę podać nazwę miasta.")]
[Display(Name="Miasto")]
public string City { get; set; }
[Required(ErrorMessage = "Proszę podać nazwę województwa")]
[Display(Name="Województwo")]
public string State { get; set; }
[Display(Name="Kod pocztowy")]
public string Zip { get; set; }
241
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
[Required(ErrorMessage = "Proszę podać nazwę kraju.")]
[Display(Name="Kraj")]
public string Country { get; set; }
public bool GiftWrap { get; set; }
}
}
Zdefiniowanie wartości Name dla atrybutu Display pozwala na wskazanie wartości, która będzie
w widoku odczytywana przez właściwość DisplayName. Efekt wprowadzonych zmian możesz zobaczyć
po uruchomieniu aplikacji i przejściu na stronę składania zamówienia (patrz rysunek 9.6).
Rysunek 9.6. Efekt użycia atrybutu Display w typie modelu
Ten przykład pokazuje dwa różne aspekty pracy na platformie ASP.NET MVC. Pierwszy: można
znaleźć rozwiązanie w każdej sytuacji, gdy zachodzi potrzeba uproszczenia kodu znaczników lub
zwykłego kodu. Drugi: wprawdzie rola widoków we wzorcu MVC jest ograniczona do wyświetlania
danych i kodu znaczników, ale narzędzia oferowane przez Razor i C# przeznaczone do wymienionych
celów są na tyle rozbudowane i elastyczne, że pozwalają na pracę także z typami metadanych.
Implementowanie mechanizmu przetwarzania zamówień
Potrzebujemy jeszcze komponentu aplikacji, do którego będziemy mogli przekazywać szczegóły zamówienia
do przetworzenia. Aby zachować zasady modelu MVC, zdefiniujemy interfejs dla tej funkcji,
przygotujemy implementację tego interfejsu, a następnie skojarzymy ze sobą te dwa elementy przy użyciu
kontenera DI — Ninject.
Definiowanie interfejsu
Do katalogu Abstract w projekcie SportsStore.Domain dodaj nowy interfejs o nazwie IOrderProcessor i umieść
w nim kod z listingu 9.14.
Listing 9.14. Zawartość pliku IOrderProcessor.cs
using SportsStore.Domain.Entities;
namespace SportsStore.Domain.Abstract {
public interface IOrderProcessor {
void ProcessOrder(Cart cart, ShippingDetails shippingDetails);
}
}
242
ROZDZIAŁ 9.  SPORTSSTORE — UKOŃCZENIE KOSZYKA NA ZAKUPY
Implementowanie interfejsu
Nasza implementacja interfejsu IOrderProcessor będzie przetwarzała zamówienia przez ich przesłanie pocztą
elektroniczną do administratora witryny. Oczywiście upraszczamy proces sprzedaży. Większość witryn
typu e-commerce nie przesyła zamówienia pocztą elektroniczną, a w naszym przykładzie brakuje obsługi
przetwarzania kart kredytowych lub innych form płatności. Chcemy jednak skupić się na MVC, dlatego
wystarczy nam e-mail.
Do katalogu Concrete w projekcie SportsStore.Domain dodaj nowy plik klasy o nazwie EmailOrderProcessor.cs
i umieść w nim kod z listingu 9.15. Przy wysyłaniu poczty elektronicznej klasa ta korzysta z mechanizmów SMTP
dostępnych na platformie .NET.
Listing 9.15. Zawartość pliku EmailOrderProcessor.cs
using
using
using
using
using
System.Net;
System.Net.Mail;
System.Text;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
namespace SportsStore.Domain.Concrete {
public class EmailSettings {
public string MailToAddress = "[email protected]";
public string MailFromAddress = "[email protected]";
public bool UseSsl = true;
public string Username = "UżytkownikSmtp";
public string Password = "HasłoSmtp";
public string ServerName = "smtp.przyklad.pl";
public int ServerPort = 587;
public bool WriteAsFile = false;
public string FileLocation = @"c:\sports_store_emails";
}
public class EmailOrderProcessor :IOrderProcessor {
private EmailSettings emailSettings;
public EmailOrderProcessor(EmailSettings settings) {
emailSettings = settings;
}
public void ProcessOrder(Cart cart, ShippingDetails shippingInfo) {
using (var smtpClient = new SmtpClient()) {
smtpClient.EnableSsl = emailSettings.UseSsl;
smtpClient.Host = emailSettings.ServerName;
smtpClient.Port = emailSettings.ServerPort;
smtpClient.UseDefaultCredentials = false;
smtpClient.Credentials
= new NetworkCredential(emailSettings.Username, emailSettings.Password);
if (emailSettings.WriteAsFile) {
smtpClient.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
smtpClient.PickupDirectoryLocation = emailSettings.FileLocation;
smtpClient.EnableSsl = false;
}
StringBuilder body = new StringBuilder()
.AppendLine("Nowe zamówienie")
243
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
.AppendLine("---")
.AppendLine("Produkty:");
foreach (var line in cart.Lines) {
var subtotal = line.Product.Price * line.Quantity;
body.AppendFormat("{0} x {1} (wartość: {2:c}", line.Quantity,
line.Product.Name, subtotal);
}
body.AppendFormat("Wartość całkowita: {0:c}", cart.ComputeTotalValue())
.AppendLine("---")
.AppendLine("Wysyłka dla:")
.AppendLine(shippingInfo.Name)
.AppendLine(shippingInfo.Line1)
.AppendLine(shippingInfo.Line2 ?? "")
.AppendLine(shippingInfo.Line3 ?? "")
.AppendLine(shippingInfo.City)
.AppendLine(shippingInfo.State ?? "")
.AppendLine(shippingInfo.Country)
.AppendLine(shippingInfo.Zip)
.AppendLine("---")
.AppendFormat("Pakowanie prezentu: {0}", shippingInfo.GiftWrap ? "Tak" : "Nie");
MailMessage mailMessage = new MailMessage(
emailSettings.MailFromAddress, // od
emailSettings.MailToAddress, // do
"Otrzymano nowe zamówienie!", // temat
body.ToString()); // treść
if (emailSettings.WriteAsFile) {
mailMessage.BodyEncoding = Encoding.ASCII;
}
smtpClient.Send(mailMessage);
}
}
}
}
Aby uprościć kod, na listingu 9.15 zdefiniowaliśmy również klasę EmailSettings. Obiekt tej klasy zawiera
wszystkie ustawienia wymagane do skonfigurowania klas e-mail .NET i jest oczekiwany przez konstruktor
EmailOrderProcessor.
 Wskazówka Jeżeli nie masz dostępnego serwera SMTP, nie przejmuj się tym. Jeśli ustawisz wartość true
właściwości EmailSettings.WriteAsFile, wiadomości poczty elektronicznej będą zapisywane jako pliki do katalogu
zdefiniowanego we właściwości FileLocation. Katalog ten musi istnieć i mieć nadane uprawnienia do zapisu.
Pliki będą zapisane z rozszerzeniem .eml, ale można je odczytać w dowolnym edytorze tekstu. W omawianym
przykładzie wskazano katalog c:\sports_store_emails.
Rejestrowanie implementacji
Teraz, gdy mamy implementację interfejsu IOrderProcessor i mechanizm jej konfigurowania, możemy
użyć Ninject do tworzenia egzemplarzy tego interfejsu. Otwórz plik NinjectDependencyResolver.cs
z katalogu Infrastructure w projekcie SportsStore.WebUI i wprowadź do metody AddBinding zmiany
zamieszczone na listingu 9.16.
244
ROZDZIAŁ 9.  SPORTSSTORE — UKOŃCZENIE KOSZYKA NA ZAKUPY
Listing 9.16. Dodanie w pliku NinjectDependencyResolver.cs powiązań Ninject dla IOrderProcessor
using
using
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Configuration;
System.Web.Mvc;
Moq;
Ninject;
SportsStore.Domain.Abstract;
SportsStore.Domain.Concrete;
SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Infrastructure
{
public class NinjectDependencyResolver : IDependencyResolver
{
private IKernel kernel;
public NinjectDependencyResolver(IKernel kernelParam)
{
kernel = kernelParam;
AddBindings();
}
public object GetService(Type serviceType) {
return kernel.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType) {
return kernel.GetAll(serviceType);
}
private void AddBindings() {
kernel.Bind<IProductRepository>().To<EFProductRepository>();
EmailSettings emailSettings = new EmailSettings {
WriteAsFile = bool.Parse(ConfigurationManager
.AppSettings["Email.WriteAsFile"] ?? "false")
};
kernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>()
.WithConstructorArgument("settings", emailSettings);
}
}
}
Utworzyliśmy obiekt EmailSettings, który wykorzystujemy w metodzie Ninject WithConstructorArgument
w celu wstrzyknięcia go do konstruktora EmailOrderProcessor w momencie tworzenia nowego egzemplarza
w odpowiedzi na żądanie interfejsu IOrderProcessor. Na listingu 9.16 zdefiniowaliśmy wartość wyłącznie
dla jednej właściwości EmailSettings: WriteAsFile. Odczytujemy wartość tej właściwości za pomocą
ConfigurationManager.AppSettings, która pozwala odwoływać się do ustawień aplikacji umieszczonych w pliku
Web.config (tego w głównym katalogu projektu), jak pokazano na listingu 9.17.
Listing 9.17. Ustawienia aplikacji w pliku Web.config
...
<appSettings>
<add key="webpages:Version" value="3.0.0.0" />
<add key="webpages:Enabled" value="false" />
245
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
<add key="ClientValidationEnabled" value="true"/>
<add key="UnobtrusiveJavaScriptEnabled" value="true"/>
<add key="Email.WriteAsFile" value="true"/>
</appSettings>
...
Zakończenie pracy nad kontrolerem koszyka
Aby dokończyć klasę CartController, musimy zmodyfikować konstruktor w taki sposób, aby oczekiwał
implementacji interfejsu IOrderProcessor, oraz dodać nową metodę akcji, która obsłuży żądania POST wysyłane
w momencie kliknięcia przycisku Zakończ zamówienie. Obie zmiany są pokazane na listingu 9.18.
Listing 9.18. Zakończenie pracy nad kontrolerem zdefiniowanym w pliku CartController.cs
using
using
using
using
using
System.Linq;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers {
public class CartController : Controller {
private IProductRepository repository;
private IOrderProcessor orderProcessor;
public CartController(IProductRepository repo, IOrderProcessor proc) {
repository = repo;
orderProcessor = proc;
}
//…istniejące metody zostały pominięte w celu zachowania zwięzłości…
public ViewResult Checkout() {
return View(new ShippingDetails());
}
[HttpPost]
public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails) {
if (cart.Lines.Count() == 0) {
ModelState.AddModelError("", "Koszyk jest pusty!");
}
if (ModelState.IsValid) {
orderProcessor.ProcessOrder(cart, shippingDetails);
cart.Clear();
return View("Completed");
} else {
return View(shippingDetails);
}
}
}
}
246
ROZDZIAŁ 9.  SPORTSSTORE — UKOŃCZENIE KOSZYKA NA ZAKUPY
Jak można zauważyć, dodana przez nas metoda Checkout jest opatrzona atrybutem HttpPost, który powoduje,
że będzie ona wywołana wyłącznie w celu przetworzenia żądania POST — w tym przypadku w momencie przesłania
formularza przez użytkownika. Kolejny raz bazujemy na systemie łączników modelu, zarówno dla parametru
ShippingDetails (który jest tworzony automatycznie na podstawie danych formularza HTTP), jak i parametru
Cart (tworzonego z użyciem naszego własnego łącznika).
 Uwaga Zmiana w konstruktorze wymusza na nas aktualizację testów jednostkowych utworzonych dla klasy
CartController. Przekazanie null do nowego parametru konstruktora pozwoli skompilować test.
Platforma MVC sprawdza zasady kontroli poprawności zdefiniowane w klasie ShippingDetails za pomocą
atrybutów adnotacji i każde naruszenie jest przekazywane do naszej metody akcji poprzez właściwość
ModelState. Możemy sprawdzić, czy wystąpiły jakiekolwiek problemy, przez odczytanie wartości właściwości
ModelState.IsValid. Zwróć uwagę, że w przypadku braku produktów w koszyku wywołujemy metodę
ModelState.AddModelError w celu zarejestrowania komunikatu o błędzie. Sposób wyświetlania takich
komunikatów wyjaśnię wkrótce, a na temat dołączania modelu oraz kontroli poprawności więcej będzie
w rozdziałach 24. i 25.
Test jednostkowy — przetwarzanie zamówień
Aby dokończyć testowanie klasy CartController, musimy sprawdzić działanie nowej, przeciążonej wersji metody
Checkout. Choć metoda ta wygląda na krótką i prostą, zastosowanie dołączania modelu na platformie MVC
powoduje, że wiele operacji do przetestowania jest realizowanych w tle.
Powinniśmy przetwarzać zamówienie jedynie wtedy, gdy w koszyku znajdują się produkty i gdy klient dostarczył
prawidłowe dane do wysyłki. W każdym innym przypadku klient powinien zobaczyć komunikat o błędzie. Poniżej
zamieszczona jest pierwsza metoda testowa:
...
[TestMethod]
public void Cannot_Checkout_Empty_Cart() {
// przygotowanie — tworzenie imitacji procesora zamówień
Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();
// przygotowanie — tworzenie pustego koszyka
Cart cart = new Cart();
// przygotowanie — tworzenie danych do wysyłki
ShippingDetails shippingDetails = new ShippingDetails();
// przygotowanie — tworzenie egzemplarza kontrolera
CartController target = new CartController(null, mock.Object);
// działanie
ViewResult result = target.Checkout(cart, shippingDetails);
// asercje — sprawdzenie, czy zamówienie zostało przekazane do procesora
mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()),
Times.Never());
// asercje — sprawdzenie, czy metoda zwraca domyślny widok
Assert.AreEqual("", result.ViewName);
247
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
// asercje — sprawdzenie, czy przekazujemy prawidłowy model do widoku
Assert.AreEqual(false, result.ViewData.ModelState.IsValid);
}
...
Test ten zapewnia, że nie będzie można przejść do zamówienia z pustym koszykiem. Sprawdzamy to przez
upewnienie się, że metoda ProcessOrder z imitacji IOrderProcessor nie jest nigdy wywołana, metoda zwraca
domyślny widok (który ponownie wyświetla dane wprowadzone przez klienta i daje szansę na ich poprawienie)
oraz że stan modelu przekazanego do widoku jest oznaczony jako nieprawidłowy. Ten zbiór asercji może się wydawać
przesadny, ale potrzebujemy wszystkich trzech, aby mieć pewność, że nasz kod funkcjonuje prawidłowo. Następna
metoda testowa działa mniej więcej w ten sam sposób, ale wstrzykuje komunikat o błędzie do modelu widoku
w celu zasymulowania problemu raportowanego przez łącznik obiektu (co w środowisku produkcyjnym stanie się
przy wprowadzeniu niewłaściwych danych do wysyłki):
...
[TestMethod]
public void Cannot_Checkout_Invalid_ShippingDetails() {
// przygotowanie — tworzenie imitacji procesora zamówień
Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();
// przygotowanie — tworzenie koszyka z produktem
Cart cart = new Cart();
cart.AddItem(new Product(), 1);
// przygotowanie — tworzenie egzemplarza kontrolera
CartController target = new CartController(null, mock.Object);
// przygotowanie — dodanie błędu do modelu
target.ModelState.AddModelError("error", "error");
// działanie — próba zakończenia zamówienia
ViewResult result = target.Checkout(cart, new ShippingDetails());
// asercje — sprawdzenie, czy zamówienie nie zostało przekazane do procesora
mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()),
Times.Never());
// asercje — sprawdzenie, czy metoda zwraca domyślny widok
Assert.AreEqual("", result.ViewName);
// asercje — sprawdzenie, czy przekazujemy nieprawidłowy model do widoku
Assert.AreEqual(false, result.ViewData.ModelState.IsValid);
}
...
Po sprawdzeniu, że pusty koszyk lub niewłaściwe dane uniemożliwiają przetworzenie zamówienia, musimy
upewnić się, że jesteśmy w stanie przetworzyć zamówienie, gdy podane są prawidłowe dane. Test ten jest następujący:
...
[TestMethod]
public void Can_Checkout_And_Submit_Order() {
// przygotowanie — tworzenie imitacji procesora zamówień
Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();
// przygotowanie — tworzenie koszyka z produktem
Cart cart = new Cart();
cart.AddItem(new Product(), 1);
248
ROZDZIAŁ 9.  SPORTSSTORE — UKOŃCZENIE KOSZYKA NA ZAKUPY
// przygotowanie — tworzenie egzemplarza kontrolera
CartController target = new CartController(null, mock.Object);
// działanie — próba zakończenia zamówienia
ViewResult result = target.Checkout(cart, new ShippingDetails());
// asercje — sprawdzenie, czy zamówienie nie zostało przekazane do procesora
mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()),
Times.Once());
// asercje — sprawdzenie, czy metoda zwraca widok Completed
Assert.AreEqual("Completed", result.ViewName);
// asercje — sprawdzenie, czy przekazujemy prawidłowy model do widoku
Assert.AreEqual(true, result.ViewData.ModelState.IsValid);
}
...
Zwróć uwagę, że nie musimy sprawdzać, czy możemy zidentyfikować prawidłowe dane do wysyłki. Jest
to obsługiwane automatycznie przez łącznik modelu wykorzystujący atrybuty dodane do właściwości klasy
ShippingDetails.
Wyświetlanie informacji o błędach systemu kontroli poprawności
Platforma MVC użyje zdefiniowanych w klasie ShippingDetails atrybutów kontroli poprawności w celu
sprawdzenia danych wejściowych użytkownika. Jednak musimy wprowadzić kilka zmian, aby wyświetlić
użytkownikowi informacje o ewentualnych problemach. Przede wszystkim konieczne jest dostarczenie
podsumowania w przypadku wystąpienia jakichkolwiek błędów. Ma to szczególne znaczenie podczas
rozwiązywania problemów niepowiązanych z konkretnymi polami, na przykład gdy użytkownik próbuje
złożyć zamówienie, mając pusty koszyk.
W celu wyświetlenia użytecznego podsumowania dotyczącego błędów kontroli poprawności możemy
wykorzystać metodę pomocniczą Html.ValidationSummary, podobnie jak to zrobiliśmy w rozdziale 2.
Na listingu 9.19 zamieszczone są zmiany konieczne do wprowadzenia w widoku Checkout.cshtml.
Listing 9.19. Dodanie podsumowania kontroli poprawności w pliku Checkout.cshtml
...
@using (Html.BeginForm()) {
@Html.ValidationSummary()
<h3>Wysyłka dla:</h3>
<div class="form-group">
<label>Nazwisko:</label>
@Html.TextBoxFor(x => x.Name, new {@class = "form-control"})
</div>
<h3>Adres</h3>
...
Kolejnym krokiem jest utworzenie pewnych stylów CSS przeznaczonych dla klas używanych
w podsumowaniu kontroli poprawności oraz dodawanych przez platformę MVC do nieprawidłowych
elementów. W katalogu Content projektu SportsStore.WebUI tworzymy nowy plik arkusza stylów o nazwie
ErrorStyles.css i umieszczamy w nim zawartość przedstawioną na listingu 9.20. Tego samego zestawu stylów
użyliśmy już w rozdziale 2.
249
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 9.20. Zawartość pliku ErrorStyles.css
.field-validation-error
.field-validation-valid
.input-validation-error
.validation-summary-errors
.validation-summary-valid
{color: #f00;}
{ display: none;}
{ border: 1px solid #f00; background-color: #fee; }
{ font-weight: bold; color: #f00;}
{ display: none;}
W celu zastosowania nowych stylów uaktualniamy plik _Layout.cshtml i dodajemy element <link>
odpowiedzialny za wczytanie arkusza stylów ErrorStyless.css. Zmianę do wprowadzenia przedstawiono
na listingu 9.21.
Listing 9.21. Dodanie elementu <link> w pliku _Layout.cshtml
...
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<link href="~/Content/ErrorStyles.css" rel="stylesheet" />
<title>@ViewBag.Title</title>
</head>
...
Po wprowadzeniu powyższych zmian błędy wykryte w trakcie kontroli poprawności zostaną zgłoszone,
a problematyczne pola wyróżnione, jak pokazano na rysunku 9.7.
Rysunek 9.7. Wyświetlanie komunikatów kontroli poprawności
250
ROZDZIAŁ 9.  SPORTSSTORE — UKOŃCZENIE KOSZYKA NA ZAKUPY
 Wskazówka Dane wysyłane przez użytkownika do serwera są sprawdzane przed ich przetworzeniem, co nosi
nazwę weryfikacji po stronie serwera. Platforma MVC zapewnia doskonałą obsługę dla tego rodzaju weryfikacji.
Problem z weryfikacją po stronie serwera polega na tym, że użytkownik nie jest informowany o błędach aż do
chwili przekazania danych do serwera, przetworzenia ich i wygenerowania strony wynikowej. W przypadku
obciążonego serwera cały proces może zabrać nawet wiele sekund. Z tego powodu weryfikacja po stronie serwera
jest najczęściej uzupełnieniem weryfikacji po stronie klienta, w której wartości wprowadzone przez użytkownika są
sprawdzane za pomocą języka JavaScript przed ich wysłaniem do serwera. Weryfikację po stronie klienta omówię
w rozdziale 25.
Wyświetlanie strony podsumowania
Aby zakończyć proces zamawiania, wyświetlimy klientom stronę potwierdzającą fakt przetworzenia zamówienia
i zawierającą podziękowania za zakupy. W katalogu Views/Cart utwórz nowy plik widoku o nazwie
Completed.cshtml i umieść w nim kod przedstawiony na listingu 9.22.
Listing 9.22. Zawartość pliku Completed.cshtml
@{
ViewBag.Title = "Sklep portowy: zamówienie zostało przesłane";
}
<h2>Dziękujemy!</h2>
Dziękujemy za złożenie zamówienia. Wyślemy produkty tak szybko, jak tylko będzie to możliwe.
Nie trzeba wprowadzać żadnych zmian w kodzie w celu integracji tego widoku w aplikacji, ponieważ
niezbędne polecenia zostały już dodane podczas definiowania metody akcji Checkout na listingu 9.18. Teraz
klient może przejść przez cały proces wybierania produktów i składania zamówienia. Jeżeli klient poda
prawidłowe dane do wysyłki (i będzie miał towary w koszyku), po kliknięciu przycisku Zakończ zamówienie
zobaczy stronę podsumowania pokazaną na rysunku 9.8.
Rysunek 9.8. Strona z podziękowaniem
251
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Podsumowanie
Zakończyliśmy już wszystkie ważne części aplikacji SportsStore wykorzystywane przez klientów. Prawdopodobnie
udziałowcy firmy Amazon niezbyt się tym zmartwią, ale mamy katalog produktów, który można przeglądać
według kategorii i stron, koszyk na zakupy i prosty proces składania zamówienia.
Dobrze podzielona architektura pozwala łatwo zmieniać działanie dowolnej części aplikacji bez obawy
o wprowadzenie niespójności lub skutków ubocznych w innych miejscach. Na przykład możemy przetwarzać
zamówienia przez zapisanie ich w bazie danych i nie będzie miało to wpływu na koszyk na zakupy, katalog
produktów ani żaden inny obszar aplikacji. W następnym rozdziale użyjemy dwóch odmiennych technik
w celu utworzenia mobilnej wersji aplikacji SportsStore.
252
ROZDZIAŁ 10.

SportsStore — wersja mobilna
Nie da się uciec przed popularnością urządzeń takich jak smartfony i tablety. Jeżeli chcesz zapewnić swojej
aplikacji jak największą bazę użytkowników, będziesz musiał wejść do świata mobilnych przeglądarek
internetowych. Być może nie zabrzmiało to zbyt entuzjastycznie, ponieważ wyrażenie mobilne przeglądarki
internetowe obejmuje całą gamę przeglądarek internetowych, począwszy od szybkich i nowoczesnych, które
mogą zaoferować możliwości porównywalne z ich tradycyjnymi odpowiednikami, a skończywszy na wolnych,
niespójnych i przestarzałych.
Nie ulega wątpliwości, że opracowanie dobrego produktu dla użytkowników urządzeń mobilnych jest
trudne, znacznie trudniejsze niż przygotowanie aplikacji dla komputerów biurowych. Wymaga starannego
zaplanowania i zaprojektowania aplikacji, a ponadto przeprowadzenia niezwykle dokładnych testów. Jednak
nawet wtedy istnieje niebezpieczeństwo, że w nowym smartfonie lub tablecie aplikacja nie będzie działała
zgodnie z oczekiwaniami.
Kontekst programowania sieciowego
dla urządzeń mobilnych
Platforma ASP.NET MVC oferuje pewne funkcje, które mogą pomóc w przygotowywaniu aplikacji dla urządzeń
mobilnych. Jednak ASP.NET MVC to platforma działająca po stronie serwera, otrzymująca żądania HTTP
i generująca odpowiedzi HTML. Ma więc niewielkie pole manewru w zakresie zróżnicowanych możliwości,
którymi charakteryzują się urządzenia mobilne. Stopień, w jakim platforma MVC może pomóc, zależy od
przyjętej strategii mobilnej. Istnieją trzy podstawowe strategie mobilne, które można zastosować. Wszystkie
zostaną omówione w kolejnych punktach.
 Wskazówka Mamy jeszcze czwartą opcję, którą jest utworzenie aplikacji rodzimej dla urządzeń mobilnych. Nie będziemy
tej opcji omawiać w książce, ponieważ nie jest ona bezpośrednio powiązana z platformą MVC.
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Odstąpienie od działania
(lub jego podjęcie na minimalnym możliwym poziomie)
Uniknięcie podejmowania jakichkolwiek kroków w celu utworzenia wersji mobilnej wydaje się dziwnym
podejściem, ale niektóre urządzenia mobilne potrafią obsługiwać zawartość prawie tak samo, jak przeglądarki
internetowe w komputerach biurowych. Wiele — oczywiście najnowszych wersji urządzeń mobilnych —
zostało wyposażonych w ekrany o wysokiej rozdzielczości i gęstości pikseli, a także w dużą ilość pamięci
operacyjnej. Dlatego też ich przeglądarki internetowe mogą bardzo szybko wygenerować dokument HTML
i uruchomić kod JavaScript. Jeżeli tworzona przez Ciebie aplikacja nie ma zbyt dużych wymagań, wówczas
może się okazać, że urządzenia mobilne nie będą miały żadnych problemów z wyświetleniem treści
generowanej przez tę aplikację. Na przykład na rysunku 10.1 pokazano, jak tablet iPad wyświetla aplikację
SportsStore bez żadnych modyfikacji.
Rysunek 10.1. Wyświetlenie aplikacji SportsStore w tablecie
 Uwaga Rysunki w tym rozdziale zostały utworzone za pomocą serwisu http://www.browserstack.com/. To niezależna
od platformy sprzętowej usługa testowania, z której korzystam podczas pracy nad własnymi projektami. Na pewno
nie jest to idealna usługa. Bardzo często działa wolno, dostęp spoza USA bywa zawodny, a urządzenia mobilne są
emulowane. Korzystam z tej usługi przede wszystkim do testowania moich projektów w przeglądarkach dla komputerów
biurowych, co sprawdza się całkiem dobrze. Otrzymuję przyzwoite wyniki i nie muszę przygotowywać własnego
zbioru emulatorów. Wymieniony serwis oferuje bezpłatny okres próbny, dzięki któremu będziesz mógł wypróbować
przykłady przedstawione w książce. Jeżeli wolisz skorzystać z innych konkurencyjnych dla Browser Stack serwisów,
znajdziesz ich wiele. Muszę w tym miejscu koniecznie dodać, że nie jestem w żaden sposób powiązany z Browser
Stack. Pozostaję jedynie ich zwykłym klientem, zapłaciłem pełną kwotę za wybraną usługę i nie jestem przez nich
traktowany w specjalny sposób.
254
ROZDZIAŁ 10.  SPORTSSTORE — WERSJA MOBILNAI
Aplikacja prezentuje się całkiem dobrze. Jedyny problem dotyczy łączy stronicowania wyświetlanych na
dole strony, ale to można bardzo łatwo poprawić przez zmianę układu strony bądź też zmianę liczby
produktów wyświetlanych na stronie.
Użycie układu responsywnego
Kolejną strategią jest utworzenie zawartości w taki sposób, aby dostosowywała się do możliwości urządzenia,
w którym jest wyświetlana. Takie podejście nosi nazwę układu responsywnego. Standard CSS zawiera funkcje
pozwalające na zmianę stylów nadawanych elementom na podstawie możliwości danego urządzenia. Ta technika
jest najczęściej używana do zmiany układu treści na podstawie szerokości ekranu.
Układ responsywny jest obsługiwany przez klienta za pomocą stylów CSS, a nie bezpośrednio zarządzany
przez platformę MVC. Szczegółowe omówienie układu responsywnego znajdziesz w innej mojej książce —
Pro ASP.NET MVC 5 Client, wydanej przez Apress. Natomiast tutaj pokażę, jak można zastosować tę technikę,
a także wspomnę o kilku kwestiach dotyczących platformy MVC. Wykorzystamy pewne funkcje układu
responsywnego oferowane przez bibliotekę Bootstrap, która jest używana do nadania stylów aplikacji
SportsStore. (Zdecydowałem się na Bootstrap, ponieważ jest to jedna z bibliotek dołączonych przez Microsoft
do standardowego szablonu projektu MVC 5 w Visual Studio 2013).
Moim celem jest dostosowanie układu części głównej aplikacji w taki sposób, aby był prawidłowo
wyświetlany na ekranie smartfona iPhone. Wspomniana wcześniej strategia „nie rób nic” okazuje się
niewystarczająca dla iPhone’a, ponieważ to urządzenie ma wąski ekran, jak pokazano na rysunku 10.2.
Rysunek 10.2. Wyświetlenie aplikacji SportsStore w smartfonie
255
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Z przedstawionym problemem zmierzymy się w kolejnych punktach i skoncentrujemy na aspektach
związanych z układem strony. Celem jest zachowanie całej funkcjonalności aplikacji, ale przedstawienie jej
w odmienny sposób.
 Wskazówka Platforma MVC nie jest aktywnym członkiem w układzie responsywnym. Wszystkim przeglądarkom
internetowym wysyła tę samą zawartość i pozostawia im określenie, które fragmenty mają być wyświetlone. Oznacza
to brak sensownego sposobu na dodanie testów jednostkowych układu responsywnego w projekcie Visual Studio.
Ta technika wymaga starannego przetestowania aplikacji i jednocześnie pozostaje trudna do automatyzacji.
Utworzenie responsywnego nagłówka
Pracę rozpoczniemy od nagłówka strony zawierającego nazwę sklepu, podsumowanie koszyka na zakupy
i przycisk Zamów. Wprawdzie najprostszym rozwiązaniem będzie usunięcie nazwy sklepu i tym samym
zwolnienie wystarczającej ilości miejsca na pozostałą zawartość, ale wspomnianą nazwę postanowiłem
pozostawić (patrz ramka Akceptacja realiów promowania marki) i umieścić nagłówek w dwóch wierszach.
Akceptacja realiów promowania marki
Jednym z najłatwiejszych sposobów zwolnienia pewnej ilości miejsca na ekranie jest pozbycie się promowania
marki z aplikacji. W omawianym przykładzie wyświetlamy jedynie tekst Sklep sportowy, ale możesz zobaczyć, ile
to zajmuje miejsca. Ilość zajmowanego miejsca, którą w przeglądarce komputera biurowego określamy jako niewielką,
w urządzeniu mobilnym staje się ogromna.
Jednak pozbycie się promocji marki jest trudne. Nie chodzi tutaj o kwestie techniczne. Większość działów
marketingu ma obsesję stosowania promowania marki wszędzie, gdzie tylko to możliwe. Dlatego też w sali
konferencyjnej znajdziesz długopisy z logo firmy, w jadalni stoją kubki z logo firmy, a pracownicy co pewien czas
dostają wizytówki z nowym logo firmy. Firmy często przeprowadzają odświeżanie marki, ponieważ osoby się tym
zajmujące wiedzą, że tak naprawdę nie mają prawdziwego zajęcia. Dlatego też nieustanne kładzenie nacisku na
logo i schematy kolorów tworzy złudzenie ich niezwykłej aktywności, która odciąga ich od nieustannego lęku
o przyszłość pojawiającego się w chwilach przerwy od szaleństwa odświeżania marki.
Moja rada brzmi następująco: powinieneś pogodzić się z faktem, że pewna ilość miejsca na ekranie zawsze
będzie przeznaczona na promowanie marki, nawet na najmniejszym urządzeniu oferującym minimalne możliwości.
Wprawdzie możesz próbować z tym walczyć, ale osoby odpowiedzialne za promowanie marki są z reguły pracownikami
działu marketingu. Z kolei dział marketingu zwykle przekazuje raporty szefowi sprzedaży, który ma gorącą linię
z szefem firmy, ponieważ zyski to jedyne, co tak naprawdę się liczy dla udziałowców firmy. Pewne argumenty po
prostu nie mają siły przebicia.
Na listingu 10.1 możesz zobaczyć, jak dostosowałem zawartość nagłówka w pliku _Layout.cshtml w
projekcie SportsStore.WebUI.
Listing 10.1. Dodanie responsywnej zawartości do pliku _Layout.cshtml
<!DOCTYPE
<html>
<head>
<meta
<meta
<link
<link
256
html>
charset="utf-8" />
name="viewport" content="width=device-width, initial-scale=1.0">
href="~/Content/bootstrap.css" rel="stylesheet" />
href="~/Content/bootstrap-theme.css" rel="stylesheet" />
ROZDZIAŁ 10.  SPORTSSTORE — WERSJA MOBILNAI
<link href="~/Content/ErrorStyles.css" rel="stylesheet" />
<title>@ViewBag.Title</title>
<style>
.navbar-right {
float: right !important;
margin-right: 15px; margin-left: 15px;
}
</style>
</head>
<body>
<div class="navbar navbar-inverse">
<a class="navbar-brand" href="#">
<span class="hidden-xs">Sklep sportowy</span>
<div class="visible-xs">Sklep</div>
<div class="visible-xs">sportowy</div>
</a>
@Html.Action("Summary", "Cart")
</div>
<div class="row panel">
<div id="categories" class="col-xs-3">
@Html.Action("Menu", "Nav")
</div>
<div class="col-xs-8">
@RenderBody()
</div>
</div>
</body>
</html>
Bootstrap definiuje zbiór klas, które można wykorzystać do wyświetlenia lub ukrycia elementów na
podstawie szerokości ekranu urządzenia. Tego rodzaju zadanie zwykle było wykonywane ręcznie za pomocą
zapytań CSS, ale klasy Bootstrap są zintegrowane w innych stylach.
W przypadku promowania marki w aplikacji SportsStore zdecydowałem się na użycie klas visible-xs
i hidden-xs pozwalających na wyświetlenie wspomnianych wcześniej dwóch wierszy, gdy szerokość okna
przeglądarki internetowej jest mniejsza niż 768 pikseli. Bootstrap oferuje parę klas wyświetlających
i ukrywających elementy w zależności od wielkości okna przeglądarki internetowej. Nazwy wspomnianych klas
zaczynają się od visible- i hidden-. W omawianej aplikacji zostały użyte klasy *-xs (na przykład visible-xs
i hidden-xs). Klasy *-sm są stosowane w oknach szerszych niż 768 pikseli, klasy *-md w oknach szerszych niż
992 piksele, natomiast klasy *-lg w oknach o szerokości większej niż 1200 pikseli.
 Ostrzeżenie Responsywne funkcje CSS, takie jak oferowane przez Bootstrap, są oparte na wielkości okna przeglądarki
internetowej, a nie ekranu urządzenia. Przeglądarki internetowe w urządzeniach mobilnych zwykle są wyświetlane
w trybie pełnego ekranu, co oznacza, że wielkość okna i ekranu pozostają takie same. Jednak nie można przyjmować
założenia, że zawsze tak jest. Podobnie jak zawsze, należy przeprowadzić dokładne testy z uwzględnieniem urządzeń
docelowych i upewnić się, że nie zostały przyjęte nieprawidłowe założenia.
Efekt wprowadzonych zmian można zobaczyć po uruchomieniu aplikacji i wyświetleniu katalogu
produktów w tradycyjnej przeglądarce internetowej komputera biurowego, która daje możliwość zmiany
wielkości okna. Jeżeli teraz zmniejszysz szerokość okna do poniżej 768 pikseli, tekst Sklep sportowy zostanie
wyświetlony w dwóch wierszach, jak pokazano na rysunku 10.3.
257
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 10.3. Użycie responsywnego układu Bootstrap w celu dostosowania promowania marki w aplikacji
Zmiana wydaje się być niewielka, ale ma ogromne znaczenie w urządzeniach wyposażonych w mniejsze
ekrany, zwłaszcza w połączeniu ze zmianami wprowadzonymi w pliku Views/Cart/Summary.cshtml, który
dostarcza podsumowanie koszyka na zakupy i jego zawartości. Zmiany we wspomnianym pliku przedstawiono
na listingu 10.2.
Listing 10.2. Dodanie responsywnej zawartości do pliku Summary.cshtml
@model SportsStore.Domain.Entities.Cart
<div class="navbar-right hidden-xs">
@Html.ActionLink("Zamów", "Index", "Cart",
new { returnUrl = Request.Url.PathAndQuery },
new { @class = "btn btn-default navbar-btn" })
</div>
<div class="navbar-right visible-xs">
<a [email protected]("Index", "Cart", new { returnUrl = Request.Url.PathAndQuery })
class="btn btn-default navbar-btn">
<span class="glyphicon glyphicon-shopping-cart"></span>
</a>
</div>
<div class="navbar-text navbar-right">
<b class="hidden-xs">Twój koszyk:</b>
@Model.Lines.Sum(x => x.Quantity) sztuk,
@Model.ComputeTotalValue().ToString("c")
</div>
To jest dokładnie ta sama technika, którą zastosowałem wcześniej względem pliku _Layout.cshtml w celu
selektywnego wyświetlania i ukrywania zawartości. Jednak w omawianym przykładzie przycisk Zamów jest
na małych ekranach ukrywany i zastępowany przez przycisk ikony, jeden z dostarczanych wraz z pakietem
Bootstrap.
Ikony Bootstrap są nakładane za pomocą elementu <span>, co oznacza brak możliwości użycia metody
pomocniczej Html.Action, ponieważ nie oferuje ona możliwości zdefiniowania zawartości tworzonego
elementu. Zamiast tego definiujemy więc bezpośrednio element <a> i stosujemy metodę pomocniczą
Url.Action (w rozdziale 23. znajdziesz jej dokładne omówienie) do wygenerowania adresu URL dla atrybutu
href. Wynikiem jest element <a> wraz z takimi samymi atrybutami jak w przypadku utworzenia za pomocą
metody Html.Action, ale wygenerowany jest element <span>. Efekt zmian wprowadzonych w obu plikach
możesz zobaczyć na rysunku 10.4, na którym pokazano nagłówek wyświetlony w iPhonie.
258
ROZDZIAŁ 10.  SPORTSSTORE — WERSJA MOBILNAI
Rysunek 10.4. Zmodyfikowany nagłówek wyświetlony w symulatorze smartfona iPhone
Przede wszystkim wersja mobilna kontra przede wszystkim wersja biurowa
Większość projektów aplikacji sieciowych jest przygotowywanych pod kątem klientów działających w komputerach
biurowych, a dopiero później są opracowywane wersje dla urządzeń mobilnych, podobnie jak to zrobiłem w tej
książce. Takie podejście jest określane mianem przede wszystkim wersja biurowa. Największy problem polega na
tym, że działające po stronie serwera komponenty aplikacji są praktycznie ukończone, zanim rozpoczną się prace
nad wersją mobilną aplikacji. Skutkiem jest niedopasowana wersja mobilna aplikacji, często oparta na sztuczkach
mających zmusić do działania w środowisku mobilnym funkcje przygotowane z myślą o oferujących znacznie
większe możliwości klientach w komputerach biurowych.
Alternatywna filozofia nosi nazwę przede wszystkim wersja mobilna, w której — jak sama nazwa wskazuje
— prace rozpoczynają się od utworzenia wersji mobilnej, jako podstawy aplikacji. Następnie są dodawane kolejne
funkcje pozwalające na wykorzystanie znacznie większych możliwości przeglądarek internetowych w komputerach
biurowych.
Ujmując rzecz inaczej, jeżeli najpierw zostanie opracowana wersja dla komputerów biurowych, wówczas
zawiera pełny zestaw funkcji, które następnie są elegancko redukowane dla urządzeń o mniejszych możliwościach.
Natomiast po przygotowaniu najpierw wersji mobilnej aplikacja na początku ma mniejszy zestaw funkcji, który
jest elegancko rozbudowywany dla urządzeń charakteryzujących się większymi możliwościami.
Obie metody mają swoje zalety. Ja preferuję podejście pierwsze (najpierw wersja biurowa aplikacji), ponieważ
przeglądarki internetowe w komputerach biurkowych niezwykle ułatwiają wczytywanie zawartości z lokalnej stacji
roboczej programisty. To jest zaskakująco trudne w przypadku pracy z rzeczywistymi urządzeniami mobilnymi.
Zwykle pracuję w trybie „tworzenie kodu — kompilacja — sprawdzanie” (co oznacza częste odświeżanie strony
w przeglądarkach internetowych) i jestem poirytowany utrudnieniami, jakie napotykam podczas stosowania tego
cyklu w trakcie pracy z użyciem urządzeń mobilnych.
259
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Niebezpieczeństwo faworyzowania jednej grupy użytkowników polega na tym, że tworzysz odmienny
standard aplikacji dla innej grupy użytkowników. Zwolennicy tworzenia najpierw wersji mobilnej aplikacji twierdzą, że to
nie będzie miało miejsca, jeśli pracę zaczniesz od przygotowania podstawowego zestawu funkcji, a następnie
przeprowadzisz skalowanie w górę. Moje doświadczenia jednak tego nie potwierdzają.
Bardzo ważne jest przygotowanie solidnego planu jasno określającego, które funkcje i układy mają być
dostępne dla wszystkich urządzeń. Taki plan trzeba opracować jeszcze przed rozpoczęciem prac nad którąkolwiek
z funkcji lub nad układem. Kiedy masz przygotowany plan, wtedy nie ma znaczenia, od którego rodzaju
urządzenia rozpoczniesz pracę. Działające po stronie serwera kluczowe komponenty aplikacji będą od początku
zbudowane z uwzględnieniem obsługi szerokiej gamy klientów.
Tworzenie responsywnej listy produktów
W celu zakończenia adaptacji responsywnej konieczne jest przygotowanie listy produktów prawidłowo
wyświetlanej w wąskich urządzeniach. Największy problem dotyczący użycia poziomej przestrzeni wiąże się
z przyciskami kategorii produktów. Pozbędziemy się tych przycisków, opisy poszczególnych produktów będą
zajmowały wówczas całą szerokość ekranu urządzenia. Na listingu 10.3 możesz zobaczyć kolejne modyfikacje
wprowadzone w pliku _Layout.cshtml.
Listing 10.3. Utworzenie responsywnej listy produktów w pliku _Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<link href="~/Content/ErrorStyles.css" rel="stylesheet" />
<title>@ViewBag.Title</title>
<style>
.navbar-right {
float: right !important;
margin-right: 15px; margin-left: 15px;
}
</style>
</head>
<body>
<div class="navbar navbar-inverse">
<a class="navbar-brand" href="#">
<span class="hidden-xs">Sklep sportowy</span>
<div class="visible-xs">Sklep</div>
<div class="visible-xs">sportowy</div>
</a>
@Html.Action("Summary", "Cart")
</div>
<div class="row panel">
<div class="col-sm-3 hidden-xs">
@Html.Action("Menu", "Nav")
</div>
<div class="col-xs-12 col-sm-8">
@RenderBody()
</div>
</div>
</body>
</html>
260
ROZDZIAŁ 10.  SPORTSSTORE — WERSJA MOBILNAI
W układzie może być tylko jedno wywołanie metody RenderBody. Szczegóły dotyczące układu zostaną
przedstawione w rozdziale 20., ale efektem wymienionego ograniczenia jest brak możliwości posiadania
powielonego zbioru elementów do wyświetlenia lub ukrycia, gdzie każdy zbiór będzie zawierał własne
wywołanie RenderBody. Zamiast tego konieczna jest zmiana układu na siatce zawierającego wywołanie metody
RenderBody, aby elementy znajdujące się w układzie były odpowiednio przygotowane dla zawartości
wyświetlanej przez widok.
Jednym z powodów użycia w rozdziale 7. siatki Bootstrap do przygotowania struktury dla zawartości
w pliku _Layout.cshtml był fakt, że biblioteka Bootstrap zawiera pewne funkcje układu responsywnego
pozwalającego na ominięcie ograniczenia związanego z RenderBody. Układ siatki Bootstrap obsługuje 12
kolumn, liczbę kolumn zajmowanych przez element wskazujesz przez przypisanie odpowiedniej klasy, na
przykład jak w rozdziale 7.:
...
<div class="col-xs-8" >
@RenderBody()
</div>
...
Podobnie jak wspomniane wcześniej klasy hidden-* i visible-*, Bootstrap dostarcza także zbiór klas
pozwalających na wskazanie liczby kolumn zajmowanych przez dany element na siatce opartej na szerokości okna.
Klasy col-xs-* określają stałą szerokość i nie zmieniają wartości na podstawie szerokości ekranu.
W przypadku klasy col-xs-8 informujemy Bootstrap, że dany element <div> powinien zajmować 8 z 12
dostępnych kolumn, a widoczność elementu nie powinna ulegać zmianie na podstawie szerokości okna. Klasy
col-sm-* definiują kolumny, gdy okno ma szerokość co najmniej 768 pikseli, klasy col-md-* działają z oknami
o szerokości co najmniej 992 pikseli, natomiast col-lg-* działają z oknami o szerokości przynajmniej 1200
pikseli. Mając to wszystko na uwadze, poniżej przedstawiłem klasy zastosowane w elemencie <div> zawierającym
na listingu 10.3 wywołanie metody RenderBody:
...
<div class="col-xs-12 col-sm-8">
@RenderBody()
</div>
...
Efekt zastosowania obu wymienionych klas polega na tym, że element <div> domyślnie zajmuje na siatce
wszystkie 12 kolumn lub 8, gdy szerokość ekranu wynosi co najmniej 768 pikseli. Pozostałe kolumny na siatce
zawierają przyciski kategorii, jak przedstawiono poniżej:
...
<div class="col-sm-3 hidden-xs">
@Html.Action("Menu", "Nav")
</div>
...
Ten element będzie zabierał 3 kolumny, gdy szerokość ekranu jest większa niż 768 pikseli. W przypadku
mniejszych ekranów zostanie ukryty. W połączeniu z innymi zastosowanymi klasami opisy produktów będą
wypełniały małe ekrany, natomiast na większych współdzielą miejsce z przyciskami kategorii. Oba układy
pokazano na rysunku 10.5. Wykorzystałem tutaj tradycyjną przeglądarkę internetową, ponieważ pozwala ona
na bardzo łatwą zmianę szerokości okna.
261
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 10.5. Użycie responsywnej siatki w układzie listy produktów
Ułatwienie kontrolerowi wyboru odpowiedniego widoku
Nie chcę pozostawić użytkowników urządzeń mobilnych bez możliwości filtrowania produktów, co oznacza
konieczność przedstawienia kategorii w inny sposób. Dlatego też w katalogu Views/Nav utworzymy widok
o nazwie MenuHorizontal.cshtml wraz z zawartością przedstawioną na listingu 10.4.
Listing 10.4. Zawartość pliku MenuHorizontal.cshtml
@model IEnumerable<string>
<div class="btn-group btn-group-sm btn-group-justified">
@Html.ActionLink("Home", "List", "Product",
new { @class = "btn btn-default btn-sm" })
@foreach (var link in Model) {
@Html.RouteLink(link, new {
controller = "Product",
action = "List",
category = link,
page = 1
}, new {
@class = "btn btn-default btn-sm"
+ (link == ViewBag.SelectedCategory ? " btn-primary" : "")
})
}
</div>
To jest pewna odmiana pierwotnego układu zdefiniowanego w pliku Menu.cshtml, ale zawiera element
<div> i klasy Bootstrap w celu utworzenia poziomego układu przycisków. Jednak podstawowa funkcjonalność
pozostaje taka sama. Kod generuje zestaw łączy pozwalających na filtrowanie produktów według kategorii.
Zestaw przycisków kategorii jest generowany za pomocą metody akcji Menu kontrolera Nav, którą musimy
uaktualnić, aby wybierany był odpowiedni plik widoku na podstawie żądanej orientacji przycisków. Zmiany
konieczne do wprowadzenia przedstawiono na listingu 10.5.
262
ROZDZIAŁ 10.  SPORTSSTORE — WERSJA MOBILNAI
Listing 10.5. Uaktualnienie metody akcji Menu w pliku NavController.cs
using
using
using
using
System.Collections.Generic;
System.Web.Mvc;
SportsStore.Domain.Abstract;
System.Linq;
namespace SportsStore.WebUI.Controllers {
public class NavController : Controller {
private IProductRepository repository;
public NavController(IProductRepository repo) {
repository = repo;
}
public PartialViewResult Menu(string category = null,
bool horizontalLayout = false) {
ViewBag.SelectedCategory = category;
IEnumerable<string> categories = repository.Products
.Select(x => x.Category)
.Distinct()
.OrderBy(x => x);
string viewName = horizontalLayout ? "MenuHorizontal" : "Menu";
return PartialView(viewName, categories);
}
}
}
W kodzie zdefiniowano nowy parametr dla metody akcji wskazujący orientację. Ten parametr jest
używany do wyboru nazwy widoku przekazywanej metodzie PartialView. Aby ustawić wartość parametru,
należy powrócić do pliku _Layout.cshtml i wprowadzić w nim kolejne zmiany przedstawione na listingu 10.6.
Listing 10.6. Uaktualnienie pliku _Layout.cshtml, aby pozwalał na wyświetlanie przycisków w poziomie
...
<body>
<div class="navbar navbar-inverse">
<a class="navbar-brand" href="#">
<span class="hidden-xs">Sklep sportowy</span>
<div class="visible-xs">Sklep</div>
<div class="visible-xs">sportowy</div>
</a>
@Html.Action("Summary", "Cart")
</div>
<div class="visible-xs">
@Html.Action("Menu", "Nav", new { horizontalLayout = true })
</div>
<div class="row panel">
<div class="col-sm-3 hidden-xs">
@Html.Action("Menu", "Nav")
</div>
<div class="col-xs-12 col-sm-8">
@RenderBody()
</div>
</div>
</body>
...
263
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Opcjonalny trzeci argument metody Html.Action to obiekt pozwalający na ustawienie wartości
dla systemu routingu, który zostanie dokładnie omówiony w rozdziałach 15. i 16. Ta funkcja została tutaj
wykorzystana do wskazania, który widok powinien wybrać kontroler. Ogólny efekt wprowadzonych zmian
pokazano na rysunku 10.6.
Rysunek 10.6. Uaktualniony katalog produktów wyświetlany w urządzeniach o małych ekranach
Jak możesz zobaczyć, przeniesienie przycisków na początek katalogu produktów pozostawia ilość miejsca
wystarczającą na prawidłowe wyświetlenie poszczególnych produktów. Mógłbym kontynuować usprawnianie
kolejnych widoków, ale w tym momencie powinieneś już wiedzieć, o co tutaj chodzi. Pomijając krótką
demonstrację sposobu użycia responsywnych klas CSS, chciałem w tym miejscu wskazać zarówno pewne
ograniczenia nakładane przez platformę MVC (na przykład dotyczące liczby wywołań metody RenderBody),
jak i funkcje pomagające w generowaniu zawartości na różne sposoby (na przykład przekazywanie danych
z widoku do kontrolera za pomocą systemu routingu i metody pomocniczej Html.Action).
 Wskazówka Wprawdzie w omawianym przykładzie skoncentrowałem się na wymaganiach smartfona iPhone,
ale nie zapominaj, że większość urządzeń mobilnych pozwala na pracę w dwóch orientacjach. W rzeczywistych
projektach koniecznie powinieneś to uwzględnić.
Wyeliminowanie powielania widoków
W poprzednim przykładzie chciałem pokazać, jak kontroler może wybierać widok na podstawie informacji
routingu przekazywanych przez wywołanie metody pomocniczej Html.Action. To jest bardzo ważna i użyteczna
funkcja. Mimo tego nie wykorzystałbym jej w rzeczywistym projekcie, ponieważ pozostawia mnie z dwoma
widokami, Menu.cshtml i MenuHorizontal.cshtml, które w dużej mierze składają się z podobnego kodu
znaczników i wyrażeń Razor. Takie rozwiązanie okaże się kłopotliwe podczas konserwacji aplikacji.
264
ROZDZIAŁ 10.  SPORTSSTORE — WERSJA MOBILNAI
Wszelkie zmiany w przyciskach filtrowania kategorii będą musiały być wprowadzane w dwóch miejscach.
Rozwiązaniem, które tutaj zastosuję, jest konsolidacja wymienionych widoków. W katalogu Views/Nav
tworzymy więc nowy plik widoku o nazwie FlexMenu.cshtml i umieszczamy w nim kod przedstawiony
na listingu 10.7.
Listing 10.7. Zawartość pliku FlexMenu.cshtml
@model IEnumerable<string>
@{
bool horizontal = ((bool)(ViewContext.RouteData.Values["horizontalLayout"] ?? false));
string wrapperClasses =
horizontal ? "btn-group btn-group-sm btn-group-justified" : null;
}
<div class="@wrapperClasses">
@Html.ActionLink("Home", "List", "Product",
new { @class = horizontal ? "btn btn-default btn-sm" :
"btn btn-block btn-default btn-lg"
})
@foreach (var link in Model) {
@Html.RouteLink(link, new {
controller = "Product",
action = "List",
category = link,
page = 1
}, new {
@class = (horizontal ? "btn btn-default btn-sm"
: "btn btn-block btn-default btn-lg" )
+ (link == ViewBag.SelectedCategory ? " btn-primary" : "")
})
}
</div>
Kosztem eliminacji powielania kodu jest znacznie bardziej skomplikowany widok zdolny do wygenerowania
przycisków w obu orientacjach. Warto w tym miejscu dodać, że wybór konkretnego podejścia zależy od preferencji
programisty. Jeżeli podobnie jak ja preferujesz unikanie powielania kodu, wówczas na listingu 10.7 znajdziesz
wiele użytecznych technik, które możesz zastosować w tworzonych widokach.
Pierwsza to możliwość uzyskania dostępu do informacji routingu bezpośrednio z poziomu widoku.
Właściwość ViewContext dostarcza informacje o aktualnym stanie przetwarzanego żądania, między innymi
szczegóły związane z routingiem, jak przedstawiono poniżej:
...
bool horizontal = ((bool)(ViewContext.RouteData.Values["horizontalLayout"] ?? false));
...
Druga to możliwość tworzenia zmiennych lokalnych w widoku. Taka możliwość wynika ze sposobu,
w jaki widoki Razor są kompilowane na postać klas (to zostanie omówione w rozdziale 20.). W omawianym
przykładzie utworzyłem zmienną lokalną o nazwie horizontal. Oznacza to, że nie muszę sprawdzać danych
trasy w całym listingu, aby ustalić, w której orientacji powinien być użyty widok.
 Ostrzeżenie Zmienne lokalne powinny być używane oszczędnie, ponieważ to prowadzi do tworzenia widoków,
które są trudne do konserwacji i przetestowania. Jednak zdarzają się sytuacje, taka jak omawiana tutaj, gdy użycie
zmiennej lokalnej jest możliwym do zaakceptowania kosztem uproszczenia widoku.
265
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
W podobny sposób silnik Razor będzie warunkowo ustawiał atrybuty na podstawie zmiennych. W kodzie
mamy zdefiniowany ciąg tekstowy nazw klas. Wspomniany ciąg tekstowy ma postać zmiennej lokalnej w widoku:
...
string wrapperClasses =
horizontal ? "btn-group btn-group-sm btn-group-justified" : null;
...
Wartością zmiennej wrapperClasses jest ciąg tekstowy nazw klas używanych w układzie poziomym
lub też wartość null. Ta zmienna jest używana wraz z atrybutem class w następujący sposób:
...
<div class="@wrapperClasses">
...
Kiedy zmienna wrapperClasses ma wartość null, wówczas silnik Razor całkowicie usuwa atrybut
class z elementu <div> i generuje element w następującej postaci:
<div>
Natomiast jeśli wymieniona zmienna ma wartość inną niż null, Razor wstawi tę wartość i pozostawi
atrybut class w niezmienionej postaci, generując wynik podobny do poniższego:
<div class="btn-group btn-group-sm btn-group-justified">
To jest elegancki sposób dopasowania charakterystyki języka C# do semantyki HTML. Ta możliwość
okazuje się niezmiernie użyteczna podczas tworzenia skomplikowanych widoków, ponieważ nie wstawia
wartości null do atrybutów i nie generuje pustych atrybutów, które spowodowałyby problemy z selektorami
CSS (a także bibliotekami JavaScript używającymi atrybutów do wybierania elementów, przykładem może być
tutaj jQuery).
 Wskazówka Atrybuty warunkowe będą działały z dowolną zmienną, a nie tylko ze zmiennymi, które zdefiniowano
w widoku. Oznacza to możliwość wykorzystania omawianej funkcjonalności także z właściwościami modelu oraz
z ViewBag.
Aby można było użyć skonsolidowanego widoku, konieczne jest zmodyfikowanie metody akcji
Menu w kontrolerze Nav, jak przedstawiono na listingu 10.8.
Listing 10.8. Uaktualnienie metody akcji Menu w pliku NavController.cs
...
public PartialViewResult Menu(string category = null) {
ViewBag.SelectedCategory = category;
IEnumerable<string> categories = repository.Products
.Select(x => x.Category)
.Distinct()
.OrderBy(x => x);
return PartialView("FlexMenu", categories);
}
...
266
ROZDZIAŁ 10.  SPORTSSTORE — WERSJA MOBILNAI
Z kodu usunięto parametr otrzymujący informacje o orientacji, a także zmieniono wywołanie metody
PartialView, aby zawsze wybierany był widok FlexMenu. Wprowadzone zmiany nie mają wpływu na
rozmieszczenie zawartości ani na responsywny układ, ale pozwalają na wyeliminowanie powielania kodu
w widokach oraz na usunięcie widoków Menu.cshtml i MenuHorizontal.cshtmlm z projektu Visual Studio.
Obie orientacje przycisków filtrowania kategorii są teraz generowane przez widok FlexMenu.cshtml.
Ograniczenia układu responsywnego
Istnieją pewne problemy związane z użyciem układu responsywnego jako sposobu obsługi klientów mobilnych.
Pierwszy polega na powielaniu dużej ilości zawartości oraz wysyłaniu jej do serwera, aby zawartość mogła być
wyświetlana w różnych sytuacjach. W poprzednim punkcie widziałeś, że kod HTML wygenerowany przez układ
zawiera wiele zestawów elementów dla nagłówka strony i przycisków filtrowania kategorii. Dodatkowe elementy
nie są zbyt duże, biorąc pod uwagę pojedyncze żądanie. Jednak ogólnym efektem zastosowania takiego rozwiązania
w obciążonej aplikacji będzie gwałtowne zwiększenie zapotrzebowania na przepustowość, co nieuchronnie podnosi
koszty działania aplikacji.
Drugi problem polega na tym, że responsywny układ może być nieporęczny i wymaga nieustannego testowania.
Nie wszystkie urządzenia będą prawidłowo obsługiwały wykorzystane w projekcie funkcje CSS włączające możliwość
użycia układu responsywnego (tzw. media queries). Jeżeli nie zachowasz wystarczającej ostrożności, aplikacja
będzie po prostu poprawnie działała we wszystkich urządzeniach, nie wykorzystując pełni możliwości żadnego
z nich i uwzględniając dziwactwa występujące we wszystkich.
Układ responsywny może być użyteczny, gdy zostanie zastosowany bardzo rozważnie. Jednak bardzo łatwo
może powstać aplikacja pełna kompromisów, przez które żaden przeciętny użytkownik nie będzie zadowolony
ze sposobu jej działania.
Utworzenie zawartości specjalnie
dla urządzeń mobilnych
Układ responsywny dostarcza tę samą zawartość wszystkim urządzeniom i używa stylów CSS w celu określenia
sposobu prezentacji treści. Ten proces nie wykorzystuje działających po stronie serwera komponentów aplikacji.
Oznacza przyjęcie założenia, że wszystkie urządzenia mają być traktowane jako odmiany tego samego
podstawowego motywu. Alternatywne podejście polega na użyciu serwera do ustalenia możliwości oferowanych
przez przeglądarkę internetową klienta, a następnie wysyłanie różnego kodu HTML odmiennym klientom.
Takie rozwiązanie sprawdza się doskonale, jeżeli chcesz przedstawiać całkowicie odmienne aspekty aplikacji
klientom biurowym i na przykład tabletom.
 Wskazówka Nie musisz wybierać między układem responsywnym i zawartością przeznaczoną specjalnie dla urządzeń
mobilnych. W większości projektów konieczne okazuje się użycie obu metod, aby otrzymać dobry wynik w urządzeniu
docelowym. Na przykład możesz przygotować zawartość specjalnie dla tabletów, a następnie wykorzystać układ
responsywny do utworzenia orientacji poziomej i pionowej, które są obsługiwane przez większość tabletów.
Platforma MVC obsługuje funkcję o nazwie tryby wyświetlania, która pozwala na tworzenie odmiennych
widoków na podstawie rodzaju klienta wykonującego żądanie. Wymieniona funkcja jest dostarczana przez
platformę ASP.NET. Szczegółowe omówienie tworzenia i zarządzania trybami wyświetlania znajdziesz
w innej mojej książce, Pro ASP.NET MVC 5 Platform (wydanej przez Apress), ale w aplikacji SportsStore
wykorzystamy najprostszą postać trybów wyświetlania, gdzie wszystkie urządzenia mobilne są traktowane
jako takie same. Moim celem jest przygotowanie wersji dla urządzeń mobilnych z wykorzystaniem
267
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
popularnej biblioteki jQuery Mobile. Natomiast dotychczasową zawartość pozostawiam do wyświetlania
przez tradycyjne urządzenia biurowe.
 Wskazówka Nie zamierzam tutaj zagłębiać się w tajniki biblioteki jQuery Mobile, a jedynie pokazać, jak można ją
wykorzystać w celu dostarczenia zawartości dla urządzeń mobilnych. Dokładne omówienie jQuery Mobile
znajdziesz w innej mojej książce, Pro jQuery 2.0, wydanej przez Apress.
Utworzenie układu dla urządzeń mobilnych
W celu przygotowania zawartości specjalnie dla urządzeń mobilnych trzeba jedynie przygotować odpowiednie
widoki i układy, których pliki muszą mieć rozszerzenie .Mobile.cshtml. W katalogu Views/Shared tworzymy
więc nowy plik układu o nazwie _Layout.Mobile.cshtml i umieszczamy w nim zawartość przedstawioną na
listingu 10.9.
Listing 10.9. Zawartość pliku _Layout.Mobile.cshtml
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet"
href="http://code.jquery.com/mobile/1.3.2/jquery.mobile-1.3.2.min.css" />
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script
src="http://code.jquery.com/mobile/1.3.2/jquery.mobile-1.3.2.min.js"></script>
<title>@ViewBag.Title</title>
</head>
<body>
<div data-role="page" id="page1">
<div data-theme="a" data-role="header" data-position="fixed">
<h3>SportsStore</h3>
@Html.Action("Menu", "Nav")
</div>
<div data-role="content">
<ul data-role="listview" data-divider-theme="b" data-inset="false">
@RenderBody()
</ul>
</div>
</div>
</body>
</html>
 Wskazówka Ponieważ nazwa widoku zawiera dodatkową kropkę, widok tworzysz przez kliknięcie prawym przyciskiem
myszy katalogu Shared, a następnie z menu kontekstowego wybierasz opcję Dodaj/Strona układu MVC 5 (Razor).
Przedstawiony powyżej układ wykorzystuje bibliotekę jQuery Mobile pobraną z sieci CDN (ang. content
delivery network). Dzięki temu unikamy konieczności instalacji pakietu NuGet dla niezbędnych plików
JavaScript i CSS.
268
ROZDZIAŁ 10.  SPORTSSTORE — WERSJA MOBILNAI
 Wskazówka Tutaj zaledwie dotknąłem tematu tworzenia widoków przeznaczonych specjalnie dla urządzeń mobilnych,
ponieważ wykorzystałem te same kontrolery i metody akcji przeznaczone dla tradycyjnych klientów biurowych.
Przygotowanie oddzielnych widoków pozwala na stosowanie odmiennych kontrolerów opracowanych specjalnie
dla określonej grupy odbiorców. Dzięki temu można opracować zupełnie odmienne funkcje dla różnych typów klientów.
Platforma MVC automatycznie identyfikuje klienty mobilne i używa pliku _Layout.Mobile.cshtml podczas
generowania widoków. W ten sposób bezproblemowo zastępuje plik _Layout.cshtml używany w trakcie
generowania widoków dla innych klientów. Efekt wprowadzonych zmian pokazano na rysunku 10.7.
Rysunek 10.7. Efekt utworzenia układu dla urządzeń mobilnych w aplikacji SportsStore
Jak możesz zobaczyć, układ przeznaczony dla urządzeń mobilnych jest inny, ale ogólny efekt to zupełny
bałagan. Wynika to z konieczności utworzenia mobilnej wersji widoku głównego obsługującego żądania
i widoku częściowego używanego przez przyciski filtrujące kategorie.
Utworzenie widoków dla urządzeń mobilnych
Rozpoczniemy od poprawienia filtrowania kategorii, co oznacza utworzenie w katalogu Views/Nav pliku
o nazwie FlexMenu.Mobile.cshtml i umieszczenie w nim zawartości przedstawionej na listingu 10.10.
Listing 10.10. Zawartość pliku FlexMenu.Mobile.cshtml
@model IEnumerable<string>
<div data-role="navbar">
269
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
<ul>
@foreach (var link in Model) {
<li>
@Html.RouteLink(link, new {
controller = "Product",
action = "List",
category = link,
page = 1
}, new {
data_transition = "fade",
@class = (link == ViewBag.SelectedCategory
? "ui-btn-active" : null)
})
</li>
}
</ul>
</div>
Ten widok używa wyrażenia Razor foreach do wygenerowania elementów <li> dla kategorii produktów.
W ten sposób elementy zostają zorganizowane w sposób oczekiwany przez jQuery Mobile, a następnie
umieszczone na pasku nawigacyjnym, który znajduje się na górze strony. Uzyskany efekt pokazano na
rysunku 10.8.
Rysunek 10.8. Efekt utworzenia widoku przeznaczonego dla urządzeń mobilnych
270
ROZDZIAŁ 10.  SPORTSSTORE — WERSJA MOBILNAI
 Wskazówka Do formatowania elementów biblioteka jQuery Mobile wykorzystuje atrybuty danych. Wspomniane
atrybuty danych mają prefiks data- i były nieoficjalnym sposobem definiowania własnych atrybutów na długo
wcześniej, zanim stały się oficjalnie częścią standardu HTML5. Na przedstawionym powyżej listingu używamy
atrybutu data-transition dla elementów <li>. Nie możemy jednak użyć data-transition jako nazwy
właściwości dla obiektu anonimowego, ponieważ to będzie wyrażenie C#. Problem wiąże się z myślnikiem,
w nazwach właściwości Razor zastępuje myślnik znakiem podkreślenia. Dlatego też po użyciu data_transition
na listingu, w wygenerowanym elemencie otrzymujemy atrybut data-transition.
Informacje o produktach nadal pozostają nieuporządkowane, ale przyciski kategorii są teraz generowane
przez nowy widok, przeznaczony specjalnie dla urządzeń mobilnych. Warto się na chwilę zatrzymać
i przeanalizować, co tak naprawdę platforma MVC robi podczas generowania zawartości pokazanej
na rysunku 10.8.
Żądanie HTTP z przeglądarki internetowej dotyczy metody akcji List w kontrolerze Product, a więc
platforma MVC generuje plik widoku List.cshtml. Ponieważ platforma MVC wie, że żądanie pochodzi
z przeglądarki internetowej w urządzeniu mobilnym, to rozpoczyna się wyszukiwanie widoków przeznaczonych
specjalnie dla tego rodzaju urządzeń. Jednak w aplikacji nie znajduje się plik List.Mobile.cshtml, a więc
przetwarzany będzie plik List.cshtml. Wymieniony widok opiera się na układzie zdefiniowanym w pliku
_Layout.cshtml, ale platforma MVC dostrzega dostępność tego układu w wersji dla urządzeń mobilnych
i dlatego użyje pliku _Layout.Mobile.cshtml. Wymieniony układ wymaga pliku FlexMenu.cshtml, którego
wersja także istnieje i będzie użyta itd.
W efekcie przeglądarka internetowa otrzymuje odpowiedź wygenerowaną na podstawie widoków
ogólnych i przeznaczonych dla urządzeń mobilnych. Platforma MVC używa najlepiej dopasowanego pliku
widoku, w razie konieczności elegancko stosując rozwiązania awaryjne.
Dwa problemy w omawianym przykładzie
Przykład omówiony w tym rozdziale miał na celu zaprezentowanie sposobu, w jaki platforma MVC może dostarczyć
zawartość przeznaczoną dla urządzeń mobilnych. Byłoby jednak niedbalstwem z mojej strony, gdybym nie
wspomniał o dwóch poważnych problemach, jakie ten przykład wprowadza w aplikacji SportsStore.
Pierwszy to dostarczenie mniejszej funkcjonalności wersji mobilnej w porównaniu z wersją dla tradycyjnych
komputerów biurowych. Na przykład w nagłówku strony nie znajdziesz podsumowania koszyka. Pewne funkcje
opuściłem, aby uprościć zmiany konieczne do wprowadzenia. Zalecam jednak unikanie oferowania zredukowanej
funkcjonalności jakiemukolwiek urządzeniu, chyba że istnieją ograniczenia techniczne uniemożliwiające temu
urządzeniu obsługę danej funkcji. Możliwości urządzeń mobilnych stają się coraz większe i wielu użytkowników
będzie korzystać z Twojej aplikacji jedynie za pomocą mobilnej przeglądarki internetowej. Bezpowrotnie minęły
już czasy, gdy wersję mobilną można było uznawać jedynie za uzupełnienie wersji biurowej aplikacji.
Drugi problem wiąże się z brakiem zaoferowania użytkownikowi możliwości powrotu do układu przeznaczonego
dla przeglądarek biurkowych. Możesz być zdziwiony, jak wielu użytkowników preferuje wyświetlanie w urządzeniu
mobilnym aplikacji w układzie biurkowym, nawet jeśli jej wygląd pozostawi wiele do życzenia, a sama obsługa
będzie wymagała przybliżania i przewijania zawartości na małym ekranie. Pewne urządzenia mobilne pozwalają
na podłączanie większych monitorów, a to rzadko będzie wykryte przez stosowany na platformie ASP.NET
mechanizm przeznaczony do identyfikacji urządzeń mobilnych. Zawsze powinieneś oferować użytkownikom
urządzeń mobilnych możliwość wyboru, który układ chcą wyświetlać na ekranie.
Wprawdzie żaden z wymienionych problemów nie uniemożliwia wdrożenia aplikacji, ale z pewnością będą
źródłem frustracji dla użytkowników aplikacji w wersji mobilnej. Zapewnienie obsługi urządzeń mobilnych to ważna
kwestia dla każdej nowoczesnej aplikacji sieciowej. Powinieneś dołożyć starań, aby tej kategorii użytkowników
zapewnić dobre wrażenia podczas używania Twojej aplikacji.
271
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Ostatnia zmiana polega na utworzeniu przeznaczonego dla urządzeń mobilnych widoku odpowiedzialnego
za wygenerowanie podsumowania produktów. W katalogu Views/Shared utwórz plik o nazwie
ProductSummary.Mobile.cshtml i umieść w nim zawartość przedstawioną na listingu 10.11.
Listing 10.11. Zawartość pliku ProductSummary.Mobile.cshtml
@model SportsStore.Domain.Entities.Product
<div data-role="collapsible" data-collapsed="false" data-content-theme="c">
<h2>
@Model.Name
</h2>
<div class="ui-grid-b">
<div class="ui-block-a">
@Model.Description
</div>
<div class="ui-block-b">
<strong>(@Model.Price.ToString("c"))</strong>
</div>
<div class="ui-block-c">
@using (Html.BeginForm("AddToCart", "Cart")) {
@Html.HiddenFor(x => x.ProductID)
@Html.Hidden("returnUrl", Request.Url.PathAndQuery)
<input type="submit" data-inline="true"
data-mini="true" value="Kup teraz" />
}
</div>
</div>
</div>
Ten widok używa widżetu jQuery Mobile, aby pozwolić użytkownikom na wyświetlanie i ukrywanie
obszarów zawartości. Ponadto zmieniliśmy tekst przycisku na Kup teraz, aby mieścił się na ekranie smartfona.
Nie jest to idealny sposób prezentacji informacji o produktach, ale za to jest prosty i pozwolił mi na położenie
nacisku w tym punkcie na zawartość przeznaczoną dla urządzeń mobilnych, a nie na bibliotekę jQuery
Mobile. Efekt użycia nowego widoku pokazano na rysunku 10.9.
W rzeczywistym projekcie utworzyłbym oczywiście przeznaczone dla urządzeń mobilnych wersje
widoków odpowiedzialnych za wyświetlanie łączy stronicowania, koszyka na zakupy i formularza składania
zamówienia. Nie zrobiłem tego w omawianej aplikacji, ponieważ na podstawie wprowadzonych dotąd zmian
przekonałeś się, jak platforma MVC pomaga w obsłudze urządzeń mobilnych.
Podsumowanie
W tym rozdziale przedstawiłem dwie techniki przeznaczone do obsługi urządzeń mobilnych: układ
responsywny i tworzenie zawartości przeznaczonej dla urządzeń mobilnych. Układ responsywny nie jest
bezpośrednio powiązany z platformą MVC, która wysyła tę samą zawartość dla wszystkich przeglądarek
internetowych i pozwala im na określenie, jak obsłużyć otrzymane dane. Jak pokazałem w rozdziale, istnieją
pewne ograniczenia w sposobie działania widoków. To wymaga dokładnego przemyślenia rozwiązania
i użycia pewnych funkcji silnika Razor, aby ułatwić sobie cały proces.
Utworzenie zawartości przeznaczonej specjalnie dla urządzeń mobilnych to zadanie, w realizacji którego
platforma MVC aktywnie uczestniczy przez automatyczne stosowanie widoków i układów mobilnych, o ile są
dostępne, oraz bezproblemowe wykorzystywanie ich w procesie generowania kodu HTML dla klientów.
W kolejnym rozdziale dodamy podstawowe funkcje niezbędne do administrowania katalogiem produktów
w aplikacji SportsStore.
272
ROZDZIAŁ 10.  SPORTSSTORE — WERSJA MOBILNAI
Rysunek 10.9. Efekt użycia widoku przeznaczonego specjalnie dla urządzeń mobilnych
273
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
274
ROZDZIAŁ 11.

SportsStore — administracja
W tym rozdziale będziemy kontynuować budowę aplikacji SportsStore i zapewnimy administratorom witryny
możliwość zarządzania katalogiem produktów. Dodamy funkcje tworzenia, edytowania i usuwania elementów
z repozytorium produktów, jak również przesyłania i wyświetlania zdjęć produktów w katalogu.
Dodajemy zarządzanie katalogiem
Zazwyczaj w przypadku aplikacji zarządzającej kolekcją elementów użytkownik ma do dyspozycji dwa ekrany
— stronę z listą i stronę edycji, jak pokazano na rysunku 11.1.
Rysunek 11.1. Szkic interfejsu użytkownika typu CRUD dla katalogu produktów
Dzięki nim użytkownicy mogą tworzyć, odczytywać, modyfikować i usuwać elementy z tej kolekcji.
Jak wspomniałem w jednym z wcześniejszych rozdziałów, akcje te są często określane jako CRUD.
Programiści muszą implementować operacje CRUD tak często, że w Visual Studio zaoferowano możliwość
generowania kontrolerów MVC posiadających akcje dla operacji CRUD oraz odpowiednie szablony widoku.
Jednak podobnie jak w przypadku wszystkich szablonów Visual Studio, uważam, że lepszym rozwiązaniem
jest nauczenie się, jak bezpośrednio korzystać z funkcji platformy MVC.
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Tworzenie kontrolera CRUD
Do obsługi funkcji administracyjnych utworzymy nowy kontroler. Kliknij prawym przyciskiem myszy katalog
Controllers w projekcie SportsStore.WebUI i wybierz Dodaj/Kontroler… z menu kontekstowego. Wybierz szablon
Kontroler MVC 5 - pusty, jako nazwę kontrolera wpisz AdminController i kliknij przycisk Dodaj, aby utworzyć
plik Controllers/AdminController.cs. Następnie zmodyfikuj kod kontrolera, aby odpowiadał
przedstawionemu na listingu 11.1.
Listing 11.1. Zawartość pliku AdminController.cs
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
namespace SportsStore.WebUI.Controllers {
public class AdminController : Controller {
private IProductRepository repository;
public AdminController(IProductRepository repo) {
repository = repo;
}
public ViewResult Index() {
return View(repository.Products);
}
}
}
Konstruktor kontrolera deklaruje zależność od interfejsu IProductRepository, która zostanie rozwiązana
przez Ninject w chwili tworzenia egzemplarza. Kontroler ma zdefiniowaną pojedynczą metodę akcji o nazwie
Index wywołującą metodę View w celu wyboru domyślnego widoku dla akcji. Metodzie View przekazywany jest
zbiór produktów w bazie danych — to będzie model widoku.
Test jednostkowy — akcja Index
Metoda Index w kontrolerze Admin powinna prawidłowo zwracać obiekty Product znajdujące się w repozytorium.
Możemy to przetestować przez utworzenie imitacji repozytorium i porównanie danych testowych z danymi
zwróconymi przez metodę akcji. Poniżej przedstawiono test jednostkowy umieszczony w pliku o nazwie
AdminTests.cs w projekcie SportsStore.UnitTests:
using
using
using
using
using
using
using
using
using
Microsoft.VisualStudio.TestTools.UnitTesting;
Moq;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
SportsStore.WebUI.Controllers;
System;
System.Collections.Generic;
System.Linq;
System.Web.Mvc;
namespace SportsStore.UnitTests {
[TestClass]
public class AdminTests {
[TestMethod]
public void Index_Contains_All_Products() {
276
ROZDZIAŁ 11.  SPORTSSTORE — ADMINISTRACJA
// przygotowanie — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"},
new Product {ProductID = 3, Name = "P3"},
});
// przygotowanie — utworzenie kontrolera
AdminController target = new AdminController(mock.Object);
// działanie
Product[] result = ((IEnumerable<Product>)target.Index().
ViewData.Model).ToArray();
// asercje
Assert.AreEqual(result.Length, 3);
Assert.AreEqual("P1", result[0].Name);
Assert.AreEqual("P2", result[1].Name);
Assert.AreEqual("P3", result[2].Name);
}
}
}
Tworzenie nowego pliku układu
Dla widoków administracyjnych aplikacji zastosujemy nowy plik układu silnika Razor. Będzie to prosty układ
tworzący pojedyncze miejsce pozwalające na wprowadzanie zmian do wszystkich widoków
administracyjnych.
Aby utworzyć ten układ, kliknij prawym przyciskiem myszy katalog Views/Shared w projekcie
SportsStore.WebUI, a następnie wybierz opcję Dodaj/Strona układu MVC 5 (Razor) i nazwij go
_AdminLayout.cshtml (nie zapomnij o znaku podkreślenia na początku). Kliknij przycisk Dodaj, aby utworzyć
plik Views/Shared/_AdminLayout.cshtml. Zawartość nowego pliku dopasuj do przedstawionej na listingu 11.2.
 Uwaga Jak już wcześniej wspomniałem, istnieje konwencja, według której nazwy układów zaczynają się od
podkreślenia (_). Silnik Razor jest używany również w innej technologii firmy Microsoft, WebMatrix, w której używa
się podkreślenia do zablokowania możliwości przesyłania plików układu do przeglądarki. MVC nie wymaga takiego
zabezpieczenia, ale konwencja nazewnictwa układów została przeniesiona do aplikacji MVC.
Listing 11.2. Zawartość pliku _AdminLayout.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
277
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
<link href="~/Content/ErrorStyles.css" rel="stylesheet" />
<title></title>
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>
W kodzie umieściłem wywołanie metody RenderBody, aby zawartość widoku wykorzystującego ten układ była
wstawiana do odpowiedzi generowanej przez serwer. (Nie musiałbym tego robić, gdybym użył opcji Dodaj/Nowy
element… i pozwolił Visual Studio na przygotowanie szablonu układu. Zdecydowałem się jednak na użycie skrótu
i bezpośrednie utworzenie widoku, co oznacza konieczność edycji nowego pliku, aby uzyskać wymaganą zawartość).
W kodzie układu umieściłem również elementy <link> odpowiedzialne za wczytanie plików Bootstrap oraz arkusza
stylów CSS przygotowanego do wyróżniania błędów wykrytych podczas kontroli poprawności danych.
Implementowanie widoku listy
Po utworzeniu pliku układu możemy dodać do projektu widok dla metody akcji Index kontrolera Admin.
Wprawdzie nie jestem fanem funkcji szkieletów kodu i szablonu Visual Studio, ale dla metody Index utworzę widok,
wykorzystując szkielet kodu, aby pokazać taką możliwość. Nawet jeżeli ja nie lubię domyślnie generowanego kodu,
to nie oznacza, że nie powinieneś z niego korzystać.
W projekcie SportsStore.WebUI kliknij prawym przyciskiem myszy katalog Views/Admin i wybierz opcję
Dodaj/Widok… z menu kontekstowego. Ustaw nazwę widoku na Index, wybierz szablon List (w tej liście
rozwijanej zwykle wybieram opcję Empty), wskaż Product jako klasę modelu. Ponadto zaznacz opcję użycia
strony układu i wybierz plik _AdminLayout.cshtml z katalogu Views/Shared. Wszystkie opcje
konfiguracyjne pokazano na rysunku 11.2.
Rysunek 11.2. Tworzenie widoku Index
 Uwaga Gdy używamy szablonu List, Visual Studio zakłada, że korzystamy z sekwencji IEnumerable obiektów
typu widoku modelu, więc możemy po prostu wybrać pojedynczą klasę z listy.
278
ROZDZIAŁ 11.  SPORTSSTORE — ADMINISTRACJA
Kliknij przycisk Dodaj, aby utworzyć widok. Szkielet widoku utworzony przez Visual Studio jest zamieszczony
na listingu 11.3 (zawartość pliku została nieco uporządkowana, aby była czytelna na stronie książki).
Listing 11.3. Zawartość pliku Views/Admin/Index.cshtml
@model IEnumerable<SportsStore.Domain.Entities.Product>
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Utwórz nowy", "Create")
</p>
<table>
<tr>
<th>@Html.DisplayNameFor(model => model.Name)</th>
<th>@Html.DisplayNameFor(model => model.Description)</th>
<th>@Html.DisplayNameFor(model => model.Price)</th>
<th>@Html.DisplayNameFor(model => model.Category)</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>@Html.DisplayFor(modelItem => item.Name) </td>
<td>@Html.DisplayFor(modelItem => item.Description) </td>
<td>@Html.DisplayFor(modelItem => item.Price) </td>
<td>@Html.DisplayFor(modelItem => item.Category) </td>
<td>
@Html.ActionLink("Edytuj", "Edit", new { id=item.ProductID }) |
@Html.ActionLink("Szczegóły", "Details", new { id=item.ProductID }) |
@Html.ActionLink("Usuń", "Delete", new { id=item.ProductID })
</td>
</tr>
}
</table>
Visual Studio sprawdza typ obiektu widoku modelu i generuje w tabeli elementy odpowiadające
właściwościom zdefiniowanym w obiekcie. Widok ten można wyświetlić po uruchomieniu aplikacji
i przejściu do adresu URL /Admin/Index, jak pokazano na rysunku 11.3.
Szkielet widoku jest całkiem niezłym sposobem na przygotowanie solidnych podstaw dla widoku.
Mamy kolumny dla każdej z właściwości klasy Product oraz łącza do innych operacji CRUD, które odwołują
się do metod akcji w kontrolerze Admin. (Ponieważ wymieniony kontroler został utworzony bez użycia szkieletu,
metody akcji nie istnieją).
Zastosowanie szkieletu jest użyteczne, ale generowane w ten sposób widoki pozostają nijakie i na tyle ogólne,
że są bezużyteczne w projektach o dowolnym poziomie skomplikowania. Moja rada brzmi, aby rozpoczynać pracę
od utworzenia pustych kontrolerów, widoków i układów, a dopiero później dodawać wymaganą funkcjonalność,
gdy okaże się to konieczne.
Powracając do podejścia w stylu „zrób to sam”, przeprowadź edycję pliku Index.cshtml, aby jego zawartość
odpowiadała kodowi przedstawionemu na listingu 11.4.
279
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 11.3. Gotowy szkielet widoku listy
Listing 11.4. Zmodyfikowany widok Index.cshtml
@model IEnumerable<SportsStore.Domain.Entities.Product>
@{
ViewBag.Title = "Administracja: Wszystkie produkty";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<div class="panel panel-default">
<div class="panel-heading">
<h3>Wszystkie produkty</h3>
</div>
<div class="panel-body">
<table class="table table-striped table-condensed table-bordered">
<tr>
<th>ID</th>
<th>Nazwa</th>
<th class="NumericCol">Cena</th>
<th>Akcje</th>
</tr>
@foreach (var item in Model) {
<tr>
<td>@item.ProductID</td>
<td>@Html.ActionLink(item.Name, "Edit", new { item.ProductID })</td>
<td class="NumericCol">@item.Price.ToString("c")</td>
<td>
@using (Html.BeginForm("Delete", "Admin")) {
@Html.Hidden("ProductID", item.ProductID)
<input type="submit" value="Usuń"/>
}
</td>
</tr>
}
280
ROZDZIAŁ 11.  SPORTSSTORE — ADMINISTRACJA
</table>
</div>
<div class="panel-footer">
@Html.ActionLink("Dodaj nowy produkt", "Create", null,
new { @class = "btn btn-default" })
</div>
</div>
Widok ten prezentuje informacje w nieco bardziej zwięzłej postaci — zostały pominięte niektóre
właściwości klasy Product, a także zastosowano style zdefiniowane przez Bootstrap. Nowy wygląd jest
przedstawiony na rysunku 11.4.
Rysunek 11.4. Wygenerowany zmodyfikowany widok Index
Mamy już nieźle wyglądającą stronę z listą. Administrator może teraz przeglądać produkty w katalogu
i ma do dyspozycji łącza oraz przyciski pozwalające na dodawanie, usuwanie i przeglądanie elementów. W kolejnych
punktach dodamy kod umożliwiający wykonanie każdej z tych operacji.
Test jednostkowy — metoda akcji Edit
W metodzie akcji Edit chcemy przetestować dwie operacje. Po pierwsze, chcemy wiedzieć, czy otrzymamy
oczekiwany produkt, gdy podamy prawidłową wartość identyfikatora. Oczywiście musimy mieć pewność, że będziemy
modyfikować ten produkt, którego oczekiwaliśmy. Po drugie, chcemy upewnić się, że nie otrzymamy żadnego
produktu, jeżeli zażądamy wartości identyfikatora, którego nie ma w repozytorium. Poniżej przedstawiono metody
testowe, które trzeba umieścić w pliku AdminTests.cs:
...
[TestMethod]
281
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
public void Can_Edit_Product() {
// przygotowanie — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"},
new Product {ProductID = 3, Name = "P3"},
});
// przygotowanie — utworzenie kontrolera
AdminController target = new AdminController(mock.Object);
// działanie
Product p1 = target.Edit(1).ViewData.Model as Product;
Product p2 = target.Edit(2).ViewData.Model as Product;
Product p3 = target.Edit(3).ViewData.Model as Product;
// asercje
Assert.AreEqual(1, p1.ProductID);
Assert.AreEqual(2, p2.ProductID);
Assert.AreEqual(3, p3.ProductID);
}
[TestMethod]
public void Cannot_Edit_Nonexistent_Product() {
// przygotowanie — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"},
new Product {ProductID = 3, Name = "P3"},
});
// przygotowanie — utworzenie kontrolera
AdminController target = new AdminController(mock.Object);
// działanie
Product result = (Product)target.Edit(4).ViewData.Model;
// asercje
Assert.IsNull(result);
}
...
Edycja produktów
Aby zrealizować funkcje tworzenia i aktualizacji, utworzymy stronę edycji produktu podobną do pokazanej
na rysunku 11.1. Zadanie to jest dwuczęściowe:
 wyświetlenie strony pozwalającej administratorowi na zmianę wartości właściwości produktu,
 dodanie metody akcji umożliwiającej przetwarzanie tych zmian po przesłaniu danych.
282
ROZDZIAŁ 11.  SPORTSSTORE — ADMINISTRACJA
Tworzenie metody akcji Edit
Na listingu 11.5 pokazana jest metoda Edit, którą trzeba dodać do klasy AdminController. Jest to metoda akcji,
której użyliśmy w wywołaniach metody pomocniczej Html.ActionLink w widoku Index.
Listing 11.5. Dodanie metody akcji Edit do pliku AdminControllers.cs
using
using
using
using
System.Linq;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Controllers {
public class AdminController : Controller {
private IProductRepository repository;
public AdminController(IProductRepository repo) {
repository = repo;
}
public ViewResult Index() {
return View(repository.Products);
}
public ViewResult Edit(int productId) {
Product product = repository.Products
.FirstOrDefault(p => p.ProductID == productId);
return View(product);
}
}
}
Ta prosta metoda wyszukuje produkt z identyfikatorem odpowiadającym wartości parametru productId
i przekazuje go metodzie View jako obiekt modelu widoku.
Tworzenie widoku edycji
Po zdefiniowaniu metody akcji możemy utworzyć dla niej widok. Kliknij prawym przyciskiem myszy katalog
Views/Admin w oknie Eksplorator rozwiązania, a następnie wybierz Dodaj/Strona widoku MVC 5 (Razor).
Nowemu plikowi widoku nadaj nazwę Edit.cshtml i kliknij przycisk Dodaj, tworząc w ten sposób plik.
Teraz zawartość pliku zmodyfikuj tak, aby odpowiadała przedstawionej na listingu 11.6.
Listing 11.6. Zawartość pliku Edit.cshtml
@model SportsStore.Domain.Entities.Product
@{
ViewBag.Title = "Administracja: edycja " + @Model.Name;
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<h1>Edycja @Model.Name</h1>
@using (Html.BeginForm()) {
@Html.EditorForModel()
<input type="submit" value="Zapisz" />
@Html.ActionLink("Anuluj i wróć do listy ", "Index")
}
283
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Zamiast ręcznie tworzyć kod dla każdej etykiety i pola wprowadzania danych, wywołaliśmy metodę
pomocniczą Html.EditorForModel. Metoda ta powoduje wygenerowanie interfejsu edycji przez platformę MVC,
co jest realizowane przez analizę typu modelu — w tym przypadku klasy Product. Aby zobaczyć stronę
wygenerowaną za pomocą widoku Edit, uruchom aplikację i przejdź do /Admin/Index. Kliknij jedną z nazw
produktów — wyświetli się strona pokazana na rysunku 11.5.
Rysunek 11.5. Strona wygenerowana za pomocą metody pomocniczej EditorForModel
Bądźmy szczerzy — metoda EditorForModel jest wygodna, ale nie generuje zbyt atrakcyjnych wyników.
Dodatkowo nie chcemy, aby administrator widział i mógł zmieniać atrybut ProductID; ponadto pole tekstowe
dla właściwości Description jest o wiele za małe.
Możemy przekazać platformie MVC wskazówki na temat sposobu tworzenia edytorów dla właściwości
przez użycie metadanych modelu. Pozwala to nam zastosować atrybuty właściwości i wpłynąć na wynik działania
metody Html.EditorForModel. Na listingu 11.7 pokazane jest wykorzystanie metadanych w klasie Product,
znajdującej się w projekcie SportsStore.Domain.
Listing 11.7. Użycie metadanych modelu w pliku Product.cs
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace SportsStore.Domain.Entities {
public class Product {
[HiddenInput(DisplayValue=false)]
public int ProductID { get; set; }
[Display(Name="Nazwa")]
public string Name { get; set; }
[DataType(DataType.MultilineText), Display(Name="Opis")]
public string Description { get; set; }
[Display(Name="Cena")]
public decimal Price { get; set; }
284
ROZDZIAŁ 11.  SPORTSSTORE — ADMINISTRACJA
[Display(Name="Kategoria")]
public string Category { get; set; }
}
}
Atrybut HiddenInput informuje platformę MVC o konieczności wygenerowania dla właściwości ukrytego
elementu formularza, a atrybut DataType pozwala zdefiniować sposób prezentowania i edytowania wartości.
W tym przypadku wybraliśmy opcję MultilineText. Atrybut HiddenInput wchodzi w skład przestrzeni nazw
System.Web.Mvc, natomiast atrybut DataType jest częścią przestrzeni nazw
System.ComponentModel.DataAnnotations. Teraz już wiesz, dlaczego w rozdziale 7. musiałeś dodać do projektu
SportsStore.Domain odniesienia do wymienionych podzespołów.
Na rysunku 11.6 przedstawiona jest strona edycji po zastosowaniu metadanych modelu. Właściwość ProductId
nie jest już wyświetlana, a do wprowadzenia opisu służy wielowierszowe pole tekstowe. Jednak interfejs użytkownika
nadal wygląda nieciekawie.
Rysunek 11.6. Efekt zastosowania metadanych
Problem polega na tym, że metoda pomocnicza Html.EditorForModel nie ma żadnej wiedzy o klasie
Product i generuje pewien podstawowy oraz bezpieczny kod HTML. Mamy trzy sposoby na rozwiązanie
problemu. Pierwszy to zdefiniowanie stylów CSS dla zawartości generowanej przez wymienioną metodę
pomocniczą. Takie podejście jest łatwiejsze dzięki klasom automatycznie dodawanym do elementów
HTML przez platformę MVC.
Jeśli spojrzysz na źródło strony pokazanej na rysunku 11.6, to zauważysz, że element textarea
utworzony dla opisu produktu ma przypisaną klasę CSS "text-box-multi-line":
<textarea class="text-box multi-line" id="Description" name="Description">
Innym elementom HTML również są przypisane podobne klasy, a więc możemy poprawić wygląd widoku
Edit przez zdefiniowanie dla nich stylów CSS. Takie podejście sprawdza się doskonale podczas tworzenia własnych
stylów, ale nie ułatwia stosowania istniejących klas, takich jak zdefiniowanych w bibliotece Bootstrap.
Drugie podejście polega na przygotowaniu metody pomocniczej wraz z szablonami, które można wykorzystać
do wygenerowania elementów wraz z wymaganymi przez nie stylami. Takie rozwiązanie poznasz w rozdziale 22.
Trzecie podejście to bezpośrednie utworzenie niezbędnych elementów bez użycia metody pomocniczej na
poziomie modelu. Lubię idee metody pomocniczej, ale rzadko ją stosuję. Preferuję samodzielne tworzenie kodu
HTML i używanie metod pomocniczych dla poszczególnych właściwości, jak przedstawiono na listingu 11.8.
285
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 11.8. Uaktualnienie pliku Edit.cshtml
@model SportsStore.Domain.Entities.Product
@{
ViewBag.Title = "Administracja: edycja " + @Model.Name;
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<div class="panel">
<div class="panel-heading">
<h3>Edycja @Model.Name</h3>
</div>
@using (Html.BeginForm()) {
<div class="panel-body">
@Html.HiddenFor(m => m.ProductID)
@foreach (var property in ViewData.ModelMetadata.Properties) {
if (property.PropertyName != "ProductID") {
<div class="form-group">
<label>@(property.DisplayName ?? property.PropertyName)</label>
@if (property.PropertyName == "Description") {
@Html.TextArea(property.PropertyName, null,
new { @class = "form-control", rows = 5 })
} else {
@Html.TextBox(property.PropertyName, null,
new { @class = "form-control" })
}
</div>
}
}
</div>
<div class="panel-footer">
<input type="submit" value="Zapisz" class="btn btn-primary"/>
@Html.ActionLink("Anuluj i wróć do listy", "Index", null, new {
@class = "btn btn-default"
})
</div>
}
</div>
To jest wariant techniki dodania metadanych, którą wykorzystaliśmy w rozdziale 9. Takie rozwiązanie
często stosuję we własnych projektach, nawet jeśli podobny wynik mógłbym uzyskać za pomocą metod
pomocniczych HTML wraz z omówionymi w rozdziale 22. technikami dostosowania tych metod do własnych
potrzeb. Istnieje coś przyjemnego w powyższym podejściu, co cementuje stosowany przeze mnie styl
programowania. Jednak podobnie jak w przypadku wielu innych zadań, platforma MVC oferuje wiele
różnych podejść możliwych do zastosowania, jeśli przetwarzanie metadanych uznasz za rozwiązanie
nieodpowiednie dla Twoich potrzeb. Zmodyfikowany widok pokazano na rysunku 11.7.
Aktualizowanie repozytorium produktów
W celu zrealizowania operacji edycji musimy rozszerzyć repozytorium produktów, dodając możliwość zapisu
zmian. Na początek dodamy do interfejsu IProductRepository nową metodę, zamieszczoną na listingu 11.9.
(Przypominam, że wymieniony interfejs znajduje się w katalogu Abstract projektu SportsStore.Domain).
286
ROZDZIAŁ 11.  SPORTSSTORE — ADMINISTRACJA
Rysunek 11.7. Wyświetlenie strony edytora produktów
Listing 11.9. Dodawanie metody do interfejsu repozytorium w pliku IProductRepository.cs
using System.Collections.Generic;
using SportsStore.Domain.Entities;
namespace SportsStore.Domain.Abstract {
public interface IProductRepository {
IEnumerable<Product> Products { get; }
void SaveProduct(Product product);
}
}
Następnie możemy dodać tę metodę do naszej implementacji repozytorium zdefiniowanej w pliku
Concrete/EFProductRepository.cs, jak pokazano na listingu 11.10.
Listing 11.10. Implementacja metody SaveProduct w pliku EFProductRepository.cs
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using System.Collections.Generic;
namespace SportsStore.Domain.Concrete {
public class EFProductRepository : IProductRepository {
private EFDbContext context = new EFDbContext();
public IEnumerable<Product> Products {
get { return context.Products; }
}
287
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
public void SaveProduct(Product product) {
if (product.ProductID == 0) {
context.Products.Add(product);
} else {
Product dbEntry = context.Products.Find(product.ProductID);
if (dbEntry != null) {
dbEntry.Name = product.Name;
dbEntry.Description = product.Description;
dbEntry.Price = product.Price;
dbEntry.Category = product.Category;
}
}
context.SaveChanges();
}
}
}
Implementacja metody SaveChanges dodaje produkt do repozytorium, jeżeli wartością ProductId jest 0;
w przeciwnym razie zapisuje zmiany do istniejącego produktu.
Nie będę się tutaj zagłębiał w szczegóły platformy Entity Framework, ponieważ — jak już wcześniej
wspomniałem — jest to odrębny temat, jednak w metodzie SaveProduct jest coś, co ma wpływ na projekt
aplikacji MVC.
Wiemy, że musimy przeprowadzić uaktualnienie po otrzymaniu parametru Product, którego ProductID
ma wartość inną niż zero. W tym celu pobieramy z repozytorium obiekt Product o takiej samej wartości
ProductID, a następnie uaktualniamy wszystkie jego właściwości tak, aby odpowiadały obiektowi parametru.
Robimy tak, ponieważ platforma Entity Framework śledzi obiekty tworzone z bazy danych. Obiekt
przekazywany metodzie SaveChanges jest utworzony przez platformę Entity Framework za pomocą
domyślnego łącznika modelu. Dlatego też Entity Framework nic nie wie o obiekcie parametru i tym samym
nie uaktualni bazy danych. Istnieje wiele sposobów rozwiązania tego problemu, w omawianej aplikacji
zastosowano najprostszy — polega on na odszukaniu odpowiedniego obiektu znanego platformie Entity
Framework i jego wyraźnym uaktualnieniu.
Alternatywne podejście polega na utworzeniu własnego łącznika modelu odpowiedzialnego jedynie za
pobieranie obiektów z repozytorium. To może wydawać się eleganckim rozwiązaniem, ale wymaga dodania
do interfejsu repozytorium funkcji wyszukiwania, aby było możliwe wyszukiwanie obiektów Product na
podstawie ich wartości ProductID.
Obsługa żądań POST w widoku edycji
W tym momencie jesteśmy gotowi do zaimplementowania przeciążonej metody akcji Edit, która będzie obsługiwała
żądania POST wysyłane w momencie kliknięcia przycisku Zapisz przez administratora. Nowa metoda jest
zamieszczona na listingu 11.11.
Listing 11.11. Dodanie do pliku AdminControllers.cs metody akcji Edit obsługującej żądania POST
using
using
using
using
System.Linq;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Controllers {
public class AdminController : Controller {
private IProductRepository repository;
public AdminController(IProductRepository repo) {
repository = repo;
}
288
ROZDZIAŁ 11.  SPORTSSTORE — ADMINISTRACJA
public ViewResult Index() {
return View(repository.Products);
}
public ViewResult Edit(int productId) {
Product product = repository.Products
.FirstOrDefault(p => p.ProductID == productId);
return View(product);
}
[HttpPost]
public ActionResult Edit(Product product) {
if (ModelState.IsValid) {
repository.SaveProduct(product);
TempData["message"] = string.Format("Zapisano {0} ",product.Name);
return RedirectToAction("Index");
} else {
// błąd w wartościach danych
return View(product);
}
}
}
}
Poprzez odczyt wartości właściwości ModelState.IsValid upewniamy się, że łącznik modelu ma możliwość
kontroli poprawności danych przesłanych przez użytkownika. Jeżeli wszystko jest w porządku, zapisujemy
zmiany do repozytorium, a następnie wywołujemy metodę akcji Index, co pozwala wrócić do listy produktów.
Jeżeli w danych został znaleziony błąd, ponownie generujemy widok Edit, dzięki czemu użytkownik może
wprowadzić poprawki.
Gdy zmiany zostaną zapisane w repozytorium, zapisujemy komunikat, wykorzystując funkcję TempData.
Jest to słownik klucz-wartość podobny do danych sesji oraz używanej wcześniej funkcji ViewBag. Kluczową
różnicą jest to, że zawartość TempData jest usuwana na końcu żądania HTTP.
Zwróć uwagę, że metoda Edit zwraca wartość typu ActionResult. Do tej pory korzystaliśmy z typu ViewResult.
Klasa ViewResult dziedziczy po ActionResult i jest używana, gdy platforma ma wygenerować widok. Dostępne
są również inne typy ActionResult, a jeden z nich jest zwracany przez metodę RedirectToAction. Korzystamy
z niej w metodzie akcji Edit do wywołania metody akcji Index. Zbiór wyników akcji zostanie omówiony
w rozdziale 17.
Ne możemy użyć w tym przypadku ViewBag, ponieważ użytkownik wykonuje przekierowanie. Kontener
ViewBag przekazuje dane pomiędzy kontrolerem a widokiem i nie może przechowywać danych dłużej, niż trwa
obsługa bieżącego żądania HTTP. Moglibyśmy wykorzystać dane sesji, ale komunikat taki byłby stale
przechowywany do momentu jego jawnego usunięcia, czego chcemy uniknąć. Dlatego kontener TempData
doskonale się tu sprawdza. Dane takie są ograniczone do jednej sesji użytkownika (dzięki czemu jeden użytkownik
nie widzi danych innych użytkowników) i są przechowywane do momentu ich odczytania. Odczytamy te dane
w widoku generowanym przez metodę akcji, do której przekierujemy użytkownika.
Test jednostkowy — przesyłanie danych edycji
W przypadku metody akcji Edit przetwarzającej żądania POST musimy upewnić się, że zmiany obiektu Product,
generowane przez łącznik modelu, są przekazywane do repozytorium produktów w celu zapisania. Chcemy również
sprawdzić, czy nieudane aktualizacje — w których wystąpiły błędy modelu — nie są przekazywane do repozytorium.
Implementacje metod testowych są następujące:
...
[TestMethod]
289
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
public void Can_Save_Valid_Changes() {
// przygotowanie — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
// przygotowanie — tworzenie kontrolera
AdminController target = new AdminController(mock.Object);
// przygotowanie — tworzenie produktu
Product product = new Product {Name = "Test"};
// działanie — próba zapisania produktu
ActionResult result = target.Edit(product);
// asercje — sprawdzenie, czy zostało wywołane repozytorium
mock.Verify(m => m.SaveProduct(product));
// asercje — sprawdzenie typu zwracanego z metody
Assert.IsNotInstanceOfType(result, typeof(ViewResult));
}
[TestMethod]
public void Cannot_Save_Invalid_Changes() {
// przygotowanie — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
// przygotowanie — tworzenie kontrolera
AdminController target = new AdminController(mock.Object);
// przygotowanie — tworzenie produktu
Product product = new Product { Name = "Test" };
// przygotowanie — dodanie błędu do stanu modelu
target.ModelState.AddModelError("error", "error");
// działanie — próba zapisania produktu
ActionResult result = target.Edit(product);
}
...
// asercje — sprawdzenie, czy nie zostało wywołane repozytorium
mock.Verify(m => m.SaveProduct(It.IsAny<Product>()), Times.Never());
// asercje — sprawdzenie typu zwracanego z metody
Assert.IsInstanceOfType(result, typeof(ViewResult));
Wyświetlanie komunikatu potwierdzającego
W pliku układu _AdminLayout.cshtml możemy teraz obsłużyć komunikat zapisany wcześniej w TempData.
Obsługując komunikaty w szablonie, możemy je tworzyć w dowolnym widoku korzystającym z szablonu bez
konieczności tworzenia dodatkowych bloków kodu Razor. Zmiany konieczne do wprowadzenia w pliku są
zamieszczone na listingu 11.12.
Listing 11.12. Obsługa w pliku _AdminLayout.cshtml komunikatu z ViewBag w pliku układu
@{
Layout = null;
}
<!DOCTYPE html>
<html>
290
ROZDZIAŁ 11.  SPORTSSTORE — ADMINISTRACJA
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<link href="~/Content/ErrorStyles.css" rel="stylesheet" />
<title></title>
</head>
<body>
<div>
@if (TempData["message"] != null) {
<div class="alert alert-success">@TempData["message"]</div>
}
@RenderBody()
</div>
</body>
</html>
 Wskazówka Jedną z zalet obsługi komunikatu w pliku szablonu jest to, że użytkownik zobaczy komunikat
niezależnie od rodzaju strony wygenerowanej po jego zapisaniu. W tym przypadku wracamy do listy produktów,
ale możemy zmienić przebieg działania programu i wygenerować inny widok — użytkownik i tak zobaczy
komunikat (o ile następny widok korzysta z tego samego układu).
Mamy już wszystkie elementy potrzebne do przetestowania edycji produktów. Uruchom aplikację, przejdź
do adresu Admin/Index i wprowadź kilka zmian. Kliknij przycisk Zapisz. Spowoduje to powrót do widoku listy
z wyświetlonym komunikatem zapisanym w TempData, jak pokazano na rysunku 11.8.
Rysunek 11.8. Edycja produktu i wyświetlenie komunikatu z TempData
291
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Komunikat zniknie po odświeżeniu listy produktów, ponieważ dane TempData są usuwane po ich odczytaniu.
Jest to bardzo wygodne, ponieważ nie chcemy, aby stare komunikaty pozostawały na stronie.
Dodanie kontroli poprawności modelu
W większości projektów musimy dodać zasady kontroli poprawności do encji modelu. W tym momencie
administrator może wpisać ujemne ceny lub puste opisy, a aplikacja SportsStore zapisze te informacje w bazie
danych. To, czy nieprawidłowe dane będą przechowywane, zależy od ich zgodności z ograniczeniami
nałożonymi na tabele bazy danych w trakcie ich tworzenia w rozdziale 7. Na listingu 11.13 przedstawiony
jest przykład zastosowania adnotacji danych w klasie Product, podobnie jak zrobiliśmy to dla klasy
ShippingDetails w rozdziale 9.
Listing 11.13. Atrybuty kontroli poprawności w klasie Product
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace SportsStore.Domain.Entities {
public class Product {
[HiddenInput(DisplayValue=false)]
public int ProductID { get; set; }
[Required(ErrorMessage = "Proszę podać nazwę produktu.")]
[Display(Name="Nazwa")]
public string Name { get; set; }
[DataType(DataType.MultilineText), Display(Name="Opis")]
[Required(ErrorMessage = "Proszę podać opis.")]
public string Description { get; set; }
[Required]
[Range(0.01, double.MaxValue, ErrorMessage = "Proszę podać dodatnią cenę.")]
[Display(Name="Cena")]
public decimal Price { get; set; }
[Required(ErrorMessage = "Proszę określić kategorię.")]
[Display(Name="Kategoria")]
public string Category { get; set; }
}
}
Metody pomocnicze Html.TextBox i Html.TextArea użyte w widoku Edit.cshtml do utworzenia elementów
<input> i <textarea> będą wykorzystane przez platformę MVC w celu zasygnalizowania problemów
z poprawnością danych. Wspomniane sygnały są wysyłane za pomocą klas zdefiniowanych w pliku
Content/ErrorStyles.css i powodują wyróżnienie elementów sprawiających problemy. Użytkownikowi
należy przekazać informacje szczegółowe o problemach, które wystąpiły. Odpowiednie zmiany do
wprowadzenia przedstawiono na listingu 11.14.
Listing 11.14. Dodanie do pliku Edit.cs komunikatów procesu kontroli poprawności danych
...
<div class="panel-body">
@foreach (var property in ViewData.ModelMetadata.Properties) {
if (property.PropertyName != "ProductID") {
<div class="form-group">
<label>@(property.DisplayName ?? property.PropertyName)</label>
292
ROZDZIAŁ 11.  SPORTSSTORE — ADMINISTRACJA
@if (property.PropertyName == "Description") {
@Html.TextArea(property.PropertyName, null,
new { @class = "form-control", rows = 5 })
} else {
@Html.TextBox(property.PropertyName, null,
new { @class = "form-control" })
}
@Html.ValidationMessage(property.PropertyName)
</div>
}
}
</div>
...
W rozdziale 9. zastosowałem metodę pomocniczą Html.ValidationSummary do utworzenia skonsolidowanej listy
wszystkich problemów kontroli poprawności danych, jakie wystąpiły w formularzu. W omawianym tutaj
listingu użyłem metody pomocniczej Html.ValidationMessage, która wyświetla komunikat dla pojedynczej
właściwości modelu. Metodę pomocniczą Html.ValidationMessage można umieścić gdziekolwiek w widoku,
ale wedle konwencji (i rozsądku) ulokowanie jej w pobliżu elementu sprawiającego problem z kontrolą
poprawności daje użytkownikowi pewien kontekst. Na rysunku 11.9 przedstawiono komunikaty kontroli
poprawności wyświetlane podczas edycji produktu, gdy wprowadzone zostają dane sprzeczne z regułami
zdefiniowanymi dla klasy Product.
Rysunek 11.9. Kontrola poprawności przy edycji produktów
293
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Aktywowanie kontroli poprawności po stronie klienta
Obecnie kontrola poprawności jest realizowana wyłącznie po przesłaniu danych do serwera. Większość
użytkowników oczekuje natychmiastowej reakcji w przypadku wystąpienia problemów z wprowadzonymi danymi.
Dlatego właśnie programiści sieciowi często chcą przeprowadzać kontrolę poprawności po stronie klienta, gdzie
dane są sprawdzane w przeglądarce z użyciem kodu JavaScript. Platforma MVC może przeprowadzać kontrolę
poprawności po stronie klienta, bazując na adnotacjach danych zastosowanych w klasie modelu domeny.
Funkcja ta jest domyślnie włączona, ale nie jest aktywna, ponieważ nie dodaliśmy odwołań do wymaganych
bibliotek JavaScript. Microsoft zapewnia obsługę kontroli poprawności po stronie klienta w oparciu o bibliotekę
jQuery oraz popularną wtyczkę dla jQuery o nazwie jQuery Validation. Wymienione narzędzia są przez
Microsoft rozbudowane o obsługę atrybutów kontroli poprawności.
Pierwszym krokiem jest instalacja pakietu kontroli poprawności. Z menu Narzędzia wybierz więc opcję
Menedżer pakietów NuGet/Konsola menedżera pakietów, co spowoduje wyświetlenie przez Visual Studio okna
wiersza poleceń menedżera NuGet. Następnie wydaj poniższe polecenia:
Install-Package Microsoft.jQuery.Unobtrusive.Validation -version 3.0.0 -projectname SportsStore.WebUI
 Wskazówka Nie przejmuj się komunikatem informującym, że wskazany pakiet jest już zainstalowany. Visual Studio
dołączy pakiet do projektu, jeśli przypadkowo zaznaczysz opcję Odwołaj się do biblioteki skryptów podczas wykorzystania
szkieletu kodu w trakcie tworzenia widoku.
Kolejnym krokiem jest dodanie elementów <script> odpowiedzialnych za wczytanie plików JavaScript
w kodzie HTML aplikacji. Najlepszym miejscem do dodania tych łączy jest plik _AdminLayout.cshtml, dzięki
czemu kontrola poprawności na kliencie może być realizowana na dowolnej stronie korzystającej z tego
układu. Zmiany konieczne do wprowadzenia w pliku układu są zamieszczone na listingu 11.15.
Listing 11.15. Importowanie w _AdminLayout.cshtml plików JavaScript do realizacji kontroli poprawności
po stronie klienta
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title></title>
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<link href="~/Content/ErrorStyles.css" rel="stylesheet" />
<script src="~/Scripts/jquery-1.9.1.js"></script>
<script src="~/Scripts/jquery.validate.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.js"></script>
</head>
<body>
<div>
@if (TempData["message"] != null) {
<div class="alert alert-success">@TempData["message"]</div>
}
@RenderBody()
</div>
</body>
</html>
294
ROZDZIAŁ 11.  SPORTSSTORE — ADMINISTRACJA
Po dodaniu brakujących bibliotek kontrola poprawności po stronie klienta będzie działała dla wszystkich
naszych widoków administracyjnych. Użytkownik będzie informowany o nieprawidłowych wartościach,
zanim zostanie wysłany formularz. Wygląd komunikatów o błędach jest jednakowy, ponieważ klasy CSS
używane do kontroli poprawności na serwerze są również wykorzystywane do kontroli poprawności na kliencie,
ale reakcja jest natychmiastowa i nie wymaga przesłania żądania na serwer. W większości sytuacji kontrola
poprawności po stronie klienta jest bardzo użyteczną funkcją, ale jeżeli z jakiegoś powodu nie chcemy, aby
była aktywna, możemy umieścić poniższe polecenia w pliku widoku:
...
@{
ViewBag.Title = "Administracja: edycja " + @Model.Name;
Layout = "~/Views/Shared/_AdminLayout.cshtml";
HtmlHelper.ClientValidationEnabled = false;
HtmlHelper.UnobtrusiveJavaScriptEnabled = false;
}
...
Jeżeli umieścimy te polecenia w widoku lub kontrolerze, kontrola poprawności na kliencie zostanie
wyłączona tylko dla bieżącej akcji. Kontrola poprawności po stronie klienta może być zablokowana dla całej
aplikacji przez dodanie poniższych wartości do pliku Web.config:
...
<configuration>
<appSettings>
<add key="ClientValidationEnabled" value="false"/>
<add key="UnobtrusiveJavaScriptEnabled" value="false"/>
</appSettings>
</configuration>
...
Tworzenie nowych produktów
Następnie zdefiniujemy metodę akcji Create, która jest użyta w łączu Dodaj nowy produkt na stronie z listą
produktów. Pozwala ona administratorowi na dodawanie nowych pozycji do katalogu produktów. Zrealizowanie
funkcji tworzenia nowych produktów będzie wymagało tylko niewielkich uzupełnień i jednej małej zmiany
w naszej aplikacji. Jest to świetlny przykład potęgi i elastyczności dobrze przemyślanej aplikacji MVC. Na początek
dodamy metodę Create, pokazaną na listingu 11.16, do klasy AdminController.
Listing 11.16. Dodanie metody akcji Create do kontrolera AdminController
using
using
using
using
System.Linq;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Controllers
{
public class AdminController : Controller
{
private IProductRepository repository;
public AdminController(IProductRepository repo)
{
repository = repo;
}
295
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
// …inne metody akcji zostały pominięte w celu zachowania zwięzłości…
public ViewResult Create() {
return View("Edit", new Product());
}
}
}
Metoda Create nie generuje domyślnego widoku. Zamiast tego określa, że powinien być użyty widok Edit.
Całkowicie akceptowalne jest to, żeby metoda akcji korzystała z widoku, który zwykle jest skojarzony z innym
widokiem. W tym przypadku wstrzykujemy nowy obiekt Product do widoku modelu, dzięki czemu widok Edit
będzie miał puste pola.
 Uwaga Nie dodajemy testu jednostkowego dla tej metody akcji. Gdybyśmy ją dodali, zyskamy jedynie możliwość
przetestowania, czy platforma MVC może przetworzyć obiekt ViewResult zwracany przez metodę akcji, a tego jesteśmy
absolutnie pewni. (Zwykle nie tworzę testów dla frameworków, o ile nie podejrzewam wystąpienia problemu).
Doprowadziło to nas do wymaganej modyfikacji. Zwykle oczekujemy, że formularz przesyła dane do metody
akcji, która go wygenerowała, i takie założenie jest wykorzystane w metodzie Html.BeginForm. Jednak nie będzie
to działało prawidłowo dla naszej metody Create, ponieważ chcemy, aby formularz był przesłany do metody
akcji Edit, gdzie dane nowego produktu zostaną zapisane. Aby to poprawić, możemy użyć przeciążonej
wersji metody pomocniczej Html.BeginForm w celu określenia, że celem formularza jest metoda akcji Edit
z kontrolera Admin, jak pokazano na listingu 11.17. W wymienionym listingu przedstawiono zmiany, które
należy wprowadzić w pliku widoku Views/Admin/Edit.cshtml.
Listing 11.17. Jawne określanie w pliku Edit.cshtml metody akcji i kontrolera w formularzu
@model SportsStore.Domain.Entities.Product
@{
ViewBag.Title = "Admin: Edycja " + @Model.Name;
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<div class="panel">
<div class="panel-heading">
<h3>Edycja @Model.Name</h3>
</div>
@using (Html.BeginForm("Edit", "Admin")) {
<div class="panel-body">
@Html.HiddenFor(m => m.ProductID)
@foreach (var property in ViewData.ModelMetadata.Properties)
{
if (property.PropertyName != "ProductID")
{
<div class="form-group">
<label>@(property.DisplayName ?? property.PropertyName)</label>
@if (property.PropertyName == "Description")
{
@Html.TextArea(property.PropertyName, null,
new { @class = "form-control", rows = 5 })
}
else
296
ROZDZIAŁ 11.  SPORTSSTORE — ADMINISTRACJA
{
@Html.TextBox(property.PropertyName, null,
new { @class = "form-control" })
}
@Html.ValidationMessage(property.PropertyName)
</div>
}
}
</div>
<div class="panel-footer">
<input type="submit" value="Zapisz" class="btn btn-primary" />
@Html.ActionLink("Anuluj i wróć do listy", "Index", null, new
{
@class = "btn btn-default"
})
</div>
}
</div>
Teraz formularz będzie zawsze przesyłany do metody akcji Edit, niezależnie od tego, która metoda akcji
go wygenerowała. Możemy zatem tworzyć produkty przez kliknięcie łącza Dodaj nowy produkt i wypełnienie
wymaganych danych (rysunek 11.10).
Rysunek 11.10. Dodawanie nowego produktu do katalogu
297
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Usuwanie produktów
Dodanie obsługi usuwania elementów jest dosyć proste. Na początek dodamy do interfejsu IProductRepository
nową metodę, zamieszczoną na listingu 11.18.
Listing 11.18. Dodanie do pliku IProductRepository.cs metody służącej do usuwania produktów
using System.Collections.Generic;
using SportsStore.Domain.Entities;
namespace SportsStore.Domain.Abstract {
public interface IProductRepository {
IEnumerable<Product> Products { get; }
void SaveProduct(Product product);
Product DeleteProduct(int productID);
}
}
Następnie zaimplementujemy tę metodę w naszej klasie repozytorium korzystającej z Entity Framework,
EFProductRepository, w sposób pokazany na listingu 11.19.
Listing 11.19. Implementacja procesu usuwania produktów w pliku EFProductRepository.cs
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using System.Collections.Generic;
namespace SportsStore.Domain.Concrete {
public class EFProductRepository : IProductRepository {
private EFDbContext context = new EFDbContext();
public IEnumerable<Product> Products {
get { return context.Products; }
}
public void SaveProduct(Product product) {
if (product.ProductID == 0) {
context.Products.Add(product);
} else {
Product dbEntry = context.Products.Find(product.ProductID);
if (dbEntry != null) {
dbEntry.Name = product.Name;
dbEntry.Description = product.Description;
dbEntry.Price = product.Price;
dbEntry.Category = product.Category;
}
}
context.SaveChanges();
}
public Product DeleteProduct(int productID) {
Product dbEntry = context.Products.Find(productID);
if (dbEntry != null) {
context.Products.Remove(dbEntry);
298
ROZDZIAŁ 11.  SPORTSSTORE — ADMINISTRACJA
context.SaveChanges();
}
return dbEntry;
}
}
}
Ostatnim krokiem będzie zaimplementowanie metody akcji Delete w kontrolerze AdminController.
Ta metoda akcji powinna obsługiwać żądania POST, ponieważ usuwanie obiektów nie jest operacją powtarzalną.
Jak wyjaśnię w rozdziale 16., przeglądarki i bufory sieciowe mogą wykonywać żądania GET bez wiedzy
użytkownika, więc należy unikać wprowadzania zmian poprzez żądania GET. Nowa metoda akcji jest zamieszczona
na listingu 11.20.
Listing 11.20. Metoda akcji Delete w pliku AdminController.cs
using
using
using
using
System.Linq;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Controllers
{
public class AdminController : Controller
{
private IProductRepository repository;
public AdminController(IProductRepository repo)
{
repository = repo;
}
// …inne metody akcji zostały pominięte w celu zachowania zwięzłości…
[HttpPost]
public ActionResult Delete(int productId) {
Product deletedProduct = repository.DeleteProduct(productId);
if (deletedProduct != null) {
TempData["message"] = string.Format("Usunięto {0}", deletedProduct.Name);
}
return RedirectToAction("Index");
}
}
}
Test jednostkowy — usuwanie produktów
W metodzie akcji Delete chcemy przetestować podstawowe działanie wymienionej metody. Gdy jako parametr
przekażemy prawidłową wartość ProductID, metoda akcji wywoła DeleteProduct z repozytorium i przekaże
właściwy obiekt Product do usunięcia. Test ten jest następujący:
...
[TestMethod]
public void Can_Delete_Valid_Products() {
299
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
// przygotowanie — tworzenie produktu
Product prod = new Product { ProductID = 2, Name = "Test" };
// przygotowanie — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
prod,
new Product {ProductID = 3, Name = "P3"},
});
// przygotowanie — tworzenie kontrolera
AdminController target = new AdminController(mock.Object);
// działanie — usunięcie produktu
target.Delete(prod.ProductID);
// asercje — upewnienie się, że metoda repozytorium
// została wywołana z właściwym produktem
mock.Verify(m => m.DeleteProduct(prod));
}
...
Możemy sprawdzić naszą nową funkcję w działaniu, klikając jeden z przycisków Usuń znajdujących
się na stronie z listą produktów, co jest pokazane na rysunku 11.11 — wykorzystaliśmy tu zmienną
TempData do wyświetlenia komunikatu po usunięciu produktu z katalogu.
Rysunek 11.11. Usuwanie produktu z katalogu
300
ROZDZIAŁ 11.  SPORTSSTORE — ADMINISTRACJA
Podsumowanie
W tym rozdziale przedstawiłem sposób dodania funkcji administracyjnych do aplikacji oraz pokazałem, jak
zaimplementować operacje pozwalające administratorowi na tworzenie, odczytywanie, uaktualnianie i usuwanie
produktów z repozytorium. W następnym rozdziale pokażę Ci, jak zabezpieczyć funkcje administracyjne,
aby nie były dostępne dla wszystkich użytkowników. W ten sposób zakończymy pracę nad funkcjami aplikacji
SportsStore.
301
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
302
ROZDZIAŁ 12.

SportsStore — bezpieczeństwo
i ostatnie usprawnienia
W poprzednim rozdziale do aplikacji SportsStore dodaliśmy obsługę funkcji administracyjnych. Nie możemy
zapominać, że jeżeli opublikujemy teraz naszą aplikację, każdy będzie mógł modyfikować katalog produktów.
Wystarczy, że ktoś domyśli się, że funkcje administracyjne są dostępne poprzez URL Admin/Index.
Abyś mógł uniemożliwić nieuprawnionym użytkownikom korzystanie z funkcji administracyjnych,
pokażę Ci teraz, jak zabezpieczyć hasłem dostęp do całego kontrolera AdminController. Po wprowadzeniu
zabezpieczeń przystąpimy do ukończenia aplikacji SportsStore — ostatnim zadaniem będzie zaimplementowanie
obsługi zdjęć produktów. Wydaje się to prostą funkcją, ale wymaga zastosowania pewnych interesujących
technik MVC.
Zabezpieczanie kontrolera administracyjnego
Ponieważ platforma ASP.NET MVC jest zbudowana na bazie platformy ASP.NET, automatycznie ma dostęp
do funkcji autoryzacji i uwierzytelniania. Wspomniane funkcje to ogólny system pozwalający na śledzenie
zalogowanych użytkowników.
Szczegółowe omówienie funkcji zabezpieczeń w ASP.NET
W tym rozdziale zaledwie dotkniemy tematu dostępnych funkcji zabezpieczeń. Po części wynika to z faktu,
że wspomniane funkcje stanowią część platformy ASP.NET, a nie MVC. Ponadto dostępnych jest wiele różnych
podejść w zakresie ich stosowania. Szczegółowe omówienie wszystkich funkcji uwierzytelniania i autoryzacji
znajdziesz w innej mojej książce, Pro ASP.NET MVC 5 Platform, wydanej przez Apress. To nie oznacza, że musisz
kupić kolejną moją książkę, aby dowiedzieć się czegoś na tak ważny temat, jakim jest zapewnienie bezpieczeństwa
aplikacji sieciowej. Wydawnictwo Apress zgodziło się umieścić dotyczące bezpieczeństwa rozdziały z wymienionej
książki w witrynie http://www.apress.com/.
Zdefiniowanie prostej polityki bezpieczeństwa
Pracę rozpoczynamy od konfiguracji uwierzytelniania formularzy. To jest jeden ze sposobów, na jakie użytkownicy
mogą być uwierzytelnieni w aplikacji ASP.NET. Na listingu 12.1 przedstawiono zmiany, jakie należy wprowadzić
w pliku Web.config projektu SportsStore.WebUI (mówimy tutaj o pliku znajdującym się w katalogu głównym
projektu, a nie w katalogu Views).
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 12.1. Konfiguracja uwierzytelniania formularzy w pliku Web.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
</configSections>
<connectionStrings>
<add name="EFDbContext" connectionString="Data Source=(localdb)\v11.0;Initial
Catalog=SportsStore;Integrated Security=True" providerName="System.Data.SqlClient" />
</connectionStrings>
<appSettings>
<add key="webpages:Version" value="3.0.0.0" />
<add key="webpages:Enabled" value="false" />
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
<add key="Email.WriteAsFile" value="true"/>
</appSettings>
<system.web>
<compilation debug="true" targetFramework="4.5.1" />
<httpRuntime targetFramework="4.5.1" />
<globalization uiCulture="en-US" culture="en-US" />
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" timeout="2880" />
</authentication>
</system.web>
</configuration>
Uwierzytelnianie jest konfigurowane za pomocą elementu authentication, natomiast używając atrybutu
mode, wskazaliśmy, że chcemy użyć uwierzytelniania formularzy, które jest najczęściej stosowane w aplikacjach
internetowych. Na platformie ASP.NET 4.5.1 firma Microsoft dodała obsługę szerokiej gamy opcji
uwierzytelniania odpowiednich dla aplikacji internetowych. Ich omówienie znajdziesz we wspomnianej
wcześniej książce Pro ASP.NET MVC 5 Platform. W naszej przykładowej aplikacji pozostanę przy
uwierzytelnianiu formularzy, ponieważ ta metoda działa z danymi uwierzytelniającymi użytkownika lokalnego,
a ponadto jest prosta w konfiguracji oraz w zarządzaniu.
 Uwaga Główną alternatywą dla uwierzytelniania formularzy jest uwierzytelnianie systemu Windows, w którym
do identyfikowania użytkowników wykorzystywane są dane uwierzytelniające pochodzące z systemu operacyjnego.
Kolejną alternatywą jest uwierzytelnianie organizacyjne, w którym użytkownik jest uwierzytelniany za pomocą usługi
w chmurze, takiej jak Windows Azure. Nie będę tutaj omawiał wspomnianych opcji, ponieważ nie są one powszechnie
stosowane w aplikacjach internetowych.
Atrybut loginUrl informuje ASP.NET, który adres URL powinien być użyty w razie potrzeby uwierzytelnienia
użytkownika — w tym przypadku wywoływana jest strona /Account/Logon. Atrybut timeout określa, jak długo
użytkownik jest uwierzytelniony po zalogowaniu. Domyślnie jest to 48 godzin (2880 minut).
Następny krok to wskazanie platformie ASP.NET, skąd ma wziąć dane dotyczące użytkowników aplikacji.
Zdecydowałem się wykonać to zadanie w oddzielnym kroku, aby pokazać coś, czego absolutnie nigdy nie zrobię
w rzeczywistym projekcie: nazwa użytkownika i hasło zostaną umieszczone w pliku Web.config. Odpowiednie
zmiany do wprowadzenia przedstawiono na listingu 12.2.
Listing 12.2. Definiowanie użytkownika i hasła w pliku Web.config
...
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" timeout="2880">
304
ROZDZIAŁ 12.  SPORTSSTORE — BEZPIECZEŃSTWO I OSTATNIE USPRAWNIENIA
<credentials passwordFormat="Clear">
<user name="admin" password="sekret" />
</credentials>
</forms>
</authentication>
...
Zdecydowałem się na bardzo proste rozwiązanie, aby móc skoncentrować się na sposobach, na jakie
platforma MVC pozwala zastosować uwierzytelnianie i autoryzację w aplikacji sieciowej. Umieszczenie
danych uwierzytelniających w pliku Web.config to prosta droga do katastrofy, zwłaszcza jeśli atrybut passwordFormat
w elemencie <credentials> ma wartość Clear, ponieważ oznacza to przechowywanie haseł w postaci
zwykłego tekstu.
 Ostrzeżenie Nie przechowuj danych uwierzytelniających użytkownika w pliku Web.config, a także nie przechowuj haseł
w postaci zwykłego tekstu. Informacje dotyczące zarządzania użytkownikami za pomocą bazy danych znajdziesz
w dostępnych bezpłatnie rozdziałach mojej książki Pro ASP.NET MVC 5 Platform (jak wspomniano na początku
rozdziału).
Wprawdzie przedstawione tutaj podejście jest nieodpowiednie dla rzeczywistych projektów, ale umieszczenie
danych uwierzytelniających w pliku Web.config pozwala mi skoncentrować się na funkcjach platformy MVC bez
niepotrzebnego odwracania naszej uwagi w kierunku aspektów platformy ASP.NET. W wyniku zastosowanego
podejścia plik Web.config zawiera na stałe wpisaną nazwę użytkownika (admin) i hasło (sekret).
Realizacja uwierzytelniania z użyciem filtrów
Platforma ASP.NET MVC posiada oferujący potężne możliwości mechanizm nazywany filtrami. Są to atrybuty
.NET, które można stosować do metod akcji lub klas kontrolerów. Wprowadzają one dodatkową logikę
w czasie przetwarzania żądania pozwalającą na zmianę zachowania platformy MVC.
Dostępne są różne filtry, ale można też tworzyć własne, co pokażę w rozdziale 18. Interesującym nas
obecnie filtrem jest domyślny filtr uwierzytelniania, Authorize. Zastosujemy go do klasy AdminControler
w sposób pokazany na listingu 12.3.
Listing 12.3. Dodanie atrybutu Authorize w pliku AdminController.cs
using
using
using
using
System.Linq;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Controllers {
[Authorize]
public class AdminController : Controller {
private IProductRepository repository;
public AdminController(IProductRepository repo) {
repository = repo;
}
// …metody akcji pominięte w celu zachowania zwięzłości…
}
}
305
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Gdy atrybut Authorize jest użyty bez żadnych parametrów, pozwala na dostęp do metod akcji kontrolera
wyłącznie uwierzytelnionym użytkownikom. Oznacza to, że jeżeli jesteśmy zalogowani, automatycznie mamy
dostęp do funkcji administracyjnych. Jest to wystarczające dla SportsStore, gdzie znajduje się tylko jeden
zestaw zabezpieczonych metod oraz tylko jeden użytkownik.
 Uwaga Filtry możemy stosować do pojedynczych metod akcji lub do kontrolera. Gdy dodamy filtr do kontrolera, to filtr
ten działa tak, jakby był dołączony do każdej metody akcji w klasie kontrolera. Na listingu 12.3 zastosowaliśmy filtr
Authorize do klasy, więc wszystkie metody akcji w kontrolerze AdminController są dostępne wyłącznie dla
uwierzytelnionych użytkowników.
Efekt działania filtra Authorize możemy sprawdzić przez uruchomienie aplikacji i przejście do adresu
/Admin/Index. Powinniśmy zobaczyć komunikat o błędzie podobny do zamieszczonego na rysunku 12.1.
Rysunek 12.1. Efekt działania filtra Authorize
Gdy próbujemy odwołać się do metody akcji Index z kontrolera AdminController, platforma MVC wykrywa
filtr Authorize. Ponieważ nie jesteśmy uwierzytelnieni, wykonywane jest przekierowanie do adresu URL
zdefiniowanego w pliku Web.config, w sekcji authentication — Account/Login. Nie utworzyliśmy jeszcze
kontrolera AccountController — co jest przyczyną wystąpienia błędu pokazanego na rysunku — ale fakt, że
platforma MVC próbuje przekierować żądanie, oznacza działanie uwierzytelniania.
Tworzenie dostawcy uwierzytelniania
Użycie funkcji uwierzytelniania formularzy wymaga wywołania dwóch metod statycznych z klasy
System.Web.Security.FormsAuthentication:
 metoda Authenicate pozwala sprawdzić dane uwierzytelniania podane przez użytkownika,
 metoda SetAuthCookie dodaje plik cookie do odpowiedzi dla przeglądarki, dzięki czemu użytkownik
nie musi uwierzytelniać się przy każdym żądaniu.
Jednak wywołanie metod statycznych w metodzie akcji powoduje, że testowanie jednostkowe kontrolera staje
się trudne. Platformy imitacji, takie jak Moq, mogą tworzyć imitacje wyłącznie składowych egzemplarza. Klasy
tworzące platformę MVC zostały zaprojektowane z uwzględnieniem testów jednostkowych, natomiast klasa
FormsAuthentication jest starsza niż przyjazny testowaniu projekt MVC.
Najlepszym sposobem rozwiązania tego problemu jest oddzielenie kontrolera od klasy z metodami
statycznymi przy wykorzystaniu interfejsu. Dodatkową zaletą takiego rozwiązania jest wpisanie się w szerszy
wzorzec projektowy MVC i ułatwienie przełączenia się na inny system uwierzytelniania.
306
ROZDZIAŁ 12.  SPORTSSTORE — BEZPIECZEŃSTWO I OSTATNIE USPRAWNIENIA
Zacznijmy od zdefiniowania interfejsu dostawcy uwierzytelniania. W projekcie SportsStore.WebUI
utwórz w katalogu Infrastructure nowy podkatalog o nazwie Abstract i dodaj do niego nowy interfejs o nazwie
IAuthProvider. Zawartość tego interfejsu jest przedstawiona na listingu 12.4.
Listing 12.4. Zawartość pliku IAuthProvider.cs
namespace SportsStore.WebUI.Infrastructure.Abstract {
public interface IAuthProvider {
bool Authenticate(string username, string password);
}
}
Teraz możemy utworzyć implementację tego interfejsu, która będzie pełniła funkcję opakowania dla metod
statycznych klasy FormsAuthentication. Utwórz kolejny podkatalog wewnątrz Infrastructure — tym razem
nazwij go Concrete — a w nim nową klasę, o nazwie FormsAuthProvider. Kod tej klasy jest przedstawiony na
listingu 12.5.
Listing 12.5. Zawartość pliku FormsAuthProvider.cs
using System.Web.Security;
using SportsStore.WebUI.Infrastructure.Abstract;
namespace SportsStore.WebUI.Infrastructure.Concrete {
public class FormsAuthProvider : IAuthProvider {
public bool Authenticate(string username, string password) {
bool result = FormsAuthentication.Authenticate(username, password);
if (result) {
FormsAuthentication.SetAuthCookie(username, false);
}
return result;
}
}
}
 Uwaga W Visual Studio otrzymasz komunikat ostrzeżenia informujący, że metoda FormsAuthentication.Authenticate
jest przestarzała. Po części wynika to z faktu, że firma Microsoft nieustannie podejmuje wysiłki w celu poprawy
bezpieczeństwa użytkowników, co jest drażliwym tematem w każdym frameworku aplikacji sieciowych. Na potrzeby
niniejszego rozdziału użycie przestarzałej metody jest wystarczające i pozwala na przeprowadzenie uwierzytelnienia za
pomocą statycznych danych, które wcześniej wstawiliśmy do pliku Web.config.
Implementacja metody Authenticate wywołuje metody statyczne, które chcieliśmy wydzielić z kontrolera.
Ostatnim krokiem jest zarejestrowanie FormsAuthProvider w metodzie AddBindings klasy NinjectDependencyResolver,
jak pokazano na listingu 12.6.
Listing 12.6. Rejestracja dostawcy uwierzytelniania w pliku NinjectDependencyResolver.cs
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Configuration;
System.Web.Mvc;
Moq;
Ninject;
SportsStore.Domain.Abstract;
307
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
using
using
using
using
SportsStore.Domain.Entities;
SportsStore.Domain.Entities;
SportsStore.WebUI.Infrastructure.Abstract;
SportsStore.WebUI.Infrastructure.Concrete;
namespace SportsStore.WebUI.Infrastructure {
public class NinjectDependencyResolver : IDependencyResolver {
private IKernel kernel;
public NinjectDependencyResolver(IKernel kernelParam) {
kernel = kernelParam;
AddBindings();
}
public object GetService(Type serviceType) {
return kernel.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType) {
return kernel.GetAll(serviceType);
}
private void AddBindings() {
kernel.Bind<IProductRepository>().To<EFProductRepository>();
EmailSettings emailSettings = new EmailSettings {
WriteAsFile = bool.Parse(ConfigurationManager
.AppSettings["Email.WriteAsFile"] ?? "false")
};
kernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>()
.WithConstructorArgument("settings", emailSettings);
kernel.Bind<IAuthProvider>().To<FormsAuthProvider>();
}
}
}
Tworzenie kontrolera AccountController
Kolejnym krokiem będzie utworzenie kontrolera AccountController i metody akcji Login, do której odwołanie
zostało umieszczone w pliku Web.config. W zasadzie konieczne będzie utworzenie dwóch wersji metody
Login. Pierwsza będzie generowała widok zawierający formularz logowania, a druga będzie obsługiwać
żądania POST po przesłaniu danych przez użytkownika.
Na początek utworzymy klasę widoku modelu, która będzie przekazywana pomiędzy kontrolerem i widokiem.
Dodaj nową klasę w katalogu Models w projekcie SportsStore.WebUI, nazwij ją LoginViewModel i umieść w niej
kod z listingu 12.7.
Listing 12.7. Zawartość pliku LoginViewModel.cs
using System.ComponentModel.DataAnnotations;
namespace SportsStore.WebUI.Models {
public class LoginViewModel {
[Required(ErrorMessage = "Proszę podać nazwę użytkownika.")]
public string UserName { get; set; }
308
ROZDZIAŁ 12.  SPORTSSTORE — BEZPIECZEŃSTWO I OSTATNIE USPRAWNIENIA
}
[Required(ErrorMessage = "Proszę podać hasło.")]
[DataType(DataType.Password)]
public string Password { get; set; }
}
Klasa ta zawiera właściwości dla nazwy użytkownika i hasła oraz korzysta z adnotacji danych w celu
zdefiniowania wymagalności obu pól. Ponieważ mamy tylko dwie właściwości, możesz się zastanawiać,
czy nie zrezygnować z modelu widoku i bazować wyłącznie na przekazywaniu danych poprzez ViewBag.
Jednak dobrą praktyką jest definiowanie modeli widoku, dzięki czemu dane przekazywane z kontrolera
do widoku oraz z łącznika modelu do metody akcji są typowane w sposób spójny.
Następnie utworzymy nowy kontroler, AccountController, odpowiedzialny za obsługę uwierzytelniania.
W katalogu Controllers utwórz więc nowy plik o nazwie AccountController.cs i umieść w nim kod
zamieszczony na listingu 12.8.
Listing 12.8. Zawartość pliku AccountController.cs
using System.Web.Mvc;
using SportsStore.WebUI.Infrastructure.Abstract;
using SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers {
public class AccountController : Controller {
IAuthProvider authProvider;
public AccountController(IAuthProvider auth) {
authProvider = auth;
}
public ViewResult Login() {
return View();
}
[HttpPost]
public ActionResult Login(LoginViewModel model, string returnUrl) {
if (ModelState.IsValid) {
if (authProvider.Authenticate(model.UserName, model.Password)) {
return Redirect(returnUrl ?? Url.Action("Index", "Admin"));
} else {
ModelState.AddModelError("", "Nieprawidłowa nazwa użytkownika lub niepoprawne
hasło.");
return View();
}
} else {
return View();
}
}
}
}
Tworzenie widoku
W celu przygotowania widoku pozwalającego użytkownikowi na podanie danych uwierzytelniających
utwórz katalog Views/Account w projekcie SportsStore.WebUI. Kliknij prawym przyciskiem myszy nowy
katalog i wybierz Dodaj/Strona widoku MVC 5 (Razor) z menu kontekstowego. Nowemu widokowi nadaj
nazwę Login i kliknij przycisk Dodaj, aby utworzyć plik Login.cshtml, a następnie umieść w nim kod z listingu 12.9.
309
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 12.9. Zawartość pliku Login.cshtml
@model SportsStore.WebUI.Models.LoginViewModel
@{
ViewBag.Title = "Administracja: logowanie";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<div class="panel">
<div class="panel-heading">
<h3>Zaloguj się</h3>
</div>
<div class="panel-body">
<p class="lead">Proszę się zalogować, aby uzyskać dostęp do obszaru administracyjnego:</p>
@using(Html.BeginForm()) {
@Html.ValidationSummary()
<div class="form-group">
<label>Nazwa użytkownika:</label>
@Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
</div>
<div class="form-group">
<label>Hasło:</label>
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
</div>
<input type="submit" value="Zaloguj się" class="btn btn-primary" />
}
</div>
</div>
Widok używa układu _AdminLayout.cshtml i klas Bootstrap do nadania stylów dla zawartości. Nie zastosowano
tutaj żadnych nowych technik, poza użyciem metody pomocniczej Html.PasswordFor. Wymieniona metoda
generuje element <input>, którego atrybut type ma wartość password. Wszystkie metody pomocnicze zostaną
omówione w rozdziale 21. Nowy widok w działaniu możesz zobaczyć po uruchomieniu aplikacji i przejściu do
adresu URL /Admin/Index, jak pokazano na rysunku 12.2.
Rysunek 12.2.Widok Login
310
ROZDZIAŁ 12.  SPORTSSTORE — BEZPIECZEŃSTWO I OSTATNIE USPRAWNIENIA
Atrybut Required, którego użyliśmy dla właściwości modelu widoku, wymusił zastosowanie kontroli
poprawności po stronie klienta (wymagane biblioteki JavaScript zostały dołączone w pliku układu
_AdminLayout.cshtml w poprzednim rozdziale). Użytkownicy mogą przesłać dane formularza wyłącznie
wtedy, gdy podali zarówno nazwę użytkownika, jak i hasło, a uwierzytelnianie jest wykonywane na
serwerze w momencie wywołania metody FormsAuthentication.Authenticate.
 Ostrzeżenie Zazwyczaj kontrola poprawności po stronie klienta jest dobrym pomysłem. Odciąża ona w pewnym
stopniu serwer i daje użytkownikowi natychmiastową informację na temat poprawności wpisywanych danych.
Jednak nie powinniśmy przenosić uwierzytelniania na stronę klienta, ponieważ zwykle wymaga to wysłania
klientowi poprawnych danych uwierzytelniających w celu sprawdzenia podanej przez niego nazwy użytkownika
i hasła, lub przynajmniej zaufania klientowi, że został prawidłowo uwierzytelniony. Uwierzytelnianie powinno być
zawsze realizowane na serwerze.
Gdy otrzymujemy nieprawidłowe dane uwierzytelniające, dodajemy błąd do ModelState i ponownie
generujemy widok. Powoduje to wyświetlenie komunikatu w obszarze podsumowania kontroli poprawności,
który utworzyliśmy za pomocą wywołania metody pomocniczej Html.ValidationSummary w widoku. W ten
sposób zapewniliśmy zabezpieczenie funkcji administracyjnych aplikacji SportsStore. Użytkownicy będą
mogli korzystać z tych funkcji wyłącznie po podaniu prawidłowych danych logowania i otrzymaniu pliku
cookie, który będzie dołączany do kolejnych żądań.
Test jednostkowy — uwierzytelnianie
Testowanie kontrolera AccountController wymaga sprawdzenia dwóch funkcji — użytkownik powinien
być uwierzytelniony po podaniu prawidłowych danych i nie powinien być uwierzytelniony po podaniu danych
nieprawidłowych. Możemy wykonać te testy przez utworzenie imitacji implementacji interfejsu IAuthProvider
i sprawdzenie typu oraz rodzaju wyniku metody Login. Przedstawione poniżej testy zostały umieszczone w nowym
pliku testów jednostkowych o nazwie AdminSecurityTests.cs:
using
using
using
using
using
using
Microsoft.VisualStudio.TestTools.UnitTesting;
Moq;
SportsStore.WebUI.Controllers;
SportsStore.WebUI.Infrastructure.Abstract;
SportsStore.WebUI.Models;
System.Web.Mvc;
namespace SportsStore.UnitTests {
[TestClass]
public class AdminSecurityTests {
[TestMethod]
public void Can_Login_With_Valid_Credentials() {
// przygotowanie — utworzenie imitacji dostawcy uwierzytelniania
Mock<IAuthProvider> mock = new Mock<IAuthProvider>();
mock.Setup(m => m.Authenticate("admin", "sekret")).Returns(true);
// przygotowanie — utworzenie modelu widoku
LoginViewModel model = new LoginViewModel {
UserName = "admin",
Password = "sekret"
};
// przygotowanie — utworzenie kontrolera
AccountController target = new AccountController(mock.Object);
311
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
// działanie — uwierzytelnienie z użyciem prawidłowych danych
ActionResult result = target.Login(model, "/MyURL");
// asercje
Assert.IsInstanceOfType(result, typeof(RedirectResult));
Assert.AreEqual("/MyURL", ((RedirectResult)result).Url);
}
[TestMethod]
public void Cannot_Login_With_Invalid_Credentials() {
// przygotowanie — utworzenie imitacji dostawcy uwierzytelniania
Mock<IAuthProvider> mock = new Mock<IAuthProvider>();
mock.Setup(m => m.Authenticate("nieprawidłowyUżytkownik",
"nieprawidłoweHasło")).Returns(false);
// przygotowanie — utworzenie modelu widoku
LoginViewModel model = new LoginViewModel {
UserName = "nieprawidłowyUżytkownik",
Password = "nieprawidłoweHasło"
};
// przygotowanie — utworzenie kontrolera
AccountController target = new AccountController(mock.Object);
// działanie — uwierzytelnienie z użyciem nieprawidłowych danych
ActionResult result = target.Login(model, "/MyURL");
// asercje
Assert.IsInstanceOfType(result, typeof(ViewResult));
Assert.IsFalse(((ViewResult)result).ViewData.ModelState.IsValid);
}
}
}
Przesyłanie zdjęć
Tworzenie aplikacji SportsStore zakończymy czymś bardziej skomplikowanym. Dodamy możliwość przesyłania
przez administratorów zdjęć produktów — zdjęcia te będą zapisywane w bazie danych, a następnie wyświetlane
w katalogu produktów. To nie jest szczególnie interesująca lub użyteczna funkcjonalność, ale pozwoli mi na
zademonstrowanie pewnych ważnych funkcji platformy MVC.
Rozszerzanie bazy danych
Otwórz okno Eksplorator bazy danych w Visual Studio i w bazie danych utworzonej w rozdziale 7. przejdź
do tabeli Products. Nazwa połączenia może zostać zmieniona na EFDbContext, czyli nazwę, którą w rozdziale 7.
przypisaliśmy połączeniu w pliku Web.config. Visual Studio zachowuje się nieco niekonsekwentnie po
zmianie nazwy połączenia, więc możesz widzieć także oryginalną nazwę połączenia wyświetlaną w trakcie
jego tworzenia. Kliknij prawym przyciskiem myszy tabelę i wybierz Nowe Zapytanie z menu
kontekstowego. Następnie w polu tekstowym wprowadź poniższe zapytanie SQL:
ALTER TABLE [dbo].[Products]
ADD [ImageData]
VARBINARY (MAX) NULL,
[ImageMimeType] VARCHAR (50)
NULL;
312
ROZDZIAŁ 12.  SPORTSSTORE — BEZPIECZEŃSTWO I OSTATNIE USPRAWNIENIA
Kliknij przycisk Execute (to ikona z trójkątem skierowanym w prawo) w lewym górnym rogu okna Visual
Studio. W ten sposób uaktualnisz bazę danych, dodając dwie nowe kolumny do tabeli. Aby przetestować
poprawność uaktualnienia, kliknij prawym przyciskiem myszy tabelę Products w oknie Eksplorator bazy danych,
a następnie wybierz opcję Otwórz definicję tabeli z menu kontekstowego. Powinieneś zobaczyć dwie nowe
kolumny, jak pokazano na rysunku 12.3.
Rysunek 12.3. Dodawanie nowych kolumn do tabeli Products
 Wskazówka Jeżeli kolumny będą niewidoczne, wtedy zamknij okno, kliknij prawym przyciskiem myszy połączenie
z bazą danych w oknie Eksplorator serwera i wybierz opcję Odśwież. Nowe kolumny powinny być teraz widoczne,
gdy ponownie wybierzesz opcję Otwórz definicję tabeli.
Rozszerzanie modelu domeny
Musimy teraz dodać dwa nowe pola do klasy Product znajdującej się w projekcie SportsStore.Domain,
odpowiadające kolumnom dodanym do bazy danych. Zmiany konieczne do wprowadzenia przedstawiono
na listingu 12.10.
Listing 12.10. Dodawanie właściwości do klasy Product
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace SportsStore.Domain.Entities {
public class Product {
[HiddenInput(DisplayValue=false)]
public int ProductID { get; set; }
[Required(ErrorMessage = "Proszę podać nazwę produktu.")]
public string Name { get; set; }
[Required(ErrorMessage = "Proszę podać opis.")]
[DataType(DataType.MultilineText)]
public string Description { get; set; }
[Required]
[Range(0.01, double.MaxValue, ErrorMessage = "Proszę podać cenę dodatnią.")]
public decimal Price { get; set; }
313
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
[Required(ErrorMessage = "Proszę określić kategorię.")]
public string Category { get; set; }
public byte[] ImageData { get; set; }
public string ImageMimeType { get; set; }
}
}
 Ostrzeżenie Upewnij się, że nazwy właściwości dodane do klasy Product odpowiadają dokładnie nazwom kolumn
dodanych do bazy danych.
Tworzenie interfejsu użytkownika do przesyłania plików
Naszym następnym krokiem będzie dodanie obsługi przesyłania plików. Wymaga to utworzenia interfejsu
pozwalającego administratorom na przesyłanie zdjęć. Zmień widok Views/Admin/Edit.cshtml w sposób
przedstawiony na listingu 12.11.
Listing 12.11. Dodawanie obsługi zdjęć w pliku Edit.cshtml
@model SportsStore.Domain.Entities.Product
@{
ViewBag.Title = "Administracja: Edycja " + @Model.Name;
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<div class="panel">
<div class="panel-heading">
<h3>Edycja @Model.Name</h3>
</div>
@using (Html.BeginForm("Edit", "Admin",
FormMethod.Post, new { enctype = "multipart/form-data" })) {
<div class="panel-body">
@Html.HiddenFor(m => m.ProductID)
@foreach (var property in ViewData.ModelMetadata.Properties) {
switch (property.PropertyName) {
case "ProductID":
case "ImageData":
case "ImageMimeType":
// Brak operacji.
break;
default:
<div class="form-group">
<label>@(property.DisplayName ?? property.PropertyName)</label>
@if (property.PropertyName == "Description") {
@Html.TextArea(property.PropertyName, null,
new { @class = "form-control", rows = 5 })
} else {
@Html.TextBox(property.PropertyName, null,
new { @class = "form-control" })
}
@Html.ValidationMessage(property.PropertyName)
314
ROZDZIAŁ 12.  SPORTSSTORE — BEZPIECZEŃSTWO I OSTATNIE USPRAWNIENIA
</div>
break;
}
}
<div class="form-group">
<div style="position:relative;">
<label>Zdjęcie</label>
<a class='btn' href='javascript:;'>
Wybierz plik...
<input type="file" name="Image" size="40"
style="position:absolute;z-index:2;top:0;
left:0;filter: alpha(opacity=0); opacity:0;
background-color:transparent;color:transparent;"
onchange='$("#upload-file-info").html($(this).val());'>
</a>
<span class='label label-info' id="upload-file-info"></span>
</div>
@if (Model.ImageData == null) {
<div class="form-control-static">Brak zdjęcia</div>
} else {
<img class="img-thumbnail" width="150" height="150"
src="@Url.Action("GetImage", "Product",
new { Model.ProductID })" />
}
</div>
</div>
<div class="panel-footer">
<input type="submit" value="Zapisz" class="btn btn-primary" />
@Html.ActionLink("Anuluj i wróć do listy", "Index", null, new {
@class = "btn btn-default"
})
</div>
}
</div>
Warto pamiętać, że przeglądarka prawidłowo przesyła pliki, jeżeli znacznik <form> zawiera atrybut enctype
o wartości multipart/form-data. Inaczej mówiąc, aby prawidłowo przesłać dane, znacznik <form> musi wyglądać
w następujący sposób:
...
<form action="/Admin/Edit" enctype="multipart/form-data" method="post">
...
Bez atrybutu enctype przeglądarka prześle tylko nazwę pliku bez zawartości, co nie jest nam w ogóle przydatne.
Aby upewnić się, że zostanie wygenerowany atrybut enctype, musimy użyć przeciążonej wersji metody
pomocniczej Html.BeginForm, która pozwala definiować atrybuty HTML:
...
@using (Html.BeginForm("Edit", "Admin",
FormMethod.Post, new { enctype = "multipart/form-data" })) {
...
W widoku wprowadziliśmy jeszcze dwie inne zmiany. Pierwsza polega na zastąpieniu wyrażenia Razor if
konstrukcją switch podczas generowania elementów <input>. Efekt końcowy jest taki sam, ale to rozwiązanie
pomaga w zwięzły sposób wskazać właściwości, które mają być pominięte. Dzięki temu pewne właściwości
związane ze zdjęciem nie będą bezpośrednio wyświetlane użytkownikowi.
315
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Zamiast tego wprowadziliśmy drugą zmianę, jaką jest dodanie elementu <input>, którego atrybut type ma
wartość file. Umożliwia on przekazanie pliku do serwera. Następnie element <img> wyświetli zdjęcie
powiązane z produktem, o ile takie istnieje w bazie danych.
Ogromny bałagan w osadzonym kodzie CSS i JavaScript wynika z wad biblioteki Bootstrap: nie potrafi
ona prawidłowo nadać stylu elementom <input>. Istnieje wiele rozszerzeń dodających brakującą funkcjonalność,
ale zdecydowałem się na magiczne zaklęcie w listingu, ponieważ takie rozwiązanie jest solidne i niezależne
od innych komponentów. Nie powoduje zmiany sposobu działania platformy MVC, a jedynie zmienia sposób
nadawania stylu elementom w pliku Edit.cshtml.
Zapisywanie zdjęć do bazy danych
Musimy teraz rozszerzyć obsługującą żądania POST odmianę metody Edit z klasy AdminController, aby pobierała
przesłane dane zdjęcia i zapisywała je do bazy danych. Na listingu 12.12 pokazane są wymagane zmiany.
Listing 12.12. Obsługa danych zdjęcia w pliku AdminController.cs
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Controllers {
[Authorize]
public class AdminController : Controller {
private IProductRepository repository;
public AdminController(IProductRepository repo) {
repository = repo;
}
public ViewResult Index() {
return View(repository.Products);
}
public ViewResult Edit(int productId) {
Product product = repository.Products
.FirstOrDefault(p => p.ProductID == productId);
return View(product);
}
[HttpPost]
public ActionResult Edit(Product product, HttpPostedFileBase image = null) {
if (ModelState.IsValid) {
if (image != null) {
product.ImageMimeType = image.ContentType;
product.ImageData = new byte[image.ContentLength];
image.InputStream.Read(product.ImageData, 0, image.ContentLength);
}
repository.SaveProduct(product);
TempData["message"] = string.Format("Zapisano {0}", product.Name);
return RedirectToAction("Index");
} else {
// wykryto problemy z przesłanymi danymi
return View(product);
}
}
316
ROZDZIAŁ 12.  SPORTSSTORE — BEZPIECZEŃSTWO I OSTATNIE USPRAWNIENIA
public ViewResult Create() {
return View("Edit", new Product());
}
[HttpPost]
public ActionResult Delete(int productId) {
Product deletedProduct = repository.DeleteProduct(productId);
if (deletedProduct != null) {
TempData["message"] = string.Format("{0} was deleted",
deletedProduct.Name);
}
return RedirectToAction("Index");
}
}
}
Do metody Edit dodaliśmy nowy parametr, dzięki któremu dane przesłanego pliku platforma MVC
przekaże metodzie akcji. Następnie sprawdzamy, czy wartością parametru jest null; jeżeli nie, kopiujemy
dane oraz typ MIME z parametru do obiektu Product, dzięki czemu plik zostanie zapisany w bazie danych.
Konieczne jest również uaktualnienie klasy EFProductRepository w projekcie SportsStore.Domain w celu
zagwarantowania, że wartości przypisane właściwościom ImageData i ImageMimeType są przechowywane w bazie
danych. Na listingu 12.13 przedstawiono zmiany, które trzeba wprowadzić w metodzie SaveProduct.
Listing 12.13. Wprowadzone w pliku EFProductRepository.cs zmiany gwarantujące zapis danych zdjęć w bazie
danych
...
public void SaveProduct(Product product)
{
if (product.ProductID == 0)
{
context.Products.Add(product);
}
else
{
Product dbEntry = context.Products.Find(product.ProductID);
if (dbEntry != null)
{
dbEntry.Name = product.Name;
dbEntry.Description = product.Description;
dbEntry.Price = product.Price;
dbEntry.Category = product.Category;
dbEntry.ImageData = product.ImageData;
dbEntry.ImageMimeType = product.ImageMimeType;
}
}
context.SaveChanges();
}
...
Implementowanie metody akcji GetImage
Na listingu 12.11 dodaliśmy do widoku element <img>, którego zawartość była pozyskiwana za pomocą
metody akcji GetImage kontrolera Product. Musimy teraz zaimplementować tę metodę, dzięki czemu
będziemy mogli wyświetlać zdjęcia znajdujące się w bazie danych. Na listingu 12.14 zamieszczona jest
metoda, którą trzeba dodać do klasy ProductController.
317
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 12.14. Metoda akcji GetImage w pliku ProductController.cs
using
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers {
public class ProductController : Controller {
private IProductRepository repository;
public int PageSize = 4;
public ProductController(IProductRepository productRepository) {
this.repository = productRepository;
}
public ViewResult List(string category, int page = 1) {
ProductsListViewModel model = new ProductsListViewModel {
Products = repository.Products
.Where(p => category == null || p.Category == category)
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo {
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = category == null ?
repository.Products.Count() :
repository.Products.Where(e => e.Category == category).Count()
},
CurrentCategory = category
};
return View(model);
}
public FileContentResult GetImage(int productId) {
Product prod = repository.Products.FirstOrDefault(p => p.ProductID == productId);
if (prod != null) {
return File(prod.ImageData, prod.ImageMimeType);
} else {
return null;
}
}
}
}
Metoda ta próbuje wyszukać produkt, którego identyfikator jest równy wartości przekazanej w parametrze.
Jeżeli chcemy przesłać plik do przeglądarki klienta, metoda akcji powinna zwrócić obiekt typu FileContentResult,
a egzemplarze obiektu są tworzone za pomocą metody File z bazowej klasy kontrolera. Typy wartości, jakie mogą
być zwracane z metod akcji, przedstawię w rozdziale 17.
318
ROZDZIAŁ 12.  SPORTSSTORE — BEZPIECZEŃSTWO I OSTATNIE USPRAWNIENIA
Test jednostkowy — odczyt zdjęć
Chcemy się upewnić, że metoda GetImage zwraca prawidłowy typ MIME z repozytorium, oraz sprawdzić,
czy w przypadku podania nieistniejącego identyfikatora produktu nie zostaną zwrócone żadne dane. Implementacje
metod testowych zostały umieszczone w nowym pliku testów jednostkowych o nazwie ImageTests.cs i są
następujące:
using
using
using
using
using
using
using
Microsoft.VisualStudio.TestTools.UnitTesting;
Moq;
SportsStore.Domain.Abstract;
SportsStore.Domain.Entities;
SportsStore.WebUI.Controllers;
System.Linq;
System.Web.Mvc;
namespace SportsStore.UnitTests {
[TestClass]
public class ImageTests {
[TestMethod]
public void Can_Retrieve_Image_Data() {
// przygotowanie — tworzenie produktu z danymi zdjęcia
Product prod = new Product {
ProductID = 2,
Name = "Test",
ImageData = new byte[] {},
ImageMimeType = "image/png"
};
// przygotowanie — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
prod,
new Product {ProductID = 3, Name = "P3"}
}.AsQueryable());
// przygotowanie — tworzenie kontrolera
ProductController target = new ProductController(mock.Object);
// działanie — wywołanie metody akcji GetImage
ActionResult result = target.GetImage(2);
}
// asercje
Assert.IsNotNull(result);
Assert.IsInstanceOfType(result, typeof(FileResult));
Assert.AreEqual(prod.ImageMimeType, ((FileResult)result).ContentType);
[TestMethod]
public void Cannot_Retrieve_Image_Data_For_Invalid_ID() {
// przygotowanie — tworzenie imitacji repozytorium
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"}
}.AsQueryable());
319
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
// przygotowanie — tworzenie kontrolera
ProductController target = new ProductController(mock.Object);
// działanie — wywołanie metody akcji GetImage
ActionResult result = target.GetImage(100);
// asercje
Assert.IsNull(result);
}
}
}
W przypadku prawidłowego identyfikatora produktu sprawdzamy, czy z metody akcji otrzymamy obiekt
FileResult i czy typ zawartości będzie taki sam jak podany w danych testowych. Klasa FileResult nie pozwala
nam na dostęp do danych binarnych pliku, więc musimy zadowolić się nie do końca doskonałym testem. Aby wykryć
żądanie nieprawidłowego identyfikatora produktu, wystarczy, że sprawdzimy, czy zwracana jest wartość null.
Administrator może teraz przesyłać zdjęcia produktów. Możesz to sprawdzić samodzielnie, uruchamiając
aplikację, przechodząc do adresu URL /Admin/Index i modyfikując wybrany produkt. Przykład jest pokazany
na rysunku 12.4.
Rysunek 12.4. Dodawanie zdjęcia do listy produktów
320
ROZDZIAŁ 12.  SPORTSSTORE — BEZPIECZEŃSTWO I OSTATNIE USPRAWNIENIA
Wyświetlanie zdjęć produktów
Pozostało nam wyświetlić zdjęcia obok opisu w katalogu produktów. Otwórz widok Views/Shared/
ProductSummary.cshtml i umieść w nim zmiany zaznaczone pogrubioną czcionką na listingu 12.15.
Listing 12.15. Dodanie w pliku ProductSummary.cs kodu odpowiedzialnego za wyświetlanie zdjęć w katalogu
produktów
@model SportsStore.Domain.Entities.Product
<div class="well">
@if (Model.ImageData != null) {
<div class="pull-left" style="margin-right: 10px">
<img class="img-thumbnail" width="75" height="75"
src="@Url.Action("GetImage", "Product", new { Model.ProductID })" />
</div>
}
<h3>
<strong>@Model.Name</strong>
<span class="pull-right label label-primary">@Model.Price.ToString("c")</span>
</h3>
@using(Html.BeginForm("AddToCart", "Cart")) {
<div class="pull-right">
@Html.HiddenFor(x => x.ProductID)
@Html.Hidden("returnUrl", Request.Url.PathAndQuery)
<input type="submit" class="btn btn-success" value="Dodaj do koszyka" />
</div>
}
<span class="lead">@Model.Description</span>
</div>
Gdy zmiany te zostaną wprowadzone, klienci będą widzieli w katalogu zdjęcia uzupełniające opis produktu,
jak pokazano na rysunku 12.5.
Rysunek 12.5. Wyświetlanie zdjęć produktów
321
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Podsumowanie
W tym oraz w poprzednich rozdziałach pokazałem, jak można użyć ASP.NET MVC do utworzenia
realistycznej aplikacji typu e-commerce. Ten obszerny przykład zawiera wiele kluczowych funkcji platformy:
kontrolery, metody akcji, routing, widoki, dołączanie modelu, metadane, kontrolę poprawności, układy,
uwierzytelnianie itd. Pokazałem również, w jaki sposób można użyć kilku najważniejszych technologii
związanych z MVC. Są to Entity Framework, Ninject, Moq oraz obsługa testów jednostkowych w Visual
Studio. W efekcie otrzymaliśmy aplikację korzystającą z czystej, zorientowanej komponentowo architektury,
w której różne zadania są rozdzielone, dzięki czemu pracujemy na bazie kodu, który można bardzo łatwo
rozszerzać i obsługiwać. W kolejnym rozdziale pokażę Ci, jak wdrożyć aplikację SportsStore
w środowisku produkcyjnym.
322
ROZDZIAŁ 13.

Wdrażanie aplikacji
Ostatnim (i mającym krytyczne znaczenie) krokiem podczas tworzenia aplikacji jest jej wdrożenie, czyli
udostępnienie użytkownikom. W tym rozdziale pokażę, w jaki sposób przygotować aplikację SportsStore
do instalacji, oraz przedstawię przykładowe wdrożenie.
Istnieje wiele różnych sposobów, a także miejsc wdrażania aplikacji MVC. Jedną z możliwości jest
wykorzystanie komputera działającego pod kontrolą systemu Windows Server wraz z uruchomionym
serwerem Internet Information Services (IIS), co pozwala na lokalne wdrożenie i zarządzanie aplikacją.
Kolejna możliwość to użycie zdalnej usługi hostingu, co zwalnia Cię z konieczności zarządzania serwerami,
ponieważ tym zajmuje się dostawca usługi. Jeszcze inna możliwość to wykorzystanie infrastruktury w chmurze,
która zapewni odpowiednie skalowanie aplikacji, gdy zajdzie potrzeba.
Zastanawiałem się, jak utworzyć użyteczny przykład wdrożenia aplikacji, który mógłbym przedstawić
w tym rozdziale. Zdecydowałem się pominąć temat bezpośredniego wdrażania do serwera IIS, ponieważ
proces konfiguracji serwera jest długi i skomplikowany, a większość programistów MVC stosujących
wdrażanie lokalne zleca to zadanie działowi IT. Zdecydowałem się także pominąć omówienie wdrażania
z użyciem hostingu oferowanego przez wiele firm, ponieważ każda z nich stosuje własny proces wdrażania,
a żadna firma nie definiuje standardu hostingu.
Niejako padło więc na przedstawienie procesu wdrażania aplikacji w usłudze Windows Azure, czyli
oferowanej przez Microsoft platformie chmury, która zapewnia doskonałą obsługę aplikacji MVC. Wcale nie
twierdzę, że Windows Azure to doskonałe rozwiązanie we wszystkich przypadkach, ale lubię sposób działania
tej usługi. Jej wykorzystanie w rozdziale pozwala nam skoncentrować się na samym procesie wdrażania, a nie
na zmaganiu się z problemami konfiguracyjnymi Windows i serwera IIS. W trakcie pisania niniejszej książki
Microsoft oferuje 90-dniowy bezpłatny okres próbny (niektóre usługi subskrypcji MSDN również obejmują
Azure). Oznacza to, że możesz wypróbować techniki przedstawione w rozdziale, nawet jeśli ostatecznie nie
masz zamiaru korzystać z Windows Azure.
 Ostrzeżenie Gorąco zachęcam, aby najpierw przećwiczyć proces instalacji aplikacji w serwerze testowym, a dopiero
później zająć się instalowaniem aplikacji w środowisku produkcyjnym. Podobnie jak każdy inny element cyklu
programowania, instalacja również powinna podlegać testom. Słyszałem straszne opowieści o zespołach, które
zniszczyły działające aplikacje, korzystając z szybko przygotowanych i źle przetestowanych procedur instalacji.
Nie można powiedzieć, aby funkcje instalacji z ASP.NET były szczególnie niebezpieczne — one takie nie są — ale
każda interakcja z aplikacją operującą na rzeczywistych danych użytkowników wymaga przemyślenia i zaplanowania.
Wdrażanie aplikacji sieciowej było uznawane za proces dość żmudny i podatny na błędy. Na szczęście firma
Microsoft włożyła wiele wysiłku w poprawę oferowanych przez Visual Studio narzędzi wdrażania aplikacji.
Dlatego też, nawet jeśli zamierzasz wdrażać aplikację w innego rodzaju strukturze, Visual Studio i tak wykona
za Ciebie większość pracy.
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Przygotowanie do użycia Windows Azure
Zanim będziesz mógł skorzystać z usługi Azure, najpierw musisz utworzyć konto, co wymaga przejścia do
witryny http://www.windowsazure.com/pl-pl/. W czasie pisania tej książki Microsoft oferował bezpłatne konto
próbne, a ponadto niektóre subskrypcje MSDN również zawierały pakiety usług Azure. Po utworzeniu konta
możesz nim zarządzać po przejściu do witryny http://manage.windowsazure.com/ i podaniu danych
uwierzytelniających. Na początku zobaczysz pokazany na rysunku 13.1 widok podsumowania.
Rysunek 13.1. Strona podsumowania w portalu Azure
Tworzenie witryny internetowej i bazy danych
Pracę należy rozpocząć od utworzenia nowej witryny internetowej i usługi bazy danych — to są dwie usługi
chmury oferowane przez Azure. Kliknij duży przycisk plus wyświetlany w lewym dolnym rogu okna portalu
i wybierz opcję Compute/Web Site/Custom Create. Na ekranie zostanie wyświetlony formularz pokazany na
rysunku 13.2.
Rysunek 13.2. Tworzenie nowej witryny internetowej wraz z bazą danych
324
ROZDZIAŁ 13.  WDRAŻANIE APLIKACJI
Konieczne jest wybranie adresu URL dla aplikacji. W przypadku bezpłatnych i podstawowych usług
Azure jesteś ograniczony jedynie do domeny azurewebsites.net. W omawianym przykładzie wybrałem
mvc5sportsstore, ale Ty będziesz musiał wybrać inną nazwę, ponieważ każda witryna Azure musi mieć
unikatową nazwę.
Wybierz region, w którym ma zostać wdrożona aplikacja, i upewnij się o zaznaczeniu opcji Create a new
SQL database w polu Database. (W usłudze Azure można użyć bazy danych MySQL, ale nasza przykładowa
aplikacja nie jest skonfigurowana do jej użycia, dlatego należy wybrać SQL Server).
W polu DB Connection String Name podaj nazwę EFDbContext, czyli nazwę ciągu tekstowego połączenia
z bazą danych stosowanego w aplikacji SportsStore. Tej samej nazwy używamy w usłudze Azure, aby mieć
pewność, że kod aplikacji będzie bez żadnych modyfikacji prawidłowo działał po wdrożeniu.
Po wypełnieniu formularza kliknij przycisk strzałki, co spowoduje przejście do formularza pokazanego
na rysunku 13.3.
Rysunek 13.3. Konfiguracja bazy danych
Wybierz nazwę dla bazy danych. Ja zdecydowałem się na mvc5sportsstore_db, aby było jasne, dla której
aplikacji jest przeznaczona ta baza danych. W polu Server wybierz opcję New SQL Data Server, a następnie
podaj nazwę użytkownika i hasło. W omawianym przykładzie w roli nazwy użytkownika użyto sportsstore,
natomiast hasło zostało utworzone z zachowaniem przedstawionych we wcześniejszej części książki wskazówek
dotyczących haseł (połączenie małych i wielkich liter, cyfr oraz innych znaków). Zanotuj nazwę użytkownika
i hasło, ponieważ będziesz ich potrzebował w dalszej części procesu wdrażania. Kliknij przycisk „ptaszka”
w celu zakończenia procesu konfiguracji. Usługa Azure rozpocznie tworzenie nowej witryny internetowej
wraz z bazą danych, co może potrwać kilka minut. Po zakończeniu procesu zostaniesz przeniesiony na stronę
podsumowania. Jak będziesz mógł zobaczyć, kategorie Web Sites i Databases zawierają po jednym elemencie
(rysunek 13.4).
Przygotowanie bazy danych do zdalnej administracji
Kolejnym krokiem jest przeprowadzenie konfiguracji bazy danych Azure, aby zawierała dokładnie ten sam
schemat i dane, których użyliśmy w rozdziale 7. Kliknij łącze SQL Databases na stronie podsumowania Azure,
a następnie kliknij wpis pojawiający się w tabeli SQL Databases. (Jeżeli zaakceptowałeś ustawienia domyślne,
baza danych będzie miała nazwę mvc5sportsstore_db).
Portal wyświetli szczegółowe informacje dotyczące bazy danych i różne opcje przeznaczone do jej
konfiguracji oraz zarządzania nią. Kliknij łącze Set up Windows Azure firewall rules for this address w sekcji
Design Your Database, a zobaczysz komunikat informujący, że Twój aktualny adres IP (przypisany stacji
roboczej) nie znajduje się w regułach zapory sieciowej. Kliknij przycisk Yes, jak pokazano na rysunku 13.5.
325
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 13.4. Efekt utworzenia nowej witryny internetowej wraz z bazą danych
Rysunek 13.5. Dodanie adresu IP stacji roboczej do reguł zapory sieciowej Azure
 Ostrzeżenie Visual Studio oferuje obsługę wdrożenia aplikacji wraz z bazą danych. Jestem przeciwnikiem tej funkcji,
ponieważ chwila nieuwagi podczas wyboru opcji w menu może spowodować usunięcie danych rzeczywistej
aplikacji. Bazę danych zawsze powinieneś uaktualniać oddzielnie, a wcześniej przeprowadzać dokładne testy.
Tworzenie schematu bazy danych
Kolejnym krokiem jest utworzenie schematu bazy danych. Kliknij łącze Design your SQL database w sekcji
Database. Wprowadź nazwę bazy danych (mvc5sportsstore_db), nazwę użytkownika (sportsstore) i hasło
zdefiniowane podczas tworzenia bazy danych. Następnie kliknij przycisk Log on, jak pokazano na rysunku 13.6.
Rysunek 13.6. Nawiązanie połączenia z bazą danych
326
ROZDZIAŁ 13.  WDRAŻANIE APLIKACJI
 Wskazówka Do zarządzania bazą danych wymagana jest wtyczka Silverlight. Przed przejściem dalej będziesz musiał
ją zainstalować w przeglądarce internetowej.
Na górze okna możesz zobaczyć przycisk New Query. Po jego kliknięciu zostanie wyświetlone pole tekstowe
pozwalające na wprowadzanie poleceń SQL. W tym miejscu podamy polecenia SQL odpowiedzialne za
utworzenie niezbędnej nam tabeli bazy danych.
Pobieranie schematu bazy danych
Odpowiednie polecenie SQL możemy pobrać z Visual Studio. Przejdź do okna Eksplorator serwera, rozwiń
wyświetlane w nim elementy, a następnie odszukaj tabelę Products. Po kliknięciu tabeli prawym przyciskiem
myszy wybierz opcję Otwórz definicję tabeli. W Visual Studio zostanie wyświetlony schemat tabeli. W panelu
T-SQL zobaczysz kod SQL przedstawiony na listingu 13.1.
Listing 13.1. Polecenie SQL tworzące tabelę Products
CREATE TABLE [dbo].[Products] (
[ProductID]
INT
IDENTITY (1, 1) NOT NULL,
[Name]
NVARCHAR (100) NOT NULL,
[Description]
NVARCHAR (500) NOT NULL,
[Category]
NVARCHAR (50)
NOT NULL,
[Price]
DECIMAL (16, 2) NOT NULL,
[ImageData]
VARBINARY (MAX) NULL,
[ImageMimeType] VARCHAR (50)
NULL,
PRIMARY KEY CLUSTERED ([ProductID] ASC)
);
Skopiuj to polecenie w Visual Studio, a następnie wklej w polu tekstowym w przeglądarce internetowej
i kliknij przycisk Run położony w górnej części okna przeglądarki. Po chwili zobaczysz komunikat informujący
o zakończonym powodzeniem wykonaniu operacji. W tym momencie baza danych w usłudze Azure zawiera
tabelę Products o takim samym schemacie jak zdefiniowany w aplikacji SportsStore.
Dodanie danych tabeli
Po utworzeniu tabeli można ją wypełnić danymi produktów, które wykorzystaliśmy w rozdziale 7. Powróć
do tabeli Products w oknie eksploratora serwera, a następnie kliknij prawym przyciskiem myszy tabelę
i wybierz opcję Pokaż dane tabeli z menu kontekstowego. W górnej części okna znajdziesz przycisk Script,
jak pokazano na rysunku 13.7.
Rysunek 13.7. Przycisk Script w panelu wyświetlającym dane tabeli
327
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
W Visual Studio zostanie wyświetlony nowy panel zawierający kolejne polecenia SQL, które zostały
przedstawione na listingu 13.2.
Listing 13.2. Polecenia SQL zawierające dane, które trzeba dodać do tabeli Products
SET IDENTITY_INSERT [dbo].[Products] ON
INSERT INTO [dbo].[Products] ([ProductID], [Name], [Description], [Category], [Price],
[ImageMimeType]) VALUES (1, N'Kajak', N'Łódka dla jednej osoby', N'Sporty wodne', CAST(275.00 AS
Decimal(16, 2)), NULL)
INSERT INTO [dbo].[Products] ([ProductID], [Name], [Description], [Category], [Price],
[ImageMimeType]) VALUES (4, N'Kamizelka ratunkowa', N'Chroni i dodaje uroku', N'Sporty wodne',
CAST(48.95 AS Decimal(16, 2)), NULL)
INSERT INTO [dbo].[Products] ([ProductID], [Name], [Description], [Category], [Price],
[ImageMimeType]) VALUES (5, N'Piłka', N'Zatwierdzone przez FIFA wielkość i waga', N'Piłka nożna',
CAST(19.50 AS Decimal(16, 2)), NULL)
INSERT INTO [dbo].[Products] ([ProductID], [Name], [Description], [Category], [Price],
[ImageMimeType]) VALUES (6, N'Flagi narożne', N'Nadadzą twojemu boisku profesjonalny wygląd',
N'Piłka nożna', CAST(34.95 AS Decimal(16, 2)), NULL)
INSERT INTO [dbo].[Products] ([ProductID], [Name], [Description], [Category], [Price],
[ImageMimeType]) VALUES (7, N'Stadion', N'Składany stadion na 35 000 osób', N'Piłka nożna',
CAST(79500.00 AS Decimal(16, 2)), NULL)
INSERT INTO [dbo].[Products] ([ProductID], [Name], [Description], [Category], [Price],
[ImageMimeType]) VALUES (8, N'Czapka', N'Zwiększa efektywność mózgu o 75%', N'Szachy',
CAST(16.00 AS Decimal(16, 2)), N'image/jpeg')
INSERT INTO [dbo].[Products] ([ProductID], [Name], [Description], [Category], [Price],
[ImageMimeType]) VALUES (9, N'Niestabilne krzesło', N'Zmniejsza szanse przeciwnika',
N'Szachy', CAST(29.95 AS Decimal(16, 2)), NULL)
INSERT INTO [dbo].[Products] ([ProductID], [Name], [Description], [Category], [Price],
[ImageMimeType]) VALUES (10, N'Ludzka szachownica', N'Gra dla całej rodziny', N'Szachy',
CAST(75.00 AS Decimal(16, 2)), NULL)
INSERT INTO [dbo].[Products] ([ProductID], [Name], [Description], [Category], [Price],
[ImageMimeType]) VALUES (11, N'Błyszczący król ', N'Pokryty złotem i wysadzany diamentami król',
N'Szachy', CAST(1200.00 AS Decimal(16, 2)), NULL)
SET IDENTITY_INSERT [dbo].[Products] OFF
Wyczyść pole tekstowe w oknie przeglądarki internetowej, w którym jesteś zalogowany do usługi Azure.
Następnie wklej polecenia SQL przedstawione na listingu i kliknij przycisk Run. Polecenia zostaną wykonane,
a odpowiednie dane dodane do bazy danych.
Wdrażanie aplikacji
Po zakończeniu konfiguracji samo wdrażanie aplikacji jest stosunkowo łatwym procesem. Powróć do głównego
widoku portalu Azure, kliknij przycisk Web Sites i wybierz witrynę internetową mvc5sportsstore. Po wyświetleniu
widoku Dashboard kliknij łącze Download the publish profile w sekcji Publish your app. Pobrany plik zapisz
w łatwo dostępnym miejscu.
Dla omawianej tutaj przykładowej aplikacji wdrażanej w usłudze Azure plik będzie nosił nazwę
mvc5sportsstore.azurewebsites.net.PublishingSettings. Zapisz go np. na pulpicie. W wymienionym pliku znajdują
się informacje szczegółowe potrzebne Visual Studio do opublikowania aplikacji w infrastrukturze Azure.
Powróć do Visual Studio i kliknij prawym przyciskiem myszy projekt SportsStore.WebUI w oknie
Eksplorator rozwiązania, a następnie wybierz opcję Publikuj… z menu kontekstowego. Na ekranie zostanie
wyświetlone pokazane na rysunku 13.8 okno dialogowe pozwalające na opublikowanie aplikacji.
Kliknij przycisk Import… i wskaż plik konfiguracyjny pobrany z Azure. Visual Studio przetworzy plik
i wyświetli szczegółowe informacje dotyczące konfiguracji usługi Azure, jak pokazano na rysunku 13.9.
Wyświetlone tutaj informacje odzwierciedlają dane podane podczas konfiguracji witryny internetowej
w portalu Azure.
328
ROZDZIAŁ 13.  WDRAŻANIE APLIKACJI
Rysunek 13.8. Okno dialogowe publikowania aplikacji sieciowej
Rysunek 13.9. Szczegółowe informacje dotyczące usługi Azure, w której aplikacja będzie wdrażana
Nie ma potrzeby zmiany jakichkolwiek danych wyświetlonych w oknie dialogowym. Kliknij przycisk Next,
co spowoduje przejście do kolejnego kroku procesu wdrażania (rysunek 13.10).
329
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 13.10. Ustawienia wdrażanej aplikacji
Masz teraz możliwość wyboru konfiguracji, która będzie użyta podczas wdrażania. Z reguły to będzie
Release, ale równie dobrze możesz wybrać Debug, jeśli chcesz testować aplikację w infrastrukturze Azure,
a tym samym użyć ustawień debugowania dla kompilatora i paczek aplikacji.
Pozostała część procesu wdrażania to konfiguracja połączenia z bazą danych. Visual Studio daje możliwość
utworzenia mapowania pomiędzy zdefiniowanymi w projekcie połączeniami z bazą danych i bazami danych
istniejącymi w usłudze Azure. Wcześniej zagwarantowaliśmy, że plik Web.config zawiera tylko jeden ciąg
tekstowy połączenia. Ponieważ utworzyliśmy tylko jedną bazę danych w usłudze Azure, to mapowanie domyślne
jest wystarczające. Jeżeli w aplikacji masz zdefiniowanych więcej połączeń, musisz upewnić się o powiązaniu
odpowiedniej bazy danych Azure z poszczególnymi połączeniami w aplikacji.
Kliknij przycisk Next, aby zobaczyć podgląd procesu wdrażania (rysunek 13.11). Po kliknięciu przycisku
Start Preview Visual Studio przejdzie przez proces wdrażania, ale nie wyśle do serwera żadnych plików. Jeżeli
uaktualniasz już wcześniej wdrożoną aplikację, ten krok może być użyteczny, ponieważ pozwala na sprawdzenie,
czy zastąpione będą właściwe pliki.
Rysunek 13.11. Sekcja podglądu w oknie dialogowym wdrażania aplikacji
330
ROZDZIAŁ 13.  WDRAŻANIE APLIKACJI
Omawiana aplikacja jest wdrażana po raz pierwszy, więc — jak pokazano na rysunku 13.12 — w oknie
podglądu pojawią się wszystkie pliki. Zwróć uwagę na pole wyboru wyświetlane obok każdego pliku.
Wprawdzie masz możliwość wykluczenia poszczególnych plików z procesu wdrażania, ale powinieneś
zachować wówczas szczególną ostrożność. Pod względem wykluczania plików jestem dość konserwatywny
i wolę umieścić w serwerze niepotrzebne pliki, niż zapomnieć o chociaż jednym ważnym pliku.
Rysunek 13.12. Podgląd zmian wprowadzonych podczas wdrażania
Kliknięcie przycisku Publish spowoduje rozpoczęcie właściwego wdrażania aplikacji w infrastrukturze Azure.
Okno dialogowe publikowania aplikacji zostanie zamknięte, a informacje szczegółowe dotyczące procesu
wdrażania będą wyświetlane w oknie danych wyjściowych Visual Studio, jak pokazano na rysunku 13.13.
Rysunek 13.13. Wdrażanie aplikacji na platformie Azure
Proces wdrażania aplikacji może potrwać kilka minut. Po zakończeniu procesu Visual Studio wyświetli
okno przeglądarki internetowej i przejdzie do adresu URL strony aplikacji na platformie Azure. W omawianym
przypadku to będzie adres URL http://mvc5sportsstore.azurewebsites.net/, jak pokazano na rysunku 13.14.
331
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 13.14. Aplikacja SportsStore uruchomiona na platformie Azure
Podsumowanie
W rozdziale tym pokazałem, w jaki sposób wdrożyć aplikację MVC na platformie Windows Azure. Istnieje
wiele różnych sposobów wdrażania aplikacji i wiele platform docelowych, ale proces zaprezentowany
w rozdziale przedstawia to, czego możesz się spodziewać, nawet jeśli nie używasz Azure.
Na tym kończymy pracę nad aplikacją SportsStore w tej części książki. W części drugiej książki zajmiemy
się szczegółami. Dokładnie poznasz funkcje, których użyłem podczas tworzenia aplikacji SportsStore.
332
ROZDZIAŁ 14.

Przegląd projektu MVC
Zanim zagłębię się w szczegółach funkcji platformy MVC, podam nieco informacji ogólnych. W tym rozdziale
przedstawię strukturę i naturę aplikacji ASP.NET MVC, w tym domyślną strukturę projektu oraz konwencje
nazewnictwa. Niektóre konwencje są opcjonalne, z kolei inne na sztywno definiują sposób, w jaki działa
platforma MVC.
Korzystanie z projektów MVC z Visual Studio
Gdy tworzymy nowy projekt MVC, Visual Studio daje nam możliwość wyboru jednego z kilku punktów
startowych. Celem jest ułatwienie procesu nauki nowym programistom, a także zastosowanie pewnych
pozwalających na oszczędność czasu najlepszych praktyk podczas implementacji najczęściej używanych
funkcji. Tego rodzaju wsparcie oferowane programistom ma postać szablonów wykorzystywanych do
tworzenia kontrolerów i widoków przygotowywanych z użyciem kodu szablonu do wymiany obiektów
danych, edycji właściwości modelu itd.
W Visual Studio 2013 oraz MVC 5 firma Microsoft uaktualniła szablony i tak zwane szkielety kodu,
niwelując różnice między poszczególnymi rodzajami projektów ASP.NET. Ma to na celu dostarczenie
szerszej gamy szablonów projektów oraz konfiguracji.
Po lekturze pierwszej części książki nie powinieneś mieć wątpliwości, że nie jestem fanem podejścia
polegającego na użyciu szablonów projektów. Intencje Microsoftu są dobre, ale wykonanie pozostawia sporo
do życzenia. Jedną z cech charakterystycznych, którą niezwykle cenię w platformach ASP.NET i MVC,
jest ogromna elastyczność pozwalająca na dostosowanie platformy do preferowanego przez daną osobę
stylu programowania. Tworzone i wypełniane kodem przez Visual Studio projekty, klasy i widoki sprawiają,
że czuję się ograniczony i zmuszony do pracy w stylu zupełnie kogoś innego. Ponadto automatycznie generowana
zawartość i konfiguracja wydają się być zbyt ogólne, aby stały się szczególnie użyteczne. W rozdziale 10.
wspomniałem, że jednym z niebezpieczeństw użycia układu responsywnego dla urządzeń mobilnych jest
uzyskanie przeciętnego kodu, który jest dopasowany do jak największej liczby urządzeń. W podobny sposób
można określić szablony Visual Studio. Microsoft nie wie, jakiego rodzaju aplikacje będziesz chciał tworzyć,
i dlatego stara się zapewnić obsługę maksymalnej liczby scenariuszy. Wynik jest tak bezbarwny i uogólniony,
że zawartość generowaną przez Visual Studio wyrzucam od razu na początku pracy z projektem.
Moja rada (udzielana każdemu, kto popełnia błąd, pytając o nią) brzmi: rozpoczynaj pracę z pustym
projektem, a następnie dodawaj niezbędne katalogi, pliki i pakiety. Dzięki takiemu podejściu nie tylko lepiej
poznasz sposób działania platformy MVC, ale również zachowasz pełną kontrolę nad zawartością aplikacji.
Moje osobiste preferencje nie muszą pasować do Twojego doświadczenia w zakresie programowania. Być
może dostarczane przez Visual Studio szablony i szkielety kodu uznasz za dużo bardziej użyteczne, niż są dla
mnie, zwłaszcza jeżeli dopiero zaczynasz programowanie na platformie ASP.NET i nie wykształciłeś jeszcze
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
swojego stylu programowania. Ponadto szablony projektów możesz uznać za użyteczny zasób i źródło idei.
Powinieneś jednak zachować ostrożność podczas dodawania funkcji do aplikacji, zanim dokładnie nie
poznasz jej sposobu działania.
Tworzenie projektu
Kiedy po raz pierwszy tworzysz projekt MVC, do dyspozycji masz dwa punkty startowe: szablony Empty i MVC.
Nazwy szablonów są nieco mylące, ponieważ podstawowe katalogi i podzespoły niezbędne dla platformy
MVC można dodać do każdego szablonu projektu. W tym celu należy zaznaczyć pole wyboru MVC w sekcji
Dodaj foldery i podstawowe odwołania dla: okna dialogowego Nowy projekt, jak pokazano na rysunku 14.1.
W przypadku szablonu projektu MVC odpowiednia opcja jest zaznaczona domyślnie.
Rysunek 14.1. Wybór typu projektu, katalogów i podzespołów dla nowego projektu
Faktyczna różnica polega na umieszczeniu dodatkowej zawartości w szablonie projektu MVC. W ten sposób,
tworząc nowy projekt, programista otrzymuje prawdziwy punkt startowy, zawierający pewne domyślne kontrolery,
widoki, konfigurację zabezpieczeń, popularne pakiety JavaScript i CSS (na przykład jQuery i Bootstrap), a układ
jest oparty na bibliotece Bootstrap, dostarczającej motyw graficzny dla zawartości aplikacji. Z kolei szablon
Empty zawiera po prostu podstawowe odwołania wymagane przez platformę MVC oraz najprostszą strukturę
katalogów. Szablon MVC dodaje znaczną ilość różnego rodzaju kodu, a różnica między omawianymi szablonami
jest wyraźnie widoczna na rysunku 14.2, który pokazuje zawartość dwóch nowo utworzonych projektów. Projekt
po lewej stronie utworzono na podstawie szablonu Empty wraz z zaznaczonym polem wyboru MVC. Okna po prawej
stronie pokazują zawartość projektu utworzonego na podstawie szablonu MVC. Aby zmieścić na stronie książki listę
wszystkich plików, zawartość niektórych katalogów projektu musiałem otworzyć w oddzielnych oknach.
W przeciwnym razie cała lista nie zmieściłaby się na stronie książki.
Wprawdzie liczba dodatkowych plików umieszczanych w projekcie opartym na szablonie MVC może
przerażać, ale nie jest tak źle. Część plików jest powiązana z mechanizmem uwierzytelniania, inne to pliki
JavaScript i CSS dostarczane w postaci zarówno zwykłej, jak i zminimalizowanej. (Sposób użycia tych plików
przedstawię w rozdziale 26.).
334
ROZDZIAŁ 14.  PRZEGLĄD PROJEKTU MVC
Rysunek 14.2. Początkowa zawartość domyślnie dodawana do projektów Empty i MVC
 Wskazówka Podzespoły Visual Studio są przez szablon MVC tworzone za pomocą pakietów NuGet. Oznacza to, że
użyte pakiety możesz zobaczyć po wybraniu opcji Narzędzia/Menedżer pakietów NuGet/Zarządzaj pakietami NuGet
dla rozwiązania…. To jednocześnie wskazuje na możliwość dodawania tych samych pakietów do dowolnego projektu,
w tym także utworzonego na podstawie szablonu Empty. (Takie rozwiązanie zastosowałem w pierwszej części książki).
Niezależnie od rodzaju, szablony pozwalają na tworzenie projektów o podobnej strukturze. Niektóre
z elementów projektu mają specjalne role, wbudowane w ASP.NET lub platformę MVC. Inne są wynikiem
konwencji nazewnictwa. Każdy z tych plików i katalogów został opisany w tabeli 14.1. Część plików może
nie znajdować się w domyślnych projektach, ale zostaną omówione w dalszych rozdziałach.
335
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Tabela 14.1. Podsumowanie elementów projektu MVC
Katalog lub plik
Opis
Uwagi
/App_Data
W katalogu tym umieszczamy prywatne dane,
takie jak pliki XML lub bazy danych
wykorzystywane przez SQL Server Express,
SQLite bądź inne repozytoria plikowe.
Ten katalog zawiera pewne ustawienia
początkowe projektu, między innymi
definicje tras, filtry oraz paczki plików.
IIS nie udostępnia zawartości tego
katalogu.
/App_Start
/Areas
/bin
Obszary umożliwiają partycjonowanie
ogromnej aplikacji na mniejsze fragmenty.
Umieszczane są tu skompilowane podzespoły
aplikacji MVC wraz z wszystkimi
wykorzystywanymi podzespołami,
które nie znajdują się w GAC.
/Content
Jest to katalog na statyczną treść, na przykład
pliki CSS oraz obrazy.
/Controllers
Znajdują się tu klasy kontrolerów.
/Models
Jest to miejsce na klasy modelu widoku oraz
modelu domeny, choć oprócz najprostszych
aplikacji lepiej jest definiować model domeny
w dedykowanym projekcie, jak pokazałem
to w aplikacji SportsStore.
Jest to katalog przeznaczony na biblioteki
JavaScript dla naszej aplikacji.
/Scripts
/Views
/Views/Shared
/Views/
Web.config
336
Katalog ten jest przeznaczony na widoki i widoki
częściowe, zwykle grupowane w katalogach
mających nazwy kontrolerów, z którymi
są skojarzone.
Katalog ten jest przeznaczony na pliki układów
i widoków, które nie są skojarzone z konkretnym
kontrolerem.
To nie jest plik konfiguracyjny dla aplikacji.
Zawiera on konfigurację wymaganą do tego,
aby widoki działały w ASP.NET, oraz blokuje
możliwość udostępniania widoków przez IIS.
Przestrzenie nazw są domyślnie importowane
do widoków.
System routingu zostanie omówiony
w rozdziałach 15. i 16., filtry
w rozdziale 18., natomiast paczki
plików w rozdziale 26.
Obszary zostaną omówione
w rozdziale 15.
Nie zobaczysz katalogu bin w oknie
Eksplorator rozwiązania, o ile nie
klikniesz przycisku Pokaż wszystkie
pliki. Ponieważ te pliki binarne
są generowane w czasie kompilacji,
nie powinieneś ich przechowywać
w systemie kontroli wersji.
Jest to konwencja, ale niewymagana.
Statyczne dane można umieścić
w dowolnym odpowiadającym
nam miejscu projektu.
Jest to konwencja. Klasy kontrolerów
można umieszczać w dowolnym
katalogu, ponieważ są kompilowane
do tego samego podzespołu.
Jest to konwencja. Klasy modelu
można definiować w dowolnym
katalogu projektu lub w osobnym
projekcie.
Jest to konwencja. Pliki skryptów
można umieścić w dowolnej
lokalizacji, ponieważ są one innym
typem zawartości statycznej. Więcej
informacji na temat zarządzania
plikami skryptów znajdziesz
w rozdziale 26.
Plik /Views/Web.config uniemożliwia
udostępnianie zawartości tych
katalogów. Widoki są generowane
za pomocą metod akcji.
ROZDZIAŁ 14.  PRZEGLĄD PROJEKTU MVC
Tabela 14.1. Podsumowanie elementów projektu MVC (ciąg dalszy)
Katalog lub plik
Opis
Uwagi
/Global.asax
Definiuje globalną klasę aplikacji ASP.NET.
Jego klasa kodu ukrytego (/Global.asax.cs) jest
miejscem, w którym rejestrujemy konfigurację
routingu, jak również dodajemy kod, jaki
powinien wykonać się w czasie inicjalizacji
lub wyłączenia aplikacji albo w przypadku
wystąpienia nieobsłużonego wyjątku.
Plik Global.asax ma w aplikacji
MVC taką samą funkcję
jak w aplikacji Web Forms.
/Web.config
Jest to plik konfiguracyjny dla naszej aplikacji.
Plik Web.config ma w aplikacji MVC
taką samą funkcję jak w aplikacji
Web Forms.
Przedstawienie konwencji MVC
W projektach MVC występują dwa rodzaje konwencji. Pierwszy rodzaj to raczej sugestia na temat tego, w jaki
sposób możemy tworzyć strukturę projektu. Jest to na przykład konwencja zachęcająca nas do umieszczenia
wszystkich plików JavaScript w katalogu Scripts. Programiści MVC oczekują, że znajdą je w tym właśnie katalogu.
Menedżer pakietów NuGet również umieszcza w nim pliki JavaScript dołączane do projektu MVC.
Możemy jednak zmienić nazwę katalogu Scripts lub całkiem go usunąć i umieścić skrypty w dowolnym
innym miejscu. Nie spowoduje to, że platforma MVC nie będzie w stanie uruchomić aplikacji.
Inny rodzaj konwencji wynika z zasady konwencja przed konfiguracją, która była jedną z przyczyn tak ogromnej
popularności Ruby on Rails. Konwencja przed konfiguracją oznacza, że nie musimy jawnie konfigurować połączeń
pomiędzy kontrolerami i ich widokami. Po prostu stosujemy określone konwencje nazewnictwa i wszystko działa
bez zarzutu. W przypadku tego typu konwencji mamy mniejsze możliwości zmiany struktury projektu.
W kolejnych punktach przedstawimy konwencje stosowane zamiast konfiguracji.
 Wskazówka Wszystkie konwencje mogą być zmienione przez użycie własnego silnika widoku, co opiszę
w rozdziale 20., ale to nie jest łatwe zadanie. W większości przypadków w projektach MVC będziesz miał jednak
do czynienia z wymienionymi konwencjami.
Stosowanie konwencji dla klas kontrolerów
Klasa kontrolera musi kończyć się słowem Controller, np.: ProductsController, AdminController czy też
HomeController. Odwołując się do kontrolera z poziomu projektu, na przykład podczas użycia metody
pomocniczej HTML, podajemy pierwszą część nazwy (na przykład Product), a platforma MVC automatycznie
doda Controller do nazwy i zacznie szukać klasy kontrolera.
 Wskazówka Można zmienić to zachowanie przez utworzenie własnej implementacji interfejsu
IControllerFactory, co opiszę w rozdziale 19.
Stosowanie konwencji dla widoków
Widoki i widoki częściowe powinny być umieszczone w katalogu /Views/nazwakontrolera. Na przykład
widok skojarzony z klasą ProductController powinien znajdować się w katalogu /Views/Product.
 Wskazówka Zwróć uwagę, że pomijamy drugą część nazwy klasy w podkatalogu Views; używamy katalogu
/Views/Product, a nie /Views/ProductController. Może Ci się to wydawać na początku mało intuicyjne, ale szybko
stanie się Twoją drugą naturą.
337
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Platforma MVC oczekuje, że domyślny widok dla metody akcji powinien nosić nazwę tej metody. Na przykład
widok skojarzony z metodą akcji List powinien mieć nazwę List.cshtml. Dlatego domyślny widok dla metody
akcji List z klasy ProductController powinien znajdować się w /Views/Product/List.cshtml. Domyślny widok
jest używany, gdy z metody akcji zwrócimy wynik wywołania metody View, na przykład:
...
return View();
...
Możemy również podać nazwę innego widoku, na przykład:
...
return View("InnyWidok");
...
Zwróć uwagę, że nie podajemy rozszerzenia nazwy pliku ani ścieżki dostępu do widoku. Platforma MVC
szuka widoku w katalogu o nazwie kontrolera, a następnie w katalogu /Views/Shared. Dlatego widoki stosowane
przez więcej niż jeden kontroler możemy umieścić w katalogu /Views/Shared; platforma znajdzie je w razie
potrzeby ich użycia.
Stosowanie konwencji dla układów
Konwencją nazewnictwa dla układów jest poprzedzanie ich nazw znakiem podkreślenia. Pliki układów są
umieszczane w katalogu /Views/Shared. Visual Studio tworzy plik układu o nazwie _Layout.cshtml, który
wchodzi w skład wszystkich szablonów projektów poza Pusta. Układ ten jest stosowany domyślnie do
wszystkich widoków poprzez plik /Views/_ViewStart.cshtml. Jeżeli nie chcesz, aby domyślny widok był
stosowany do widoku, możesz zmienić ustawienie w pliku _ViewStart.cshtml, definiując w nim inny plik
układu (lub usuwając ten plik).
@{
Layout = "~/Views/Shared/_MyLayout.cshtml";
}
Można również zablokować wszystkie układy dla pojedynczego widoku:
@{
Layout = null;
}
Debugowanie aplikacji MVC
Aplikację ASP.NET MVC można debugować dokładnie tak samo jak aplikację ASP.NET Web Forms. Debuger
w Visual Studio jest niezwykle zaawansowanym i elastycznym narzędziem, które ma wiele funkcji i zastosowań.
W książce tej przedstawię jedynie kilka podstawowych funkcji. Pokażę, jak skonfigurować debuger
i przeprowadzać różne zadania związane z usuwaniem błędów w projekcie MVC.
Tworzenie przykładowego projektu
Aby zademonstrować działanie debugera, utworzymy nowy projekt MVC, korzystający z szablonu MVC. W ten
sposób będziesz mógł zobaczyć, jak przygotowywana jest zawartość i konfiguracja podstawowa projektu, a także jaki
efekt ma zastosowanie motywu domyślnego w widokach. Nazwijmy nasz projekt DebuggingDemo. Jak pokazano na
rysunku 14.3, jako uwierzytelnianie wybrano opcję Indywidualne konta użytkowników, która oznacza użycie
podstawowego systemu uwierzytelniania użytkowników.
338
ROZDZIAŁ 14.  PRZEGLĄD PROJEKTU MVC
Rysunek 14.3. Tworzenie projektu DebuggingDemo
Po kliknięciu przycisku OK Visual Studio utworzy projekt, umieści w nim katalogi i pliki pakietów
domyślnych znajdujących się w szablonie MVC. Dodane do projektu pliki i sposób ich konfiguracji możesz
zobaczyć po uruchomieniu aplikacji (rysunek 14.4).
Rysunek 14.4. Efekt działania plików znajdujących się w szablonie projektu MVC
Projekt zawiera pewne miejsca zarejestrowane pozwalające na podanie nazwy aplikacji i promocję
marki, a także oferuje łącza do dokumentów MVC, pakietów NuGet oraz opcji dotyczących hostingu.
339
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Pasek nawigacyjny znajduje się na górze strony i ma taką samą postać, jakiej użyłem w aplikacji
SportsStore. Ponadto w kodzie zastosowano pewne funkcje układu responsywnego. Aby się o tym
przekonać, zmień szerokość okna przeglądarki internetowej.
Tworzenie kontrolera
Wprawdzie Visual Studio tworzy kontroler Home jako część projektu początkowego, ale jego kod zastąpimy
przedstawionym na listingu 14.1.
Listing 14.1. Zawartość pliku HomeController.cs
using System.Web.Mvc;
namespace DebuggingDemo.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
int firstVal = 10;
int secondVal = 5;
int result = firstVal / secondVal;
ViewBag.Message = "Witamy na platformie ASP.NET MVC!";
return View(result);
}
}
}
Tworzenie widoku
Visual Studio tworzy także plik widoku Views/Home/Index.cshtml, jako część pierwotnej zawartości projektu.
Ponieważ nie potrzebujemy zawartości domyślnej tego widoku, zastąp ją kodem przedstawionym na listingu 14.2.
Listing 14.2. Zawartość pliku Index.cshtml
@model int
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
<title>Index</title>
</head>
<body>
<h2 class="message">@ViewData["Message"]</h2>
<p>
Wynik obliczeń: @Model
</p>
</body>
</html>
340
ROZDZIAŁ 14.  PRZEGLĄD PROJEKTU MVC
Ostatnim krokiem w trakcie tych przygotowań projektu jest dodanie stylu do pliku /Content/Site.css
przedstawionego na listingu 14.3 oraz zmiana jednego z istniejących. Plik Site.css jest tworzony przez Visual
Studio jako część szablonu projektu MVC, stanowi domyślne miejsce dla stylów CSS aplikacji. (W przedstawionym
na listingu 14.2 kodzie widoku dodałem element <link> importujący plik CSS do widoku Index.cshtml).
Listing 14.3. Dodanie stylu do pliku /Content/Site.css
body { padding-top: 5px; padding-bottom: 5px; }
.field-validation-error { color: #b94a48; }
.field-validation-valid { display: none; }
input.input-validation-error { border: 1px solid #b94a48; }
input[type="checkbox"].input-validation-error { border: 0 none; }
.validation-summary-errors { color: #b94a48; }
.validation-summary-valid { display: none; }
.no-color { background-color: white; border-style:none; }
.message { font-size: 20pt; text-decoration: underline; }
Uruchamianie debugera Visual Studio
Domyślnie Visual Studio włącza możliwość debugowania nowych projektów, choć warto wiedzieć, jak
można samodzielnie to zmienić. Najważniejsze ustawienie znajduje się w pliku Web.config, położonym
w katalogu głównym aplikacji. Odpowiednie ustawienie jest w elemencie <system.web>, jak pokazano
na listingu 14.4.
 Ostrzeżenie Nie należy instalować aplikacji na serwerze produkcyjnym bez wcześniejszego ustawienia wartości false
opcji debug. Jeżeli do wdrożenia aplikacji używasz Visual Studio (podobnie jak to pokazałem w rozdziale 13.),
wówczas odpowiednia zmiana zostanie wprowadzona automatycznie po wybraniu konfiguracji Release w projekcie.
Listing 14.4. Atrybut Debug w pliku Web.config
...
<system.web>
<httpRuntime targetFramework="4.5.1" />
<compilation debug="true" targetFramework="4.5.1" />
</system.web>
...
Spora liczba operacji kompilacji w projekcie MVC jest przeprowadzana, gdy aplikacja działa w serwerze IIS.
W trakcie prac nad aplikacją musisz więc się upewnić, że atrybutowi debug jest przypisana wartość true. W ten
sposób debuger będzie mógł operować na plikach klas wygenerowanych podczas kompilacji na żądanie.
Oprócz zmiany w pliku konfiguracyjnym Web.config, konieczne jest upewnienie się, że Visual Studio
umieszcza informacje debugowania w tworzonych plikach klas. Wprawdzie to nie ma znaczenia krytycznego,
ale może powodować problemy, jeśli poszczególne ustawienia debugowania nie są zsynchronizowane.
Upewnij się o wybraniu opcji konfiguracyjnej Debug na pasku narzędziowym Visual Studio, jak pokazano
na rysunku 14.5.
Aby rozpocząć debugowanie aplikacji na platformie MVC, wybierz opcję Start Debugging z menu Debuguj
w Visual Studio lub kliknij zieloną ikonę strzałki na pasku narzędziowym Visual Studio (wspomnianą ikonę
widać na rysunku 14.5 tuż obok nazwy przeglądarki internetowej używanej do wyświetlenia aplikacji
— w omawianym przykładzie jest to Google Chrome).
Jeżeli w pliku konfiguracyjnym Web.config wartością atrybutu debug jest false, wtedy podczas
uruchamiania debugera Visual Studio wyświetli okno dialogowe pokazane na rysunku 14.6. Wybierz opcję
pozwalającą Visual Studio na przeprowadzenie modyfikacji pliku Web.config, a następnie kliknij przycisk OK.
Debuger zostanie uruchomiony.
341
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 14.5. Wybór opcji konfiguracyjnej Debug
Rysunek 14.6. Okno dialogowe wyświetlane przez Visual Studio, gdy debugowanie jest wyłączone w pliku
Web.config
W tym momencie aplikacja jest uruchomiona i wyświetlona w oknie przeglądarki, jak pokazano na rysunku 14.7.
Debuger został dołączony do naszej aplikacji, ale nie zauważymy żadnej różnicy do momentu przerwania jej
działania przez debuger (przedstawię to w następnym punkcie). Aby zatrzymać debuger, wybierz opcję Stop
Debugging z menu Debuguj lub zamknij okno przeglądarki internetowej.
Rysunek 14.7. Uruchomienie debugera
Przerywanie pracy aplikacji przez debuger Visual Studio
Aplikacja działająca z podłączonym debugerem zachowuje się normalnie do momentu wystąpienia przerwania,
w którym działanie aplikacji jest zatrzymywane i sterowanie jest przekazywane do debugera. W tym momencie
możemy przeglądać i modyfikować stan aplikacji. Przerwania pojawiają się z dwóch głównych powodów:
gdy zostanie napotkany punkt przerwania lub gdy wystąpi nieobsłużony wyjątek. Przykłady przedstawię
w kolejnych punktach.
342
ROZDZIAŁ 14.  PRZEGLĄD PROJEKTU MVC
Użycie punktów przerwania
Punkt przerwania to instrukcja informująca debuger o konieczności zatrzymania wykonywania aplikacji
i przekazania kontroli programiście. W tym momencie możemy przeglądać stan aplikacji, sprawdzać, co się
w niej dzieje i — opcjonalnie — wznowić działanie aplikacji.
Aby utworzyć punkt przerwania, kliknij prawym przyciskiem myszy kod i wybierz opcję Punkt przerwania/
Insert Breakpoint z menu kontekstowego. W celu zademonstrowania działania punktów przerwania umieszczamy
jeden taki punkt w pierwszym poleceniu metody akcji Index, w klasie Home. Na marginesie edytora tekstów
pojawi się czerwona kropka (rysunek 14.8).
Rysunek 14.8. Dodawanie punktu przerwania w pierwszym poleceniu metody akcji Index
Rysunek 14.9. Napotkanie punktu przerwania
Aby zobaczyć efekt dodania punktu przerwania, musisz uruchomić debuger poprzez wybranie opcji Start
Debugging z menu Debuguj w Visual Studio. Aplikacja będzie działała aż do chwili dotarcia do polecenia
oznaczonego punktem przerwania. W tym momencie debuger przerwie działanie aplikacji i przekaże kontrolę
programiście. Jak pokazano na rysunku 14.9, Visual Studio podświetla wiersz kodu, w którym nastąpiło
zatrzymanie działania aplikacji.
343
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
 Uwaga Punkt przerwania działa tylko w momencie, gdy skojarzona z nim instrukcja jest wykonywana. W naszym
przykładzie punkt przerwania jest osiągany od razu po uruchomieniu aplikacji, ponieważ znajduje się w metodzie
akcji wywoływanej w chwili otrzymania żądania domyślnego adresu URL. Jeżeli umieścisz punkt przerwania
wewnątrz innej metody akcji, musisz użyć przeglądarki do wywołania adresu URL skojarzonego z tą metodą.
Może to oznaczać, że konieczne będzie skorzystanie z aplikacji w taki sposób, w jaki korzystają z niej użytkownicy,
lub bezpośrednie przejście do adresu URL w oknie przeglądarki.
Po przejęciu kontroli nad wykonywaniem aplikacji możesz przejść do kolejnego polecenia, śledzić
wykonywanie w innych metodach i ogólnie przeglądać stan aplikacji. Do tego celu wykorzystujesz przyciski
znajdujące się na pasku narzędzi w Visual Studio bądź opcje dostępne w menu Debuguj. Oprócz kontroli
nad wykonywaniem aplikacji, Visual Studio dostarcza Ci także wielu użytecznych informacji dotyczących
stanu aplikacji. W rzeczywistości wspomnianych informacji jest tak wiele, że w niniejszej książce nie ma
wystarczająco dużo miejsca na przedstawienie czegokolwiek więcej poza podstawami.
Przeglądanie wartości danych w edytorze kodu
Najczęstszym sposobem użycia punktów przerwania jest próba znalezienia błędów w kodzie. Zanim będziesz
mógł usunąć błąd z kodu, najpierw musisz ustalić, co tak naprawdę się dzieje. Jedną z najbardziej użytecznych
funkcji oferowanych przez Visual Studio jest możliwość przeglądania i monitorowania wartości zmiennych
bezpośrednio w edytorze kodu.
Przykładowo, uruchom aplikację w debugerze i zaczekaj na zatrzymanie działania aplikacji po dotarciu
do dodanego wcześniej punktu przerwania. Kiedy debuger zatrzyma działanie aplikacji, umieść kursor
myszy nad poleceniem definiującym zmienną result. Po chwili na ekranie zobaczysz małe wyskakujące
okno przedstawiające bieżącą wartość wspomnianej zmiennej (rysunek 14.10). Ponieważ wspomniane
okno jest małe, na rysunku pokazano także jego powiększoną wersję.
Rysunek 14.10. Wyświetlenie wartości zmiennej w edytorze kodu Visual Studio
Wykonywanie poleceń w metodzie akcji Index nie dotarło do miejsca, w którym następuje przypisanie
wartości zmiennej result, więc Visual Studio pokazuje wartość domyślną wymienionej zmiennej, czyli 0 dla
typu int. Wybieraj opcję Step Over w menu Debuguj (lub naciskaj klawisz F10) dopóty, dopóki nie dotrzesz
do polecenia, w którym następuje zdefiniowanie właściwości ViewBag.Message. Teraz ponownie umieść
kursor myszy nad zmienną result. Po wykonaniu polecenia przypisującego wartość zmiennej result wynik
wykonania tego polecenia możesz zobaczyć na rysunku 14.11.
344
ROZDZIAŁ 14.  PRZEGLĄD PROJEKTU MVC
Rysunek 14.11. Efekt przypisania wartości zmiennej
Funkcji tej używamy w celu rozpoczęcia procesu wyszukiwania błędu, ponieważ daje ona natychmiastowy
wgląd do tego, co się dzieje w aplikacji. Omawiana funkcja okazuje się szczególnie użyteczna w wykrywaniu
wartości null oznaczających, że zmiennej nie została przypisana wartość (to źródło wielu błędów, jak wynika
z mojego doświadczenia).
W wyświetlonym oknie, po prawej stronie wartości, możesz dostrzec ikonę pinezki. Jeżeli ją klikniesz,
dane okno na stałe pozostanie wyświetlone na ekranie i będzie wskazywało zmianę wartości zmiennej.
W ten sposób możesz monitorować jedną lub więcej zmiennych i natychmiast dowiadywać się o zmianie
ich wartości i poznawać nowo przypisane wartości.
Przegląd stanu aplikacji w oknie debugera
Visual Studio zawiera wiele różnych okien, które można wykorzystać do pobierania informacji o aplikacji, gdy
jej działanie zostało zatrzymane w punkcie przerwania. Pełna lista okien jest dostępna w menu Debuguj/Okna,
ale dwa najważniejsze z nich to Locals i Call Stack. W oknie Locals automatycznie są wyświetlane wartości
wszystkich zmiennych w aktualnym zasięgu, co pokazano na rysunku 14.12. W ten sposób otrzymujesz
pojedynczy widok zawierający wszystkie zmienne, którymi możesz być zainteresowany.
Rysunek 14.12. Okno Locals
Zmienne, których wartości uległy zmianie w trakcie poprzednio wykonanego polecenia, są wyświetlone
w kolorze czerwonym. Na rysunku widać, że wartość zmiennej result jest wyświetlona na czerwono,
ponieważ w poprzednim poleceniu nastąpiło przypisanie jej wartości.
 Wskazówka Zestaw zmiennych wyświetlonych w oknie Locals ulega zmianie wraz z poruszaniem się po aplikacji.
Jeżeli chcesz globalnie śledzić wartość wybranej zmiennej, kliknij ją prawym przyciskiem myszy, a następnie
z menu kontekstowego wybierz opcję Add Watch. Elementy w oknie Watch nie ulegają zmianie podczas
wykonywania kolejnych poleceń w aplikacji i tym samym masz doskonałe miejsce na ich śledzenie.
345
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
W oknie Call Stack jest wyświetlana sekwencja wywołań, które doprowadziły do aktualnego stanu
aplikacji. To może być bardzo użyteczne, jeśli próbujesz znaleźć powód dziwnego zachowania aplikacji
— możesz wówczas przejrzeć stos wywołań i poznać przyczyny, które doprowadziły do wywołania danego
punktu przerwania. (Na rysunku nie pokazano okna Call Stack, ponieważ w omawianej prostej aplikacji nie
wystąpiło wystarczająco dużo wywołań, aby zapewnić użyteczny wgląd w nie. Zachęcam Cię do zapoznania
się z omawianymi oraz pozostałymi oknami w Visual Studio, aby w ten sposób dowiedzieć się, jakie informacje
możesz uzyskać z debugera).
 Wskazówka Możliwe jest debugowanie widoków przez wstawianie do nich punktów przerwania. Może to być
bardzo pomocne w kontrolowaniu wartości właściwości modelu widoku. Aby dodać punkt przerwania do widoku, należy
wykonać taką samą operację jak w przypadku pliku kodu — kliknąć prawym przyciskiem myszy interesującą nas
instrukcję Razor i wybrać Punkt przerwania/Insert Breakpoint.
Przerywanie aplikacji przez wyjątki
Nieobsłużone wyjątki są smutnym faktem. Jednym z powodów wykonywania wielu testów jednostkowych
i integracyjnych jest minimalizacja prawdopodobieństwa wystąpienia takiego wyjątku w środowisku produkcyjnym.
Debuger Visual Studio jest uruchamiany automatycznie w przypadku pojawienia się nieobsłużonego wyjątku.
 Uwaga Jedynie nieobsłużone wyjątki powodują wywołanie debugera. Wyjątek staje się obsłużony, gdy przechwycimy
go w bloku try ... catch. Obsłużone wyjątki są użytecznym narzędziem programistycznym. Są one wykorzystywane
do obsługiwania scenariuszy, w których metoda nie jest w stanie dokończyć zadania i musimy poinformować
o tym wywołującego. Nieobsłużone wyjątki są mankamentem, ponieważ reprezentują nieoczekiwane warunki
w aplikacji (i powodują wyświetlenie użytkownikowi informacji o błędzie).
Aby zademonstrować przerwanie pracy aplikacji w przypadku wyjątku, do naszej metody akcji Index
wprowadzimy małą zmianę pokazaną na listingu 14.5.
Listing 14.5. Dodatkowe polecenie w pliku HomeController.cs powodujące wystąpienie wyjątku
using System.Web.Mvc;
namespace DebuggingDemo.Controllers {
public class HomeController : Controller {
public ActionResult Index() {
int firstVal = 10;
int secondVal = 0;
int result = firstVal / secondVal;
ViewBag.Message = "Witamy w ASP.NET MVC!";
return View(result);
}
}
}
Zmieniliśmy wartość zmiennej secondVal na 0, co spowoduje wyjątek w instrukcji, w której firstVal jest
dzielona przez secondVal.
 Uwaga Z metody akcji Index usunięto także punkt przerwania poprzez jego kliknięcie prawym przyciskiem myszy
i wybranie opcji Delete Breakpoint z wyświetlonego menu kontekstowego.
346
ROZDZIAŁ 14.  PRZEGLĄD PROJEKTU MVC
Jeżeli uruchomisz debuger, aplikacja będzie działała do momentu zgłoszenia wyjątku, gdy pojawi się okno
informacji o wyjątku pokazane na rysunku 14.13.
Rysunek 14.13. Okno pomocnicze obsługi wyjątku
W tym oknie pomocniczym znajdują się informacje na temat wyjątku. Gdy debuger zostanie wywołany
w wierszu powodującym wyjątek, możemy skontrolować stan aplikacji i sterować jej działaniem, podobnie jak
w przypadku punktu przerwania.
Użycie opcji Edit and Continue
Jedną z najbardziej interesujących funkcji debugera Visual Studio jest Edit and Continue. Gdy zostanie wywołany
debuger, można zmodyfikować kod, a następnie kontynuować debugowanie. Visual Studio ponownie skompiluje
aplikację, po czym odtworzy jej stan w momencie aktywowania debugera.
Włączanie opcji Edit and Continue
Konieczne jest włączenie opcji Edit and Continue w dwóch miejscach:
 Upewnij się, że w sekcji Edit and Continue dla opcji Debugging zaznaczona jest opcja Enable Edit and
Continue (wybierz Opcje… z menu Narzędzia), jak pokazano na rysunku 14.14.
 We właściwościach projektu (wybierz Właściwości DebuggingDemo… z menu Projekt) przejdź do sekcji
Sieć Web i upewnij się, że zaznaczona jest opcja Włącz tryb edycji i kontynuuj (rysunek 14.15).
Modyfikowanie projektu
Funkcja Edit and Continue jest nieco kapryśna. Istnieją przypadki, w których nie będzie ona działać. Jeden
z nich jest pokazany dla metody Index z klasy HomeController — użyte są w niej obiekty dynamiczne.
Rozwiązaniem problemu jest umieszczenie znaków komentarza na początku wiersza, w którym korzystamy
z funkcji ViewBag, w pliku HomeController.cs, jak przedstawiono na listingu 14.6.
347
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 14.14. Włączenie opcji Edit and Continue w oknie dialogowym Opcje
Rysunek 14.15. Włączanie trybu edycji i kontynuacji we właściwościach projektu
Listing 14.6. Usunięcie wywołania ViewBag z metody Index w pliku HomeController.cs
using System.Web.Mvc;
namespace DebuggingDemo.Controllers {
public class HomeController : Controller {
public ActionResult Index() {
int firstVal = 10;
int secondVal = 0;
int result = firstVal / secondVal;
// poniższe polecenie zostało poprzedzone znakiem komentarza
// ViewBag.Message = "Witamy w ASP.NET MVC!";
return View(result);
}
}
}
348
ROZDZIAŁ 14.  PRZEGLĄD PROJEKTU MVC
Analogiczną zmianę musimy wykonać w widoku Index.cshtml, co jest pokazane na listingu 14.7.
Listing 14.7. Usunięcie wywołania ViewBag z widoku
@model int
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
<title>Index</title>
</head>
<body>
<!-- Poniższy element został umieszczony w komentarzu. -->
<!--<h2 class="message">@ViewData["Message"]</h2>-->
<p>
Wartość obliczeń to: @Model
</p>
</body>
</html>
Edycja i kontynuowanie pracy
Jesteśmy już gotowi do użycia funkcji Edit and Continue. Zaczniemy od wybrania opcji Start Debugging
z menu Debuguj. Aplikacja uruchomi się z dołączonym debugerem i będzie realizowała metodę Index do
momentu wykonania wiersza, w którym przeprowadzamy obliczenia. Wartość drugiego parametru wynosi
zero, co spowoduje zgłoszenie wyjątku. W tym momencie debuger przerwie działanie i wyświetli się
okno informacji o wyjątku (jak pokazano na wcześniejszym rysunku 14.13).
Kliknij łącze Włącz edytowanie w oknie wyjątku. W edytorze kodu zmień wyrażenie obliczające wartość
zmiennej result na następujące:
...
int result = firstVal / 2;
...
Usunęliśmy odwołanie do zmiennej secondVal i zastąpiliśmy je wartością 2. Z menu Debuguj wybierz
Continue. Aplikacja będzie kontynuowała działanie. Nowa wartość przypisana zmiennej zostanie użyta do
wygenerowania wyniku zmiennej result, a przeglądarka wyświetli stronę, zamieszczoną na rysunku 14.16.
Rysunek 14.16. Efekt usunięcia błędu dzięki użyciu funkcji Edit and Continue
Poświęć chwilę na analizę wyniku tych działań. Uruchomiliśmy aplikację zawierającą błąd — próbę dzielenia
przez zero. Debuger wykrył wyjątek i zatrzymał wykonywanie programu. Aby poprawić błąd, zmodyfikowaliśmy
kod, zamieniając odwołanie do zmiennej na literał o wartości 5. Następnie wznowiliśmy działanie debugera.
W tym momencie aplikacja została ponownie skompilowana przez Visual Studio, dzięki czemu nasza zmiana jest
349
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
uwzględniona w procesie kompilacji, stan aplikacji jest przywrócony, a następnie kontynuowany w normalny
sposób. Przeglądarka otrzyma wygenerowany wynik uwzględniający naszą poprawkę. Bez opcji Edit and
Continue musielibyśmy zatrzymać aplikację, wprowadzić zmiany, skompilować aplikację i ponownie
uruchomić debuger. Następnie musielibyśmy powtórzyć w przeglądarce kroki, które doprowadziły do
momentu wywołania debugera. Uniknięcie tego ostatniego kroku jest tu najważniejsze. Odtworzenie
skomplikowanych błędów może wymagać wykonania wielu operacji w aplikacji, a możliwość testowania
potencjalnych rozwiązań bez potrzeby powtarzania tych kroków pozwala zaoszczędzić czas i nerwy programisty.
Użycie funkcji połączonych przeglądarek
Visual Studio 2013 zawiera funkcję o nazwie połączone przeglądarki, która pozwala na jednoczesne wyświetlanie
aplikacji w wielu przeglądarkach internetowych i ich odświeżanie po wprowadzeniu zmiany. Ta funkcja
okazuje się najbardziej użyteczna, gdy działanie aplikacji jest stabilne i pozostało już tylko dopracowanie
kodu HTML i CSS generowanego przez widoki (wkrótce to wyjaśnię).
W celu użycia funkcji połączonych przeglądarek na pasku narzędzi w Visual Studio kliknij mały trójkąt
skierowany w dół obok nazwy wybranej przeglądarki internetowej, a następnie z menu wybierz opcję
Przeglądaj za pomocą…, jak pokazano na rysunku 14.17.
Rysunek 14.17. Przygotowanie do wyboru przeglądarek internetowych używanych wraz z funkcją Browser Link
Na ekranie zostanie wyświetlone okno dialogowe Przeglądaj w. Naciśnij klawisz Control, a następnie
zaznacz przeglądarki internetowe, których chcesz używać. Na rysunku 14.18 widać, że wybrałem Google
Chrome i Opera Internet Browser. Za pomocą tego okna dialogowego możesz również dodać nowe
przeglądarki, choć Visual Studio całkiem dobrze radzi sobie z wykrywaniem większości najważniejszych
przeglądarek internetowych.
Po kliknięciu przycisku Przeglądaj Visual Studio uruchomi wybrane przeglądarki internetowe i wczyta
aktualny projekt w każdej z nich. Teraz możesz przeprowadzić edycję kodu w aplikacji, a następnie uaktualnić
wszystkie okna przeglądarek internetowych, wybierając opcję Odśwież połączone przeglądarki z paska
narzędzi w Visual Studio, jak pokazano na rysunku 14.19. Aplikacja zostanie automatycznie skompilowana
i będziesz mógł zobaczyć wprowadzone zmiany.
Omawiana funkcja działa przez wysłanie przeglądarce internetowej pewnego kodu JavaScript w dokumencie
HTML i zapewnia elegancki sposób programowania iteracyjnego. Zalecam jej stosowanie jedynie podczas pracy
z widokami, ponieważ wtedy istnieje najmniejsze prawdopodobieństwo, że serwer IIS wyśle przeglądarce
internetowej komunikaty błędów HTTP. Wspomniane komunikaty są generowane, gdy w kodzie występuje
błąd. Kod JavaScript nie jest dodawany do odpowiedzi dotyczących błędów, a tym samym następuje utrata
połączenia między Visual Studio i przeglądarkami internetowymi. W takim przypadku trzeba ponownie
przejść do okna dialogowego Przeglądaj w. Funkcja połączonych przeglądarek jest użyteczna, ale użycie kodu
JavaScript okazuje się problemem. Podczas pracy nad projektami w innych technologiach niż ASP.NET
korzystam z podobnego narzędzia, o nazwie LiveReload (http://livereload.com/). Wymienione narzędzie
350
ROZDZIAŁ 14.  PRZEGLĄD PROJEKTU MVC
Rysunek 14.18. Wybór dwóch przeglądarek internetowych
Rysunek 14.19. Odświeżenie przeglądarek internetowych
oferuje lepsze podejście, ponieważ jego działanie opiera się na wtyczkach przeglądarek internetowych,
na które komunikaty błędów HTTP nie mają wpływu. Wartość funkcji połączonych przeglądarek w Visual
Studio będzie ograniczona, dopóki Microsoft nie zastosuje podobnego rozwiązania.
Podsumowanie
W rozdziale tym omówiłem strukturę projektu Visual Studio MVC i wyjaśniłem, jak są połączone ze sobą jej
części. Wskazałem również jedną z najważniejszych cech platformy MVC — możliwość stosowania konwencji.
Do omówionych tematów będę stale wracać w kolejnych rozdziałach, przedstawiając sposób działania
platformy MVC.
351
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
352
ROZDZIAŁ 15.

Routing URL
Przed wprowadzeniem platformy MVC założono w ASP.NET, że istnieje bezpośrednia relacja pomiędzy
adresem URL żądania a plikiem na dysku serwera. Zadaniem serwera było odczytanie żądania wysłanego
przez przeglądarkę i dostarczenie wyniku z odpowiedniego pliku.
Podejście to działa świetnie dla Web Forms, gdzie każda strona ASPX jest plikiem i zawiera odpowiedź
na żądanie. Nie ma to sensu dla aplikacji MVC, w których żądania są przetwarzane przez metody akcji w klasach
kontrolera i nie ma bezpośredniej korelacji z plikami na dysku.
Aby obsługiwać adresy URL MVC, platforma ASP.NET korzysta z systemu routingu. W tym rozdziale
pokażę, jak konfigurować i wykorzystywać routing w celu utworzenia zaawansowanego i elastycznego systemu
obsługi adresów URL dla naszych projektów. Jak się przekonasz, system routingu oferuje możliwość tworzenia
dowolnych wzorców URL i opisywania ich w jasny i spójny sposób. System routingu ma dwie funkcje:
 Analiza przychodzącego żądania URL i określenie kontrolera i akcji przeznaczonych dla tego żądania.
 Generowanie wychodzących adresów URL. Są to adresy URL pojawiające się w stronach HTML
generowanych na podstawie naszych widoków, dzięki czemu po kliknięciu łącza przez użytkownika
generowane są odpowiednie akcje (i stają się ponownie przychodzącymi żądaniami URL).
W tym rozdziale skupimy się na definiowaniu tras i korzystaniu z nich do przetwarzania przychodzących
adresów URL, dzięki którym użytkownik wywołuje nasze kontrolery i akcje. Istnieją dwa sposoby tworzenia
tras w aplikacji MVC: routing oparty na konwencji i atrybuty routingu. Jeżeli używałeś wcześniejszych wersji
platformy MVC, to powinieneś już znać routing oparty na konwencji. Natomiast atrybuty routingu są
nowością na platformie MVC 5. W tym rozdziale wyjaśnię oba podejścia.
Następnie w kolejnym rozdziale pokażę, w jaki sposób korzystać z tych samych tras do wygenerowania
wychodzących adresów URL, które musimy dołączać do widoków. Dowiesz się również, jak system routingu
dostosować do własnych potrzeb i jak używać funkcji o nazwie obszary. W tabeli 15.1 znajdziesz podsumowanie
materiału omówionego w rozdziale.
Utworzenie przykładowego projektu
Aby zademonstrować działanie systemu routingu, potrzebujemy projektu, w którym możemy dodawać trasy.
Na potrzeby tego rozdziału tworzymy nową aplikację MVC z wykorzystaniem szablonu Empty i nadajemy
jej nazwę UrlsAndRoutes. Do rozwiązania Visual Studio dodajemy projekt testów o nazwie UrlAndRoutes.Tests
przez zaznaczenie opcji Dodaj testy jednostkowe, jak pokazano na rysunku 15.1.
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Tabela 15.1. Podsumowanie materiału omówionego w rozdziale
Temat
Rozwiązanie
Listing (nr)
Mapowanie między adresami URL
i metodami akcji
Zdefiniowanie trasy
Od 1. do 8.
Zezwolenie na pominięcie segmentów
adresu URL
Zdefiniowanie wartości domyślnych dla zmiennych
segmentu
9. i 10.
Dopasowanie segmentów URL,
które nie mają odpowiadających
im zmiennych routingu
Użycie segmentów statycznych
Od 11. do 14.
Przekazanie segmentów URL
do metod akcji
Zdefiniowanie własnych zmiennych segmentu
Od 15. do 18.
Zezwolenie na pominięcie segmentów
adresu URL, dla których nie podano
wartości domyślnych
Zdefiniowanie segmentów opcjonalnych
Od 19. do 22.
Zdefiniowanie tras dopasowujących Użycie segmentu o nazwie catchall
dowolną liczbę segmentów adresu URL
23.
Uniknięcie niejasności związanych
z nazwami kontrolerów
Od 24. do 27.
Określenie w trasie priorytetowych przestrzeni nazw
Ograniczenie liczby adresów URL,
Zastosowanie ograniczeń dla trasy
które mogą być dopasowane przez trasę
Od 28. do 34.
Włączenie routingu atrybutu
Wywołanie metody MapMvcAttributeRoutes
35.
Zdefiniowanie trasy w kontrolerze
Zastosowanie atrybutu Route w metodach akcji
36. i 37.
Ograniczenie trasy atrybutu
Zastosowanie ograniczenia dla zmiennej segmentu
we wzorcu trasy
38. i 39.
Zdefiniowanie prefiksu dla wszystkich
tras atrybutu w kontrolerze
Zastosowanie atrybutu RoutePrefix w klasie
kontrolera
40.
Rysunek 15.1. Tworzenie pustego projektu MVC wraz z testami jednostkowymi
354
ROZDZIAŁ 15.  ROUTING URL
W rozdziale dotyczącym aplikacji SportsStore dowiedziałeś się, jak ręcznie utworzyć testy jednostkowe.
Zaznaczenie wymienionej opcji daje taki sam efekt i automatycznie obsługuje odwołania między projektami.
Jednak nadal trzeba dodać Moq, a więc w konsoli pakietów NuGet wydaj poniższe polecenie:
Install-Package Moq -version 4.1.1309.1617 -projectname UrlsAndRoutes.Tests
Utworzenie przykładowych kontrolerów
W celu zademonstrowania funkcji routingu konieczne jest dodanie kilku prostych kontrolerów do
utworzonej przed chwilą aplikacji. W rozdziale koncentrujemy się jedynie na sposobie interpretacji adresów
URL w celu wywołania metod akcji. Jako modeli widoków będziemy więc używać ciągów tekstowych
zdefiniowanych w ViewBag, które podają nazwę kontrolera i metody akcji. Jako pierwszy utwórz kontroler
HomeController i umieść w nim kod przedstawiony na listingu 15.1.
Listing 15.1. Zawartość pliku HomeController.cs
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers {
public class HomeController : Controller {
public ActionResult Index() {
ViewBag.Controller = "Home";
ViewBag.Action = "Index";
return View("ActionName");
}
}
}
Następnie utwórz kontroler CustomerController i umieść w nim kod przedstawiony na listingu 15.2.
Listing 15.2. Zawartość pliku CustomerController.cs
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers {
public class CustomerController : Controller {
public ActionResult Index() {
ViewBag.Controller = "Customer";
ViewBag.Action = "Index";
return View("ActionName");
}
public ActionResult List() {
ViewBag.Controller = "Customer";
ViewBag.Action = "List";
return View("ActionName");
}
}
}
Utwórz kolejny kontroler i nadaj mu nazwę AdminController, a następnie umieść w nim kod
przedstawiony na listingu 15.3.
355
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 15.3. Zawartość pliku AdminController.cs
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers {
public class AdminController : Controller {
public ActionResult Index() {
ViewBag.Controller = "Admin";
ViewBag.Action = "Index";
return View("ActionName");
}
}
}
Utworzenie widoku
We wszystkich metodach akcji utworzonych kontrolerów został użyty widok ActionName, który pozwala
na zdefiniowanie jednego widoku i jego użycie w całej aplikacji. W katalogu Views projektu utwórz nowy
podkatalog Shared, a następnie dodaj widok o nazwie ActionName.cshtml i umieść w nim kod przedstawiony
na listingu 15.4.
Listing 15.4. Kod widoku ActionName.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>ActionName</title>
</head>
<body>
<div>Nazwa kontrolera: @ViewBag.Controller</div>
<div>Nazwa akcji: @ViewBag.Action</div>
</body>
</html>
Ustawienie początkowego adresu URL i przetestowanie aplikacji
Jak wyjaśniłem w pierwszej części książki, na podstawie pliku edytowanego w chwili uruchamiania debugera
Visual Studio próbuje określić adres URL, który powinien być żądany przez przeglądarkę internetową.
Wprawdzie to jest dobra idea, ale bardzo szybko staje się irytująca i dlatego zawsze wyłączam tę funkcję.
Z menu Projekt w Visual Studio wybierz opcję Właściwości UrlsAndRoutes…, przejdź do karty Sieć Web
i zaznacz opcję Określ stronę w sekcji Uruchom akcję. Nie musisz podawać żadnej wartości, zaznaczenie
wymienionej opcji jest w zupełności wystarczające. Po uruchomieniu aplikacji otrzymasz komunikaty
widoczne na rysunku 15.2.
Rysunek 15.2. Efekt uruchomienia przykładowej aplikacji
356
ROZDZIAŁ 15.  ROUTING URL
Wprowadzenie do wzorców URL
System routingu działa dzięki wykorzystaniu zbioru tras. Trasy te są nazywane schematem URL dla aplikacji
i definiują zbiór adresów URL, jakie aplikacja rozpoznaje i na jakie odpowiada.
Nie musimy ręcznie wpisywać wszystkich adresów URL, jakie chcemy obsługiwać. Zamiast tego każda
trasa zawiera wzorzec URL, który jest porównywany z przychodzącym adresem URL. Jeżeli wzorzec pasuje do
adresu, jest używany do przetworzenia tego adresu URL. Zacznijmy od przykładowego adresu URL aplikacji
utworzonej w rozdziale:
http://witryna.pl/Admin/Index
Adresy URL mogą być podzielone na segmenty. Są to te części adresu URL, które są rozdzielane znakiem /
z pominięciem nazwy hosta oraz ciągu tekstowego zapytania. W przykładowym adresie URL występują
dwa segmenty, jak pokazano na rysunku 15.3.
Rysunek 15.3. Segmenty przykładowego adresu URL
Pierwszy segment zawiera słowo Admin, a drugi słowo Index. Dla ludzkiego oka jest oczywiste, że pierwszy
argument odnosi się do kontrolera, a drugi do akcji. Jasne jest, że musimy wyrazić tę relację w sposób zrozumiały
dla systemu routingu. Wzorzec URL realizujący to zadanie wygląda następująco:
{controller}/{action}
W czasie przetwarzania przychodzącego adresu URL zadaniem systemu routingu jest dopasowanie adresu
URL do wzorca oraz pobranie wartości do zmiennych segmentu zdefiniowanych we wzorcu. Zmienne segmentu
są zapisywane z użyciem nawiasów klamrowych (znaków { oraz }). Przykładowy wzorzec zawiera dwie zmienne
segmentu, o nazwach controller i action. Dlatego też wartością zmiennej segmentu controller jest Admin,
natomiast wartością zmiennej segmentu action jest Index.
Mówimy o dopasowaniu wzorca, ponieważ aplikacja MVC zwykle zawiera kilka tras, a system routingu
będzie dopasowywał przychodzący adres URL do wzorca kolejnych tras do momentu znalezienia dopasowania.
 Uwaga System routingu nie posiada żadnych informacji na temat kontrolerów i akcji. Po prostu pobiera wartości
do zmiennych segmentów. W dalszej części operacji przetwarzania żądania, gdy żądanie trafi do platformy MVC, są
one wiązane ze zmiennymi kontrolera i akcji. Dzięki temu system routingu może być używany w Web Forms i Web
API (interfejs Web API zostanie omówiony w rozdziale 27., natomiast dokładne omówienie procesu obsługi żądania
znajdziesz w innej mojej książce, zatytułowanej Pro ASP.NET MVC 5 Platform).
Domyślnie wzorce URL są dopasowywane do dowolnego adresu URL mającego właściwą liczbę segmentów.
Wzorzec {controller}/{action} jest dopasowywany do dowolnego adresu URL z dwoma segmentami,
jak pokazano w tabeli 15.2.
W tabeli 15.2 przedstawione są dwie kluczowe cechy wzorców URL:
 Wzorce URL są konserwatywne i pasują wyłącznie do adresów, które mają taką samą liczbę segmentów
jak wzorzec. Można to zauważyć w czwartym i piątym przykładzie z tabeli.
 Wzorce URL są liberalne. Jeżeli adres URL posiada prawidłową liczbę segmentów, zostanie pobrana wartość
zmiennej segmentu, niezależnie od tego, jaka ta wartość jest.
Są to kluczowe zależności, które trzeba znać, aby zrozumieć sposób domyślnego działania wzorców URL.
W dalszej części rozdziału wyjaśnimy, jak zmienić to domyślne działanie.
357
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Tabela 15.2. Dopasowanie adresów URL
Żądany URL
Zmienne segmentu
http://witryna.pl/Admin/Index
controller = Admin
action = Index
controller = Index
action = Admin
controller = Apples
action = Oranges
http://witryna.pl/Index/Admin
http://witryna.pl/Apples/Oranges
http://witryna.pl/Admin
http://witryna.pl/Admin/Index/Apples
Brak dopasowania — za mało segmentów
Brak dopasowania — za dużo segmentów
Jak wspomniałem, system routingu nie ma żadnych informacji na temat aplikacji MVC, dlatego wzorce
URL będą dopasowywane nawet w przypadku, gdy nie ma kontrolera lub akcji pasującej do wartości pobranych
z adresu URL. Jest to pokazane w drugim przykładzie z tabeli 15.2. Zamieniliśmy w nim segmenty Admin i Index,
przez co również wartości pobrane z URL są zamienione, pomimo że w omawianym projekcie nie ma
kontrolera Index.
Tworzenie i rejestrowanie prostej trasy
Po zapoznaniu się z wzorcami URL możemy użyć ich do zdefiniowania trasy. Trasy są definiowane w pliku
RouteConfig.cs, który znajduje się w katalogu App_Start projektu. Początkowy kod wspomnianego pliku
wygenerowany przez Visual Studio przedstawiono na listingu 15.5.
Listing 15.5. Domyślny kod w pliku RouteConfig.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index",
id = UrlParameter.Optional }
);
}
}
}
Zdefiniowana w pliku RouteConfig.cs metoda statyczna RegisterRoutes jest wywoływana z pliku
Global.asax.cs, który konfiguruje podstawowe komponenty platformy MVC podczas uruchamiania aplikacji.
Domyślna zawartość pliku Global.asax.cs została przedstawiona na listingu 15.6, a wywołanie metody
RouteConfig.RegisterRoutes z metody Application_Start oznaczono pogrubioną czcionką.
358
ROZDZIAŁ 15.  ROUTING URL
Listing 15.6. Domyślna zawartość pliku Global.asax.cs
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Http;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class MvcApplication : System.Web.HttpApplication {
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
}
}
Metoda Application_Start jest wywoływana przez platformę ASP.NET w trakcie pierwszego uruchomienia
aplikacji MVC, co prowadzi do wywołania metody RouteConfig.RegisterRoutes. Parametrem metody jest
wartość właściwości statycznej RouteTable.Routes, która jest egzemplarzem klasy RouteCollection (funkcje
wymienionej klasy zostaną wkrótce przedstawione).
 Wskazówka Drugie wywołanie w metodzie Application_Start powoduje konfigurację funkcji o nazwie obszary,
która zostanie omówiona w następnym rozdziale.
Na listingu 15.7 pokazałem, w jaki sposób możemy utworzyć trasę w metodzie RegisterRoutes z pliku
RouteConfig.cs za pomocą przykładowego wzorca URL z poprzedniego punktu. (Pozostałe polecenia
w metodzie zostały usunięte, aby umożliwić Ci skoncentrowanie się na przykładzie).
Listing 15.7. Rejestrowanie trasy w pliku RouteConfig.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler());
routes.Add("MyRoute", myRoute);
}
}
}
Tworzymy tu nowy obiekt Route, przekazując do konstruktora wzorzec URL jako parametr. Przekazaliśmy
do niego również obiekt MvcRouteHendler. Różne technologie ASP.NET zawierają różne klasy do obsługi routingu;
w aplikacjach ASP.NET MVC będziemy używać właśnie tej klasy. Utworzoną trasę dodajemy do obiektu
RouteCollection za pomocą metody Add — przekazujemy nazwę, pod jaką powinna być zarejestrowana trasa,
oraz samą trasę.
359
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
 Wskazówka Nazywanie tras jest opcjonalne i podnoszone są argumenty, że w ten sposób poświęca się czystą
separację zadań, którą można uzyskać przy użyciu systemu routingu. Osobiście nie przywiązuję wielkiej wagi
do kwestii nazywania tras, ale na wszelki wypadek przedstawiam związane z tym problemy w punkcie
„Generowanie adresu URL na podstawie wybranej trasy” w rozdziale 16.
Wygodniejszą metodą rejestrowania tras jest użycie metody MapRoute, zdefiniowanej w klasie RouteCollection.
Na listingu 15.8 przedstawione jest zastosowanie tej metody do zarejestrowania naszej trasy. Otrzymany
efekt jest dokładnie taki sam jak w poprzednim przykładzie, ale sama składnia jest bardziej przejrzysta.
Listing 15.8. Rejestrowanie trasy za pomocą metody MapRoute w pliku RouteConfig.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}");
}
}
}
Podejście takie jest nieco bardziej zwięzłe, głównie dlatego, że nie trzeba tworzyć obiektu klasy MvcRouteHandler
(zostaje utworzony automatycznie w tle). Metoda MapRoute jest przeznaczona wyłącznie dla aplikacji MVC.
Aplikacje ASP.NET Web Forms mogą korzystać z metody MapPageRoute, zdefiniowanej również w klasie
RouteCollection.
Test jednostkowy — testowanie przychodzących adresów URL
Zalecam, aby nawet w przypadku, gdy nie tworzymy testów jednostkowych dla reszty aplikacji, tworzyć testy
jednostkowe dla tras, dzięki czemu można się upewnić, że przetwarzanie przychodzących adresów URL działa
w oczekiwany sposób. Schematy URL mogą być dosyć rozbudowane w dużych aplikacjach, więc łatwo jest utworzyć
coś, co będzie dawało nieoczekiwane wyniki.
W poprzednich rozdziałach unikałem tworzenia metod pomocniczych, współdzielonych przez wiele testów,
aby każdy test był niezależny. W tym rozdziale przyjmiemy inne podejście. Testowanie schematu routingu dla
aplikacji będzie realizowane w najbardziej czytelny sposób, gdy połączymy kilka testów w jedną metodę.
Najłatwiej możemy to zrealizować przy użyciu metod pomocniczych.
Aby testować trasy, musimy utworzyć imitacje trzech klas: HttpRequestBase, HttpContextBase oraz
HttpResponseBase (ostatnia z nich jest potrzebna do testowania wychodzących adresów URL, które przedstawię
w następnym rozdziale). Klasy te pozwalają odtworzyć fragment infrastruktury MVC obsługującej system routingu.
Do projektu testowego dodajemy nowy plik testów jednostkowych o nazwie RouteTests.cs. Poniżej zamieszczona
jest metoda pomocnicza tworząca imitacje obiektów HttpContextBase:
using
using
using
using
using
360
Microsoft.VisualStudio.TestTools.UnitTesting;
Moq;
System;
System.Reflection;
System.Web;
ROZDZIAŁ 15.  ROUTING URL
using System.Web.Routing;
namespace UrlsAndRoutes.Tests {
[TestClass]
public class RouteTests {
private HttpContextBase CreateHttpContext(string targetUrl = null,
string httpMethod = "GET") {
// tworzenie imitacji żądania
Mock<HttpRequestBase> mockRequest = new Mock<HttpRequestBase>();
mockRequest.Setup(m => m.AppRelativeCurrentExecutionFilePath)
.Returns(targetUrl);
mockRequest.Setup(m => m.HttpMethod).Returns(httpMethod);
// tworzenie imitacji odpowiedzi
Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>();
mockResponse.Setup(m => m.ApplyAppPathModifier(
It.IsAny<string>())).Returns<string>(s => s);
// tworzenie imitacji kontekstu z użyciem żądania i odpowiedzi
Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
mockContext.Setup(m => m.Request).Returns(mockRequest.Object);
mockContext.Setup(m => m.Response).Returns(mockResponse.Object);
// zwraca imitację kontekstu
return mockContext.Object;
}
}
}
Konfiguracja jest prostsza, niż się wydaje. Udostępniamy URL do testowania poprzez właściwość
AppRelativeCurrentExecutionFilePath klasy HttpRequestBase; udostępniamy także HttpRequestBase
poprzez właściwość Request imitacji klasy HttpContextBase. Nasza następna metoda pomocnicza pozwala
testować trasę:
...
private void TestRouteMatch(string url, string controller, string action,
object routeProperties = null, string httpMethod = "GET") {
// przygotowanie
RouteCollection routes = new RouteCollection();
RouteConfig.RegisterRoutes(routes);
// działanie — przetwarzanie trasy
RouteData result = routes.GetRouteData(CreateHttpContext(url, httpMethod));
// asercje
Assert.IsNotNull(result);
Assert.IsTrue(TestIncomingRouteResult(result, controller,
action, routeProperties));
}
...
Parametr tej metody pozwala nam określić adres URL do testowania, oczekiwane wartości dla zmiennych
segmentów kontrolera i akcji oraz obiekt zawierający oczekiwane wartości dowolnych innych zdefiniowanych
zmiennych. Sposób tworzenia takich zmiennych pokażę w dalszej części rozdziału oraz w rozdziale następnym.
Zdefiniowaliśmy również parametr dla metody HTTP, którego użyjemy w punkcie „Ograniczanie tras”.
Metoda TestRouteMatch bazuje na innej metodzie, TestIncomingRouteResult, która porównuje wyniki
uzyskane z systemu routingu z oczekiwanymi wartościami zmiennych segmentów. Metody te korzystają z refleksji
361
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
.NET, dzięki czemu możemy używać typów anonimowych do definiowania dodatkowych zmiennych segmentów.
Jeżeli to, co tu napisałem, nie ma dla Ciebie sensu, nie przejmuj się, nie jest to wymagane do zrozumienia
mechanizmów MVC, lecz jedynie ułatwia testowanie. Poniżej zamieszczona jest metoda TestIncommingRouteResult:
...
private bool TestIncomingRouteResult(RouteData routeResult, string controller,
string action, object propertySet = null) {
Func<object, object, bool> valCompare = (v1, v2) => {
return StringComparer.InvariantCultureIgnoreCase.Compare(v1, v2) == 0;
};
bool result = valCompare(routeResult.Values["controller"], controller)
&& valCompare(routeResult.Values["action"], action);
if (propertySet != null) {
PropertyInfo[] propInfo = propertySet.GetType().GetProperties();
foreach (PropertyInfo pi in propInfo) {
if (!(routeResult.Values.ContainsKey(pi.Name)
&& valCompare(routeResult.Values[pi.Name],
pi.GetValue(propertySet, null)))) {
result = false;
break;
}
}
}
return result;
}
...
Potrzebujemy również sprawdzić niedziałający adres URL. Jak pokażę, może to być ważna część definiowania
schematu URL.
...
private void TestRouteFail(string url) {
// przygotowanie
RouteCollection routes = new RouteCollection();
RouteConfig.RegisterRoutes(routes);
// działanie — przetwarzanie trasy
RouteData result = routes.GetRouteData(CreateHttpContext(url));
// asercje
Assert.IsTrue(result == null || result.Route == null);
}
...
Metody TestRouteMatch oraz TestRouteFail zawierają wywołania metody Assert, która zgłasza wyjątek,
jeżeli asercja się nie powiedzie. Ponieważ wyjątki C# są propagowane w górę stosu, możemy utworzyć prostą
metodę testową, która pozwoli sprawdzić zestaw adresów URL. Poniżej znajduje się metoda testująca trasę
zdefiniowaną na listingu 15.8.
...
[TestMethod]
public void TestIncomingRoutes() {
362
ROZDZIAŁ 15.  ROUTING URL
// sprawdzenie, czy otrzymamy adres URL, jakiego oczekiwaliśmy
TestRouteMatch("~/Admin/Index", "Admin", "Index");
// sprawdzenie wartości uzyskanych z segmentów
TestRouteMatch("~/One/Two", "One", "Two");
// upewnienie się, że za mało lub za dużo segmentów spowoduje błąd dopasowania
TestRouteFail("~/Admin/Index/Segment");
TestRouteFail("~/Admin");
}
...
Test ten korzysta z metody TestRouteMatch do sprawdzenia oczekiwanego adresu URL, a także
do sprawdzenia adresu w tym samym formacie, aby można było się upewnić, że wartości kontrolera i akcji
są pozyskiwane w prawidłowych segmentach URL. Wykorzystaliśmy również metodę TestRouteFail w celu
upewnienia się, że nasza aplikacja nie zaakceptuje adresów URL mających inną liczbę segmentów. Przy testowaniu
musimy poprzedzić adres URL znakiem tyldy (~), ponieważ w taki sposób platforma ASP.NET prezentuje
adresy URL systemowi routingu.
Zwróć uwagę, że nie musimy definiować tras w metodach testowych. Wczytujemy tu trasy bezpośrednio
z metody RegisterRoutes, zdefiniowanej w klasie RouteConfig.
Użycie prostej trasy
Możemy zobaczyć efekt działania utworzonych tras po uruchomieniu aplikacji. Gdy przeglądarka
zażąda głównego adresu URL, aplikacja zwróci błąd. Jeżeli jednak podasz trasę dopasowaną do wzorca
{controller}/{action}, wówczas otrzymasz wynik pokazany na rysunku 15.4. Na wspomnianym rysunku
pokazano efekt przejścia w aplikacji do adresu URL /Admin/Index.
Rysunek 15.4. Nawigacja za pomocą prostej trasy
Nasza prosta trasa zdefiniowana na listingu 15.8 nie informuje platformy MVC, w jaki sposób ma
odpowiadać na żądania dotyczące głównego adresu URL, i obsługuje tylko jeden, konkretny wzorzec URL.
Tymczasowo wykonaliśmy więc krok wstecz względem funkcjonalności zdefiniowanej przez Visual Studio
w pliku RouteConfig.cs podczas tworzenia projektu MVC. W dalszej części rozdziału pokażę, jak tworzyć
bardziej złożone trasy i wzorce.
Definiowanie wartości domyślnych
Powodem pojawienia się błędu w przypadku domyślnego adresu URL dla aplikacji jest brak dopasowania
do zdefiniowanej przez nas trasy. Domyślny adres URL jest przedstawiany systemowi routingu jako ~/,
więc nie ma w nim segmentów, które mogłyby być dopasowane do zmiennych controller oraz action.
Jak wcześniej wyjaśniłem, wzorce URL są konserwatywne, więc pasują wyłącznie do adresów URL
o zdefiniowanej liczbie segmentów. Wspominałem również, że jest to domyślne zachowanie. Jednym ze sposobów
363
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
zmiany tego zachowania jest użycie wartości domyślnych. Wartości domyślne są stosowane, gdy adres URL
nie zawiera segmentu, który można dopasować do wartości. Na listingu 15.9 zamieściłem przykład trasy
zawierającej wartość domyślną.
Listing 15.9. Określanie wartości domyślnej w pliku RouteConfig.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}",
new { action = "Index" });
}
}
}
Wartości domyślne są dostarczane jako właściwości w typie anonimowym. Na listingu 15.9 zdefiniowaliśmy
wartość domyślną Index dla zmiennej action. Trasa ta będzie dopasowywana do wszystkich dwusegmentowych
adresów URL, tak jak poprzednio. Gdy zażądamy na przykład adresu URL http://witryna.pl/Home/Index,
trasa pobierze Home jako wartość dla controller oraz Index jako wartość action.
Teraz mamy jednak przekazaną wartość domyślną dla segmentu action, więc trasa będzie dopasowywana
również dla jednosegmentowych adresów URL. Przetwarzając adres URL, system routingu pobierze wartość
zmiennej controller z jedynego segmentu adresu URL oraz użyje wartości domyślnej dla zmiennej action.
Zatem gdy zażądamy adresu URL http://witryna.pl/Home, zostanie wywołana metoda akcji Index
z kontrolera Home.
Możemy pójść dalej i zdefiniować adresy URL niezawierające żadnych zmiennych segmentów, bazując
przy identyfikowaniu kontrolera i akcji wyłącznie na wartościach domyślnych. Możemy w ten sposób zdefiniować
domyślny URL, korzystając z wartości domyślnych dla obu zmiennych, jak pokazano na listingu 15.10.
Listing 15.10. Określanie domyślnych wartości dla kontrolera i akcji w pliku RouteConfig.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
}
}
}
Definiując wartości domyślne dla zmiennych controller i action, utworzyliśmy trasę, która pasuje
do adresów URL mających zero, jeden lub dwa segmenty, co jest pokazane w tabeli 15.3.
364
ROZDZIAŁ 15.  ROUTING URL
Tabela 15.3. Dopasowanie adresów URL
Liczba segmentów
Przykład
Mapowany na
0
witryna.pl
controller = Home
action = Index
1
witryna.pl/Customer
controller = Customer
action = Index
2
witryna.pl/Customer/List
controller = Customer
action = List
3
witryna.pl/Customer/List/All
Brak dopasowania — za dużo segmentów
Im mniej segmentów otrzymamy w przychodzącym adresie URL, tym bardziej polegamy na wartościach
domyślnych aż do otrzymania adresu URL pozbawionego segmentów — w takim przypadku będą użyte
jedynie wartości domyślne. Efekt zdefiniowania wartości domyślnych możesz zobaczyć po ponownym
uruchomieniu aplikacji. Przeglądarka ponownie zażąda domyślnego adresu URL, ale tym razem nasza nowa
trasa doda nasze domyślne wartości dla kontrolera i akcji, dzięki czemu przychodzący adres URL zostanie
odwzorowany na akcję Index w kontrolerze Home, jak pokazano na rysunku 15.5.
Rysunek 15.5. Efekt użycia wartości domyślnych w celu rozszerzenia zasięgu trasy
Testy jednostkowe — wartości domyślne
Nie musimy wykonywać żadnych specjalnych akcji, jeżeli użyjemy naszych metod pomocniczych do definiowania
tras korzystających z wartości domyślnych. Poniżej zamieszczona jest uaktualniona metoda TestIncomingRoutes
z pliku RouteTests.cs dla trasy zdefiniowanej na listingu 15.10:
...
[TestMethod]
public void TestIncomingRoutes() {
TestRouteMatch("~/", "Home", "Index");
TestRouteMatch("~/Customer", "Customer", "Index");
TestRouteMatch("~/Customer/List", "Customer", "List");
TestRouteFail("~/Customer/List/All");
}
...
Trzeba tylko pamiętać o podawaniu domyślnego adresu URL jako ~/, ponieważ ASP.NET w taki sposób prezentuje
adresy URL systemowi routingu. Jeżeli podamy pusty ciąg ("") lub /, system routingu zgłosi wyjątek i test się nie
powiedzie.
365
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Użycie statycznych segmentów adresu URL
Nie wszystkie segmenty we wzorcu URL muszą być zmiennymi. Można również tworzyć wzorce mające segmenty
statyczne. Załóżmy, że chcemy dopasować poniższy adres URL w celu obsługi adresów URL poprzedzonych
słowem Public:
http://witryna.pl/Public/Home/Index
Możemy zrobić to przez użycie wzorca zamieszczonego na listingu 15.11.
Listing 15.11. Wzorzec URL z segmentem statycznym w pliku RouteConfig.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
routes.MapRoute("", "Public/{controller}/{action}",
new { controller = "Home", action = "Index" });
}
}
}
Wzorzec ten pasuje wyłącznie do adresów URL posiadających trzy segmenty, z których pierwszym musi
być Public. Pozostałe dwa segmenty mogą zawierać dowolną wartość i będą używane dla zmiennych controller
oraz action. Jeżeli dwa ostatnie segmenty zostaną pominięte, wtedy użyte będą wartości domyślne.
Możemy również tworzyć wzorce URL mające segmenty zawierające zarówno elementy statyczne,
jak i zmienne (listing 15.12).
Listing 15.12. Wzorzec URL z segmentem mieszanym w pliku RouteConfig.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("", "X{controller}/{action}");
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
routes.MapRoute("", "Public/{controller}/{action}",
new { controller = "Home", action = "Index" });
366
ROZDZIAŁ 15.  ROUTING URL
}
}
}
Wzorzec w tej trasie pasuje do dowolnego dwusegmentowego adresu URL, w którym pierwszy segment
zaczyna się od litery X. Wartość zmiennej controller jest pobierana z pierwszego segmentu, poza początkową
literą X. Wartość zmiennej action jest pobierana z drugiego segmentu. Efekt działania tego rodzaju trasy
możesz zobaczyć po uruchomieniu aplikacji i przejściu do adresu URL /XHome/Index, co zostało pokazane
na rysunku 15.6.
Rysunek 15.6. Połączenie statycznych i zmiennych elementów w pojedynczym segmencie
Kolejność tras
Na listingu 15.12 zdefiniowaliśmy nową trasę i umieściliśmy ją w metodzie RegisterRoutes przed wszystkimi
innymi. Zrobiliśmy to, ponieważ trasy są stosowane w kolejności, w jakiej występują w obiekcie RouteCollection.
Metoda MapRoute dodaje trasę na koniec kolekcji, co oznacza, że trasy są zwykle przetwarzane w kolejności
dodawania. Użyłem słowa „zwykle”, ponieważ istnieją metody pozwalające na wstawianie tras w wybranym
miejscu. Zazwyczaj nie korzystam z tych metod, gdyż uporządkowanie tras w kolejności ich wykonywania
pozwala łatwiej zrozumieć routing w aplikacji.
System routingu próbuje dopasować przychodzący adres URL do wzorca URL trasy zdefiniowanej jako pierwsza
i jeżeli się to nie uda, przechodzi do następnej. Trasy są wypróbowywane po kolei, aż do wyczerpania ich zbioru.
W konsekwencji musimy definiować najbardziej szczegółowe trasy jako pierwsze. Trasa dodana na listingu 15.12
jest bardziej szczegółowa niż następna. Załóżmy, że odwrócimy kolejność w poniższy sposób:
...
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
routes.MapRoute("", "X{controller}/{action}");
...
Teraz pierwszą użytą trasą będzie ta, która pasuje do każdego adresu URL posiadającego zero segmentów,
jeden segment lub dwa segmenty. Bardziej szczegółowa trasa, znajdująca się na liście jako druga, nie będzie
nigdy wykorzystana. Nowa trasa powoduje usunięcie początkowego X z adresu URL, co nie jest realizowane
we wcześniejszej trasie. Z tego powodu poniższy adres URL:
http://witryna.pl/XHome/Index
zostanie skierowany do nieistniejącego kontrolera o nazwie XHome, wskutek czego nastąpi wygenerowanie
użytkownikowi informacji o błędzie 404.
367
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Możemy połączyć statyczne segmenty URL oraz wartości domyślne w celu utworzenia aliasów dla wybranych
adresów URL. Jest to przydatne, jeżeli opublikowaliśmy schemat URL w postaci kontraktu dla użytkownika.
Jeżeli zrefaktoryzujesz w takiej sytuacji aplikację, powinieneś zachować poprzedni format adresów URL, aby
nadal działały adresy dodane przez użytkownika do ulubionych lub przygotowane przez niego makra i skrypty.
Wyobraźmy sobie, że mieliśmy kontroler o nazwie Shop, który został zastąpiony przez kontroler Home.
Na listingu 15.13 pokazany jest sposób tworzenia tras pozwalających na zachowanie starego schematu URL.
Listing 15.13. Łączenie statycznych segmentów URL oraz wartości domyślnych w pliku RouteConfig.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("ShopSchema", "Shop/{action}",
new { controller = "Home" });
routes.MapRoute("", "X{controller}/{action}");
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
routes.MapRoute("", "Public/{controller}/{action}",
new { controller = "Home", action = "Index" });
}
}
}
Dodana przez nas trasa pasuje do wszystkich dwusegmentowych adresów URL, w których pierwszym
segmentem jest Shop. Wartość zmiennej action jest pobierana z drugiego segmentu. Wzorzec URL nie zawiera
zmiennej segmentu o nazwie controller, więc użyta jest podana przez nas wartość domyślna. Oznacza to, że
żądanie wykonania akcji na kontrolerze Shop jest przekształcane w żądanie dla kontrolera Home. Efekt działania
trasy można zobaczyć po uruchomieniu aplikacji i przejściu do adresu URL /Shop/Index. Jak pokazano na
rysunku 15.7, dodana trasa spowodowała, że platforma MVC wywołuje metodę akcji Index kontrolera Home.
Rysunek 15.7. Utworzenie aliasu w celu zachowania schematu URL
Możemy również pójść o krok dalej i utworzyć aliasy dla metod akcji, które zostały zrefaktoryzowane
i nie występują już w kontrolerze. W tym celu należy utworzyć statyczny URL i dostarczyć wartości
dla controller oraz action w postaci wartości domyślnych, jak pokazano na listingu 15.14.
368
ROZDZIAŁ 15.  ROUTING URL
Listing 15.14. Aliasy dla kontrolera i akcji w pliku RouteConfig.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("ShopSchema2", "Shop/OldAction",
new { controller = "Home", action = "Index" });
routes.MapRoute("ShopSchema", "Shop/{action}",
new { controller = "Home" });
routes.MapRoute("", "X{controller}/{action}");
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
routes.MapRoute("", "Public/{controller}/{action}",
new { controller = "Home", action = "Index" });
}
}
}
Zwróć uwagę, że kolejny raz umieściliśmy naszą nową trasę jako pierwszą. Jest ona bardziej szczegółowa
niż wszystkie kolejne. Jeżeli żądanie otwarcia adresu /Shop/OldAction byłoby przetworzone przez drugą z kolei
trasę, otrzymalibyśmy inny wynik, niż oczekiwaliśmy. Żądanie takie zostałoby obsłużone przez zwrócenie
informacji o błędzie 404, a nie przez przekształcenie pozwalające zachować istniejący schemat URL.
Test jednostkowy — testowanie segmentów statycznych
Kolejny raz użyjemy naszych metod pomocniczych do przetestowania tras, których wzorzec URL zawiera segmenty
statyczne. Poniżej przedstawiono zmiany wprowadzone w metodzie TestIncomingRoutes w celu przetestowania
trasy dodanej na listingu 15.14:
...
[TestMethod]
public void TestIncomingRoutes() {
TestRouteMatch("~/", "Home", "Index");
TestRouteMatch("~/Customer", "Customer", "Index");
TestRouteMatch("~/Customer/List", "Customer", "List");
TestRouteFail("~/Customer/List/All");
TestRouteMatch("~/Shop/Index", "Home", "Index");
}
...
369
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Definiowanie własnych zmiennych segmentów
Zmienne segmentu controller i action mają specjalne znaczenie na platformie MVC i — co oczywiste —
odpowiadają kontrolerowi i metodzie akcji, które będą użyte do obsługi danego żądania. Nie jesteśmy
ograniczeni wyłącznie do zmiennych controller i action. Możemy również definiować własne zmienne
w sposób pokazany na listingu 15.15. (Istniejące trasy z poprzednich sekcji zostały usunięte).
Listing 15.15. Definiowanie nowych zmiennych we wzorcu URL w pliku RouteConfig.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "DefaultId" });
}
}
}
Wzorzec trasy URL definiuje typowe zmienne controller oraz action, jak również własną zmienną
o nazwie id. Trasa ta pozwala dopasować adresy URL o długości od zera do trzech segmentów. Zawartość
trzeciego segmentu jest przypisywana do zmiennej id, a jeżeli nie wystąpi trzeci segment, użyta zostanie wartość
domyślna.
 Ostrzeżenie Niektóre nazwy są zarezerwowane i nie są dostępne dla nazw zmiennych własnych segmentów.
Nazwami tymi są controller, action oraz area. Znaczenie pierwszych dwóch jest oczywiste, a rolę trzeciej
wyjaśnię w następnym rozdziale.
W metodzie akcji możemy odczytać każdą ze zmiennych segmentów, korzystając z właściwości
RouteData.Values. Aby to zademonstrować, trzeba dodać do klasy HomeController metodę CustomVariable
(listing 15.16).
Listing 15.16. Dostęp do własnej zmiennej segmentu w metodzie akcji w pliku HomeController.cs
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers {
public class HomeController : Controller {
public ActionResult Index() {
ViewBag.Controller = "Home";
ViewBag.Action = "Index";
return View("ActionName");
}
public ActionResult CustomVariable() {
ViewBag.Controller = "Home";
ViewBag.Action = "CustomVariable";
370
ROZDZIAŁ 15.  ROUTING URL
ViewBag.CustomVariable = RouteData.Values["id"];
return View();
}
}
}
Metoda ta pozyskuje wartość zmiennej z wzorca trasy i przekazuje ją do widoku poprzez ViewBag. Aby
utworzyć widok dla metody akcji, kliknij katalog Views/Home prawym przyciskiem myszy i wybierz opcję
Dodaj/Strona widoku MVC 5 (Razor) z menu kontekstowego. Widokowi nadaj nazwę CustomVariable
i kliknij przycisk OK. Visual Studio utworzy nowy plik widoku CustomVariable.cshtml w katalogu /Views/Home.
Kod widoku zmodyfikuj tak, aby odpowiadał przedstawionemu na listingu 15.17.
Listing 15.17. Zawartość pliku CustomVariable.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>CustomVariable</title>
</head>
<body>
<div>Nazwa kontrolera: @ViewBag.Controller</div>
<div>Nazwa akcji: @ViewBag.Action</div>
<div>Nazwa własnej zmiennej: @ViewBag.CustomVariable</div>
</body>
</html>
Aby zobaczyć efekt zdefiniowania własnej zmiennej segmentu, uruchom aplikację i przejdź do adresu
URL /Home/CustomVariable/Halo. Zostanie wywołana akcja CustomVariable z kontrolera Home, a wartość
zmiennej naszego segmentu będzie pobrana z ViewBag i wyświetlona na stronie, jak pokazano na rysunku 15.8.
Rysunek 15.8. Wyświetlanie wartości własnej zmiennej segmentu
Zmiennej segmentu przypisaliśmy wartość domyślną, co oznacza, że po przejściu do adresu URL
/Home/CustomVariable otrzymasz wynik pokazany na rysunku 15.9.
Rysunek 15.9. Wyświetlanie wartości domyślnej własnej zmiennej segmentu
371
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Test jednostkowy — testowanie zmiennych własnych segmentów
W naszych metodach pomocniczych testów dodaliśmy obsługę testowania własnych zmiennych segmentów.
Metoda TestRouteMatch posiada opcjonalny parametr, który akceptuje typ anonimowy zawierający nazwy
zmiennych, jakie chcemy testować, oraz oczekiwane wartości. Poniżej przedstawiono zmodyfikowaną wersję
metody TestIncomingRoutes w celu przetestowania trasy zdefiniowanej na listingu 15.15.
...
[TestMethod]
public void TestIncomingRoutes() {
TestRouteMatch("~/", "Home", "Index", new {id = "DefaultId"});
TestRouteMatch("~/Customer", "Customer", "index", new { id = "DefaultId" });
TestRouteMatch("~/Customer/List", "Customer", "List", new { id = "DefaultId" });
TestRouteMatch("~/Customer/List/All", "Customer", "List", new { id = "All" });
TestRouteFail("~/Customer/List/All/Delete");
}
...
Użycie własnych zmiennych jako parametrów metod akcji
Użycie właściwości RouteData.Values jest jedynie pierwszym ze sposobów na dostęp do zmiennych własnych
segmentów. Inny sposób jest znacznie elegantszy. Jeżeli zdefiniujemy parametr metody akcji o nazwie
pasującej do zmiennej z wzorca URL, platforma MVC przekaże wartość pobraną z URL do tego parametru
metody akcji. Na przykład własna zmienna zdefiniowana w trasie z listingu 15.15 ma nazwę id. Możemy
zmodyfikować metodę akcji CustomVariable w taki sposób, aby posiadała analogiczny parametr,
jak pokazano na listingu 15.18.
Listing 15.18. Dodanie parametru metody akcji w pliku HomeController.cs
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
namespace UrlsAndRoutes.Controllers {
public class HomeController : Controller {
public ActionResult Index() {
ViewBag.Controller = "Home";
ViewBag.Action = "Index";
return View("ActionName");
}
public ActionResult CustomVariable(string id) {
ViewBag.Controller = "Home";
ViewBag.Action = "CustomVariable";
ViewBag.CustomVariable = id;
return View();
}
}
}
372
ROZDZIAŁ 15.  ROUTING URL
Gdy system routingu dopasuje URL do trasy zdefiniowanej na listingu 15.18, wartość trzeciego segmentu
w adresie URL zostanie przypisana do zmiennej id. Platforma MVC porówna listę zmiennych segmentów z listą
parametrów metody akcji i jeżeli zostaną znalezione pasujące nazwy, wartości z adresu URL będą przekazane
do metody.
Parametr id zdefiniowaliśmy jako string, ale platforma MVC będzie próbowała skonwertować wartość z URL
na dowolny zdefiniowany przez nas typ. Jeżeli zadeklarujemy parametr id jako int lub DateTime, otrzymamy
wartość z URL w postaci obiektu właściwego typu. Jest to elegancka i przydatna funkcja, która pozwala uniknąć
samodzielnej realizacji konwersji.
 Uwaga Przy konwersji wartości znajdujących się w adresie URL na typy .NET platforma MVC korzysta z mechanizmu
dołączania modelu, który jest w stanie obsłużyć sytuacje znacznie bardziej skomplikowane niż pokazane w tym
przykładzie. Dołączanie modelu przedstawię w rozdziale 24.
Definiowanie opcjonalnych segmentów URL
Opcjonalny segment URL to taki, który nie musi być wskazany przez użytkownika, ale dla którego nie są
podane wartości domyślne. Na listingu 15.19 pokazany jest przykład. Opcjonalność zmiennej segmentu
zaznaczyliśmy przez ustawienie domyślnej wartości parametru UrlParameter.Optional, co zostało oznaczone
czcionką pogrubioną.
Listing 15.19. Określanie opcjonalnego segmentu URL w pliku RouteConfig.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index",
id = UrlParameter.Optional });
}
}
}
Trasa ta będzie dopasowana do adresów URL niezależnie od tego, czy zostanie podany segment id.
W tabeli 15.4 pokazane jest działanie tego mechanizmu dla różnych adresów URL.
Tabela 15.4. Dopasowanie adresów URL z opcjonalną zmienną segmentu
Liczba segmentów
Przykładowy URL
Mapowany na
0
witryna.pl
1
witryna.pl/Customer
2
witryna.pl/Customer/List
3
witryna.pl/Customer/List/All
controller = Home
action = Index
controller = Customer
action = Index
controller = Customer
action = List
controller = Customer
action = List
id = All
4
witryna.pl/Customer/List/All/Delete
Brak dopasowania — za dużo segmentów
373
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 15.20. Sprawdzenie w pliku HomeController.cs, czy opcjonalnej zmiennej segmentu została przypisana
wartość
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers {
public class HomeController : Controller {
public ActionResult Index() {
ViewBag.Controller = "Home";
ViewBag.Action = "Index";
return View("ActionName");
}
public ActionResult CustomVariable(string id) {
ViewBag.Controller = "Home";
ViewBag.Action = "CustomVariable";
ViewBag.CustomVariable = id ?? "<brak wartości>" ;
return View();
}
}
}
Po uruchomieniu aplikacji i przejściu do adresu URL /Home/CustomVariable (który nie posiada
zdefiniowanej wartości domyślnej dla zmiennej id segmentu) otrzymasz wynik pokazany na rysunku 15.10.
Rysunek 15.10. Aplikacja wykryła, że adres URL nie zawiera wartości dla opcjonalnej zmiennej segmentu
Użycie opcjonalnych segmentów URL w celu wymuszenia separacji zadań
Niektórzy programiści bardzo mocno koncentrują się na separacji zadań na platformie MVC i nie lubią
umieszczania wartości domyślnych zmiennych segmentu w trasach aplikacji. Jeżeli jest to problemem również
dla Ciebie, możesz użyć funkcji parametrów opcjonalnych w C# wraz z opcjonalną zmienną segmentu
w trasie w celu zdefiniowania wartości domyślnych dla parametrów metod akcji. Jak pokazano na listingu
15.21, należy zmodyfikować metodę akcji CustomVariable i zdefiniować wartość domyślną dla parametru id,
która będzie używana, jeśli adres URL nie będzie zawierał wartości dla wspomnianego parametru.
Listing 15.21. Definiowanie wartości domyślnej dla parametru metody akcji w pliku HomeController.cs
...
public ViewResult CustomVariable(string id = "DefaultId") {
ViewBag.Controller = "Home";
ViewBag.Action = "CustomVariable";
ViewBag.CustomVariable = id;
return View();
}
...
374
ROZDZIAŁ 15.  ROUTING URL
W ten sposób zapewnimy wartość dla parametru id (albo pochodzącą z adresu URL, albo domyślną),
więc możemy usunąć kod odpowiedzialny za obsługę wartości null. Ta metoda akcji, w połączeniu z trasą
zdefiniowaną na listingu 15.21, ma taką samą funkcjonalność jak trasa zdefiniowana na listingu 15.22.
Listing 15.22. Odpowiednik trasy zdefiniowanej na poprzednim listingu
...
routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "DefaultId" });
...
Różnica polega na tym, że wartość domyślna dla zmiennej id segmentu jest zdefiniowana w kodzie
kontrolera, a nie w definicji trasy.
Testy jednostkowe — opcjonalne segmenty URL
Jedynym problemem, na który musimy zwrócić uwagę przy testowaniu opcjonalnych segmentów URL jest to,
czy zmienna segmentu nie zostanie dodana do kolekcji RouteData.Values, gdy wartość nie zostanie znaleziona
w adresie URL. Oznacza to, że nie powinniśmy dołączać zmiennej w typie anonimowym, o ile nie testujemy adresu
URL zawierającego opcjonalny segment. Poniżej przedstawiono zmiany, jakie trzeba wprowadzić w metodzie
TestIncomingRoutes, aby przetestować trasę zdefiniowaną na listingu 15.22.
...
[TestMethod]
public void TestIncomingRoutes() {
TestRouteMatch("~/", "Home", "Index");
TestRouteMatch("~/Customer", "Customer", "index");
TestRouteMatch("~/Customer/List", "Customer", "List");
TestRouteMatch("~/Customer/List/All", "Customer", "List", new { id = "All" });
TestRouteFail("~/Customer/List/All/Delete");
}
...
Definiowanie tras o zmiennej długości
Innym sposobem na zmianę domyślnego konserwatyzmu tras URL jest akceptowanie zmiennej liczby
segmentów URL. Pozwala to na obsługiwanie adresów URL o dowolnej długości przy zastosowaniu jednej
definicji trasy. Obsługę zmiennej liczby segmentów realizuje się przez wyznaczenie jednej ze zmiennych
segmentów jako zmiennej przechwytującej, co jest realizowane przez poprzedzenie jej znakiem gwiazdki (*),
jak pokazano na listingu 15.23.
Listing 15.23. Wyznaczanie w pliku RouteConfig.cs zmiennej przechwytującej
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
375
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index",
id = UrlParameter.Optional });
}
}
}
Rozszerzyliśmy trasę z poprzedniego przykładu o dodatkową zmienną segmentu przechwytującego,
o nazwie catchall. Trasa ta pasuje teraz do dowolnego adresu URL, niezależnie od liczby segmentów lub wartości
któregokolwiek z nich. Pierwsze trzy segmenty są wykorzystywane do skonfigurowania wartości zmiennych
controller, action oraz id. Jeżeli adres URL będzie zawierał kolejne segmenty, zostaną one przypisane do
zmiennej catchall w sposób pokazany w tabeli 15.5.
Tabela 15.5. Dopasowanie adresów URL ze zmienną przechwytującą segmentu
Liczba segmentów
Przykładowy URL
Mapowanie na
0
witryna.pl
controller = Home
action = Index
1
witryna.pl/Customer
controller = Customer
action = Index
2
witryna.pl/Customer/List
controller = Customer
action = List
3
witryna.pl/Customer/List/All
controller = Customer
action = List
id = All
4
witryna.pl/Customer/List/All/Delete
controller = Customer
action = List
id = All
catchall = Delete
5
witryna.pl/Customer/List/All/Delete/Perm
controller = Customer
action = List
id = All
catchall = Delete/Perm
Nie istnieje górna granica liczby segmentów możliwych do dopasowania przez wzorzec URL w tej trasie.
Zwróć uwagę, że segmenty w zmiennej przechwytującej są prezentowane w postaci segment/segment/segment.
To my jesteśmy odpowiedzialni za przetworzenie tego ciągu znaków i jego podział na pojedyncze segmenty.
Test jednostkowy — testowanie zmiennych segmentów przechwytujących
Zmienną przechwytującą możemy potraktować jak każdą inną zmienną. Jedyna różnica polega na tym, że musimy
oczekiwać otrzymania wartości wielu segmentów połączonych w jedną wartość, na przykład segment/segment/
segment. Zwróć uwagę, że nie otrzymamy początkowego ani końcowego znaku /. Poniżej przedstawiono zmiany,
jakie trzeba wprowadzić w metodzie TestIncomingRoutes, aby przetestować trasę zdefiniowaną na listingu 15.23
oraz adresy URL z tabeli 15.5:
...
[TestMethod]
public void TestIncomingRoutes() {
TestRouteMatch("~/", "Home", "Index");
TestRouteMatch("~/Customer", "Customer", "Index");
TestRouteMatch("~/Customer/List", "Customer", "List");
TestRouteMatch("~/Customer/List/All", "Customer", "List", new { id = "All" });
TestRouteMatch("~/Customer/List/All/Delete", "Customer", "List",
376
ROZDZIAŁ 15.  ROUTING URL
new { id = "All", catchall = "Delete" });
TestRouteMatch("~/Customer/List/All/Delete/Perm", "Customer", "List",
new { id = "All", catchall = "Delete/Perm" });
}
...
Definiowanie priorytetów kontrolerów na podstawie przestrzeni nazw
Gdy przychodzący adres URL zostanie dopasowany do trasy, platforma MVC odczytuje nazwę zmiennej
controller i szuka klasy o odpowiedniej nazwie. Jeżeli na przykład wartością zmiennej controller jest Home,
platforma MVC poszukuje klasy o nazwie HomeController. Jest to niekwalifikowana nazwa klasy, co oznacza,
że w przypadku znalezienia co najmniej dwóch klas o nazwie HomeController w różnych przestrzeniach nazw
platforma nie będzie „wiedziała”, którą z nich należy wybrać.
Aby zademonstrować ten problem, utwórz nowy podkatalog w katalogu głównym projektu i nadaj mu
nazwę AdditionalControllers. Następnie umieść w nim nowy kontroler HomeController, którego kod
przedstawiono na listingu 15.24.
Listing 15.24. Zawartość pliku AdditionalControllers/HomeController.cs
using System.Web.Mvc;
namespace UrlsAndRoutes.AdditionalControllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Controller = "Additional Controllers - Home";
ViewBag.Action = "Index";
return View("ActionName");
}
}
}
Po uruchomieniu aplikacji zobaczysz błąd pokazany na rysunku 15.11.
Rysunek 15.11. Błąd występujący w przypadku istnienia w aplikacji dwóch kontrolerów o takiej samej nazwie
377
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Platforma MVC szukała klasy HomeController i znalazła dwie: pierwszą w pierwotnej przestrzeni nazw
UrlsAndRoutes.Controllers i drugą w nowej przestrzeni nazw UrlsAndRoutes.AdditionalControllers. Jeżeli
wczytasz się w tekst komunikatu pokazanego na rysunku 15.11, to dowiesz się, które klasy zostały znalezione
przez platformę MVC.
Problem ten pojawia się częściej, niż można się tego spodziewać, szczególnie jeżeli pracujemy nad dużym
projektem MVC, który korzysta z bibliotek kontrolerów pochodzących od różnych zespołów lub zewnętrznych
dostawców. Naturalne jest nazywanie kontrolera związanego z kontami użytkowników ciągiem AccountController,
a jest to tylko jeden z przypadków, gdy napotkamy konflikty nazw.
Aby rozwiązać ten problem, możemy określić przestrzenie nazw, które powinny mieć wyższy priorytet
przy wyborze nazwy klasy kontrolera (listing 15.25).
Listing 15.25. Określanie w pliku RouteConfig.cs kolejności wykorzystania przestrzeni nazw
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index",
id = UrlParameter.Optional
}, new[] { "UrlsAndRoutes.AdditionalControllers"});
}
}
}
Przestrzenie nazw zapisujemy jako tablicę ciągów znakowych. W kodzie zamieszczonym na powyższym
listingu informujemy platformę MVC, aby przeszukiwała przestrzeń nazw
UrlsAndRoutes.AdditionalControllers jako pierwszą.
Jeżeli w podanej przestrzeni nazw nie zostanie znaleziony odpowiedni kontroler, platforma MVC wróci
do standardowego działania i przeszuka wszystkie dostępne przestrzenie nazw. Po ponownym uruchomieniu
aplikacji na tym etapie otrzymasz wynik pokazany na rysunku 15.12. Na wymienionym rysunku pokazano,
że żądanie skierowane do adresu głównego aplikacji, które jest przetwarzane przez metodę akcji Index kontrolera
Home, zostało przekazane kontrolerowi zdefiniowanemu w przestrzeni nazw AdditionalControllers.
Rysunek 15.12. Nadanie priorytetu kontrolerom we wskazanej przestrzeni nazw
Przestrzenie nazw dodawane do trasy mają identyczny priorytet. Platforma MVC nie sprawdza pierwszej
przestrzeni nazw, potem przechodzi do następnej itd. Dodajmy do trasy na przykład obie nasze przestrzenie
nazw w poniższy sposób:
378
ROZDZIAŁ 15.  ROUTING URL
...
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional},
new[] { "UrlsAndRoutes.AdditionalControllers", "UrlsAndRoutes.Controllers"});
...
Ponownie zobaczymy informacje o błędzie pokazane na rysunku 15.11, ponieważ platforma MVC
próbuje znaleźć klasę kontrolera we wszystkich przestrzeniach nazw dodanych do trasy. Jeżeli chcemy
zwiększyć priorytet kontrolera z jednej przestrzeni nazw, a wszystkie inne kontrolery wyszukiwać w innej
przestrzeni, musimy utworzyć wiele tras, jak pokazano na listingu 15.26.
Listing 15.26. Użycie wielu tras w pliku RouteConfig.cs do sterowania przeszukiwaniem przestrzeni nazw
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index",
id = UrlParameter.Optional },
new[] { "UrlsAndRoutes.AdditionalControllers" });
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index",
id = UrlParameter.Optional },
new[] { "UrlsAndRoutes.Controllers"});
}
}
}
Pierwsza trasa zostanie użyta, kiedy użytkownik wyraźnie wskaże adres URL, którego pierwszym
segmentem jest Home. W takim przypadku żądanie będzie skierowane do kontrolera HomeController
w katalogu AdditionalControllers. Wszystkie pozostałe żądania, łącznie z tymi, w których nie zdefiniowano
pierwszego segmentu, zostaną obsłużone przez kontrolery znajdujące się w katalogu Controllers.
Możemy również zmusić platformę MVC, aby szukała wyłącznie w podanych przez nas przestrzeniach
nazw. Jeżeli nie zostanie znaleziony odpowiedni kontroler, biblioteka nie będzie szukała go w innych przestrzeniach.
Na listingu 15.27 przedstawiony jest sposób użycia tej funkcji.
Listing 15.27. Wyłączenie w pliku RouteConfig.cs domyślnego przeszukiwania przestrzeni nazw
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
Route myRoute = routes.MapRoute("AddContollerRoute",
379
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
"Home/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index",
id = UrlParameter.Optional },
new[] { "UrlsAndRoutes.AdditionalControllers" });
myRoute.DataTokens["UseNamespaceFallback"] = false;
}
}
}
Metoda MapRoute zwraca obiekt Route. Do tej pory nie korzystaliśmy z tej metody, ponieważ nie potrzebowaliśmy
wprowadzać żadnych korekt do tworzonych tras. Aby wyłączyć przeszukiwanie kontrolerów w innych
przestrzeniach nazw, musimy pobrać obiekt Route i przypisać kluczowi UseNamespaceFallback w kolekcji
DataTokens wartość false.
Ustawienie to zostanie przekazane do komponentu odpowiedzialnego za wyszukiwanie kontrolerów,
nazywanego fabryką kontrolerów, który przedstawię szczegółowo w rozdziale 19. Efektem wprowadzonej zmiany jest
to, że żądania, które nie mogą być obsłużone przez kontroler Home z katalogu AdditionalControllers, zakończą się
niepowodzeniem.
Ograniczenia tras
Na początku rozdziału napisałem, że wzorce URL są konserwatywne przy dopasowywaniu segmentów i liberalne
przy dopasowywaniu zawartości tych segmentów. W kilku poprzednich punktach przedstawiłem różne techniki
kontrolowania poziomu konserwatyzmu — tworzenia tras pasujących do większej lub mniejszej liczby
segmentów przez użycie wartości domyślnych, zmiennych opcjonalnych itd.
Teraz czas zająć się sposobami kontrolowania liberalizmu przy dostosowywaniu zawartości segmentów URL
— możliwościami ograniczenia zbioru adresów URL — do której będzie pasowała trasa. Ponieważ mamy
kontrolę nad oboma tymi aspektami trasy, możemy tworzyć schematy URL, które działają z laserową precyzją.
Ograniczanie trasy z użyciem wyrażeń regularnych
Pierwszą techniką, jaką się zajmiemy, jest ograniczanie tras z użyciem wyrażeń regularnych. Na listingu 15.28
pokazany jest przykład.
Listing 15.28. Użycie wyrażeń regularnych do ograniczania trasy zdefiniowanej w pliku RouteConfig.cs
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = "^H.*"},
new[] { "UrlsAndRoutes.Controllers"});
}
}
}
380
ROZDZIAŁ 15.  ROUTING URL
Ograniczenia definiujemy przez przekazanie ich jako parametru do metody MapRoute. Podobnie jak
w przypadku wartości domyślnych, ograniczenia są zapisywane w postaci typu anonimowego, którego właściwości
odpowiadają nazwom zmiennych segmentów, które chcemy ograniczyć. W zamieszczonym przykładzie
użyliśmy stałej z wyrażeniem regularnym pasującym do adresu URL tylko wtedy, gdy wartość zmiennej
kontrolera zaczyna się od litery H.
 Uwaga Wartości domyślne są używane przed sprawdzeniem ograniczeń. Jeżeli zatem otworzymy URL /, zostanie
zastosowana domyślna wartość dla zmiennej controller, czyli w tym przypadku Home. Następnie są sprawdzane
ograniczenia, a ponieważ wartość zmiennej controller zaczyna się od H, domyślny URL będzie pasował do tej trasy.
Ograniczanie trasy do zbioru wartości
Wyrażenia regularne możemy wykorzystać do definiowania trasy pasującej wyłącznie do specyficznych wartości
segmentu. W tym celu zastosujemy znak |, jak pokazano na listingu 15.29.
Listing 15.29. Ograniczanie trasy zdefiniowanej w pliku RouteConfig.cs do zbioru wartości zmiennej segmentu
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = "^H.*", action = "^Index$|^About$"},
new[] { "UrlsAndRoutes.Controllers"});
}
}
}
Ograniczenie to powoduje, że trasa pasuje wyłącznie do adresów URL, których wartość segmentu action
jest Index lub About. Ograniczenia są stosowane jednocześnie, więc ograniczenia nałożone na wartości zmiennej
action są łączone z tymi, które są nałożone na zmienną controller. Oznacza to, że trasa z listingu 15.29
będzie pasowała wyłącznie do adresów URL, których zmienna controller zaczyna się od litery H, a zmienna
action ma wartość Index lub About. Teraz wiesz już, co miałem na myśli, pisząc o bardzo precyzyjnych
trasach.
Ograniczanie tras z użyciem metod HTTP
Możliwe jest ograniczanie tras w taki sposób, aby dopasowywały wyłącznie adresy URL w momencie,
gdy żądanie korzysta z wybranej metody HTTP, jak pokazano na listingu 15.30.
Listing 15.30. Zdefiniowane w pliku RouteConfig.cs ograniczanie trasy na podstawie metody HTTP
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
381
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = "^H.*", action = "Index|About",
httpMethod = new HttpMethodConstraint("GET") },
new[] { "UrlsAndRoutes.Controllers" });
}
}
}
Format definiowania metody HTTP jest trochę dziwny. Nazwa nadana właściwości nie ma znaczenia
— wystarczy, że będzie egzemplarzem klasy HttpMethodConstraint. W powyższym listingu nazwaliśmy ją
httpMethod, aby pomóc odróżnić ją od wcześniej zdefiniowanych ograniczeń wartościowych.
 Uwaga Możliwość ograniczania tras za pomocą metod HTTP nie jest związana z możliwością ograniczania metod
akcji za pomocą takich atrybutów jak HttpGet czy HttpPost. Ograniczenia tras są przetwarzane znacznie wcześniej
w potoku obsługi żądania i wyznaczają nazwę kontrolera i akcji wymaganej do przetworzenia żądania. Atrybuty
metod akcji są używane do wybrania wersji metody akcji stosowanej do obsługi żądania przez kontroler. Więcej
informacji na temat obsługi różnych rodzajów metod HTTP (w tym również rzadziej stosowanych, takich jak PUT
i DELETE) przedstawię w rozdziale 16.
Do konstruktora klasy HttpMethodConstraint przekazujemy nazwy metod HTTP, które chcemy obsługiwać.
Na wcześniejszym listingu ograniczyliśmy trasę wyłącznie do żądań GET, ale możemy łatwo dodać obsługę
innych metod:
...
httpMethod = new HttpMethodConstraint("GET", "POST") },
...
Testy jednostkowe — ograniczenia tras
Przy testowaniu ograniczeń tras ważne jest, aby sprawdzić zarówno niepasujące adresy URL, jak i adresy,
które próbujemy wykluczyć, co możemy zrealizować przez wykorzystanie metod pomocniczych wprowadzonych
na początku tego rozdziału. Poniżej przedstawiono zmodyfikowaną metodę testową TestIncomingRoutes, której
użyjemy do przetestowania trasy zdefiniowanej na listingu 15.30:
...
[TestMethod]
public void TestIncomingRoutes() {
TestRouteMatch("~/", "Home", "Index");
TestRouteMatch("~/Home", "Home", "Index");
TestRouteMatch("~/Home/Index", "Home", "Index");
TestRouteMatch("~/Home/About", "Home", "About");
TestRouteMatch("~/Home/About/MyId", "Home", "About", new { id = "MyId" });
382
ROZDZIAŁ 15.  ROUTING URL
TestRouteMatch("~/Home/About/MyId/More/Segments", "Home", "About",
new {
id = "MyId",
catchall = "More/Segments"
});
TestRouteFail("~/Home/OtherAction");
TestRouteFail("~/Account/Index");
TestRouteFail("~/Account/About");
}
...
Użycie ograniczeń dotyczących typu i wartości
Platforma MVC zawiera wiele wbudowanych ograniczeń przeznaczonych do użycia w celu ograniczenia
adresów URL, które dopasowują trasy na podstawie typu i wartości zmiennych segmentu. Na listingu 15.31
przedstawiono przykład zastosowania jednego z tego rodzaju ograniczeń w konfiguracji routingu naszej aplikacji.
Listing 15.31. Użycie w pliku RouteConfig.cs ograniczeń dotyczących wbudowanego typu i wartości
using
using
using
using
using
using
using
using
System;
System.Web;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
System.Web.Mvc.Routing.Constraints;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new {
controller = "^H.*", action = "Index|About",
httpMethod = new HttpMethodConstraint("GET"),
id = new RangeRouteConstraint(10, 20)
},
new[] { "URLsAndRoutes.Controllers" });
}
}
}
Za pomocą znajdujących się w przestrzeni nazw System.Web.Mvc.Routing.Constraints klas ograniczeń
sprawdzamy, czy zmienne segmentu są wartościami dla różnych typów C# i czy mogą przeprowadzać
proste operacje sprawdzenia. Na listingu 15.31 użyłem klasy RangeRouteConstraint do sprawdzenia, czy
wartość dostarczona przez zmienną segmentu jest poprawną wartością typu int mieszczącą się we wskazanym
zakresie, tutaj od 10 do 20. W tabeli 15.6 wymieniono wszystkie dostępne klasy ograniczeń. Warto w tym
miejscu dodać, że nie wszystkie klasy akceptują argumenty, a więc podano nazwy klas w postaci używanej
do konfiguracji tras. Zignoruj teraz kolumnę zatytułowaną Atrybut ograniczenia, powrócimy do niej w dalszej
części rozdziału, po wprowadzeniu funkcji atrybutu routingu.
383
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Tabela 15.6. Klasy ograniczania trasy
Nazwa
Opis
Atrybut ograniczenia
AlphaRouteConstraint()
Dopasowuje znaki alfabetu niezależnie od ich
wielkości (A – Z, a – z).
alpha
BoolRouteConstraint()
Dopasowuje wartość, która może być
przetworzona jako bool.
bool
DateTimeRouteConstraint()
Dopasowuje wartość, która może być
przetworzona jako DateTime.
datetime
DecimalRouteConstraint()
Dopasowuje wartość, która może być
przetworzona jako decimal.
decimal
DoubleRouteConstraint()
Dopasowuje wartość, która może być
przetworzona jako double.
double
FloatRouteConstraint()
Dopasowuje wartość, która może być
przetworzona jako float.
float
IntRouteConstraint()
Dopasowuje wartość, która może być
przetworzona jako int.
int
LengthRouteConstraint(len)
Dopasowuje wartość o podanej liczbie
znaków lub której wielkość mieści się
w zakresie definiowanym przez min i max.
length(len)
LongRouteConstraint()
Dopasowuje wartość, która może być
przetworzona jako long.
long
MaxRouteConstraint(val)
Dopasowuje wartość int, jeżeli wartość jest
mniejsza niż val.
max(val)
MaxLengthRouteConstraint(len)
Dopasowuje ciąg tekstowy składający się
z maksymalnie len znaków.
maxlength(len)
MinRouteConstraint(val)
Dopasowuje wartość int, jeżeli wartość jest
większa niż val.
min(val)
MinLengthRouteConstraint(len)
Dopasowuje ciąg tekstowy składający się
z co najmniej len znaków.
minlength(len)
RangeRouteConstraint(min, max)
Dopasowuje wartość int, jeżeli wartość jest
z zakresu od min do max.
range(min, max)
LengthRouteConstraint(min, max)
length(min, max)
Istnieje możliwość łączenia różnych ograniczeń dla pojedynczej zmiennej segmentu. W tym celu
należy użyć klasy CompoundRouteConstraint, która akceptuje tablicę ograniczeń przekazywaną jako argument
konstruktora. Na listingu 15.32 możesz zobaczyć, jak tę funkcję wykorzystałem w celu zastosowania
ograniczeń AlphaRouteConstraint i MinLengthRouteConstraint dla zmiennej id segmentu. Dzięki temu
mam gwarancję, że trasa dopasuje jedynie wartości w postaci ciągu tekstowego zawierającego co najmniej
sześć liter.
Listing 15.32. Połączenie w pliku RouteConfig.cs ograniczeń trasy
using
using
using
using
using
using
using
using
384
System;
System.Web;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
System.Web.Mvc.Routing.Constraints;
ROZDZIAŁ 15.  ROUTING URL
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new {
controller = "^H.*", action = "Index|About",
httpMethod = new HttpMethodConstraint("GET"),
id = new CompoundRouteConstraint(new IRouteConstraint[] {
new AlphaRouteConstraint(),
new MinLengthRouteConstraint(6)
})
},
new[] { "URLsAndRoutes.Controllers" });
}
}
}
Definiowanie własnych ograniczeń
Jeżeli standardowe ograniczenia nie są wystarczające do naszych potrzeb, możemy zdefiniować własne
ograniczenia przez zaimplementowanie interfejsu IRouteConstraint. Aby zademonstrować tę funkcję, do projektu
dodajemy katalog Infrastructure, w którym następnie tworzymy nowy plik klasy o nazwie UserAgentConstraint.cs
i umieszczamy w niej kod przedstawiony na listingu 15.33.
Listing 15.33. Zawartość pliku UserAgentConstraint.cs
using System.Web;
using System.Web.Routing;
namespace UrlsAndRoutes.Infrastructure {
public class UserAgentConstraint : IRouteConstraint {
private string requiredUserAgent;
public UserAgentConstraint(string agentParam) {
requiredUserAgent = agentParam;
}
public bool Match(HttpContextBase httpContext, Route route,
string parameterName, RouteValueDictionary values,
RouteDirection routeDirection) {
return httpContext.Request.UserAgent != null &&
httpContext.Request.UserAgent.Contains(requiredUserAgent);
}
}
}
Interfejs IRouteConstraint definiuje metodę Match, której implementacja wskazuje systemowi routingu,
czy ograniczenie jest spełnione. Parametry metody Match zapewniają dostęp do żądania wykonywanego przez
klienta, do kontrolowanej trasy, do zmiennych segmentów pobranych z adresu URL oraz do informacji,
czy żądanie dotyczy przychodzącego, czy wychodzącego adresu URL. W naszym przykładzie sprawdzamy,
czy wartość właściwości UserAgent w żądaniu klienta jest taka sama jak wartość przekazana do konstruktora.
Na listingu 15.34 pokazane jest nasze ograniczenie użyte w trasie.
385
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 15.34. Użycie niestandardowego ograniczenia w trasie zdefiniowanej w pliku RouteConfig.cs
using
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
System.Web.Mvc.Routing.Constraints;
UrlsAndRoutes.Infrastructure;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("ChromeRoute", "{*catchall}",
new { controller = "Home", action = "Index" },
new { customConstraint = new UserAgentConstraint("Chrome") },
new[] { "UrlsAndRoutes.AdditionalControllers" });
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new {
controller = "^H.*", action = "Index|About",
httpMethod = new HttpMethodConstraint("GET"),
id = new CompoundRouteConstraint(new IRouteConstraint[] {
new AlphaRouteConstraint(),
new MinLengthRouteConstraint(6)
})
},
new[] { "UrlsAndRoutes.Controllers" });
}
}
}
Na listingu mamy zdefiniowane ograniczenie tras pozwalające na dopasowanie wyłącznie żądań
wykonywanych z przeglądarek, których nagłówek user-agent zawiera Chrome. Jeżeli trasa zostanie dopasowana,
wówczas żądanie będzie skierowane do metody akcji Index w kontrolerze HomeController zdefiniowanym
w katalogu AdditionalControllers bez względu na strukturę i treść żądanego adresu URL. Nasz wzorzec URL
składa się ze zmiennej catchall segmentu, co oznacza, że wartości zmiennych controller i action segmentu
zawsze będą wartościami domyślnymi, a nie pobranymi z adresu URL.
Druga trasa spowoduje dopasowanie wszystkich żądań i kontrolerów docelowych w katalogu Controllers
z uwzględnieniem zdefiniowanych wcześniej ograniczeń typu i wartości. Efektem zastosowania omówionych
tras jest to, że jedna z przeglądarek zawsze będzie przechodziła do tego samego miejsca w aplikacji, co możesz
zobaczyć na rysunku 15.13. Na wymienionym rysunku pokazano efekt uruchomienia aplikacji w przeglądarce
Google Chrome.
Rysunek 15.13. Aplikacja uruchomiona w przeglądarce Google Chrome
Z kolei na rysunku 15.14 pokazano wynik uruchomienia tej samej aplikacji w przeglądarce Internet Explorer.
(Zwróć uwagę na dodanie trzeciego segmentu zawierającego sześć liter, aby druga trasa dopasowała adres URL.
Ta konieczność wynika z ograniczenia zdefiniowanego w poprzednim punkcie).
386
ROZDZIAŁ 15.  ROUTING URL
Rysunek 15.14. Aplikacja uruchomiona w przeglądarce Internet Explorer
 Uwaga Chcę postawić sprawę jasno — nie sugeruję, abyś ograniczał aplikację do obsługi przeglądarki tylko
jednego typu. Użyłem nagłówka user-agent wyłącznie w celu zademonstrowania własnych ograniczeń trasy,
ponieważ wierzę w równe szanse wszystkich przeglądarek. Naprawdę nie znoszę witryn, które wymuszają
na użytkownikach wybór przeglądarki.
Użycie atrybutów routingu
We wszystkich przykładach przedstawionych dotąd w rozdziale trasy były konfigurowane za pomocą techniki
nazywanej routing oparty na konwencji. Na platformie MVC 5 dodano nową technikę o nazwie atrybuty routingu,
w której trasy są definiowane za pomocą atrybutów języka C# stosowanych bezpośrednio w klasach kontrolera.
W tym podrozdziale zobaczysz, jak utworzyć i skonfigurować trasy za pomocą atrybutów. Tę nową technikę
można bez problemów łączyć ze standardowymi trasami zdefiniowanymi przez routing oparty na konwencji.
Routing oparty na konwencji kontra atrybuty routingu
Atrybuty routingu to jedna z najważniejszych nowych funkcji na platformie MVC 5, choć muszę przyznać,
że nie jestem jej zwolennikiem. Jak wspomniałem w rozdziale 3., jednym z głównych celów wzorca MVC jest
podział aplikacji na poszczególne części, co ma ułatwić jej tworzenie, testowanie i późniejszą obsługę. Preferuję
routing oparty na konwencji, ponieważ kontrolery nie mają wiedzy dotyczącej konfiguracji routingu w aplikacji
i pozostają niezależne od niej. Z drugiej strony atrybuty routingu wprowadzają zamieszanie i rozmazują granicę
między dwoma ważnymi komponentami aplikacji.
Jako że atrybuty routingu są obsługiwane na platformie MVC 5, warto nieco się o nich dowiedzieć i samodzielnie
wyrobić sobie zdanie na ich temat. Moja niechęć do tej funkcji nie powinna oznaczać, że będziesz unikał jej
stosowania we własnych projektach.
Dobra wiadomość jest taka, że oba podejścia w zakresie tworzenia tras korzystają z tej samej infrastruktury
na platformie MVC. Oznacza to możliwość zastosowania w pojedynczym projekcie obu podejść bez żadnych
skutków ubocznych.
Włączanie i stosowanie atrybutów routingu
Atrybuty routingu są domyślnie wyłączone. W celu ich włączenia należy użyć metody rozszerzającej
MapMvcAttributeRoutes, która jest wywoływana w obiekcie RouteCollection przekazywanym jako argument
metody statycznej o nazwie RegisterRoutes. Dodanie w pliku RouteConfig.cs wywołania wymienionej metody
przedstawiono na listingu 15.35. Możesz również dostrzec uproszczenie tras w aplikacji, co pozwoli nam
skoncentrować się na użyciu atrybutów.
387
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 15.35. Włączenie w pliku RouteConfig.cs obsługi atrybutów routingu
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
UrlsAndRoutes.Infrastructure;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapMvcAttributeRoutes();
routes.MapRoute("Default", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index",
id = UrlParameter.Optional },
new[] { "UrlsAndRoutes.Controllers" });
}
}
}
Wywołanie metody MapMvcAttributeRoutes powoduje, że system routingu przegląda klasy kontrolera
w aplikacji i wyszukuje atrybuty odpowiedzialne za konfigurację tras. Najważniejszy atrybut nosi nazwę
Route, sposób jego zastosowania w kontrolerze Customer przedstawiono na listingu 15.36.
Listing 15.36. Zastosowanie w pliku CustomerController.cs atrybutu routingu
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers {
public class CustomerController : Controller {
[Route("Test")]
public ActionResult Index() {
ViewBag.Controller = "Customer";
ViewBag.Action = "Index";
return View("ActionName");
}
public ActionResult List() {
ViewBag.Controller = "Customer";
ViewBag.Action = "List";
return View("ActionName");
}
}
}
To jest podstawowy sposób użycia atrybutu Route odpowiedzialnego za zdefiniowanie statycznej trasy
dla metody akcji. Atrybut Route definiuje dwie właściwości wymienione w tabeli 15.7.
Tabela 15.7. Parametry obsługiwane przez atrybut Route
Nazwa
Opis
Name
Przypisuje nazwę trasie. Ten parametr jest używany do generowania wychodzących
adresów URL na podstawie określonej trasy.
Template
Definiuje wzorzec, jaki będzie używany w celu dopasowania adresów URL do docelowej
metody akcji.
388
ROZDZIAŁ 15.  ROUTING URL
Jeżeli podczas stosowania atrybutu Route zdefiniujesz tylko pojedynczą wartość (jak to zrobiłem na
listingu 15.36), wówczas jest ona uznawana za wzorzec używany w celu dopasowania tras. Wzorce dla
atrybutu Route mają taką samą strukturę, jak w przypadku routingu opartego na konwencji. Istnieją jednak
pewne różnice w zakresie ograniczeń trasy (dokładniej omówię to w punkcie „Stosowanie ograniczeń
trasy” w dalszej części rozdziału). W omawianym przykładzie zastosowałem atrybut Route wskazujący,
że metoda akcji Index kontrolera Customer może być wywołana za pomocą adresu URL /Test. Efekt
zastosowania atrybutu Route pokazano na rysunku 15.15. W rozdziale 16. pokażę Ci, jak używać
właściwości Name.
Rysunek 15.15. Efekt zastosowania atrybutu Route w celu utworzenia statycznej trasy
Kiedy metoda akcji zostaje udekorowana atrybutem Route, wówczas nie jest dłużej dostępna za pomocą
definiowanych w pliku RouteConfig.cs tras opartych na konwencji. W omawianym przykładzie oznacza to brak
możliwości wywołania metody akcji Index kontrolera Customer za pomocą adresu URL /Customer/Index.
 Ostrzeżenie Atrybut Route uniemożliwia trasom zdefiniowanym na podstawie konwencji wywołanie metody akcji,
nawet jeżeli atrybut routingu jest wyłączony. Zwróć więc szczególną uwagę na wywołanie metody
MapMvcAttributeRoutes w pliku RouteConfig.cs, ponieważ w przeciwnym razie możesz utworzyć niemożliwe
do wywołania metody akcji.
Atrybut Route ma wpływ jedynie na metody, względem których został zastosowany. Oznacza to, że wprawdzie
metoda akcji Index kontrolera Customer jest dostępna za pomocą adresu URL /Test, ale akcja List nadal musi
być wywoływana za pomocą adresu URL /Customer/List.
 Wskazówka Istnieje możliwość wielokrotnego zastosowania atrybutu Route dla tej samej metody, a każdy
egzemplarz atrybutu utworzy nową trasę.
Tworzenie tras za pomocą zmiennych segmentu
Funkcja atrybutów routingu obsługuje wszystkie możliwości, jakie oferuje routing oparty na konwencji, choć
do ich wyrażenia są używane atrybuty. Dostępne możliwości obejmują tworzenie tras zawierających zmienne
segmentu, a przykład takiej trasy przedstawiono na listingu 15.37.
Listing 15.37. Utworzenie w pliku CustomerController.cs atrybutu Route wraz ze zmienną segmentu
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers {
public class CustomerController : Controller {
[Route("Test")]
public ActionResult Index() {
ViewBag.Controller = "Customer";
ViewBag.Action = "Index";
return View("ActionName");
}
389
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
[Route("Users/Add/{user}/{id}")]
public string Create(string user, int id) {
return string.Format("Użytkownik: {0}, ID: {1}", user, id);
}
public ActionResult List() {
ViewBag.Controller = "Customer";
ViewBag.Action = "List";
return View("ActionName");
}
}
}
W kodzie dodaliśmy metodę akcji o nazwie Create pobierającą argumenty string i int. W celu
zachowania prostoty wartością zwrotną metody jest string, co oddala konieczność utworzenia widoku.
Trasa zdefiniowana za pomocą atrybutu Route stanowi połączenie prefiksu statycznego (Users/Add) ze
zmiennymi segmentu user i id odpowiadającymi argumentom metody. Platforma MVC wykorzystuje
mechanizm dołączania modelu, który zostanie dokładnie omówiony w rozdziale 25. Za pomocą wymienionego
mechanizmu następuje konwersja wartości zmiennej segmentu na odpowiedni typ w celu wywołania
metody Create. Na rysunku 15.16 pokazano efekt przejścia do adresu URL /Users/Add/Adam/100.
Rysunek 15.16. Przejście do adresu URL z użyciem zmiennych segmentu
Zwróć uwagę, że każdy egzemplarz atrybutu działa niezależnie. Zyskujesz więc możliwość utworzenia
zupełnie odmiennych tras wywołujących poszczególne metody akcji w kontrolerze, jak to przedstawiono
w tabeli 15.8.
Tabela 15.8. Akcje w kontrolerze Customer i trasy pozwalające na wywoływanie tych metod akcji
Nazwa
Opis
Index
/Test
Create
/Users/Add/Adam/100 (dwa ostatnie segmenty mogą mieć dowolne wartości)
List
/Customer/List (za pomocą trasy zdefiniowanej w pliku RouteConfig.cs)
Zastosowanie ograniczeń trasy
Trasy zdefiniowane za pomocą atrybutów również mogą mieć nakładane ograniczenia, podobnie jak w przypadku
klas zdefiniowanych w pliku RouteConfig.cs. Jednak tutaj technika jest bardziej bezpośrednia. Aby
zademonstrować rozwiązanie, do kontrolera Customer dodajemy kolejną metodę akcji, jak przedstawiono
na listingu 15.38.
Listing 15.38. Dodanie do pliku CustomerController.cs metody akcji i trasy
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers {
public class CustomerController : Controller {
390
ROZDZIAŁ 15.  ROUTING URL
[Route("Test")]
public ActionResult Index() {
ViewBag.Controller = "Customer";
ViewBag.Action = "Index";
return View("ActionName");
}
[Route("Users/Add/{user}/{id:int}")]
public string Create(string user, int id) {
return string.Format("Metoda Create - użytkownik: {0}, ID: {1}", user, id);
}
[Route("Users/Add/{user}/{password}")]
public string ChangePass(string user, string password) {
return string.Format("Metoda ChangePass - użytkownik: {0}, hasło: {1}",
user, password);
}
public ActionResult List() {
ViewBag.Controller = "Customer";
ViewBag.Action = "List";
return View("ActionName");
}
}
}
Nowa metoda akcji o nazwie ChangePass pobiera dwa argumenty w postaci ciągów tekstowych.
Wykorzystaliśmy atrybut Route do powiązania akcji z tym samym wzorcem URL, jak w przypadku metody
akcji Create: statyczny prefiks /Users/Add, po którym znajdują się dwie zmienne segmentu. Aby rozróżniać
akcje, w atrybucie Route dla metody Create zastosowano następujące ograniczenie:
...
[Route("Users/Add/{user}/{id:int}")]
...
Po nazwie zmiennej segmentu (id) znajduje się dwukropek oraz słowo kluczowe int. W ten sposób
system routingu został poinformowany, że metoda akcji Create może być wywołana jedynie przez żądania,
w których wartością dostarczaną dla zmiennej id segmentu jest poprawna wartość typu int. Tak zdefiniowane
ograniczenie int odpowiada klasie ograniczenia IntRouteConstraint. W przedstawionej wcześniej tabeli 15.6
wymieniono nazwy ograniczeń, które można wykorzystać w celu uzyskania dostępu do wartości i wbudowanego
typu ograniczeń.
Efekt wprowadzonych ograniczeń możesz zobaczyć po uruchomieniu aplikacji i przejściu do adresów
URL /Users/Add/Adam/100 i /Users/Add/Adam/Sekret. Ostatni segment w pierwszym adresie URL to poprawna
wartość typu int, stąd wywołanie metody Create. Natomiast ostatni segment w drugim adresie URL nie jest
wartością typu int, a więc nastąpi przekierowanie do metody ChangePass, jak pokazano na rysunku 15.17.
Rysunek 15.17. Efekt zastosowania ograniczenia w atrybucie Route
391
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Łączenie ograniczeń
Istnieje możliwość zastosowania wielu ograniczeń dla zmiennej segmentu, aby jeszcze bardziej ograniczyć
zakres wartości, które będą dopasowywane przez trasę. Na listingu 15.39 możesz zobaczyć, jak połączyłem
ograniczenia alpha i length w trasie dla metody ChangePass.
Listing 15.39. Zastosowanie w pliku CustomerController.cs wielu ograniczeń dla trasy
...
[Route("Users/Add/{user}/{password:alpha:length(6)}")]
public string ChangePass(string user, string password) {
return string.Format("Metoda ChangePass - użytkownik: {0}, hasło: {1}",
user, password);
}
...
Wiele połączonych ze sobą ograniczeń używa tego samego formatu co pojedyncze ograniczenie: dwukropek,
nazwa ograniczenia oraz ewentualna wartość umieszczona w nawiasie. Trasa zdefiniowana przez atrybut
w powyższym przykładzie będzie dopasowywała jedynie ciągi tekstowe zawierające dokładnie sześć znaków.
 Ostrzeżenie Zachowaj ostrożność podczas stosowania ograniczeń. Trasy zdefiniowane przez atrybut Route
działają w dokładnie taki sam sposób, jak trasy zdefiniowane w pliku RouteConfig.cs. Jeżeli adres URL nie zostanie
dopasowany do metody akcji, przeglądarka internetowa otrzyma odpowiedź w postaci błędu 404 (nie znaleziono
strony). Zawsze definiuj trasę awaryjną, która będzie dopasowywała wszelkie wartości znajdujące się w adresie URL.
Użycie prefiksu trasy
Za pomocą atrybutu RoutePrefix można zdefiniować prefiks, który będzie stosowany dla wszystkich tras
zadeklarowanych w kontrolerze. Takie rozwiązanie może być użyteczne w przypadku posiadania wielu metod
akcji, które powinny być wywoływane za pomocą tego samego głównego adresu URL. Na listingu 15.40
przedstawiono przykład użycia atrybutu RoutePrefix w kontrolerze CustomerController.
Listing 15.40. Ustawienie prefiksu trasy w pliku CustomerController.cs
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers {
[RoutePrefix("Users")]
public class CustomerController : Controller {
[Route("~/Test")]
public ActionResult Index() {
ViewBag.Controller = "Customer";
ViewBag.Action = "Index";
return View("ActionName");
}
[Route("Add/{user}/{id:int}")]
public string Create(string user, int id) {
return string.Format("Metoda Create - użytkownik: {0}, ID: {1}", user, id);
}
[Route("Add/{user}/{password}")]
public string ChangePass(string user, string password) {
return string.Format("Metoda ChangePass - użytkownik: {0}, hasło: {1}",
user, password);
392
ROZDZIAŁ 15.  ROUTING URL
}
public ActionResult List() {
ViewBag.Controller = "Customer";
ViewBag.Action = "List";
return View("ActionName");
}
}
}
Atrybut RoutePrefix został użyty w celu określenia, że trasy metody akcji mają mieć zastosowany prefiks
Users. Dzięki zdefiniowanemu prefiksowi można uaktualnić atrybut Route dla metod akcji Create i ChangePass
w celu usunięcia prefiksu. Podczas tworzenia tras platforma MVC automatycznie połączy prefiks ze wzorcem
adresu URL.
Zwróć uwagę, że wzorzec adresu URL dla atrybutu Route zastosowanego w metodzie akcji Index został
zmieniony na następujący:
...
[Route("~/Test")]
...
Poprzedzenie adresu URL prefiksem ~/ wskazuje platformie MVC, że atrybut RoutePrefix nie powinien
być stosowany dla metody akcji Index. Oznacza to, że wymieniona metoda nadal może być wywołana po przejściu
do adresu URL /Test.
Podsumowanie
W tym rozdziale przedstawiłem szczegółowo system routingu. Zobaczyłeś, jak definiować trasy według konwencji,
a także za pomocą atrybutów. Pokazałem, jak są dopasowywane i obsługiwane przychodzące żądania URL,
jak dostosować trasy do własnych potrzeb przez zmianę sposobu dopasowywania segmentów URL i przez
używanie wartości domyślnych oraz segmentów opcjonalnych. Zademonstrowałem także sposób ograniczania
tras w wyniku zmniejszania zakresu dopasowywanych żądań za pomocą ograniczeń zarówno wbudowanych,
jak i samodzielnie definiowanych.
W następnym rozdziale pokażę, jak generować wychodzące żądania URL z tras w widokach oraz
jak korzystać z funkcji obszarów na platformie MVC. Wspomniana funkcja opiera się na systemie routingu
i można ją wykorzystać do zarządzania ogromnymi i skomplikowanymi aplikacjami zbudowanymi
w technologii ASP.NET MVC.
393
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
394
ROZDZIAŁ 16.

Zaawansowane funkcje routingu
W poprzednim rozdziale dowiedziałeś się, jak używać systemu routingu do obsługi przychodzących żądań
adresów URL, ale to dopiero połowa zadania. Przy wykorzystaniu schematu URL jesteśmy również w stanie
generować wychodzące adresy URL, które są używane w naszych widokach, dzięki czemu użytkownicy mogą
klikać łącza i wysyłać dane formularzy do naszej aplikacji w taki sposób, że trafią one do odpowiedniego
kontrolera oraz akcji. W tym rozdziale przedstawię różne techniki generowania wychodzących adresów
URL. Dowiesz się, jak dostosować system routingu do własnych potrzeb przez zastąpienie standardowej
implementacji klas routingu MVC oraz użycie oferowanej przez platformę MVC funkcji obszarów, dzięki
której ogromne i skomplikowane aplikacje MVC można podzielić na łatwiejsze w zarządzaniu fragmenty.
Na końcu rozdziału przedstawię wybrane najlepsze praktyki dotyczące schematów URL w aplikacjach MVC.
W tabeli 16.1 znajdziesz podsumowanie materiału omówionego w rozdziale.
Tabela 16.1. Podsumowanie materiału omówionego w rozdziale
Temat
Rozwiązanie
Listing (nr)
Wygenerowanie elementu <a>
wraz z wychodzącym adresem URL
Użycie metody pomocniczej Html.ActionLink
Od 1. do 5., 9.
Dostarczenie wartości
dla zmiennych segmentu
Przekazanie metodzie pomocniczej ActionLink
obiektu anonimowego, którego właściwości
odpowiadają nazwom zmiennych segmentu
6. i 7.
Zdefiniowanie atrybutów
dla elementu <a>
Przekazanie metodzie pomocniczej ActionLink
obiektu anonimowego, którego właściwości
odpowiadają nazwom atrybutów
8.
Wygenerowanie wychodzącego
adresu URL bez elementu <a>
Użycie metody pomocniczej Url.Action
Od 10. do 13.
Wygenerowanie adresu URL
z określonej trasy
Podanie nazwy trasy w trakcie wywoływania metody
pomocniczej
14. i 15.
Opracowanie własnej polityki
generowania i dopasowania
adresów URL
Zastosowanie klasy RouteBase
Od 16. do 21.
Zdefiniowanie własnego
mapowania między adresami
URL i metodami akcji
Implementacja interfejsu IRouteHandler
22. i 23.
Podział aplikacji na mniejsze
fragmenty
Utworzenie obszarów lub zastosowanie atrybutu
Od 24. do 27.,
30.
RouteArea
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Tabela 16.1. Podsumowanie materiału omówionego w rozdziale (ciąg dalszy)
Temat
Rozwiązanie
Listing (nr)
Rozwiązywanie problemów
związanych z niejednoznacznymi
nazwami kontrolerów w obszarach
Nadanie priorytetu przestrzeni nazw kontrolera
28. i 29.
Uniemożliwienie serwerowi IIS
i platformie ASP.NET
przetwarzania żądań plików
statycznych, zanim nie zostaną
przekazane do systemu routingu
Użycie właściwości RouteExistingFiles
Od 31. do 33.
Uniemożliwienie systemowi
routingu przetwarzania żądania
Użycie metody IgnoreRoute
34.
Utworzenie przykładowego projektu
Nadal będziemy korzystać z projektu UrlsAndRoutes z poprzedniego rozdziału, ale przed rozpoczęciem pracy
musimy wprowadzić kilka zmian. Przede wszystkim należy usunąć katalog AdditionalControllers i znajdujący
się w nim plik HomeController.cs. Aby usunąć katalog, kliknij go prawym przyciskiem myszy, a następnie
wybierz opcję Usuń z menu kontekstowego.
Uproszczenie tras
Kolejną zmianą jest uproszczenie tras w aplikacji. Przeprowadź edycję pliku App_Start/RouteConfig.cs,
aby jego zawartość odpowiadała przedstawionej na listingu 16.1.
Listing 16.1. Uproszczenie przykładowych tras w pliku RouteConfig.cs
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapMvcAttributeRoutes();
routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index",
id = UrlParameter.Optional });
}
}
}
Dodanie pakietu optymalizacyjnego
W dalszej części rozdziału omówię funkcję obszarów, która wymaga zainstalowania w projekcie nowego pakietu.
Dlatego też w konsoli menedżerów NuGet wydaj poniższe polecenie:
Install-Package Microsoft.AspNet.Web.Optimization -version 1.1.0 -projectname UrlsAndRoutes
396
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
Ten pakiet zawiera funkcjonalność niezbędną do przeprowadzenia optymalizacji plików JavaScript i CSS
w projekcie, co zostanie omówione w rozdziale 26. Wspomnianych funkcji nie będziemy bezpośrednio używać
w tym rozdziale, ale są one potrzebne do działania obszarów.
Uaktualnienie projektu testów jednostkowych
Konieczne jest wprowadzenie dwóch zmian w projekcie testów jednostkowych. Pierwsza polega na usunięciu
metody TestIncomingRoutes, której nie będziemy używać, ponieważ materiał prezentowany w rozdziale dotyczy
generowania tras wychodzących. Aby uniknąć niezaliczenia testów, po prostu usuń wymienioną metodę
z pliku RouteTests.cs.
Druga zmiana polega na dodaniu odwołania do przestrzeni nazw System.Web.Mvc, co odbywa się przez
instalację pakietu Mvc w projekcie testów jednostkowych. W konsoli menedżerów NuGet wydaj poniższe
polecenie:
Install-Package Microsoft.Aspnet.Mvc -version 5.0.0 -projectname UrlsAndRoutes.Tests
Musimy dodać pakiet MVC 5, aby mieć możliwość użycia pewnych metod pomocniczych do generowania
wychodzących adresów URL. Nie potrzebowaliśmy wymienionego pakietu w poprzednim rozdziale, ponieważ
obsługa przychodzących adresów URL jest zapewniana przez przestrzenie nazw System.Web i System.Web.Routing.
Generowanie wychodzących adresów URL w widokach
W niemal każdej aplikacji platformy MVC będziesz chciał umożliwić użytkownikom poruszanie się pomiędzy
widokami. Z reguły polega to na umieszczeniu łącza, którego kliknięcie wywołuje metodę akcji generującą
inny widok.
Kuszącym rozwiązaniem może być dodanie elementu statycznego, którego atrybut href wskazuje
metodę akcji, np.:
<a href="/Home/CustomVariable">To jest wychodzący adres URL</a>
W przypadku standardowej konfiguracji routingu ten znacznik HTML tworzy łącze z adresem, który
prowadzi do metody akcji CustomVariable w kontrolerze Home. Ręczne definiowanie adresów URL w pokazany
powyżej sposób jest szybkie i proste, ale również bardzo niebezpieczne. Za każdym razem, gdy zmienimy
schemat URL dla aplikacji, zniszczymy wszystkie na sztywno zdefiniowane adresy URL. Będziemy musieli
następnie przejrzeć wszystkie widoki w aplikacji i poprawić wszystkie odwołania do kontrolerów i metod akcji
— ten proces jest żmudny, podatny na wprowadzenie błędów i trudny do przetestowania. Znacznie lepszym
rozwiązaniem jest użycie systemu routingu do dynamicznego generowania wychodzących adresów URL
na podstawie schematu, dzięki czemu po jego zmianie zmienią się też wychodzące adresy URL w widokach.
Użycie systemu routingu do wygenerowania wychodzącego adresu URL
Najprostszym sposobem na wygenerowanie wychodzącego adresu URL jest użycie w widoku metody
Html.ActionLink (listing 16.2). Na listingu pokazano zmiany, jakie należy wprowadzić w widoku
/Views/Shared/ActionName.cshtml.
Listing 16.2. Użycie metody pomocniczej Html.ActionLink w pliku ActionName.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html>
397
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
<head>
<meta name="viewport" content="width=device-width" />
<title>ActionName</title>
</head>
<body>
<div>The controller is: @ViewBag.Controller</div>
<div>The action is: @ViewBag.Action</div>
<div>
@Html.ActionLink("To jest wychodzący adres URL", "CustomVariable")
</div>
</body>
</html>
Parametrami metody ActionLink są tekst dla łącza oraz nazwa metody akcji, na którą powinno wskazywać
łącze. Wynik wprowadzonej zmiany możesz zobaczyć po uruchomieniu aplikacji i zezwoleniu przeglądarce
internetowej na przejście do głównego adresu URL (rysunek 16.1).
Rysunek 16.1. Dodanie do widoku wychodzącego adresu URL
Kod HTML generowany przez metodę ActionLink zależy od bieżącego schematu routingu. Na przykład
przy użyciu schematu zdefiniowanego na listingu 16.1 (przy założeniu, że widok będzie generowany przez
kontroler Home) otrzymamy następujący kod HTML:
<a href="/Home/CustomVariable">To jest wychodzący adres URL</a>
Może się wydawać, że wybraliśmy dłuższe rozwiązanie w celu utworzenia ręcznie zdefiniowanego adresu
URL, który pokazano wcześniej. Jednak zaletą wybranego rozwiązania jest to, że automatycznie reaguje
na zmiany wprowadzone w konfiguracji routingu. Przykładowo, zmieniamy konfigurację routingu
przez wprowadzenie nowej trasy w pliku RouteConfig.cs, jak przedstawiono na listingu 16.3:
Listing 16.3. Dodanie nowej trasy do przykładowej aplikacji
...
public static void RegisterRoutes(RouteCollection routes) {
routes.MapMvcAttributeRoutes();
routes.MapRoute("NewRoute", "App/Do{action}",
new { controller = "Home" });
routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index",
id = UrlParameter.Optional });
}
...
Nowa trasa powoduje zmianę schematu URL dla żądań skierowanych do kontrolera Home. Jeśli uruchomisz
aplikację, to przekonasz się, że wprowadzona zmiana została odzwierciedlona w kodzie HTML wygenerowanym
przez metodę pomocniczą ActionLink:
<a href="/App/DoCustomVariable">To jest wychodzący adres URL</a>
398
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
Takie generowanie łączy znacznie upraszcza utrzymanie aplikacji. Jesteśmy w stanie bez obaw zmieniać
nasz schemat routingu, ponieważ łącza wychodzące umieszczone w widokach automatycznie odzwierciedlają
te zmiany. Oczywiście wychodzący adres URL staje się zwykłym żądaniem po kliknięciu łącza i system routingu
jest używany ponownie w celu prawidłowego wywołania metody akcji, co pokazano na rysunku 16.2.
Rysunek 16.2. Efekt kliknięcia łącza — wychodzący adres URL staje się żądaniem przychodzącym
Dopasowywanie tras do wychodzących adresów URL
Pokazałem już, jak zmiana tras definiujących schemat adresów URL wpływa na generowanie adresów wychodzących.
W aplikacjach jest zwykle wygenerowane kilka tras i ważne jest, aby rozumieć sposób wyboru trasy przy
generowaniu adresu URL. System routingu przetwarza trasy w kolejności ich dodawania do obiektu RouteCollection
przekazywanego do metody RegisterRoutes. Dla każdej trasy jest sprawdzane dopasowanie, w którym muszą
być spełnione trzy warunki:


Musi być dostępna wartość dla każdej zmiennej segmentu zdefiniowanej we wzorcu URL. Aby znaleźć
wartości dla każdej zmiennej segmentu, system routingu sprawdza dostarczone wartości (za pomocą
właściwości typu anonimowego), potem wartości dostępne w bieżącym żądaniu, a na koniec wartości
domyślne zdefiniowane w trasie (w dalszej części rozdziału wrócimy do drugiego ze źródeł wartości).
W przypadku zmiennych posiadających wyłącznie wartości domyślne każda z dostarczonych wartości
zmiennej segmentu musi być zgodna z wartością zdefiniowaną w trasie. Są to zmienne, dla których zostały
podane wartości domyślne, a które nie występują we wzorcu URL. Na przykład w poniższej definicji trasy
zmienną posiadającą wyłącznie wartości domyślne jest myVar.
routes.MapRoute("MyRoute", "{controller}/{action}",
new { myVar = "true" });

Aby trasa ta została dopasowana, nie możemy dostarczać wartości dla myVar lub wartość ta musi być taka
sama jak wartość domyślna.
Wartości dla wszystkich zmiennych segmentu muszą spełniać ograniczenia trasy. Przykłady różnych ograniczeń
są przedstawione w podrozdziale „Ograniczenia tras”, który znajduje się w poprzednim rozdziale.
Trzeba postawić sprawę jasno — system routingu nie próbuje znaleźć trasy, która jest najlepszą pasującą.
Znajduje on pierwszą pasującą trasę i wykorzystuje ją do wygenerowania adresu URL; wszystkie kolejne trasy są
ignorowane. Z tego powodu powinniśmy definiować najbardziej szczegółowe trasy na początku. Ważne jest,
aby testować generowanie adresów wychodzących. Jeżeli spróbujesz wygenerować adres URL, dla którego nie można
znaleźć pasującej trasy, zostanie wygenerowane łącze z pustym atrybutem href, na przykład:
<a href="">Informacje o aplikacji</a>
Łącze takie będzie w widoku wyglądało na prawidłowe, ale nie będzie działało w zamierzony sposób,
gdy użytkownik je kliknie. Jeżeli generujemy tylko adres URL (co pokażę w dalszej części rozdziału), to wynikiem
będzie wartość null, przekształcana w widoku na pusty ciąg znaków. Możliwe jest uzyskanie pewnej kontroli
nad dopasowaniem tras przez użycie tras nazwanych. Szczegóły są opisane w punkcie „Generowanie adresu URL
na podstawie wybranej trasy”, w dalszej części rozdziału.
399
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Pierwszy obiekt Route spełniający te kryteria zwróci niepusty adres URL, co spowoduje przerwanie procesu
generowania adresu URL. Wybrane wartości będą umieszczone w miejscu parametrów segmentu, a końcowa
sekwencja wartości domyślnych zostanie pominięta. Jeżeli podamy jawnie parametr, który nie odpowiada
parametrom segmentów lub wartościom domyślnym, to będzie on wygenerowany jako zbiór par nazwa-wartość
w ciągu zapytania.
Użycie innych kontrolerów
W domyślnej wersji metody ActionLink zakłada się, że chcemy użyć metody akcji z tego samego kontrolera,
który spowodował wygenerowanie widoku. Aby utworzyć wychodzący adres URL, korzystający z innego
kontrolera, można zastosować inną wersję tej przeciążonej metody, pozwalającą na podanie nazwy kontrolera,
jak pokazano na listingu 16.4.
Listing 16.4. Użycie innego kontrolera przy zastosowaniu metody pomocniczej ActionLink w pliku
ActionName.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>ActionName</title>
</head>
<body>
<div>Nazwa kontrolera: @ViewBag.Controller</div>
<div>Nazwa akcji: @ViewBag.Action</div>
<div>
@Html.ActionLink("To jest wychodzący adres URL", "CustomVariable")
</div>
<div>
@Html.ActionLink("To jest inny kontroler", "Index", "Admin")
</div>
</body>
</html>
Po wygenerowaniu widoku zobaczymy następujący wynikowy kod HTML:
<a href="/Admin">To jest inny kontroler</a>
 Ostrzeżenie Przy generowaniu wychodzących adresów URL system routingu nie posiada na temat naszej aplikacji
więcej informacji niż przy przetwarzaniu żądań przychodzących. Oznacza to, że wartość dostarczona dla metody
akcji i kontrolera nie jest kontrolowana i trzeba zadbać o to, aby nie podawać nieistniejących celów.
Żądanie adresu URL prowadzącego do metody akcji Index kontrolera Admin zostało przez metodę
ActionLink wyrażone jako /Admin. System routingu działa całkiem sprytnie i wie, że trasa zdefiniowana
w aplikacji będzie domyślnie używała metody akcji Index, co pozwala na pominięcie niepotrzebnych segmentów.
Podczas ustalania, która metoda akcji powinna zostać wywołana, system routingu uwzględnia również
trasy zdefiniowane za pomocą atrybutu Route. Na listingu 16.5 możesz zobaczyć, że zmieniłem nazwę
kontrolera w wywołaniu ActionLink, aby teraz żądana była metoda akcji Index w kontrolerze Customer.
400
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
Listing 16.5. Użycie w pliku ActionName.cshtml metody akcji udekorowanej atrybutem Route
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>ActionName</title>
</head>
<body>
<div>Nazwa kontrolera: @ViewBag.Controller</div>
<div>Nazwa akcji: @ViewBag.Action</div>
<div>
@Html.ActionLink("To jest wychodzący adres URL", "CustomVariable")
</div>
<div>
@Html.ActionLink("To jest inny kontroler", "Index", "Customer")
</div>
</body>
</html>
Po wygenerowaniu widoku zobaczymy następujący wynikowy kod HTML:
<a href="/Test">To jest inny kontroler</a>
Powyższy kod wynikowy HTML odpowiada atrybutowi Route, który zastosowaliśmy dla metody akcji
Index w kontrolerze Customer w rozdziale 15.:
...
[Route("~/Test")]
public ActionResult Index() {
ViewBag.Controller = "Customer";
ViewBag.Action = "Index";
return View("ActionName");
}
...
Przekazywanie dodatkowych parametrów
Możemy również przekazywać wartości do zmiennych segmentu przy użyciu typu anonimowego, którego
właściwości reprezentują segmenty. Na listingu 16.6 pokazany jest przykład — łącze akceptujące parametr
zostało dodane do pliku widoku ActionName.cshtml.
Listing 16.6. Dostarczanie wartości do zmiennych segmentów w pliku ActionName.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>ActionName</title>
401
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
</head>
<body>
<div>Nazwa kontrolera: @ViewBag.Controller</div>
<div>Nazwa akcji: @ViewBag.Action</div>
<div>
@Html.ActionLink("To jest wychodzący adres URL",
"CustomVariable", new { id = "Witaj" })
</div>
</body>
</html>
W przykładzie tym dostarczyliśmy wartość do zmiennej segmentu o nazwie id. Jeżeli nasza aplikacja korzysta
z trasy zdefiniowanej na listingu 16.3, to po wygenerowaniu widoku otrzymamy następujący HTML:
<a href="/App/DoCustomVariable?id=Witaj">To jest wychodzący adres URL</a>
Zwróć uwagę, że dostarczona przez nas wartość została dodana jako segment URL, aby dopasować się
do wzorca naszej trasy aplikacji zdefiniowanej na listingu 16.3. Wynika to z faktu, że w tej trasie żaden segment
zmiennej nie odpowiada id. Na listingu 16.7 pokazano zmiany wprowadzone w pliku RouteConfig.cs,
aby używana była tylko jedna trasa zawierająca segment id.
Listing 16.7. Edycja tras w pliku RouteConfig.cs
...
public static void RegisterRoutes(RouteCollection routes) {
routes.MapMvcAttributeRoutes();
routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index",
id = UrlParameter.Optional });
}
...
Po ponownym uruchomieniu aplikacji adres URL zdefiniowany w widoku ActionName.cshtml spowoduje
wygenerowanie poniższego elementu HTML:
<a href="/Home/CustomVariable/Witaj">To jest wychodzący adres URL</a>
Tym razem wartość przypisana właściwości id zostaje dołączona w postaci segmentu URL, zgodnie
z aktywną trasą w konfiguracji aplikacji.
Wielokrotne wykorzystanie zmiennych segmentów
Gdy opisywałem sposób dopasowania tras dla wychodzących adresów URL, wspomniałem, że przy próbie
znalezienia wartości dla każdej ze zmiennych segmentu we wzorcu URL system routingu wyszukuje wartości
z bieżącego żądania. Dla wielu programistów jest to problem, który może prowadzić do długich sesji debugowania.
Załóżmy, że aplikacja posiada jedną trasę:
...
routes.MapRoute("MyRoute", "{controller}/{action}/{color}/{page}");
...
Wyobraźmy sobie, że użytkownik obecnie przegląda stronę znajdującą się pod adresem URL /Catalog/List/Purple/123,
a my wygenerowaliśmy łącze w następujący sposób:
402
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
...
@Html.ActionLink("Kliknij mnie", "List", "Catalog", new {page=789}, null)
...
Moglibyśmy oczekiwać, że system routingu nie będzie w stanie dopasować trasy, ponieważ nie podaliśmy
wartości dla zmiennej color, dla której nie ma zdefiniowanej wartości domyślnej. Jednak nie jest to prawda. System
routingu znajdzie dopasowanie do zdefiniowanej trasy. Spowoduje to wygenerowanie następującego kodu HTML:
<a href="/Catalog/List/Purple/789">Kliknij mnie</a>
System routingu stara się tak bardzo, aby udało się dopasowanie do trasy, że jest w stanie wykorzystać
zmienną segmentu z przychodzącego adresu URL. W tym przypadku w zmiennej color otrzymaliśmy wartość
Purple, ponieważ znajdowała się w adresie URL, z którego skorzystał użytkownik.
To nie jest działanie wykonywane jako ostatnie. System routingu stosuje tę technikę w standardowym procesie
przetwarzania tras, nawet jeżeli istnieje kolejna trasa, która będzie mogła być dopasowana bez wykorzystywania
danych z bieżącego żądania. System routingu ponownie używa wartości tylko tych segmentów, które znajdują się
we wzorcu URL wcześniej niż parametry dostarczone do metody Html.ActionLink. Załóżmy, że próbujemy utworzyć
następujące łącze:
...
@Html.ActionLink("Kliknij mnie", "List", "Catalog", new {color="Aqua"}, null)
...
Dostarczyliśmy wartość dla zmiennej color, ale nie dla page. Jednak color znajduje się we wzorcu URL przed page,
więc system routingu nie wykorzysta wartości z przychodzącego adresu URL i trasa nie zostanie dopasowana.
Najlepszym sposobem na obsłużenie tej sytuacji jest zapobieganie jej powstawaniu. Gorąco zalecam, aby nie
polegać na takim działaniu i dostarczać wartości dla wszystkich zmiennych segmentu we wzorcu URL. W przeciwnym
razie kod będzie nie tylko trudniejszy do odczytywania, ale będzie także wymagać korzystania z założeń na
temat kolejności wykonywania żądań przez użytkowników, co ostatecznie zemści się na nas, gdy aplikacja przejdzie
do fazy utrzymania.
Definiowanie atrybutów HTML
Do tej pory skupiliśmy się na adresach URL generowanych przez metodę pomocniczą ActionLink, ale trzeba
pamiętać, że metoda ta generuje kompletny znacznik HTML łącza (<a>). Możemy ustawić atrybuty tego elementu,
dostarczając typ anonimowy, którego właściwości odpowiadają wymaganym atrybutom. Na listingu 16.8
zamieściłem zmodyfikowany kod widoku ActionName.cshtml, w którym ustawiono atrybut id oraz przypisano
do elementu HTML klasę CSS.
Listing 16.8. Generowanie w pliku ActionName.cshtml znacznika łącza z atrybutami
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>ActionName</title>
</head>
403
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
<body>
<div>Nazwa kontrolera: @ViewBag.Controller</div>
<div>Nazwa akcji: @ViewBag.Action</div>
<div>
@Html.ActionLink("To jest wychodzący adres URL",
"Index", "Home", null, new {id = "myAnchorID",
@class = "myCSSClass"})
</div>
</body>
</html>
Utworzyliśmy tu nowy typ anonimowy, który zawiera właściwości id oraz class, i przekazaliśmy go jako
parametr metody ActionLink. Do dodatkowej zmiennej segmentu przekazaliśmy wartość null, wskazując,
że nie mamy żadnej wartości do dostarczenia.
 Wskazówka Zwróć uwagę, że poprzedziliśmy właściwość class znakiem @. Jest to funkcja języka C#, która pozwala
nam użyć zarezerwowanych słów kluczowych jako składników klasy. Tę technikę wykorzystaliśmy także do
przypisywania elementom klas Bootstrap podczas budowy aplikacji SportsStore w pierwszej części książki.
Po wywołaniu metody ActionLink otrzymamy następujący HTML:
<a class="myCSSClass" href="/" id="myAnchorID">To jest wychodzący adres URL</a>
Generowanie w pełni kwalifikowanych adresów URL w łączach
Wszystkie wygenerowane do tej pory łącza zawierają względne adresy URL, ale można użyć metody pomocniczej
ActionLink do wygenerowania w pełni kwalifikowanych adresów URL, jak pokazano na listingu 16.9.
Listing 16.9. Generowanie w pliku ActionName.cshtml w pełni kwalifikowanego adresu URL
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>ActionName</title>
</head>
<body>
<div>Nazwa kontrolera: @ViewBag.Controller</div>
<div>Nazwa akcji: @ViewBag.Action</div>
<div>
@Html.ActionLink("To jest wychodzący adres URL", "Index", "Home",
"https", "serwer.domena.pl", " myFragmentName",
new { id = "MyId"},
new { id = "myAnchorID", @class = "myCSSClass"})
</div>
</body>
</html>
Jest to przeciążona wersja metody ActionLink, która posiada najwięcej parametrów i pozwala na
dostarczenie wartości dla protokołu (w naszym przykładzie https), nazwy serwera docelowego (serwer.domena.pl),
fragmentu URL (myFragmentName), jak również wszystkich przedstawionych wcześniej opcji. Po wygenerowaniu
widoku wywołanie przedstawione na listingu 16.9 powoduje utworzenie następującego kodu HTML:
404
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
<a class="myCSSClass" href="https://serwer.domena.pl/Home/Index/MyId#myFragmentName"
id="myAnchorID">To jest wychodzący adres URL</a>
Zalecam, aby wszędzie tam, gdzie jest to możliwe, korzystać z adresów względnych. W pełni kwalifikowane
adresy URL powodują tworzenie zależności od infrastruktury aplikacji. Widziałem już wiele dużych aplikacji
wykorzystujących bezwzględne adresy URL, które zostały uszkodzone przez nieskoordynowane zmiany
w infrastrukturze lub zasadach nazewnictwa domeny będące poza kontrolą programistów.
Generowanie adresów URL (nie łączy)
Metoda pomocnicza Html.ActionLink generuje kompletny znacznik <a>, co jest w większości przypadków
oczekiwane. Jednak istnieją sytuacje, gdy po prostu potrzebujemy adresu URL bez otaczającego go kodu HTML.
Możemy wtedy użyć metody Url.Action, aby wygenerować wyłącznie adres URL bez otaczającego go kodu
HTML, jak pokazano na listingu 16.10. Na listingu pokazano zmiany, jakie należy wprowadzić w widoku
/Views/Shared/ActionName.cshtml w celu utworzenia adresu URL za pomocą metody pomocniczej Url.Action.
Listing 16.10. Generowanie w pliku ActionName.cshtml adresu URL bez otaczającego go kodu HTML
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>ActionName</title>
</head>
<body>
<div>Nazwa kontrolera: @ViewBag.Controller</div>
<div>Nazwa akcji: @ViewBag.Action</div>
<div>
Mój URL to: @Url.Action("Index", "Home", new { id = "MyId" })
</div>
</body>
</html>
Metoda Url.Action działa identycznie jak Html.ActionLink, ale generuje wyłącznie adres. Przeciążone wersje
tej metody i akceptowane przez nie parametry są identyczne dla obu metod, więc można użyć wszystkich
kombinacji wywołań przedstawionych dla Html.ActionLink we wcześniejszych punktach. Kod z listingu 16.10
powoduje wygenerowanie efektu pokazanego na rysunku 16.3.
Rysunek 16.3. Wygenerowanie adresu URL (a nie łącza) w widoku
405
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Generowanie wychodzących adresów URL w metodach akcji
Zazwyczaj generujemy wychodzące adresy URL w widokach, ale czasami chcemy wykonać taką operację
w metodzie akcji. Jeżeli po prostu potrzebujemy wygenerować URL, możemy użyć tych samych metod
pomocniczych, z których korzystaliśmy w widoku, jak pokazano na listingu 16.11. Na wspomnianym listingu
przedstawiono nową metodę akcji dodaną do kontrolera Home.
Listing 16.11. Generowanie w pliku HomeController.cs wychodzących adresów URL
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Controller = "Home";
ViewBag.Action = "Index";
return View("ActionName");
}
public ActionResult CustomVariable(string id = "DefaultId")
{
ViewBag.Controller = "Home";
ViewBag.Action = "CustomVariable";
ViewBag.CustomVariable = id;
return View();
}
public ViewResult MyActionMethod()
{
string myActionUrl = Url.Action("Index", new { id = "MyID" });
string myRouteUrl = Url.RouteUrl(new { controller = "Home", action = "Index" });
//… wykonanie operacji na wygenerowanych adresach URL…
return View();
}
}
}
Dla trasy w przykładowej aplikacji zmienna myActionUrl będzie miała przypisaną wartość /Home/Index/MyID,
natomiast zmienna myRouteUrl będzie miała przypisaną wartość /. Oznacza to spójność z wynikami
wywołania wymienionych metod pomocniczych w widoku.
Częstszym wymaganiem jest przekierowanie przeglądarki klienta do innego adresu URL. Możemy
zrealizować to przez wywołanie metody RedirectToAction (listing 16.12).
Listing 16.12. Zdefiniowane w pliku HomeController.cs przekierowanie do innej akcji
...
public RedirectToRouteResult MyActionMethod() {
return RedirectToAction("Index");
}
...
Wynikiem wywołania metody RedirectToAction jest obiekt RedirectToRouteResult, informujący
platformę MVC o konieczności wysłania instrukcji przekierowania do adresu URL, za pomocą którego można
wywołać podaną akcję. Istnieją oczywiście różne wersje metody RedirectToAction, które pozwalają podać
kontroler oraz wartości dla zmiennych segmentu w wygenerowanym adresie.
406
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
Jeżeli chcesz wysłać żądanie przekierowania do adresu URL wygenerowanego na podstawie właściwości
obiektu, możesz użyć metody RedirectToRoute, pokazanej na listingu 16.13. Metoda ta zwraca również obiekt
RedirectToRouteResult i daje wynik dokładnie taki sam jak wywołanie metody RedirectToAction.
Listing 16.13. Zdefiniowane w pliku HomeController.cs przekierowanie do adresu URL
...
public RedirectToRouteResult MyActionMethod() {
return RedirectToRoute(new { controller = "Home", action = "Index", id = "MyID" });
}
...
Generowanie adresu URL na podstawie wybranej trasy
W poprzednich przykładach wybór trasy używanej do wygenerowania adresu URL lub łącza pozostawialiśmy
systemowi routingu. W tym podrozdziale dowiesz się, jak przejąć kontrolę nad tym procesem i wybierać
określoną trasę. Na listingu 16.14 przedstawiono zmiany, które trzeba wprowadzić w pliku RouteConfig.cs,
aby lepiej zademonstrować omawianą funkcję.
Listing 16.14. Zmiany w konfiguracji routingu w pliku RouteConfig.cs
...
public static void RegisterRoutes(RouteCollection routes) {
routes.MapMvcAttributeRoutes();
routes.MapRoute("MyRoute", "{controller}/{action}");
routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" });
}
...
Zdefiniowane zostały nazwy dla obu tras — MyRoute oraz MyOtherRoute. Istnieją dwa powody nazywania tras:
 przypomnienie przeznaczenia trasy,
 zapewnienie możliwości wybrania trasy przy generowaniu wychodzącego adresu URL.
Zdefiniowane powyżej trasy uporządkowaliśmy w taki sposób, że ogólniejsza jest zdefiniowana wcześniej.
Oznacza to, że jeżeli wygenerujemy łącze za pomocą metody ActionLink, w poniższy sposób:
...
@Html.ActionLink("Kliknij mnie", "Index", "Customer");
...
to wychodzący adres URL będzie zawsze generowany za pomocą trasy MyRoute, jak przedstawiono poniżej:
<a href="/Customer/Index">Kliknij mnie</a>
Możliwe jest zmodyfikowanie domyślnego sposobu dopasowywania tras przez zastosowanie metody
Html.RouteLink, pozwalającej podać trasę, której chcemy użyć:
...
@Html.RouteLink("Kliknij mnie", "MyOtherRoute", "Index", "Customer");
...
Wynikiem jest łącze wygenerowane za pomocą metody pomocniczej, które wygląda jak przedstawiono
poniżej:
<a Length="8" href="/App/Index?Length=5">Kliknij mnie</a>
W omawianym przykładzie wskazany kontroler (Customer) został nadpisany i łącze prowadzi
do kontrolera Home.
Istnieje również możliwość nadawania nazw trasom definiowanym za pomocą atrybutu Route.
Na listingu 16.15 możesz zobaczyć, jak nadano nazwę tego rodzaju trasie w kontrolerze Customer.
407
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 16.15. Nadanie nazwy trasie w pliku CustomerController.cs
...
[Route("Users/Add/{user}/{id:int}", Name="AddRoute")]
public string Create(string user, int id)
{
return string.Format("Metoda Create - użytkownik: {0}, ID: {1}", user, id);
}
...
Zmiana wprowadzona w tym przykładzie powoduje przypisanie wartości właściwości Name omówionej
w rozdziale 15. Tutaj trasie definiowanej przez atrybut przypisano nazwę AddRoute, co pozwala na generowanie
tras na podstawie ich nazw.
Wady użycia tras nazwanych
Gdy polegamy na nazwach tras przy generowaniu wychodzących adresów URL pojawia się problem polegający
na złamaniu zasady rozdzielenia zadań, która jest niezwykle ważnym aspektem we wzorcu projektowym MVC.
Generując łącze lub URL w metodzie akcji, chcemy skupić się na akcji i kontrolerze, do którego powinien być
skierowany użytkownik, a nie na stosowanym formacie URL. Przez użycie nazw różnych tras w widokach
i kontrolerach tworzymy zależności, których chcieliśmy uniknąć. Staram się unikać nazywania tras (przez podawanie
wartości null w parametrze). Zalecam umieszczanie komentarzy w kodzie, które pozwolą przypomnieć znaczenie
każdej ze tras.
Dostosowanie systemu routingu
Pokazałem już, jak elastyczny i konfigurowalny jest system routingu, ale jeżeli nadal nie spełnia on Twoich
wymagań, możesz dostosować jego działanie. W niniejszym punkcie przedstawię dwa sposoby realizacji tego
zadania.
Tworzenie własnej implementacji RouteBase
Jeżeli nie podoba Ci się sposób, w jaki standardowe obiekty Route dopasowują adresy URL, lub chcesz
zaimplementować coś niestandardowego, możesz utworzyć alternatywną klasę dziedziczącą po RouteBase.
Daje to kontrolę nad sposobem dopasowania adresu URL, sposobami pobierania parametrów oraz generowania
wychodzących adresów URL. Przy implementowaniu klasy dziedziczącej po RouteBase należy zdefiniować
dwie metody:
 GetRouteData(HttpContextBase httpContext) — jest to mechanizm dopasowywania przychodzących
adresów URL. Platforma wywołuje tę metodę dla każdego wpisu w RouteTable.Routes do momentu,
gdy zwróci ona wartość inną niż null.
 GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) — jest to mechanizm
dopasowywania wychodzących adresów URL. Platforma wywołuje tę metodę dla każdego wpisu
w RouteTable.Routes do momentu, gdy zwróci ona wartość inną niż null.
Aby zademonstrować dostosowanie tego rodzaju, utworzymy klasę RouteBase, która będzie obsługiwała
odziedziczone żądania URL. Wyobraźmy sobie, że migrujemy istniejącą aplikację do wersji przeznaczonej
na platformę MVC, ale niektórzy użytkownicy zapisali sobie adresy URL ze starej aplikacji lub wykorzystali je
w skryptach. Nadal chcemy obsługiwać te stare adresy. Możemy je obsłużyć za pomocą standardowego
systemu routingu, ale problem ten świetnie nadaje się jako przykład w tym punkcie.
Na początek potrzebujemy kontrolera, który będzie otrzymywał nasze odziedziczone żądania. Tworzymy
więc kontroler o nazwie LegacyController, którego zawartość jest zamieszczona na listingu 16.16.
408
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
Listing 16.16. Zawartość pliku LegacyController.cs
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers {
public class LegacyController : Controller {
public ActionResult GetLegacyURL(string legacyURL) {
return View((object)legacyURL);
}
}
}
W tym prostym kontrolerze metoda GetLegacyURL odczytuje parametr i przekazuje go jako model do widoku.
Jeżeli naprawdę implementowalibyśmy ten kontroler, użylibyśmy tej metody do odczytania żądanego pliku,
ale w tym przykładzie po prostu wyświetlimy adres URL w widoku.
 Wskazówka Zwróć uwagę, że w metodzie View na listingu 16.16 przeprowadzamy rzutowanie parametru na typ
object. Jedna z przeciążonych wersji metody View oczekuje ciągu znaków określającego nazwę widoku do
wygenerowania, więc bez tego rzutowania kompilator C# wywołałby tę właśnie wersję przeciążonej metody.
Aby tego uniknąć, wykonaliśmy rzutowanie na object, dzięki czemu zostanie wywołana wersja metody
korzystająca z modelu widoku i użyty będzie domyślny widok. Mógłbym rozwiązać to również przez zastosowanie
wersji oczekującej zarówno nazwy widoku, jak i modelu widoku, ale z zasady wolę nie tworzyć jawnych połączeń
pomiędzy metodami akcji i widokami.
Widok, który skojarzyliśmy z tą akcją, ma nazwę GetLegacyURL.cshtml i umieszczamy go w katalogu
Views/Legacy. Zawartość nowego pliku przedstawiono na listingu 16.17.
Listing 16.17. Zawartość pliku GetLegacyURL.cshtml
@model string
@{
ViewBag.Title = "GetLegacyURL";
Layout = null;
}
<h2>GetLegacyURL</h2>
Żądany URL to: @Model
Chcę pokazać jedynie działanie naszej niestandardowej trasy, więc nie będę poświęcać miejsca na opisywanie
tworzenia skomplikowanych akcji i widoków. Jesteśmy teraz w punkcie, w którym możemy zacząć tworzyć
naszą klasę dziedziczącą po RouteBase.
Kierowanie przychodzących adresów URL
Tworzymy nową klasę o nazwie LegacyRoute i umieszczamy ją w katalogu Infrastructure znajdującym się
w głównym katalogu projektu (tam umieszczamy klasy, dla których nie ma miejsca w innych katalogach).
Klasa ta jest zamieszczona na listingu 16.18.
Listing 16.18. Zawartość pliku LegacyRoute.cs
using System;
using System.Linq;
using System.Web;
409
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes.Infrastructure {
public class LegacyRoute : RouteBase {
private string[] urls;
public LegacyRoute(params string[] targetUrls) {
urls = targetUrls;
}
public override RouteData GetRouteData(HttpContextBase httpContext) {
RouteData result = null;
string requestedURL =
httpContext.Request.AppRelativeCurrentExecutionFilePath;
if (urls.Contains(requestedURL, StringComparer.OrdinalIgnoreCase)) {
result = new RouteData(this, new MvcRouteHandler());
result.Values.Add("controller", "Legacy");
result.Values.Add("action", "GetLegacyURL");
result.Values.Add("legacyURL", requestedURL);
}
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext,
RouteValueDictionary values) {
return null;
}
}
}
Konstruktor tej klasy oczekuje tablicy ciągów znaków reprezentujących adresy URL obsługiwane przez
klasę routingu. Podamy je później przy rejestrowaniu trasy. Warto zwrócić uwagę na metodę GetRouteData,
która jest wywoływana przez system routingu w celu sprawdzenia, czy możemy obsłużyć przychodzący adres URL.
Jeżeli nie możemy obsłużyć żądania, po prostu zwracamy wartość null — system routingu przejdzie do
następnej trasy na liście i powtórzy proces. Jeżeli możemy obsłużyć żądanie, musimy zwrócić obiekt klasy
RouteData zawierający wartości dla zmiennych controller, action oraz wszystkich innych, które chcemy
przekazać do metody akcji.
Tworząc obiekt RouteData, musimy przekazać procedurę, za pomocą której chcemy obsługiwać
wygenerowane wartości. Użyjemy tu standardowej klasy MvcRouteHandler, obsługującej zmienne
controller i action:
...
result = new RouteData(this, new MvcRouteHandler());
...
Dla znacznej większości aplikacji MVC jest to odpowiednia klasa, ponieważ łączy ona w nich system routingu
z modelem kontroler-akcja. Można również utworzyć własną klasę MvcRouteHandler, co wyjaśnię w punkcie
„Tworzenie własnego obiektu obsługi trasy”, w dalszej części rozdziału.
W tej implementacji routingu chcemy kierować wszystkie żądania adresów URL przekazanych do konstruktora.
Gdy otrzymamy takie żądanie, dodajemy na stałe zapisane wartości zmiennych controller i action do obiektu
RouteValues. Przekazujemy też żądany URL do właściwości legacyURL. Zwróć uwagę, że nazwa tej właściwości
pasuje do nazwy parametru z naszej metody akcji, dzięki czemu wygenerowana tu wartość będzie przekazana
do metody akcji poprzez parametr.
410
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
Ostatnim krokiem będzie zarejestrowanie nowej trasy z wykorzystaniem zdefiniowanej przez nas pochodnej
RouteBase. Realizacja tego zadania jest przedstawiona na listingu 16.19, w którym przedstawiono zmiany
konieczne do wprowadzenia w pliku RouteConfig.cs.
Listing 16.19. Rejestrowanie w pliku RouteConfig.cs własnej implementacji RouteBase
using System.Web.Mvc;
using System.Web.Routing;
using UrlsAndRoutes.Infrastructure;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.MapMvcAttributeRoutes();
routes.Add(new LegacyRoute(
"~/articles/Windows_3.1_Overview.html",
"~/old/.NET_1.0_Class_Library"));
routes.MapRoute("MyRoute", "{controller}/{action}");
routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" });
}
}
}
Tworzymy nowe egzemplarze naszych klas i przekazujemy obsługiwany adres URL. Następnie za pomocą
metody Add dodajemy obiekt do RouteCollection. Teraz, gdy uruchomimy aplikację i odwołamy się do jednego
ze zdefiniowanych adresów URL, żądanie zostanie przesłane do naszej klasy, która skieruje je do naszego
kontrolera, jak pokazano na rysunku 16.4.
Rysunek 16.4. Kierowanie żądań z użyciem własnej implementacji RouteBase
Generowanie wychodzących adresów URL
Aby obsłużyć generowanie wychodzących adresów URL, musimy w klasie LegacyRoute zaimplementować
metodę GetVirtualPath. Jeżeli klasa nie jest w stanie wygenerować określonego adresu URL, informujemy
o tym fakcie system routingu, zwracając wartość null. W przeciwnym razie zwracamy egzemplarz klasy
VirtualPathData. Nasza implementacja jest pokazana na listingu 16.20.
Listing 16.20. Implementacja metody GetVirtualPath w pliku LegacyRoute.cs
...
public override VirtualPathData GetVirtualPath(RequestContext requestContext,
RouteValueDictionary values) {
VirtualPathData result = null;
if (values.ContainsKey("legacyURL") &&
urls.Contains((string)values["legacyURL"], StringComparer.OrdinalIgnoreCase)) {
411
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
result = new VirtualPathData(this,
new UrlHelper(requestContext)
.Content((string)values["legacyURL"]).Substring(1));
}
return result;
}
...
Zmienne segmentu oraz inne informacje przekazujemy za pomocą typów anonimowych, ale wewnętrznie
system routingu konwertuje je na obiekty RouteValueDictionary, aby mogły być przetworzone przez
implementacje RouteBase. Na listingu 16.21 przedstawiono zmiany, które należy wprowadzić w pliku widoku
ActionName.cshtml, odpowiedzialne za wygenerowanie wychodzącego adresu URL za pomocą własnej trasy.
Listing 16.21. Wygenerowanie w pliku ActionName.cshtml wychodzącego adresu URL za pomocą własnej trasy
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>ActionName</title>
</head>
<body>
<div>Nazwa kontrolera: @ViewBag.Controller</div>
<div>Nazwa akcji: @ViewBag.Action</div>
<div>Mój URL to:
@Html.ActionLink("Kliknij mnie", "GetLegacyURL", new { legacyURL =
"~/articles/Windows_3.1_Overview.html" })
</div>
</body>
</html>
Kiedy widok zostanie wygenerowany, procedura pomocnicza ActionLink zgodnie z oczekiwaniami
tworzy przedstawiony poniżej kod HTML, gdy żądany będzie adres URL, taki jak /Home/Index:
<a href="/articles/Windows_3.1_Overview.html">Kliknij mnie</a>
Typ anonimowy zostaje utworzony wraz z właściwością legacyURL i jest konwertowany na obiekt klasy
RouteValueDictionary, który zawiera klucz o takiej samej nazwie. W tym przykładzie zdecydowaliśmy się
obsługiwać żądania dla wychodzących adresów URL, jeżeli znajdziemy klucz o nazwie legacyURL, którego
wartość odpowiada jednemu z adresów URL przekazanych do konstruktora. Możemy również sprawdzać
wartości zmiennych controller oraz action, ale na potrzeby tego prostego przykładu jest to wystarczające.
Jeżeli znajdziemy dopasowanie, tworzymy nowy obiekt VirtualPathData i przekazujemy do niego odwołanie
do bieżącego obiektu oraz wychodzący adres URL. Użyliśmy metody Content z klasy UrlHelper do konwersji
względnego adresu URL aplikacji na taki, który można przekazać do przeglądarki. Niestety, system routingu
dodaje do adresu znak /, więc musimy sami zadbać o usunięcie pierwszego znaku z wygenerowanego adresu.
Tworzenie własnego obiektu obsługi trasy
W naszych trasach korzystamy z obiektu MvcRouteHandler, ponieważ łączy system routingu z platformą MVC.
Jesteśmy zainteresowani platformą MVC, zatem w większości przypadków jest to oczekiwane działanie.
System routingu pozwala również definiować własny obiekt obsługi trasy przez implementację interfejsu
IRouteHandler. Przykład jest zamieszczony na listingu 16.22. Listing zawiera kod klasy CustomRouteHandler,
którą trzeba dodać do katalogu Infrastructure w omawianym projekcie.
412
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
Listing 16.22. Implementowanie interfejsu IRouteHandler w pliku CustomRouteHandler.cs
using System.Web;
using System.Web.Routing;
namespace UrlsAndRoutes.Infrastructure {
public class CustomRouteHandler : IRouteHandler {
public IHttpHandler GetHttpHandler(RequestContext requestContext) {
return new CustomHttpHandler();
}
}
public class CustomHttpHandler : IHttpHandler {
public bool IsReusable {
get { return false; }
}
public void ProcessRequest(HttpContext context) {
context.Response.Write("Witaj");
}
}
}
Zadaniem interfejsu IRouteHandler jest dostarczenie narzędzi do generowania implementacji interfejsu
IHttpHandler, która jest odpowiedzialna za przetwarzanie żądań. W implementacji tego interfejsu dla MVC
wyszukiwane są kontrolery, wywoływane są metody akcji, generowane są widoki, a wyniki są wysyłane do
strumienia odpowiedzi. Nasza implementacja jest nieco prostsza. Po prostu wysyła słowo Witaj do klienta
(nie dokument HTML zawierający to słowo, lecz tylko tekst).
 Uwaga Interfejs IHttpHandler jest definiowany przez platformę ASP.NET i stanowi część standardowego systemu
obsługi żądań, dokładnie omówionego w innej mojej książce, zatytułowanej Pro ASP.NET MVC 5 Platform, wydanej
przez Apress. Aby tworzyć aplikacje MVC, nie musisz dokładnie poznawać sposobu, w jaki ASP.NET obsługuje
żądania. Istnieją jednak pewne możliwości w zakresie rozszerzenia i dostosowania do własnych potrzeb procesu
obsługi żądań, co może być użyteczne w zaawansowanych i skomplikowanych aplikacjach.
Własny obiekt obsługi możemy zarejestrować w pliku RouteConfig.cs przy definiowaniu trasy,
jak pokazano na listingu 16.23.
Listing 16.23. Użycie w pliku RouteConfig.cs własnego obiektu obsługi trasy
...
public static void RegisterRoutes(RouteCollection routes) {
routes.MapMvcAttributeRoutes();
routes.Add(new Route("SayHello", new CustomRouteHandler()));
routes.Add(new LegacyRoute(
"~/articles/Windows_3.1_Overview.html",
"~/old/.NET_1.0_Class_Library"));
routes.MapRoute("MyRoute", "{controller}/{action}");
routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" });
}
...
413
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Gdy zażądamy adresu URL /SayHello, do obsłużenia żądania zostanie użyty nasz obiekt obsługi.
Wynik jest pokazany na rysunku 16.5.
Rysunek 16.5. Użycie własnego obiektu obsługi żądania
Implementowanie własnej obsługi żądania oznacza, że przejmujemy odpowiedzialność za funkcje, które
były zwykle obsługiwane za nas, takie jak rozpoznawanie kontrolerów i akcji. Jednak daje to nam niezwykłą
swobodę. Możemy wykorzystać pewne części platformy MVC i zignorować inne, a nawet zaimplementować
całkiem nowy wzorzec architektury.
Korzystanie z obszarów
Platforma MVC obsługuje podział aplikacji WWW na obszary, gdzie każdy obszar reprezentuje funkcjonalny
segment aplikacji, taki jak administracja, rozliczenia, obsługa klienta itd. Jest to przydatne przy dużych projektach,
ponieważ korzystanie z jednego zbioru katalogów dla wszystkich kontrolerów, widoków i modeli może być trudne
w zarządzaniu.
Każdy obszar MVC posiada własną strukturę katalogów, pozwalającą na zapewnienie rozdzielenia modułów.
Dzięki temu oczywiste staje się, które elementy odnoszą się do każdego z obszarów funkcyjnych aplikacji.
Usprawnia to pracę wielu programistów nad projektem i zmniejsza liczbę kolizji. Obszary są obsługiwane
przez system routingu, dlatego zdecydowałem się na przedstawienie ich obok adresów URL i tras. W tym
podrozdziale pokażę, w jaki sposób można tworzyć i wykorzystywać obszary w projekcie MVC.
Tworzenie obszaru
Aby dodać obszar do aplikacji MVC, kliknij prawym przyciskiem myszy projekt w oknie Eksplorator
rozwiązania i wybierz Dodaj/Obszar…. Visual Studio będzie wymagał podania nazwy obszaru, jak pokazano na
rysunku 16.6. W tym przypadku tworzymy obszar o nazwie Admin. Jest to często tworzony obszar, ponieważ
wiele aplikacji WWW wymaga rozdzielenia funkcji użytkownika i administratora. Kliknij przycisk Dodaj,
aby utworzyć obszar.
Rysunek 16.6. Dodawanie obszaru do aplikacji MVC
Po kliknięciu przycisku Dodaj możemy zauważyć kilka zmian w projekcie. Otóż w projekcie jest teraz
katalog Areas. Znajduje się w nim katalog o nazwie Admin, który zawiera właśnie utworzony obszar.
Jeżeli będziemy tworzyć kolejne obszary, będą tu powstawać kolejne katalogi.
Wewnątrz katalogu Areas/Admin znajduje się miniprojekt MVC. Są tu katalogi Controllers, Models
oraz Views. Pierwsze dwa są puste, ale katalog Views zawiera podkatalog Shared (oraz plik Web.config
do konfigurowania silnika widoków, którym zajmiemy się w rozdziale 20.).
Kolejną zmianą jest plik AdminAreaRegistration.cs, który zawiera klasę AdminAreaRegistration,
zamieszczoną na listingu 16.24.
414
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
Listing 16.24. Zawartość pliku AdminAreaRegistration.cs
using System.Web.Mvc;
namespace UrlsAndRoutes.Areas.Admin {
public class AdminAreaRegistration : AreaRegistration {
public override string AreaName {
get {
return "Admin";
}
}
public override void RegisterArea(AreaRegistrationContext context) {
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
}
Interesującą częścią tej klasy jest metoda RegisterArea. Jak można zobaczyć na listingu, metoda ta rejestruje
trasę o wzorcu URL Admin/{controller}/{action}/{id}. W metodzie tej możemy zdefiniować kolejne trasy,
które będą unikatowe dla tego obszaru.
 Ostrzeżenie Jeżeli przypiszesz nazwy do swoich tras, musisz upewnić się, że są one unikatowe w całej aplikacji,
a nie tylko w obszarze, dla którego są przeznaczone.
Nie musimy wykonywać żadnych akcji, aby upewnić się, że ta metoda rejestracji została wywołana.
Visual Studio dodaje odpowiednie polecenie do pliku Global.asax, jak pokazano na listingu 16.25,
które zajmuje się konfiguracją obszarów podczas tworzenia projektu.
Listing 16.25. Wywołanie rejestracji obszaru w pliku Global.asax
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace UrlsAndRoutes {
public class MvcApplication : System.Web.HttpApplication {
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
}
}
Wywołanie statycznej metody AreaRegistration.RegisterAllAreas spowoduje, że platforma MVC przejrzy
wszystkie klasy w aplikacji, wyszuka te, które dziedziczą po AreaRegistration, i wywoła metodę RegisterArea
dla każdej z nich.
415
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
 Ostrzeżenie Nie należy zmieniać kolejności instrukcji związanych z routingiem w metodzie Application_Start. Jeżeli
wywołasz RegisterRoutes przed AreaRegistration.RegisterAllAreas, to Twoje trasy znajdą się przed trasami
obszaru. Ponieważ trasy są analizowane w kolejności dopisania, to najprawdopodobniej żądania kontrolerów z obszaru
będą dopasowywane do niewłaściwych tras.
Klasa AreaRegistrationContext jest przekazywana do każdej metody RegisterArea i udostępnia zbiór
metod MapRoute, które mogą być użyte w obszarze do rejestrowania tras; realizujemy to identycznie jak
w głównej aplikacji, w metodzie RegisterRoutes z pliku Global.asax.
 Uwaga Metody MapRoute w klasie AreaRegistrationContext automatycznie ograniczają trasy, jakie w nich
rejestrujemy, do przestrzeni nazw zawierających kontroler dla obszaru. Powoduje to, że przy tworzeniu kontrolera
w obszarze musimy pozostawić domyślną przestrzeń nazw; w przeciwnym razie system routingu nie będzie
w stanie jej znaleźć.
Wypełnianie obszaru
W obszarze można tworzyć kontrolery, widoki i modele, tak samo jak we wcześniejszych przykładach.
Aby utworzyć kontroler, kliknij prawym przyciskiem myszy katalog Controllers wewnątrz obszaru i wybierz
Dodaj/Kontroler… z menu kontekstowego. Wybierz opcję Kontroler MVC 5 - pusty, kliknij przycisk Dodaj,
określ nazwę kontrolera i ponownie kliknij przycisk Dodaj, co spowoduje utworzenie nowej klasy
kontrolera.
Aby pokazać, jak obszary izolują poszczególne fragmenty aplikacji, w obszarze Admin utworzyłem kontroler
Home. Zawartość pliku Areas/Admin/Controllers/HomeController.cs przedstawiono na listingu 16.26.
Listing 16.26. Zawartość pliku Areas/Admin/Controllers/HomeController.cs
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
namespace UrlsAndRoutes.Areas.Admin.Controllers {
public class HomeController : Controller {
public ActionResult Index() {
return View();
}
}
}
Nie będziemy tutaj modyfikować kodu kontrolera. W zupełności wystarczy wygenerowanie jedynie
domyślnego widoku powiązanego z metodą akcji Index. Utwórz katalog Areas/Admin/Views/Home, kliknij go
prawym przyciskiem myszy w oknie Eksplorator rozwiązania, a następnie z menu kontekstowego wybierz opcję
Dodaj/Strona widoku MVC 5 (Razor). Jako nazwę dla pliku widoku podaj Index.cshtml, kliknij przycisk OK
w celu utworzenia pliku, a następnie przeprowadź jego edycję do postaci przedstawionej na listingu 16.27.
Listing 16.27. Zawartość pliku Areas/Admin/Views/Home/Index.cshtml
@{
ViewBag.Title = "Index";
Layout = null;
}
416
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
<div>
<h2>Widok Index obszaru Admin</h2>
</div>
</body>
</html>
W ten sposób chciałem pokazać, że praca z użyciem obszarów jest bardzo podobna do realizacji tych samych
zadań w głównej części projektu MVC. Jeżeli uruchomisz aplikację i przejdziesz do /Admin/Home/Index,
zobaczysz utworzony przez nas widok, pokazany na rysunku 16.7.
Rysunek 16.7. Wygląd widoku Index w obszarze Admin
Rozwiązywanie problemów z niejednoznacznością kontrolerów
Muszę się przyznać, że trochę Cię oszukałem. Jeżeli przejdziemy w poprzednim przykładzie do adresu URL
/Home/Index aplikacji, to zobaczymy stronę z informacją o błędzie, podobną do pokazanej na rysunku 16.8.
Rysunek 16.8. Błąd niejednoznaczności kontrolera
Gdy zostanie zarejestrowany obszar, wszystkie zdefiniowane w nim trasy będą ograniczone do przestrzeni
nazw związanej z obszarem. Dzięki temu możemy otworzyć URL /Admin/Home/Index i wywołać klasę
HomeController z przestrzeni nazw UrlsAndRoutes.Areas.Admin.Controllers.
417
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Jednak trasy zdefiniowane w metodzie RegisterRoutes pliku RouteConfig.cs nie są w ten sposób ograniczone.
Aktualna konfiguracja routingu omawianej tutaj przykładowej aplikacji jest zamieszczona na listingu 16.28.
Listing 16.28. Zdefiniowana w pliku RouteConfig.cs konfiguracja routingu w omawianej aplikacji MVC
...
public static void RegisterRoutes(RouteCollection routes) {
routes.MapMvcAttributeRoutes();
routes.Add(new Route("SayHello", new CustomRouteHandler()));
routes.Add(new LegacyRoute(
"~/articles/Windows_3.1_Overview.html",
"~/old/.NET_1.0_Class_Library"));
routes.MapRoute("MyRoute", "{controller}/{action}");
routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" });
}
...
Trasa o nazwie MyRoute przekształca przychodzący adres URL z przeglądarki na akcję Index w kontrolerze
Home. W tym momencie wystąpi błąd, ponieważ w trasie tej nie istnieje ograniczenie przestrzeni nazw i platforma
MVC rozpoznaje dwie klasy HomeController. Aby rozwiązać ten problem, musimy w pliku RouteConfig.cs
nadać priorytet przestrzeni nazw zawierającej główny kontroler, jak pokazano na listingu 16.29.
Listing 16.29. Rozwiązywanie w pliku RouteConfig.cs konfliktu nazw z obszarami
...
public static void RegisterRoutes(RouteCollection routes) {
routes.MapMvcAttributeRoutes();
routes.Add(new Route("SayHello", new CustomRouteHandler()));
routes.Add(new LegacyRoute(
"~/articles/Windows_3.1_Overview.html",
"~/old/.NET_1.0_Class_Library"));
routes.MapRoute("MyRoute", "{controller}/{action}", null,
new[] {"UrlsAndRoutes.Controllers"});
routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" },
new[] { "UrlsAndRoutes.Controllers" });
}
...
Dzięki tej zmianie kontrolery z głównego projektu będą miały wyższy priorytet przy obsłudze żądań.
Oczywiście, jeżeli chcesz nadać priorytet kontrolerom z obszaru, można również to zrobić.
Tworzenie obszarów za pomocą atrybutów
Obszar można utworzyć również przez zastosowanie atrybutu RouteArea do klasy kontrolera. Na listingu 16.30
pokazano sposób przypisania metod akcji w kontrolerze Customer do nowego obszaru o nazwie Services.
Listing 16.30. Utworzenie obszaru za pomocą atrybutu zastosowanego w pliku CustomerController.cs
using System.Web.Mvc;
418
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
namespace UrlsAndRoutes.Controllers
{
[RouteArea("Services")]
[RoutePrefix("Users")]
public class CustomerController : Controller
{
[Route("~/Test")]
public ActionResult Index()
{
ViewBag.Controller = "Customer";
ViewBag.Action = "Index";
return View("ActionName");
}
[Route("Add/{user}/{id:int}", Name = "AddRoute")]
public string Create(string user, int id)
{
return string.Format("Metoda Create - użytkownik: {0}, ID: {1}", user, id);
}
[Route("Add/{user}/{password}")]
public string ChangePass(string user, string password)
{
return string.Format("Metoda ChangePass - użytkownik: {0}, hasło: {1}",
user, password);
}
public ActionResult List()
{
ViewBag.Controller = "Customer";
ViewBag.Action = "List";
return View("ActionName");
}
}
}
Atrybut RouteArea powoduje przeniesienie do wskazanego obszaru wszystkich tras zdefiniowanych przez
atrybut Route. Efektem użycia wymienionego atrybutu w połączeniu z atrybutem RoutePrefix jest to, że w celu
wywołania metody akcji, na przykład Create, należy utworzyć adres URL, taki jak przedstawiono poniżej:
http://localhost:34855/Services/Users/Add/Adam/100
Atrybut RouteArea nie ma wpływu na trasy zdefiniowane za pomocą atrybutu Route, ale rozpoczynające się
od ~/. Oznacza to możliwość wywołania metody akcji Index przez użycie przedstawionego poniżej adresu URL:
http://localhost:34855/Test
Atrybut RouteArea nie ma wpływu na metody akcji, dla których nie został zdefiniowany atrybut Route.
Oznacza to, że trasa dla metody akcji List jest ustalana na podstawie zawartości pliku RouteConfig.cs,
a nie przez routing oparty na atrybutach.
Generowanie łączy do akcji z obszarów
Nie musimy podejmować żadnych specjalnych kroków w celu tworzenia łączy odwołujących się do akcji
w obszarze, z którym są powiązane aktualne żądania. Platforma MVC wykrywa, że bieżące żądanie odnosi się
do określonego obszaru i przy generowaniu wychodzących adresów URL wyszukuje wyłącznie trasy dla tego
obszaru. Na przykład poniższe polecenie znajdujące się w widoku z obszaru Admin:
419
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
...
@Html.ActionLink("Kliknij mnie", "About")
...
generuje następujący kod HTML:
<a href="/Admin/Home/About">Kliknij mnie</a>
Aby utworzyć łącze do akcji z innego obszaru lub nieznajdującej się w żadnym obszarze, musimy utworzyć
zmienną o nazwie area i podać w niej nazwę interesującego nas obszaru:
...
@Html.ActionLink("Kliknij mnie, aby przejść do innego obszaru", "Index", new { area = "Support" })
...
Dlatego właśnie słowo area jest zarezerwowaną nazwą zmiennej segmentu. Wygenerowany kod HTML
jest następujący (przy założeniu, że utworzyliśmy obszar Support, dla którego istnieje odpowiednia trasa):
<a href="/Support/Home">Kliknij mnie, aby przejść do innego obszaru</a>
Jeżeli potrzebujesz łącza do akcji z kontrolera najwyższego poziomu (z katalogu /Controllers), powinieneś
podstawić pusty ciąg do zmiennej area w następujący sposób:
...
@Html.ActionLink("Kliknij mnie, aby przejść do innego obszaru", "Index", new { area = "" })
...
Routing żądań dla plików dyskowych
Nie wszystkie żądania do aplikacji MVC odnoszą się do kontrolerów i akcji. W większości aplikacji nadal
musimy udostępniać takie dane, jak zdjęcia, statyczne pliki HTML, biblioteki JavaScript itd. Na przykład w naszej
aplikacji MVC utworzymy w katalogu Content plik o nazwie StaticContent.html oparty na szablonie Strona
HTML. Zawartość tego pliku znajduje się na listingu 16.31.
Listing 16.31. Zawartość pliku StaticContent.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Statyczny plik HTML</title></head>
<body>
To jest statyczny plik html (~/Content/StaticContent.html)
</body>
</html>
System routingu zawiera zintegrowaną obsługę tego typu treści. Jeżeli uruchomisz aplikację i przejdziesz
do adresu URL /Content/StaticContent.html, zobaczysz w przeglądarce zawartość tego prostego pliku HTML
(rysunek 16.9).
Domyślnie system routingu sprawdza, czy adres URL pasuje do pliku na dysku, zanim zacznie przetwarzać
trasy aplikacji. Dlatego też nie trzeba definiować trasy, aby otrzymać efekt pokazany na rysunku 16.9.
Jeżeli zostanie znalezione dopasowanie, plik z dysku jest udostępniany przeglądarce, a trasy nie są
przetwarzane. Możemy odwrócić ten sposób działania, aby nasze trasy były przetwarzane przed sprawdzaniem
plików na dysku — zmienić właściwość RouteExistingFiles w obiekcie RouteCollection na true (listing 16.32).
420
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
Rysunek 16.9. Żądanie przesłania pliku statycznego
Listing 16.32. Aktywowanie w pliku RouteConfig.cs przetwarzania tras przed kontrolą plików
using System.Web.Mvc;
using System.Web.Routing;
using UrlsAndRoutes.Infrastructure;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.RouteExistingFiles = true;
routes.MapMvcAttributeRoutes();
routes.Add(new Route("SayHello", new CustomRouteHandler()));
routes.Add(new LegacyRoute(
"~/articles/Windows_3.1_Overview.html",
"~/old/.NET_1.0_Class_Library"));
routes.MapRoute("MyRoute", "{controller}/{action}", null,
new[] { "UrlsAndRoutes.Controllers" });
routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" },
new[] { "UrlsAndRoutes.Controllers" });
}
}
}
Zgodnie z konwencją wspomniane polecenie powinno znajdować się blisko początku metody
RegisterRoutes, choć będzie działała nawet wtedy, gdy zostanie podane po zdefiniowaniu tras.
Konfiguracja serwera aplikacji
Visual Studio używa IIS Express jako serwera aplikacji dla projektów MVC. Nie tylko powinniśmy ustawić
wartość true właściwości RouteExistingFiles w metodzie RegisterRoutes, ale również poinformować
serwer IIS Express, aby nie przechwytywał żądań do plików na dysku, zanim nie zostaną one przekazane
systemowi routingu MVC.
Przede wszystkim uruchom IIS Express. Najłatwiejszym sposobem jest uruchomienie aplikacji MVC
w Visual Studio, co spowoduje wyświetlenie ikony IIS Express na pasku zadań. Kliknij tę ikonę prawym przyciskiem
myszy i z menu kontekstowego wybierz opcję Pokaż wszystkie aplikacje. Kliknij UrlsAndRoutes w kolumnie
Nazwa witryny, aby w ten sposób wyświetlić informacje konfiguracyjne, jak pokazano na rysunku 16.10.
Kliknij łącze Konfiguracja znajdujące się na dole okna, co spowoduje wyświetlenie w Visual Studio pliku
konfiguracyjnego IIS Express. Teraz naciśnij klawisze Ctrl+F i wyszukaj UrlRoutingModule-4.0. Odpowiedni
wpis znajduje się w sekcji modules pliku konfiguracyjnego. Naszym celem jest ustawienie atrybutowi
preCondition pustego ciągu tekstowego, np.:
...
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule"
preCondition="" />
...
421
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 16.10. Informacje konfiguracyjne serwera IIS Express
Teraz ponownie uruchom aplikację w Visual Studio, aby zmodyfikowane ustawienia zostały uwzględnione,
a następnie przejdź do strony pod adresem /Content/StaticContent.html. Zamiast zobaczyć zawartość pliku,
w oknie przeglądarki internetowej zostanie wyświetlony komunikat błędu widoczny na rysunku 16.11. Błąd
wynika z tego, że żądanie pliku HTML zostało przekazane do systemu routingu MVC, ale trasa dopasowująca
adres URL przekierowuje żądanie do nieistniejącego kontrolera Content.
Rysunek 16.11. Żądanie pliku statycznego obsłużone przez system routingu
Definiowanie tras dla plików na dysku
Gdy właściwości RouteExistingFiles została przypisana wartość true, możemy zdefiniować trasy
odpowiadające adresom URL dla plików na dysku, takie jak na listingu 16.33.
422
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
Listing 16.33. Zdefiniowana w pliku RouteConfig.cs trasa, której wzorzec URL odpowiada plikowi na dysku
using System.Web.Mvc;
using System.Web.Routing;
using UrlsAndRoutes.Infrastructure;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.RouteExistingFiles = true;
routes.MapMvcAttributeRoutes();
routes.MapRoute("DiskFile", "Content/StaticContent.html",
new {
controller = "Customer", action = "List",
});
routes.Add(new Route("SayHello", new CustomRouteHandler()));
routes.Add(new LegacyRoute(
"~/articles/Windows_3.1_Overview.html",
"~/old/.NET_1.0_Class_Library"));
routes.MapRoute("MyRoute", "{controller}/{action}", null,
new[] { "UrlsAndRoutes.Controllers" });
routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" },
new[] { "UrlsAndRoutes.Controllers" });
}
}
}
Powyższa trasa powoduje mapowanie żądań adresu URL /Content/StaticContent.html na akcję List
kontrolera Customer. Działające mapowanie pokazano na rysunku 16.12. Pokazany na rysunku efekt
otrzymasz po uruchomieniu aplikacji i przejściu do adresu URL /Content/StaticContent.html.
Rysunek 16.12. Przechwytywanie żądania pliku dyskowego z użyciem trasy
 Wskazówka Przeglądarka internetowa może buforować udzieloną wcześniej odpowiedź na to żądanie, zwłaszcza
jeśli korzystasz z omówionej w rozdziale 14. funkcji połączonych przeglądarek. Jeżeli wspomniana sytuacja wystąpi,
wystarczy odświeżyć stronę, a zobaczysz efekt pokazany na rysunku 16.12.
Routing żądań przeznaczonych dla plików dyskowych wymaga dokładnego przemyślenia, ponieważ wzorce
URL będą dopasowywać adresy tego typu równie chętnie, jak wszystkie inne. Na przykład żądanie dla
/Content/StaticContent.html może być dopasowane do wzorca URL takiego jak {controller}/{action}.
Jeżeli nie będziesz ostrożny, może się to skończyć bardzo dziwnymi wynikami i obniżoną wydajnością.
Dlatego wykorzystanie tej opcji powinno być ostatecznością.
423
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Pomijanie systemu routingu
Użycie właściwości RouteExistingFiles, przedstawionej w poprzednim podrozdziale, powoduje, że system
routingu zaczyna obsługiwać więcej żądań. Żądania, które normalnie pomijały system routingu, są teraz
dopasowywane do zdefiniowanych tras.
Przeciwieństwem tej funkcji jest możliwość ograniczenia liczby adresów URL dopasowanych do naszych tras.
Realizujemy to przez użycie metody IgnoreRoute z klasy RouteCollection, która jest pokazana na listingu 16.34.
Listing 16.34. Użycie metody IgnoreRoute w pliku RouteConfig.cs
using System.Web.Mvc;
using System.Web.Routing;
using UrlsAndRoutes.Infrastructure;
namespace UrlsAndRoutes {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.RouteExistingFiles = true;
routes.MapMvcAttributeRoutes();
routes.IgnoreRoute("Content/{filename}.html");
routes.Add(new Route("SayHello", new CustomRouteHandler()));
routes.Add(new LegacyRoute(
"~/articles/Windows_3.1_Overview.html",
"~/old/.NET_1.0_Class_Library"));
routes.MapRoute("MyRoute", "{controller}/{action}", null,
new[] { "UrlsAndRoutes.Controllers" });
routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" },
new[] { "UrlsAndRoutes.Controllers" });
}
}
}
Do dopasowania zakresów adresów URL możemy użyć zmiennych segmentu, takich jak {filename}.
W tym przypadku wzorzec URL będzie dopasowywał dowolne trójsegmentowe adresy URL, których pierwszym
segmentem jest Content; drugi segment ma rozszerzenie .html.
Metoda IgnoreRoute tworzy wpis w RouteCollection, w której obiekt zarządzania trasą jest egzemplarzem
klasy StopRoutingHandler zamiast MvcRouteHandler. System routingu ma wbudowany kod rozpoznający ten
obiekt obsługi. Jeżeli wzorzec URL przekazany do metody IgnoreRoute zostanie dopasowany, to nie będą
analizowane kolejne trasy, tak jak w przypadku dopasowania trasy standardowej. Z tego powodu ważne
jest również miejsce, w którym umieszczone jest wywołanie metody IgnoreRoute. Jeżeli uruchomisz aplikację
i ponownie przejdziesz do adresu URL /Content/StaticContent.html, będziesz mógł zobaczyć zawartość
wskazanego pliku HTML. Wynika to z faktu przetworzenia obiektu StopRoutingHandler, zanim jakakolwiek trasa
będzie mogła dopasować adres URL.
Najlepsze praktyki schematu adresów URL
Po zapoznaniu się z przedstawionymi informacjami możesz zastanawiać się, od czego zacząć projektowanie
własnego schematu URL. Możesz po prostu zaakceptować domyślny schemat generowany przez Visual Studio,
ale przemyślenie własnego schematu może być korzystniejsze. W ostatnich latach projektowanie adresów
424
ROZDZIAŁ 16.  ZAAWANSOWANE FUNKCJE ROUTINGU
URL aplikacji zaczęło być traktowane poważniej i powstało kilka ważnych reguł projektowych. Jeżeli
będziesz przestrzegał tych wzorców projektowych, poprawisz użyteczność, zgodność i pozycję aplikacji
w wyszukiwarkach.
Twórz jasne i przyjazne dla człowieka adresy URL
Użytkownicy zauważają adresy URL w Twoich aplikacjach. Jeżeli się z tym nie zgadzasz, pomyśl o przypadku,
gdy próbowałeś wysłać komuś adres URL z witryny Amazon. URL dla wcześniejszego wydania tej książki jest
następujący:
http://www.amazon.com/Pro-ASP-NET-MVC-ProfessionalApress/dp/1430242361/ref=la_B001IU0SNK_1_5?ie=UTF8&qid=1349978167&sr=1-5
Wysłanie takiego adresu e-mailem jest wystarczająco złe, a co dopiero podyktowanie go przez telefon.
Gdy ostatnio musiałem to zrobić, odszukałem numer ISBN książki i poprosiłem rozmówcę o samodzielne
jej wyszukanie. Byłoby świetnie, gdybym mógł odwołać się do książki za pomocą następującego adresu:
http://www.amazon.com/books/pro-aspnet-mvc5-framework
Tego rodzaju adres URL można przeczytać przez telefon i nie wygląda on, jakby komuś coś upadło na
klawiaturę przy pisaniu wiadomości e-mail.
Poniżej podaję kilka wskazówek na temat tworzenia przyjaznych adresów URL:
 Projektuj adresy URL, aby opisywały zawartość, a nie szczegóły implementacji aplikacji.
Stosuj /Artykuly/RaportRoczny zamiast /Witryna_v2/SerwerTresci/Cache/RaportRoczny.
 Uwaga Muszę powiedzieć jasno, że mamy najwyższe uznanie dla firmy Amazon, która sprzedaje więcej moich
książek niż wszystkie inne sklepy razem. Wiem, że każdy członek zespołu Amazon jest niezwykle inteligentną
osobą. Żaden z nich nie jest tak małostkowy, aby zaprzestać sprzedaży moich książek przez wygłoszoną tutaj
małą krytykę formatu adresów URL w tym sklepie. Kocham Amazon. Uwielbiam Amazon. Mam tylko nadzieję,
że poprawi swoje adresy URL.
 Preferuj tytuły treści zamiast numerów identyfikacyjnych. Stosuj /Artykuly/RaportRoczny zamiast
/Artykuly/2392. Jeżeli musisz użyć numeru identyfikacyjnego (aby rozróżnić elementy z identycznymi
tytułami lub uniknąć dodatkowych zapytań do bazy danych w celu wyszukania elementu według tytułu),
to korzystaj z obu wartości (np.: /Artykuly/2392/RaportRoczny). Wpisuje się to dłużej, ale ma większy
sens dla ludzi i poprawia pozycję strony w wyszukiwarce. Nasza aplikacja może po prostu zignorować
tytuł i wyświetlić element o podanym identyfikatorze.
 Nie stosuj rozszerzeń plików dla stron HTML (np.: .aspx lub .mvc); używaj ich dla odpowiednich typów
plików (np.: .jpg, .pdf, .zip). Przeglądarki nie muszą korzystać z rozszerzeń nazw plików, jeżeli prawidłowo
zostanie ustawiony typ MIME, ale ludzie oczekują, że pliki PDF będą kończyły się na .pdf.
 Twórz rozsądne hierarchie (np.: /Produkty/Męskie/Koszule/Czerwone), aby użytkownik mógł
odgadnąć URL kategorii nadrzędnej.
 Nie wprowadzaj rozróżnienia wielkości liter (ktoś może chcieć przepisać URL z wydrukowanej strony).
System routingu ASP.NET domyślnie nie rozpoznaje wielkości liter.
 Unikaj symboli, kodów i sekwencji znaków. Jeżeli potrzebujesz separatora słów, zastosuj łącznik
(np.: /mój-doskonały-artykuł). Znaki podkreślenia są mało przyjazne, a zakodowane w URL spacje
wyglądają dziwnie (jak w /mój+doskonały+artykuł) lub odpychająco (jak w /mój%20doskonały%20artykuł).
 Nie zmieniaj adresów URL. Nieprawidłowe łącza to stracony klient. Gdy zmienisz adres URL,
powinieneś kontynuować obsługę starych adresów URL za pomocą trwałych przekierowań (301)
tak długo, jak jest to możliwe.
 Bądź konsekwentny. Korzystaj z jednego formatu URL w całej aplikacji.
425
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Adresy URL powinny być krótkie, łatwe do wpisania, możliwe do edycji przez użytkownika i trwałe; powinny
wizualizować strukturę witryny. Jakob Nielsen, guru użyteczności stron internetowych, rozwinął ten temat
w artykule dostępnym pod adresem http://www.useit.com/alertbox/990321.html. Tim Barnes-Lee, twórca
WWW, oferuje podobne porady (http://www.w3.org/Provider/Style/URI).
GET oraz POST — wybierz właściwie
Jako naczelną zasadę powinniśmy przyjąć, że żądania GET powinny być używane do pobierania danych w trybie
tylko do odczytu, natomiast żądania POST powinny być wykorzystywane do operacji zapisu zmieniających
stan aplikacji. Zgodnie z terminami zawartymi w standardach żądania GET są przeznaczone do bezpiecznych
interakcji (nie mają skutków ubocznych poza pobieraniem informacji), natomiast żądania POST są przeznaczone
do interakcji niebezpiecznych (powodujących podjęcie decyzji lub zmianę danych). Konwencje te są ustalone
przez konsorcjum World Wide Web Consortium (W3C) i opisane pod adresem http://www.w3.org/Protocols/
rfc2616/rfc2616-sec9.html.
Żądania GET są adresowalne — wszystkie informacje znajdują się w adresie URL, więc możliwe jest zapisanie
zakładki lub utworzenie łącza do tego adresu.
Nie należy używać żądań GET do operacji zmieniających stan. Wielu programistów WWW przekonało
się o tym boleśnie w roku 2005, gdy został publicznie udostępniony Google Web Accelerator. Aplikacja ta wstępnie
pobierała wszystkie łącza prowadzące z danej strony, co jest dozwolone, ponieważ żądania GET powinny być
bezpieczne. Niestety, wielu programistów ignorowało konwencje HTTP i umieszczało w swoich aplikacjach
zwykłe łącza do opcji „usuń element” lub „dodaj do koszyka”. Powstał chaos.
Jedna z firm uważała, że jej system zarządzania treścią był celem wielokrotnych ataków, ponieważ treść ta była
z niego w całości usuwana. Później okazało się, że silnik wyszukiwania napotkał URL strony administracyjnej
i przeglądał wszystkie łącza „usuń”. Uwierzytelnianie może nas przed tym uchronić, ale jego zadaniem nie może
być ochrona przed prawidłowo działającymi akceleratorami sieciowymi.
Podsumowanie
W tym rozdziale przedstawiłem zaawansowane funkcje systemu routingu na platformie MVC. Pokazałem,
jak są generowane trasy wychodzące oraz jak można dostosować system routingu do własnych potrzeb.
Wprowadziłem koncepcję obszarów i omówiłem sposoby tworzenia użytecznych i znaczących schematów URL.
W następnym rozdziale skupię się na kontrolerach i akcjach, które są podstawowymi elementami modelu MVC.
Przybliżę sposób ich działania oraz pokażę, jak je wykorzystać w aplikacji, aby osiągnąć najlepsze wyniki.
426
ROZDZIAŁ 17.

Kontrolery i akcje
Każde żądanie trafiające do naszej aplikacji jest obsługiwane przez kontroler. Kontroler może obsłużyć żądanie
w dowolny sposób, jeżeli tylko nie będzie podejmował zadań przypisanych do modelu i widoków. Oznacza
to, że kontroler nie powinien zawierać i przechowywać danych ani generować interfejsu użytkownika.
Na platformie ASP.NET MVC kontrolery są klasami .NET zawierającymi kod wymagany do obsłużenia
żądania. W rozdziale 3. wyjaśniłem, że zadaniem kontrolera jest hermetyzacja logiki aplikacji. Dlatego kontrolery
są odpowiedzialne za przetwarzanie przychodzących żądań, wykonywanie operacji na modelu domeny i wybieranie
widoków wyświetlanych użytkownikowi. W tym rozdziale pokażę, jak implementować kontrolery, oraz
przedstawię różne sposoby ich użycia do pobierania danych i generowania wyników. W tabeli 17.1 znajdziesz
podsumowanie materiału omówionego w rozdziale.
Tabela 17.1. Podsumowanie materiału omówionego w rozdziale
Temat
Rozwiązanie
Listing (nr)
Utworzenie kontrolera
Implementacja interfejsu IController
lub dziedziczenie po klasie Controller
Użycie właściwości i obiektów kontekstu
lub zdefiniowanie parametrów metody akcji
Użycie obiektu kontekstu HttpResponse
Od 1. do 4.
Użycie wyniku akcji
Od 9. do 12.
Użycie ViewResult
Użycie obiektu modelu widoku lub ViewBag
13. i 14.
Od 15. do 19.
Użycie metody Redirect lub
20. i 21.
Pobranie informacji o żądaniu
Wygenerowanie odpowiedzi z kontrolera
bezpośrednio implementującego interfejs
5. i 6.
7. i 8.
IController
Wygenerowanie odpowiedzi z kontrolera
dziedziczącego po klasie Controller
Wygenerowanie widoku przez platformę MVC
Przekazanie danych z kontrolera
do widoku
Przekierowanie przeglądarki do nowego
adresu URL
Przekierowanie przeglądarki do adresu
URL wygenerowanego przez trasę
Przekierowanie przeglądarki do innej
metody akcji
Wysłanie kodu wyniku HTTP
do przeglądarki internetowej
RedirectPermanent
Użycie metody RedirectToRoute lub
22.
RedirectToRoutePermanent
Użycie metody HttpStatusCodeResult
23.
Zwrot obiektu HttpStatusCodeResult lub
użycie jednej z metod wygodnych, na
przykład HttpNotFound
Od 24. do 26.
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Utworzenie przykładowego projektu
W ramach przygotowań do tego rozdziału utwórz nowy projekt z użyciem szablonu Empty i nadaj mu
nazwę ControllersAndActions. Zaznacz pole wyboru MVC oraz utwórz projekt testów jednostkowych o nazwie
ControllersAndActions.Tests. Tworzone w tym rozdziale testy jednostkowe nie wymagają implementacji
obiektu Mock, a więc nie będziemy instalować pakietu Moq. Jednak konieczne jest zainstalowanie pakietu
MVC, aby testy mogły uzyskać dostęp do bazowych klas kontrolerów. Dlatego też w konsoli menedżerów
NuGet wydaj poniższe polecenie:
Install-Package Microsoft.AspNet.Mvc -version 5.0.0 -projectname ControllersAndActions.Tests
Ustawienie początkowego adresu URL
Po utworzeniu projektu, w oknie Eksplorator rozwiązania zaznacz projekt ControllersAndActions, a następnie
z menu Projekt wybierz opcję Właściwości ControllersAndActions…. Przejdź do karty Sieć Web i w sekcji
Uruchom akcję wybierz Określ stronę. Nie trzeba podawać żadnej wartości, wystarczy wybrać wymienioną opcję.
Wprowadzenie do kontrolerów
W dotychczasowej części książki pokazałem zastosowania kontrolerów w niemal każdym rozdziale.
Teraz przyszedł czas, aby cofnąć się o krok i zajrzeć „pod maskę”.
Tworzenie kontrolera z użyciem interfejsu IController
Na platformie MVC klasy kontrolera muszą implementować interfejs IController znajdujący się w przestrzeni
nazw System.Web.Mvc, jak przedstawiono na listingu 17.1.
Listing 17.1. Interfejs System.Web.Mvc.IController
public interface IController {
void Execute(RequestContext requestContext);
}
 Wskazówka Definicję wymienionego interfejsu otrzymałem po pobraniu kodu źródłowego platformy ASP.NET MVC,
którego analiza jest doskonałym sposobem na poznanie wewnętrznego sposobu działania platformy. Kod źródłowy
możesz pobrać z witryny http://aspnet.codeplex.com/.
Jest to bardzo prosty interfejs. Jedyna metoda, Execute, jest wywoływana w momencie, gdy żądanie jest
kierowane do klasy kontrolera. Platforma MVC sprawdza, do której klasy kontrolera jest kierowane żądanie
przez odczytanie wartości właściwości controller generowanej na podstawie danych routingu lub za pomocą
niestandardowych klas routingu, jak omówiono w rozdziałach 15. i 16.
Możemy zdecydować o tworzeniu klasy kontrolera przez implementowanie interfejsu IController, ale jest
to interfejs bardzo niskiego poziomu i trzeba będzie włożyć dużo wysiłku, aby uzyskać oczekiwany wynik. Interfejs
IController doskonale pokazuje, jak działają kontrolery. W katalogu Controllers utwórz nowy plik klasy o nazwie
BasicController.cs, a następnie umieść w nim kod przedstawiony na listingu 17.2.
Listing 17.2. Zawartość pliku BasicController.cs
using System.Web.Mvc;
using System.Web.Routing;
namespace ControllersAndActions.Controllers {
428
ROZDZIAŁ 17.  KONTROLERY I AKCJE
public class BasicController : IController {
public void Execute(RequestContext requestContext) {
string controller = (string)requestContext.RouteData.Values["controller"];
string action = (string)requestContext.RouteData.Values["action"];
requestContext.HttpContext.Response.Write(
string.Format("Kontroler: {0}, Akcja: {1}", controller, action));
}
}
}
Metoda Execute interfejsu IController jest przekazywana do obiektu System.Web.Routing.RequestContext,
który dostarcza informacje o bieżącym żądaniu oraz dopasowanej trasie (co prowadzi do wywołania
kontrolera odpowiedzialnego za przetworzenie żądania). Klasa RequestContext definiuje dwie właściwości
wymienione w tabeli 17.2.
Tabela 17.2. Właściwości definiowane przez klasę RequestContext
Nazwa
Opis
HttpContext
Zwraca obiekt HttpContextBase opisujący bieżące żądanie.
RouteData
Zwraca obiekt RouteData opisujący trasę dopasowaną do żądania.
Obiekt HttpContextBase zapewnia dostęp do zbioru obiektów opisujących bieżące żądanie. Te obiekty są
nazywane obiektami kontekstu, powrócimy jeszcze do nich w dalszej części rozdziału. Z kolei obiekt RouteData
opisuje trasę. Najważniejsze właściwości tego obiektu wymieniono w tabeli 17.3.
Tabela 17.3. Właściwości definiowane przez klasę RouteData
Nazwa
Opis
Route
Zwraca implementację RouteBase dopasowaną do trasy.
RouteHandler
Zwraca interfejs IRouteHandler odpowiedzialny za obsługę trasy.
Values
Zwraca kolekcję wartości segmentu indeksowanych według nazwy.
Klasy o nazwach kończących się członem Base
Podczas przetwarzania żądań platforma MVC bazuje na platformie ASP.NET, co ma duży sens, ponieważ ASP.NET
to sprawdzony i solidny produkt, który zawiera zintegrowany serwer aplikacji IIS. Problem polega na tym, że klasy
platformy ASP.NET stosowane w celu dostarczania informacji o żądaniach nie są przystosowane do przeprowadzania
testów jednostkowych, które stanowią kluczową zaletę użycia platformy ASP.NET MVC. Firma Microsoft, chcąc
zapewnić możliwość przeprowadzania testów jednostkowych i jednocześnie zachować zgodność z istniejącymi
aplikacjami ASP.NET Web Forms, wprowadziła tak zwane klasy Base. Te klasy mają takie same nazwy jak podstawowe
klasy na platformie ASP.NET, ale zawierają w nazwie człon Base. Na przykład informacje kontekstu o bieżącym
żądaniu oraz pewnych kluczowych usługach aplikacji platforma ASP.NET dostarcza za pomocą klasy HttpContext.
Jej odpowiednikiem wśród klas Base jest HttpContextBase, egzemplarz tej klasy został przekazany metodzie
Execute zdefiniowanej przez interfejs IController (w dalszych przykładach zobaczysz inne klasy Base). Klasy
platformy ASP.NET i ich odpowiedniki Base mają dokładnie takie same właściwości i metody. Jednak klasy Base
zawsze są abstrakcyjne, co oznacza, że ułatwiają przeprowadzanie testów jednostkowych.
Czasami otrzymasz egzemplarz jednej z pierwotnych klas ASP.NET, na przykład HttpContext, ale wtedy
trzeba go skonwertować na postać klasy Base przyjaznej platformie MVC, w omawianym przykładzie to będzie
429
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
HttpContextBase. W tym celu korzystasz z jednej z klas Wrapper, która ma taką samą nazwę jak klasa pierwotna,
ale zawiera człon Wrapper, na przykład HttpContextWrapper. Klasy Wrapper wywodzą się z klas Base i mają
konstruktory akceptujące egzemplarz klasy pierwotnej, na przykład:
...
HttpContext myContext = getOriginalObjectFromSomewhere();
HttpContextBase myBase = new HttpContextWrapper(myContext);
...
Klasy Base i Wrapper są dostępne za pomocą przestrzeni nazw System.Web, co pozwala platformie ASP.NET
na bezproblemową obsługę aplikacji — zarówno MVC, jak i starszych Web Forms.
W rozdziale 16. pokazałem, jak używać RouteBase i IRouteHandler w celu dostosowania systemu routingu
do własnych potrzeb. W omawianym tutaj przykładzie właściwość Values stosujemy do pobrania wartości
zmiennych controller i action, a następnie wykorzystania ich w odpowiedzi.
 Uwaga Podczas tworzenia własnych kontrolerów problem polega również na braku dostępu do funkcji, takich jak
widoki. Oznacza to konieczność pracy na niskim poziomie. To jest powód, dla którego tworzę zawartość bezpośrednio
dla klienta. Wartością zwrotną właściwości HttpContextBase.Response jest obiekt HttpResponseBase pozwalający
na konfigurację oraz dodanie danych do odpowiedzi przekazywanej klientowi. To jest kolejny punkt styku między
platformami ASP.NET i MVC, który szczegółowo omówiłem w innej mojej książce, zatytułowanej Pro ASP.NET MVC
5 Framework Platform, wydanej przez Apress.
Jeżeli uruchomisz aplikację i przejdziesz do /Basic/Index, zobaczysz wynik wygenerowany przez kontroler,
pokazany na rysunku 17.1.
Rysunek 17.1. Wynik generowany przez klasę BasicController
Implementowanie interfejsu IController pozwala tworzyć klasy, które platforma MVC rozpoznaje jako
kontrolery i wysyła do nich żądania, bez żadnych ograniczeń dotyczących sposobu przetwarzania żądania
i udzielania odpowiedzi. Taka możliwość okazuje się użyteczna, ponieważ pokazuje elastyczność platformy
MVC, nawet w przypadku kluczowych elementów konstrukcyjnych aplikacji, jakim niewątpliwie są kontrolery.
Jednak dość trudno przygotowywać w ten sposób złożone aplikacje.
Tworzenie kontrolera przez dziedziczenie po klasie Controller
Jak mogłeś się przekonać w poprzednim przykładzie, platforma MVC jest niezwykle rozszerzalna i łatwa
do skonfigurowania. Możemy implementować interfejs IController w celu utworzenia dowolnej klasy
obsługującej żądania i generującej wynik. Nie lubisz metod akcji? Nie potrzebujesz generowania widoków?
Możesz wziąć sprawy we własne ręce i przygotować lepszy, szybszy i elegantszy sposób obsługi żądań.
Możesz również użyć funkcji udostępnianych przez zespół MVC, dziedzicząc swoją klasę kontrolera
po System.Web.Mvc.Controller.
Klasa System.Web.Mvc.Controller zawiera metody obsługi żądania znane większości programistów MVC.
Korzystaliśmy z nich w przykładach w poprzednich rozdziałach. Klasa Controller zawiera trzy kluczowe
elementy:
430
ROZDZIAŁ 17.  KONTROLERY I AKCJE
 Metody akcji — funkcje kontrolera są podzielone na wiele metod (zamiast tylko jednej metody Execute()).
Każda metoda akcji jest udostępniona pod innym adresem URL i wywoływana z parametrami pobranymi
z przychodzącego żądania.
 Wynik akcji — mamy możliwość zwrócenia obiektu opisującego oczekiwany wynik akcji
(np. wygenerowanie widoku lub przekierowanie do innego adresu URL lub akcji), który następnie
jest wysyłany do klienta. Rozdzielenie pomiędzy specyfikowaniem wyniku i jego wykonywaniem
upraszcza testowanie jednostkowe.
 Filtry — możliwe jest hermetyzowanie operacji wielokrotnego użytku (np. omówionego w rozdziale
12. uwierzytelniania) w postaci filtrów, a następnie oznaczanie operacji w kontrolerach lub metodach
akcji przez umieszczanie atrybutów w kodzie źródłowym.
O ile nie masz bardzo specyficznych wymagań, najlepszym sposobem tworzenia kontrolerów jest ich
odziedziczenie po klasie Controller, co jest realizowane przez Visual Studio, gdy tworzymy nową klasę za pomocą
opcji menu Dodaj/Kontroler…. Na listingu 17.3 przedstawiony jest prosty kontroler utworzony w ten
sposób. Kontrolerowi nadaj nazwę DerivedController. Został on wygenerowany na podstawie szablonu
Kontroler MVC 5 - pusty. W kodzie wprowadzono kilka zmian mających na celu ustawienie właściwości
ViewBag oraz wybór widoku.
Listing 17.3. Zawartość pliku DerivedControllers.cs
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
namespace ControllersAndActions.Controllers {
public class DerivedController : Controller {
public ActionResult Index() {
ViewBag.Message = "Pozdrowienia z metody Index w klasie DerivedController.";
return View("MyView");
}
}
}
Klasa Controller jest również łączem do systemu widoków. Na powyższym listingu zwracamy wynik
za pomocą metody View, przekazując do niej parametr w postaci nazwy widoku, jaki chcemy wygenerować.
W celu utworzenia widoku kliknij prawym przyciskiem myszy katalog Views/Derived, a następnie z menu
kontekstowego wybierz opcję Dodaj/Strona widoku MVC 5 (Razor). Plikowi nowego widoku nadaj nazwę
MyView.cshtml, następnie umieść w nim kod przedstawiony na listingu 17.4.
Listing 17.4. Zawartość pliku MyView.cshtml
@{
ViewBag.Title = "MyView";
}
<h2>Widok</h2>
Komunikat: @ViewBag.Message
Jeżeli uruchomimy aplikację i przejdziemy do adresu /Derived/Index, zostanie wywołana zdefiniowana
przez nas metoda akcji i będzie wygenerowany widok, jak pokazano na rysunku 17.2.
Naszym zadaniem, jako dziedziczących po klasie Controller, jest zaimplementowanie metod akcji, pobranie
danych potrzebnych do przetworzenia żądania i wygenerowanie odpowiedniego wyniku. Wiele ze sposobów,
w jakie można to zrealizować, przedstawię w dalszej części rozdziału.
431
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Rysunek 17.2. Wynik generowany przez klasę DerivedController
Odczytywanie danych wejściowych
Kontrolery często muszą odwoływać się do przychodzących danych, takich jak wartości z ciągu zapytania, wartości
z formularzy oraz parametry wyodrębnione z adresu URL przez system routingu. Istnieją trzy podstawowe
sposoby odwoływania się do tych danych:
 pobranie ich z obiektów kontekstu,
 przekazanie danych jako parametrów do metody akcji,
 jawne wywołanie oferowanej przez platformę funkcji dołączania modelu.
Przedstawię teraz sposoby pobierania danych do metod akcji, a szczególnie skupię się na obiektach
kontekstu oraz parametrach metod akcji. W rozdziale 24. przedstawię szczegółowo dołączanie modelu.
Pobieranie danych z obiektów kontekstu
Gdy tworzymy kontroler przez odziedziczenie po klasie bazowej Controller, uzyskujemy dostęp do zestawu
wygodnych właściwości pozwalających na dostęp do informacji o żądaniu. Właściwościami tymi są Request,
Response, RouteData, HttpContext oraz Server. Każda z nich zapewnia dane dotyczące innego aspektu żądania.
Nazywamy je właściwościami ułatwiającymi, ponieważ każda z nich zawiera inny typ danych z obiektu
ControllerContext (do którego możemy się dostać za pomocą właściwości Controller.ControllerContext).
Najczęściej używane obiekty kontekstu zebrane zostały w tabeli 17.4.
Poszczególne właściwości, do których się tutaj odwołujemy — Request, HttpContext itd. — zapewniają
obiekty kontekstu. Nie zamierzam szczegółowo ich omawiać w tej książce (ponieważ stanowią część
platformy ASP.NET), ale dostarczają one pewnych użytecznych informacji i funkcji, które warto poznać.
Metoda akcji może korzystać z dowolnego z tych obiektów kontekstu w celu uzyskania informacji na temat
żądania, jak pokazano na listingu 17.5 w postaci hipotetycznej metody akcji.
Listing 17.5. Metoda akcji korzystająca z obiektów kontekstu w celu odczytania danych o żądaniu
...
public ActionResult RenameProduct() {
// dostęp do różnych właściwości z obiektów kontekstu
string userName = User.Identity.Name;
string serverName = Server.MachineName;
string clientIP = Request.UserHostAddress;
DateTime dateStamp = HttpContext.Timestamp;
AuditRequest(userName, serverName, clientIP, dateStamp, "Zmiana nazwy produktu");
// odczytanie danych z Request.Form
string oldProductName = Request.Form["OldName"];
string newProductName = Request.Form["NewName"];
bool result = AttemptProductRename(oldProductName, newProductName);
ViewData["RenameResult"] = result;
return View("ProductRenamed");
}
...
432
ROZDZIAŁ 17.  KONTROLERY I AKCJE
Tabela 17.4. Często używane obiekty kontekstu i właściwości
Właściwość
Typ
Opis
Request.QueryString
NameValueCollection
Zmienne GET wysłane z tym żądaniem
Request.Form
NameValueCollection
Zmienne POST wysłane z tym żądaniem
Request.Cookies
HttpCookieCollection
Cookie wysłane przez przeglądarkę wraz
z żądaniem
Request.HttpMethod
string
Metoda HTTP (np. GET lub POST) używana
dla tego żądania
Request.Headers
NameValueCollection
Pełny zbiór nagłówków HTTP wysłanych z tym
żądaniem
Request.Url
Uri
Wywoływany URL
Request.UserHostAddress
string
RouteData.Route
RouteBase
RouteData.Values
RouteValueDictionary
Adres IP użytkownika wysyłającego żądanie
Wybrana pozycja z RouteTable.Routes dla żądania
Aktywne parametry trasy (wyodrębnione
z adresu URL lub wartości domyślne)
Magazyn stanu aplikacji
Magazyn bufora aplikacji
Magazyn stanu dla bieżącego żądania
Magazyn stanu dla sesji użytkownika
Dane uwierzytelniania na temat zalogowanego
użytkownika
HttpContext.Application
HttpApplicationStateBase
HttpContext.Cache
Cache
HttpContext.Items
IDictionary
HttpContext.Session
HttpSessionStateBase
User
IPrincipal
TempData
TempDataDictionary
Dane tymczasowe przechowywane dla bieżącego
użytkownika
Dużą część dostępnych danych kontekstu można przeglądać z użyciem IntelliSense (w metodzie akcji wpisz
this., a następnie przeglądaj zawartość podpowiedzi) oraz dokumentacji w witrynie MSDN (zapoznaj się
z System.Web.Mvc.Controller, jej klasami bazowymi i z System.Web.Mvc.ControllerContext).
Użycie parametrów metod akcji
Jak widzieliśmy w poprzednich rozdziałach, metody akcji mogą posiadać parametry. Jest to przyjemniejszy sposób
otrzymywania danych wejściowych w stosunku do ich ręcznego pobierania z obiektów kontekstu, który dodatkowo
sprawia, że metody akcji są czytelniejsze. Załóżmy, że mamy metodę akcji korzystającą z obiektów kontekstu:
...
public ActionResult ShowWeatherForecast(){
string city = (string)RouteData.Values["city"];
DateTime forDate = DateTime.Parse(Request.Form["forDate"]);
// … tu zaimplementuj prognozę pogody …
return View();
}
...
Możemy zmodyfikować ją tak, aby korzystała z parametrów:
...
public ActionResult ShowWeatherForecast(string city, DateTime forDate){
// … tu zaimplementuj prognozę pogody …
433
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
return View();
}
...
Jest ona nie tylko czytelniejsza, ale również łatwiej ją testować — możemy utworzyć test bez konieczności
imitowania właściwości klasy kontrolera.
 Wskazówka Dla uzupełnienia warto wspomnieć, że metody akcji nie mogą posiadać parametrów out ani ref.
Nie mają one tutaj uzasadnienia. ASP.NET MVC po prostu zgłosi wyjątek, jeżeli napotka taki parametr.
Platforma MVC dostarcza wartości dla naszych parametrów, automatycznie przeszukując za nas właściwości
i obiekty kontekstu, takie jak Request.QueryString, Request.Form czy RouteData.Values. W nazwach parametrów
nie ma znaczenia wielkość liter, więc parametr city może być zainicjowany za pomocą Request.Form["City"].
Sposób tworzenia obiektów parametrów
Klasa bazowa Controller pobiera wartości dla parametrów metod akcji za pomocą komponentów MVC
nazywanych dostawcą wartości oraz łącznikiem modelu. Dostawcy wartości reprezentują zbiór danych
dostępnych dla kontrolera. Istnieją wbudowane obiekty dostawców, które pobierają dane z Request.Form,
Request.QueryString, Request.Files oraz RouteData.Values. Następnie wartości są przekazywane do łączników
modelu, które próbują dopasować je do typów wymaganych w parametrach metod akcji.
Wbudowany, domyślny łącznik modelu może tworzyć i wypełniać obiekty dowolnych typów .NET,
w tym kolekcje i własne typy. W rozdziale 11. pokazałem przykład, w którym dane ze strony administracyjnej
były prezentowane naszej metodzie akcji jako jeden obiekt Product, choć jego poszczególne wartości były
rozproszone po elementach formularza HTML. Dostawców wartości oraz łączniki modelu przedstawię
szczegółowo w rozdziale 24.
Parametry opcjonalne i obowiązkowe
Jeżeli platforma MVC nie znajdzie wartości dla parametru typu referencyjnego (takiego jak string lub object),
metoda akcji będzie wywoływana, ale parametr taki będzie miał wartość null. Jeżeli wartość nie może być znaleziona
dla typu wartościowego (takiego jak int lub double), zgłaszany jest wyjątek, a metoda akcji nie będzie wywołana.
Z tego powodu można myśleć o parametrach w inny sposób:
 Parametry o typach wartościowych są obowiązkowe. Aby zmienić je na opcjonalne, należy podać
wartość domyślną (patrz następny punkt) lub zmienić typ parametru na dopuszczający wartość null
(na przykład int? lub DateTime?), dzięki czemu platforma będzie mogła przekazać do niego wartość
null, gdy nie znajdzie odpowiedniej wartości.
 Parametry o typach referencyjnych są opcjonalne. Aby zmienić je w obowiązkowe (czyli zapewnić,
że nie będzie przekazana wartość null), należy dodać do metody akcji kod odrzucający wartości null.
Jeżeli wartość jest na przykład równa null, można zgłosić wyjątek ArgumentNullException.
Określanie domyślnych wartości parametrów
Jeżeli chcesz przetwarzać żądania, które nie zawierają wartości dla parametrów metod akcji, ale nie chcesz
sprawdzać wartości null w kodzie ani zgłaszać wyjątków, możesz zamiast tego użyć parametrów opcjonalnych
dostępnych w C#. Przykład jest zamieszczony na listingu 17.6.
Listing 17.6. Użycie parametrów opcjonalnych w metodzie akcji
...
public ActionResult Search(string query= "all", int page = 1) {
// …przetworzenie żądania…
434
ROZDZIAŁ 17.  KONTROLERY I AKCJE
return View();
}
...
Aby utworzyć parametr opcjonalny, w jego definicji przypisujemy mu wartość. Na powyższym listingu
zdefiniowaliśmy wartości domyślne dla parametrów query oraz page. Platforma MVC będzie próbowała pobrać
te wartości z danych żądania (jeżeli nie ma dostępnych wartości, zostaną użyte zdefiniowane wartości domyślne).
Dzięki temu dla naszego parametru znakowego query nie musimy sprawdzać wartości null. Jeżeli przetwarzane
żądanie nie zawiera wartości query, to nasza metoda akcji będzie wywołana z ciągiem all. Również w przypadku
parametru int nie musimy się martwić o żądania, które w normalnym przypadku powodowałyby błędy braku
wartości dla parametru page. Nasza metoda będzie wywołana z wartością domyślną równą 1. Parametry
opcjonalne mogą być używane dla typów literałowych, czyli wszystkich typów, które można zdefiniować
bez użycia słowa kluczowego new, takich jak string, int czy double.
 Ostrzeżenie Jeżeli żądanie zawiera wartość dla parametru, ale nie może być ona skonwertowana na prawidłowy
typ (gdy użytkownik poda na przykład nienumeryczny ciąg znaków dla parametru int), to platforma przekaże domyślną
wartość dla tego parametru (na przykład 0 dla parametru int) i zarejestruje w obiekcie kontekstu ModelState
błąd kontroli poprawności tej wartości. Jeżeli nie będziesz kontrolował błędów weryfikacji poprawności w ModelState,
możesz doprowadzić do dziwnej sytuacji, gdy użytkownik wprowadzi nieprawidłowe dane do formularza, a żądanie
zostanie przetworzone tak, jakby użytkownik nie podał żadnych danych lub wprowadził wartość domyślną. Więcej
informacji na temat kontroli poprawności oraz ModelState znajduje się w rozdziale 25., w którym dowiesz się,
jak unikać tego rodzaju problemów.
Tworzenie danych wyjściowych
Po zakończeniu przetwarzania żądania przez kontroler zazwyczaj musimy wygenerować odpowiedź. Gdy
utworzyliśmy nasz najprostszy kontroler przez bezpośrednie zaimplementowanie interfejsu IController,
musieliśmy zająć się każdym aspektem przetwarzania żądania, w tym generowaniem odpowiedzi dla klienta.
Jeżeli chcemy wysłać odpowiedź HTML, to musimy utworzyć i poskładać dane HTML, a następnie wysłać
je do klienta za pomocą metody Response.Write. Podobnie, jeżeli chcemy przekierować przeglądarkę użytkownika
do innego adresu URL, musimy wywołać metodę Response.Redirect i przekazać mu adres URL, jakim jesteśmy
zainteresowani. Oba te podejścia są pokazane na listingu 17.7, w którym przedstawiono usprawnioną wersję
klasy BasicController.
Listing 17.7. Generowanie wyników w pliku BasicController.cs
using System.Web.Mvc;
using System.Web.Routing;
namespace ControllersAndActions.Controllers {
public class BasicController : IController {
public void Execute(RequestContext requestContext) {
string controller = (string)requestContext.RouteData.Values["controller"];
string action = (string)requestContext.RouteData.Values["action"];
if (action.ToLower() == "redirect") {
requestContext.HttpContext.Response.Redirect("/Derived/Index");
} else {
requestContext.HttpContext.Response.Write(
string.Format("Kontroler: {0}, akcja: {1}",
controller, action));
}
435
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
}
}
}
Tego samego podejścia możemy użyć w przypadku wykorzystywania kontrolera dziedziczącego po klasie
Controller. Obiekt HttpResponseBase, zwracany przez właściwość requestContext.HttpContext.Response
w metodzie Execute, jest również dostępny poprzez właściwość Controller.Response, co pokazuję na listingu 17.8,
w którym przedstawiono usprawnioną wersję klasy DerivedController.
Listing 17.8. Użycie właściwości Response w celu wygenerowania danych wyjściowych w pliku DerivedController.cs
using System.Web.Mvc;
namespace ControllersAndActions.Controllers {
public class DerivedController : Controller {
public ActionResult Index() {
ViewBag.Message = "Pozdrowienia z metody Index kontrolera DerivedController.";
return View("MyView");
}
public void ProduceOutput() {
if (Server.MachineName == "TINY") {
Response.Redirect("/Basic/Index");
} else {
Response.Write("Kontroler: Derived, akcja: ProduceOutput");
}
}
}
}
Metoda ProduceOutput używa wartości właściwości Server.MachineName do określenia treści odpowiedzi
udzielanej klientowi. (TINY to nazwa jednego z moich komputerów). Technika ta działa, ale ma kilka wad:
 Klasy kontrolera muszą zawierać informacje na temat struktury HTML oraz adresów URL, co powoduje,
że klasy są trudniejsze do odczytywania i utrzymania.
 Trudno jest tworzyć testy jednostkowe dla kontrolera, który generuje odpowiedź bezpośrednio na wyjście.
Konieczne jest utworzenie imitacji implementacji obiektu Response, a następnie przetworzenie danych
otrzymanych z kontrolera i porównanie ich z oczekiwanym wynikiem. Może to oznaczać konieczność
analizowania słów kluczowych HTML, co jest złożonym i uciążliwym procesem.
 Taka obsługa szczegółów każdej odpowiedzi jest pracochłonnym procesem narażonym na błędy. Niektórzy
programiści lubią absolutną kontrolę, jaką daje budowanie kontrolerów od początku, ale zwykli ludzie
szybko popadają we frustrację.
Na szczęście platforma MVC posiada przydatną funkcję rozwiązującą wszystkie te problemy, nazywaną
wynikiem akcji. W kolejnym punkcie przedstawię koncepcję wyników akcji oraz pokażę różne sposoby
generowania odpowiedzi z kontrolerów.
Wyniki akcji
Platforma MVC korzysta z wyników akcji do oddzielenia definiowania intencji od wykonywania tych intencji.
Koncepcja okazuje się bardzo prosta, gdy już ją opanujesz. Jednak przywyknięcie do niej może zabrać nieco
czasu, ponieważ podejście jest nieco nietypowe.
Zamiast pracować bezpośrednio na obiekcie Response, zwracamy obiekt dziedziczący po klasie ActionResult,
który opisuje dane, jakie chcemy otrzymać z kontrolera, na przykład wygenerowanie widoku lub przekierowanie
436
ROZDZIAŁ 17.  KONTROLERY I AKCJE
do innego adresu URL bądź metody akcji. Jednak odpowiedź nie jest generowana bezpośrednio — na tym
właśnie polega nietypowość tego rozwiązania. Zamiast tego tworzysz obiekt ActionResult, który platforma
MVC będzie przetwarzać w celu wygenerowania wyniku po wykonaniu metody akcji.
 Uwaga We wzorcach projektowych system wyników akcji jest przykładem wzorca polecenie. Wzorzec ten opisuje
scenariusze, gdy przechowujemy i przekazujemy obiekty opisujące operacje do wykonania. Więcej informacji na jego
temat można znaleźć w artykule http://pl.wikipedia.org/wiki/Polecenie_(wzorzec_projektowy).
Gdy platforma MVC otrzyma obiekt ActionResult z metody akcji, wywołuje metodę ExecuteResult
zdefiniowaną w tej klasie. Implementacja wyniku akcji obsługuje za nas obiekt Response, generując
wynik odpowiadający naszym intencjom. Na listingu 17.9 pokazany jest przykład w postaci klasy
CustomRedirectResult. Klasa została zdefiniowana w nowym katalogu o nazwie Infrastructure, który należy
dodać do projektu.
Listing 17.9. Zawartość pliku CustomRedirectResult.cs
using System.Web.Mvc;
namespace ControllersAndActions.Infrastructure {
public class CustomRedirectResult : ActionResult {
public string Url { get; set; }
public override void ExecuteResult(ControllerContext context) {
string fullUrl = UrlHelper.GenerateContentUrl(Url, context.HttpContext);
context.HttpContext.Response.Redirect(fullUrl);
}
}
}
Przedstawiona powyżej klasa działa na takiej samej zasadzie jak System.Web.Mvc.RedirectResult. Jedną
z zalet udostępnienia platformy na zasadach open source jest możliwość sprawdzenia sposobu działania
każdego z mechanizmów. Klasa CustomRedirectResult jest znacznie prostsza niż jej odpowiednik na platformie
MVC, ale jednocześnie wystarczająca dla potrzeb materiału omawianego w rozdziale.
Gdy tworzymy obiekt klasy RedirectResult, przekazujemy adres URL, do którego chcemy wykonać
przekierowanie. Metoda ExecuteResult, która będzie wywołana przez platformę MVC w momencie zakończenia
naszej metody akcji, pobiera obiekt Response z obiektu ControllerContext dostarczanego przez platformę,
a następnie wywołuje metodę RedirectPermanent lub Redirect, co jest analogiczne do operacji, jakie
wykonywaliśmy na listingu 17.7. Użyjemy teraz klasy CustomRedirectResult. Na listingu 17.10 pokazana
jest zmodyfikowana wersja naszej klasy DerivedController.
Listing 17.10. Zastosowanie klasy CustomRedirectResult w kontrolerze DerivedController
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
ControllersAndActions.Infrastructure;
namespace ControllersAndActions.Controllers {
public class DerivedController : Controller {
public ActionResult Index() {
ViewBag.Message = "Pozdrowienia z metody Index kontrolera DerivedController.";
return View("MyView");
}
437
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
public ActionResult ProduceOutput() {
if (Server.MachineName == "TINY") {
return new CustomRedirectResult { Url = "/Basic/Index" };
} else {
Response.Write("Controller: Derived, Action: ProduceOutput");
return null;
}
}
}
}
Testowanie jednostkowe kontrolerów i akcji
Wiele części platformy MVC jest zaprojektowanych w celu ułatwienia testowania jednostkowego, co szczególnie jasno
widać w przypadku akcji i kontrolerów. Istnieje kilka przyczyn takiego stanu rzeczy:

Możliwe jest testowanie akcji i kontrolerów poza serwerem WWW. Obiekty kontekstu są dostępne poprzez
ich klasy bazowe (takie jak HttpRequestBase), dla których można w łatwy sposób tworzyć imitacje.

Nie ma potrzeby analizowania kodu HTML w celu przetestowania wyniku metod akcji. Możliwe jest przeglądanie
zwracanego obiektu ActionResult w celu sprawdzenia, czy zawiera on oczekiwany wynik.

Nie ma potrzeby symulowania żądań klienta. System dołączania modelu pozwala na tworzenie metod akcji,
które otrzymują dane w postaci ich parametrów. Aby przetestować metodę akcji, należy po prostu bezpośrednio
ją wywołać, dostarczając interesujące nas wartości parametrów. W dalszej części rozdziału pokażę, jak tworzyć
testy jednostkowe dla różnych rodzajów metod akcji.
Nie należy zapominać, że testowanie jednostkowe nie jest końcem drogi. Gdy metody akcji są wykonywane
jedna po drugiej, w aplikacji mogą powstać złożone problemy. Testowanie jednostkowe należy więc uzupełnić
innymi metodami testowania.
Zwróć uwagę na możliwość zmiany wyniku działania metody akcji i zwrot obiektu ActionResult.
Wartością zwrotną jest null, jeśli nie chcemy, aby platforma MVC robiła cokolwiek po wykonaniu metody akcji.
Takie rozwiązanie przyjęliśmy, gdy wartością zwrotną nie jest egzemplarz CustomRedirectResult.
Skoro dowiedziałeś się już, jak utworzyć i stosować własny wynik działania akcji, możemy powrócić do
dostarczanego przez platformę MVC, ponieważ posiada znacznie większe możliwości i został dokładnie
przetestowany przez Microsoft. Na listingu 17.11 przedstawiono zmianę konieczną do wprowadzenia.
Listing 17.11. Użycie w pliku DerivedController.cs wbudowanego obiektu RedirectResult
...
public ActionResult ProduceOutput() {
return new RedirectResult("/Basic/Index");
}
...
Z metody akcji została usunięta konstrukcja warunkowa. Oznacza to, że jeżeli uruchomisz aplikację
i przejdziesz do /Derived/ProduceOutput, przeglądarka zostanie przekierowana na adres URL /Basic/Index.
Aby uprościć nasz kod, klasa kontrolera zawiera metody pozwalające generować różne rodzaje obiektów
ActionResult. W celu osiągnięcia wyniku z listingu 17.11 możemy zwrócić wynik metody Redirect,
jak pokazano na listingu 17.12.
Listing 17.12. Użycie w pliku DerivedController.cs wygodnej metody kontrolera
...
public ActionResult ProduceOutput() {
438
ROZDZIAŁ 17.  KONTROLERY I AKCJE
return Redirect("/Basic/Index");
}
...
System wyników akcji jest bardzo prosty, a w efekcie uzyskujemy prostszy, czytelniejszy i spójniejszy kod.
Można go również łatwo testować.
W przypadku przekierowania można na przykład sprawdzić, czy metoda akcji zwróciła obiekt typu
RedirectResult, którego właściwość Url zawiera oczekiwaną wartość.
Platforma MVC zawiera kilka wbudowanych typów wyniku akcji, zebranych w tabeli 17.5. Wszystkie te typy
dziedziczą po ActionResult, a wiele z nich ma wygodne metody pomocnicze zdefiniowane w klasie Controller.
W kolejnych punktach pokażę, w jaki sposób korzystać z tych wyników oraz jak tworzyć własne wyniki akcji.
Tabela 17.5. Wbudowane typy ActionResult
Typ
Opis
Metoda pomocnicza
w kontrolerze
ViewResult
Generuje wskazany lub domyślny szablon widoku.
View
PartialViewResult
Generuje wskazany lub domyślny częściowy
szablon widoku.
PartialView
RedirectToRouteResult
Wykonuje przekierowanie HTTP 301 lub 302 do
metody akcji lub konkretnej trasy, generując adres
URL zgodnie z konfiguracją routingu.
RedirectToAction
RedirectToActionPermanent
RedirectToRoute
RedirectToRoutePermanent
RedirectResult
Wykonuje przekierowanie 301 lub 302
do podanego adresu URL.
Redirect
ContentResult
Zwraca przeglądarce internetowej niezmodyfikowane
dane tekstowe i opcjonalnie ustawia nagłówek
Content-Type.
Content
FileResult
Transferuje dane binarne (takie jak plik z dysku
lub tablica bajtowa w pamięci) bezpośrednio do
przeglądarki internetowej.
File
JsonResult
Serializuje obiekt .NET w formacie JSON, a następnie
wysyła go jako odpowiedź. Ten rodzaj odpowiedzi
jest najczęściej generowany za pomocą funkcji Web
API, która zostanie omówiona w rozdziale 27. Z tym
rodzajem akcji spotkasz się jeszcze w rozdziale 23.
Json
JavaScriptResult
Wysyła fragment kodu JavaScript, który powinien
być wykonany przez przeglądarkę internetową.
JavaScript
HttpUnauthorizedResult
Ustawia kod statusu odpowiedzi HTTP na 401
(co oznacza „brak autoryzacji”), co powoduje,
że aktywny mechanizm uwierzytelniania
(uwierzytelnianie formularzy lub Windows)
prosi użytkownika o zalogowanie się.
Brak
HttpNotFoundResult
Zwraca błąd HTTP 404.
HttpNotFound
HttpStatusCodeResult
Zwraca dowolny kod HTTP.
Brak
EmptyResult
Nic nie robi.
Brak
RedirectPermanent
439
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Zwracanie kodu HTML przez generowanie widoku
Najczęściej używanym rodzajem odpowiedzi z metody akcji jest generowanie kodu HTML i wysyłanie go
do przeglądarki. Aby zademonstrować, jak generowany jest widok, do projektu dodajemy nowy kontroler
o nazwie Example. Zawartość pliku ExampleController.cs przedstawiono na listingu 17.13.
Listing 17.13. Zawartość pliku ExampleController.cs
using System.Web.Mvc;
namespace ControllersAndActions.Controllers {
public class ExampleController : Controller {
public ViewResult Index() {
return View("Homepage");
}
}
}
Podczas użycia wyniku akcji określamy widok, który platforma MVC ma wygenerować, używając
egzemplarza klasy ViewResult. Najprostsze podejście polega na wywołaniu metody View kontrolera
i przekazaniu nazwy widoku jako argumentu. W kodzie z tego listingu korzystamy z metody View wraz
z argumentem Homepage, która wskazuje, że ma zostać użyty widok HomePage.cshtml.
 Uwaga Zauważ, że zwracanym typem jest ViewResult. Metoda ta będzie się kompilowała i działała równie dobrze,
jeżeli podamy ogólniejszy typ ActionResult. W rzeczywistości niektórzy programiści definiują wynik każdej akcji
jako ActionResult, nawet gdy wiedzą, że akcje te zawsze będą zwracać dokładniejszy typ.
Gdy platforma MVC wywoła metodę ExecuteResult na obiekcie ViewResult, rozpoczyna wyszukiwanie
podanego przez nas widoku. Jeżeli w naszym projekcie są użyte obszary, to platforma będzie przeszukiwała
następujące lokalizacje:
 /Areas/<NazwaObszaru>/Views/<NazwaKontrolera>/<NazwaWidoku>.aspx
 /Areas/<NazwaObszaru>/Views/<NazwaKontrolera>/<NazwaWidoku>.ascx
 /Areas/<NazwaObszaru>/Views/Shared/<NazwaWidoku>.aspx
 /Areas/<NazwaObszaru>/Views/Shared/<NazwaWidoku>.ascx
 /Areas/<NazwaObszaru>/Views/<NazwaKontrolera>/<NazwaWidoku>.cshtml
 /Areas/<NazwaObszaru>/Views/<NazwaKontrolera>/<NazwaWidoku>.vbhtml
 /Areas/<NazwaObszaru>/Views/Shared/<NazwaWidoku>.cshtml
 /Areas/<NazwaObszaru>/Views/Shared/<NazwaWidoku>.vbhtml
Na podstawie powyższej listy możemy zauważyć, że platforma wyszukuje widoki, jakie zostały utworzone
dla starszego silnika widoku (rozszerzenia .aspx oraz .ascx), nawet gdy przy tworzeniu projektu wskazaliśmy
silnik Razor. Ma to na celu zachowanie zgodności z wcześniejszymi wydaniami platformy MVC, które używały
funkcji generowania pochodzących z ASP.NET Web Forms.
Platforma wyszukuje również szablony Razor dla C# oraz Visual Basic (pliki .cshtml dla C# oraz .vbhtml
dla Visual Basic; składnia Razor w tych plikach jest taka sama, natomiast kod jest utworzony w odmiennych
językach programowania, na co wskazują rozszerzenia plików). Platforma MVC sprawdza po kolei, czy
istnieją wymienione pliki. Pierwszy znaleziony plik jest wykorzystywany do wygenerowania wyniku metody akcji.
Jeżeli nie korzystamy z obszarów lub używamy obszarów, ale pliki z poprzedniej listy nie zostaną znalezione,
to platforma kontynuuje wyszukiwanie w następujących lokalizacjach:
440
ROZDZIAŁ 17.  KONTROLERY I AKCJE
 /Views/<NazwaKontrolera>/<NazwaWidoku>.aspx
 /Views/<NazwaKontrolera>/<NazwaWidoku>.ascx
 /Views/Shared/<NazwaWidoku>.aspx
 /Views/Shared/<NazwaWidoku>.ascx
 /Views/<NazwaKontrolera>/<NazwaWidoku>.cshtml
 /Views/<NazwaKontrolera>/<NazwaWidoku>.vbhtml
 /Views/Shared/<NazwaWidoku>.cshtml
 /Views/Shared/<NazwaWidoku>.vbhtml
Gdy platforma MVC znajdzie plik, wyszukiwanie jest ponownie zatrzymywane, a widok jest używany
do wygenerowania odpowiedzi dla klienta.
Na listingu 17.13 nie korzystamy z widoków, więc na początku platforma sprawdzi plik /Views/Example/
Index.aspx. Zwróć uwagę, że pominięta została fraza Controller z nazwy klasy, więc utworzenie ViewResult
w ExampleController spowoduje wyszukiwanie w katalogu o nazwie Example.
Test jednostkowy — generowanie widoku
Aby przetestować widok generowany przez metodę akcji, można sprawdzić stan zwracanego obiektu ViewResult.
Nie jest to dokładnie to samo — w końcu nie przechodzimy przez cały proces w celu sprawdzenia wygenerowanego
kodu HTML — jednak jest wystarczająco bliskie ideałowi, ponieważ możemy założyć, że system widoków platformy
MVC działa prawidłowo. Do projektu testów jednostkowych należy dodać nowy plik testu jednostkowego o nazwie
ActionTests.cs.
Pierwszy test sprawdza, czy metoda akcji wybrała odpowiedni widok:
...
public ViewResult Index() {
return View("Homepage");
}
...
Aby sprawdzić, jaki widok został wybrany, odczytujemy właściwość ViewName z obiektu ViewResult, tak jak
w poniższej metodzie testowej:
using System.Web.Mvc;
using ControllersAndActions.Controllers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ControllersAndActions.Tests {
[TestClass]
public class ActionTests {
[TestMethod]
public void ViewSelectionTest() {
// przygotowanie — utworzenie kontrolera
ExampleController target = new ExampleController();
// działanie — wywołanie metody akcji
ViewResult result = target.Index();
// asercje — sprawdzenie wyniku
Assert.AreEqual("Homepage", result.ViewName);
441
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
}
}
}
Nieco inny przypadek występuje, jeżeli testujemy metodę akcji wybierającą widok domyślny, taką jak poniższa:
...
public ViewResult Index() {
return View();
}
...
W takim przypadku musimy zaakceptować pusty ciąg znaków ("") w nazwie widoku:
...
Assert.AreEqual("", result.ViewName);
...
Za pomocą pustego ciągu tekstowego obiekt ViewResult wskazuje silnikowi Razor, że wybrany został widok
domyślny powiązany z metodą akcji.
Sekwencja katalogów przeszukiwanych przez platformę MVC w celu znalezienia widoku jest kolejnym
przykładem zasady „konwencja przed konfiguracją”. Nie musimy rejestrować plików widoku na platformie.
Wystarczy umieścić je w jednej ze znanych lokalizacji, a platforma je znajdzie. Możemy również skorzystać
z konwencji, pomijając nazwę widoku do wygenerowania w wywołaniu metody View, jak pokazano na listingu 17.14.
Listing 17.14. Tworzenie w pliku ExampleController.cs obiektu ViewResult bez wskazywania widoku
using System;
using System.Web.Mvc;
namespace ControllersAndActions.Controllers {
public class ExampleController : Controller {
public ViewResult Index() {
return View();
}
}
}
W takim przypadku platforma MVC zakłada, że chcemy wygenerować widok o takiej samej nazwie jak metoda
akcji. Oznacza to, że wywołanie metody View z listingu 17.14 rozpoczyna wyszukiwanie widoku o nazwie Index.
 Uwaga Tak naprawdę platforma MVC pobiera nazwę metody akcji z wartości RouteData.Values["action"],
co zostało wyjaśnione w opisie systemu routingu w rozdziałach 15. i 16. Nazwa metody akcji i wartość pochodząca
z systemu routingu będą takie same, jeżeli używasz wbudowanych klas routingu. Sytuacja może być inna, jeśli
zaimplementowałeś własne klasy routingu, które nie stosują się do konwencji przyjętych na platformie MVC.
Dostępnych jest kilka przeciążonych wersji metody View. Pozwalają one na ustawienie różnych właściwości
tworzonego przez nie obiektu ViewResult. Możemy na przykład zmienić układ używany przez widok, podając
jawnie jego alternatywę, w następujący sposób:
442
ROZDZIAŁ 17.  KONTROLERY I AKCJE
...
public ViewResult Index() {
return View("Index", "_AlternateLayoutPage");
}
...
Definiowanie widoku z użyciem ścieżki dostępu
Konwencja nazewnictwa widoków jest prosta i wygodna, ale nie ogranicza nam możliwości wyboru widoku
do wygenerowania. Jeżeli chcesz wygenerować określony widok, możesz to zrobić przez podanie w sposób jawny
ścieżki dostępu, co powoduje pominięcie fazy przeszukiwania. Poniżej pokazany jest przykład:
using System.Web.Mvc;
namespace ControllersAndActions.Controllers {
public class ExampleController : Controller {
public ViewResult Index() {
return View("~/Views/Other/Index.cshtml");
}
}
}
Gdy widok jest podawany w ten sposób, ścieżka dostępu musi się zaczynać od / lub ~/ oraz zawierać rozszerzenie
nazwy pliku (na przykład .cshtml dla widoków Razor z kodem C#).
Jeżeli zauważysz, że korzystasz z tej funkcji, sugeruję, abyś się zastanowił, co chcesz osiągnąć. Jeżeli próbujesz
wygenerować widok należący do innego kontrolera, to być może lepiej przekierować użytkownika do metody
akcji z tego kontrolera (patrz „Przekierowanie do innej metody akcji” w dalszej części rozdziału). Jeżeli próbujesz pominąć
schemat nazewnictwa, ponieważ nie pasuje do sposobu organizacji projektu, zajrzyj do rozdziału 20., w którym
przedstawiłem implementację własnej sekwencji wyszukiwania.
Przekazywanie danych z metody akcji do widoku
Często musimy przekazywać dane z metody akcji do widoku. Platforma MVC zapewnia kilka sposobów
na wykonanie tej operacji, które zostaną omówione w kolejnych punktach. Zacznę w nich prezentowanie
widoków, którym jest poświęcony rozdział 20. Jednak przedstawię tutaj tylko te funkcje, które są nam potrzebne
do zademonstrowania interesujących nas funkcji kontrolera.
Użycie obiektu modelu widoku
Jedną z metod wysyłania obiektu do widoku jest przekazanie go jako parametru metody View, co jest pokazane
na listingu 17.15.
Listing 17.15. Użycie obiektu modelu widoku w pliku ExampleController.cs
using System;
using System.Web.Mvc;
namespace ControllersAndActions.Controllers {
public class ExampleController : Controller {
443
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
public ViewResult Index() {
DateTime date = DateTime.Now;
return View(date);
}
}
}
W tym przykładzie przekazaliśmy obiekt DateTime jako model widoku. Do tego obiektu odwołujemy się
za pomocą słowa kluczowego silnika Razor, Model. Aby pokazać sposób użycia słowa kluczowego Model,
w katalogu Views/Example tworzymy plik widoku o nazwie Index.cshtml, a następnie umieszczamy w nim
kod przedstawiony na listingu 17.16.
Listing 17.16. Uzyskanie dostępu do modelu widoku w pliku Index.cshtml
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
Dzisiejszy dzień to @(((DateTime)Model).DayOfWeek)
Widok pokazany na listingu 17.16 jest nazywany widokiem beztypowym lub słabo typowanym. Widok
nie ma żadnych informacji na temat obiektu modelu widoku i traktuje go jak egzemplarz typu object. Aby uzyskać
wartość właściwości DayOfWeak, musimy rzutować ten obiekt na typ DateTime. Takie rozwiązanie działa, ale
wynikowy widok jest mało czytelny. Można to poprawić przez utworzenie widoku silnie typowanego, w którym
określamy typ modelu widoku, jak pokazano na listingu 17.17.
Listing 17.17. Silnie typowany widok w pliku Index.cshtml
@model DateTime
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
Dzisiejszy dzień to @Model.DayOfWeek
Typ modelu widoku określamy za pomocą słowa kluczowego silnika Razor, model. Zwróć uwagę, że użyliśmy
małej litery m przy określaniu typu modelu oraz wielkiej litery M przy odczycie wartości. Zastosowanie silnego
typowania pomaga uporządkować nasz widok, ponadto Visual Studio zapewnia wsparcie IntelliSense, co jest
pokazane na rysunku 17.3.
Rysunek 17.3. Oferowana przez IntelliSense obsługa silnie typowanych widoków
444
ROZDZIAŁ 17.  KONTROLERY I AKCJE
Test jednostkowy — obiekty modelu widoku
Do obiektu modelu widoku przekazanego z metody akcji do widoku możemy się dostać poprzez właściwość
ViewResult.ViewData.Model. Poniżej przedstawiony jest test dla metody akcji z listingu 17.17. Jak możesz
zobaczyć, użyto metody Assert.IsInstanceOfType do sprawdzenia, czy obiekt modelu widoku jest egzemplarzem
typu DateTime:
...
[TestMethod]
public void ViewSelectionTest() {
// przygotowanie — utworzenie kontrolera
ExampleController target = new ExampleController();
// działanie — wywołanie metody akcji
ViewResult result = target.Index();
// asercje — sprawdzenie wyniku
Assert.AreEqual("", result.ViewName);
Assert.IsInstanceOfType(result.ViewData.Model, typeof(System.DateTime));
}
...
Konieczna była zmiana nazwy widoku, aby odzwierciedlić zmiany w metodzie akcji wprowadzone
od poprzedniego testu jednostkowego:
...
[TestMethod]
public void ControllerTest() {
// przygotowanie — utworzenie kontrolera
ExampleController target = new ExampleController();
// działanie — wywołanie metody akcji
ViewResult result = target.Index();
// asercje — sprawdzenie wyniku
Assert.AreEqual("", result.ViewName);
}
...
Przekazywanie danych z użyciem ViewBag
W rozdziale 2. przedstawiłem obiekt ViewBag. Pozwala on na definiowanie dowolnych właściwości w obiekcie
dynamicznym i odwoływać się do nich w widoku. Obiekt dynamiczny jest dostępny poprzez właściwość
Controller.ViewBag, co jest pokazane na listingu 17.18.
445
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 17.18. Użycie mechanizmu ViewBag w pliku ExampleController.cs
using System;
using System.Web.Mvc;
namespace ControllersAndActions.Controllers {
public class ExampleController : Controller {
public ViewResult Index() {
ViewBag.Message = "Witaj";
ViewBag.Date = DateTime.Now;
return View();
}
}
}
W tym listingu zdefiniowaliśmy właściwości o nazwach Message oraz Date, po prostu przypisując do nich
wartości. Przed momentem przypisania nie istniały żadne wartości ani nie musieliśmy przygotowywać się
do ich utworzenia. Aby odczytać te dane z widoku, po prostu pobieramy wartości tych samych właściwości,
które ustawiliśmy w metodzie akcji, jak pokazano na listingu 17.19.
Listing 17.19. Odczyt danych z użyciem ViewBag w pliku Index.cshtml
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
Dzisiejszy dzień to @ViewBag.Date.DayOfWeek
<p />
Komunikat: @ViewBag.Message
Przewagą obiektu ViewBag w stosunku do obiektu modelu widoku jest łatwość wysyłania wielu obiektów
do widoku. Gdybyśmy byli ograniczeni wyłącznie do modelu widoku, to aby osiągnąć taki sam wynik jak
w listingach 17.18 i 17.19, musielibyśmy utworzyć nowy typ zawierający składniki typu string oraz DateTime.
W przypadku użycia obiektów dynamicznych możemy użyć dowolnej sekwencji metod i właściwości
w wywołaniu widoku, na przykład:
...
Dzisiejszy dzień to @ViewBag.Date.DayOfWeek.Bla.Bla.Bla
...
Visual Studio nie może zapewnić wsparcia IntelliSense dla obiektów dynamicznych, takich jak ViewBag,
więc błędy, takie jak pokazane powyżej, nie będą ujawnione do momentu wywołania widoku.
Test jednostkowy — ViewBag
Wartości z ViewBag możemy odczytać, korzystając z właściwości ViewResult.ViewBag. Poniższa metoda testowa
jest przeznaczona dla metody akcji z listingu 17.18:
...
[TestMethod]
public void ControllerTest() {
446
ROZDZIAŁ 17.  KONTROLERY I AKCJE
// przygotowanie — utworzenie kontrolera
ExampleController target = new ExampleController();
// działanie — wywołanie metody akcji
ViewResult result = target.Index();
// asercje — sprawdzenie wyniku
Assert.AreEqual("Witaj", result.ViewBag.Message);
}
...
Wykonywanie przekierowań
Metoda akcji często nie tworzy bezpośrednio żadnych danych, a jedynie przekierowuje przeglądarkę użytkownika
do innego adresu URL. W większości przypadków ten URL wskazuje na inną akcję w aplikacji, która generuje
wynik oczekiwany przez użytkowników.
Wzorzec POST-Redirect-GET
Najczęstszym zastosowaniem przekierowania w metodach akcji jest przetwarzanie żądań HTTP POST.
Jak wspominałem w poprzednim rozdziale, żądania POST są używane w momencie konieczności zmiany stanu
aplikacji. Jeżeli po prostu zwracasz HTML po przetworzeniu żądania, to ryzykujesz, że użytkownik kliknie przycisk
odświeżenia w przeglądarce i ponownie prześle dane formularza, powodując nieoczekiwane i niepożądane wyniki.
Aby uniknąć tego problemu, możemy korzystać z wzorca nazywanego POST-Redirect-GET. We wzorcu tym
odbieramy żądanie POST, przetwarzamy je, a następnie przekierowujemy przeglądarkę, dzięki czemu wysyła ona
żądanie GET i pobiera kolejny adres URL. Żądania GET nie powinny zmieniać stanu aplikacji, więc jakiekolwiek
niespodziewane powtórzenia tego żądania nie powinny powodować żadnych problemów.
Gdy wykonujemy przekierowanie, wysyłamy do przeglądarki jeden z dwóch kodów HTTP:
 Kod HTTP 302, który oznacza przekierowanie tymczasowe. To najczęściej wykorzystywany rodzaj
przekierowania. W przypadku zastosowania wzorca POST-Redirect-GET jest to kod, którego
powinniśmy użyć.
 Kod HTTP 301, który oznacza przekierowanie trwałe. Kod ten powinien być stosowany ostrożnie,
ponieważ informuje odbierającego, aby nie korzystał już z tego adresu URL i zamiast tego używał
adresu otrzymanego wraz z przekierowaniem. Jeżeli masz wątpliwości, stosuj przekierowanie
tymczasowe, czyli kod 302.
Przekierowanie do jawnie podanego adresu URL
Najprostszym sposobem na wykonanie przekierowania w przeglądarce jest wywołanie metody Redirect,
która zwraca obiekt klasy RedirectResult, jak pokazano na listingu 17.20.
Listing 17.20. Przekierowanie do jawnie podanego adresu URL w pliku ExampleController.cs
using System;
using System.Web.Mvc;
namespace ControllersAndActions.Controllers {
public class ExampleController : Controller {
447
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
public ViewResult Index() {
ViewBag.Message = "Witaj";
ViewBag.Date = DateTime.Now;
return View();
}
public RedirectResult Redirect() {
return Redirect("/Example/Index");
}
}
}
Adres URL, do którego chcemy wykonać przekierowanie, jest definiowany w postaci ciągu znaków
i przekazywany jako parametr metody Redirect. Metoda Redirect wysyła żądanie przekierowania tymczasowego.
Jeżeli chcesz wysłać przekierowanie trwałe, skorzystaj z metody RedirectPermanent, użytej w kodzie z listingu 17.21.
Listing 17.21. Przekierowanie trwałe do jawnie podanego adresu URL w pliku ExampleController.cs
...
public RedirectResult Redirect() {
return RedirectPermanent("/Example/Index");
}
...
 Wskazówka Jeżeli wolisz, możesz użyć przeciążonej wersji metody Redirect, która oczekuje parametru bool
określającego, czy jest to przekierowanie trwałe.
Test jednostkowy — przekierowanie z użyciem jawnie podanego adresu URL
Przekierowania korzystające z jawnie podanego adresu URL są łatwe do testowania. Można odczytać adres URL oraz
znacznik informujący, czy przekierowanie jest trwałe, czy tymczasowe, używając właściwości Url i Permanent
w klasie RedirectResult. Poniższa metoda testowa jest przeznaczona dla metody akcji z listingu 17.21:
...
[TestMethod]
public void ControllerTest() {
// przygotowanie — utworzenie kontrolera
ExampleController target = new ExampleController();
// działanie — wywołanie metody akcji
RedirectResult result = target.Redirect();
// asercje — sprawdzenie wyniku
Assert.IsFalse(result.Permanent);
Assert.AreEqual("/Example/Index", result.Url);
}
...
Zwróć uwagę na uaktualnienie testu w celu otrzymania RedirectResult po wywołaniu metody akcji.
448
ROZDZIAŁ 17.  KONTROLERY I AKCJE
Przekierowanie do adresu URL z systemu routingu
Jeżeli przekierowujesz użytkownika do innej części aplikacji, musisz upewnić się, że wysyłany adres URL jest
prawidłowym adresem w schemacie URL. W przypadku stosowania adresów URL zapisanych w postaci literałów
jakakolwiek zmiana w schemacie routingu powoduje, że będziemy musieli przejrzeć kod i zaktualizować
adresy. Jako alternatywy możemy użyć systemu routingu do wygenerowania prawidłowego adresu URL,
korzystając z metody RedirectToRoute, która tworzy obiekt RedirectToRouteResult, w sposób pokazany
na listingu 17.22.
Listing 17.22. Przykład zastosowania w pliku ExampleController.cs przekierowania do adresu URL z systemu
routingu
using System;
using System.Web.Mvc;
namespace ControllersAndActions.Controllers {
public class ExampleController : Controller {
public ViewResult Index() {
ViewBag.Message = "Witaj";
ViewBag.Date = DateTime.Now;
return View();
}
public RedirectToRouteResult Redirect() {
return RedirectToRoute(new {
controller = "Example",
action = "Index",
ID = "MyID"
});
}
}
}
Metoda RedirectToRoute wysyła żądanie przekierowania tymczasowego. W przypadku konieczności
zastosowania przekierowania trwałego można użyć metody RedirectToRoutePermanent. Obie metody oczekują
typu anonimowego, którego właściwości są przekazywane do systemu routingu w celu wygenerowania
adresu URL. Więcej informacji na temat tego procesu można znaleźć w rozdziałach 15. i 16.
 Wskazówka Zwróć uwagę na fakt, że metoda RedirectToRoute zwraca obiekt RedirectToRouteResult, a więc
konieczne było uaktualnienie metody akcji.
Testy jednostkowe — przekierowania z użyciem tras
Poniżej mamy przykład testu jednostkowego przeznaczonego do testowania metody akcji z listingu 17.22:
...
[TestMethod]
public void ControllerTest() {
449
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
// przygotowanie — utworzenie kontrolera
ExampleController target = new ExampleController();
// działanie — wywołanie metody akcji
RedirectToRouteResult result = target.Redirect();
// asercje — sprawdzenie wyniku
Assert.IsFalse(result.Permanent);
Assert.AreEqual("Example", result.RouteValues["controller"]);
Assert.AreEqual("Index", result.RouteValues["action"]);
Assert.AreEqual("MyID", result.RouteValues["ID"]);
}
...
Jak możesz zobaczyć, wynik został przetestowany pośrednio przez sprawdzenie informacji systemu routingu
dostarczonych przez obiekt RedirectToRouteResult. Oznacza to brak konieczności analizowania adresu URL.
Przekierowanie do innej metody akcji
Przekierowanie do metody akcji można wykonać w elegancki sposób za pomocą metody RedirectToAction
(przekierowania tymczasowe) lub RedirectToActionPermanent (przekierowania trwałe). To są tylko opakowania
dla metody RedirectToRoute, która pozwala określić metodę akcji i kontroler bez potrzeby tworzenia typu
anonimowego, jak pokazano na listingu 17.23.
Listing 17.23. Przekierowanie z użyciem metody RedirectToAction w pliku ExampleController.cs
...
public RedirectToRouteResult Redirect() {
return RedirectToAction("Index");
}
...
Jeżeli podamy tylko metodę akcji, zakłada się, że jest to metoda z bieżącego kontrolera. Jeśli chcesz przekierować
użytkownika do innego kontrolera, musisz podać jego nazwę jako parametr:
...
public RedirectToRouteResult Redirect() {
return RedirectToAction("Index", "Basic");
}
...
Istnieją również inne przeciążone wersje tej metody, które pozwalają podać dodatkowe wartości potrzebne
do wygenerowania adresu URL. Są one zapisywane w postaci typu anonimowego, co powoduje zmniejszenie
wygody funkcji, ale mimo to kod ten nadal jest czytelniejszy.
 Uwaga Wartości przekazywane do metody akcji i kontrolera nie są weryfikowane przed przekazaniem ich do systemu
routingu. To my jesteśmy odpowiedzialni za upewnienie się, że podany adres faktycznie istnieje.
450
ROZDZIAŁ 17.  KONTROLERY I AKCJE
Zachowywanie danych pomiędzy przekierowaniami
Przekierowanie powoduje wysłanie przez przeglądarkę zupełnie nowego żądania HTTP, co oznacza, że nie mamy
dostępu do danych pierwotnego żądania. Jeżeli chcesz przekazać dane z jednego żądania do następnego, możesz
użyć funkcji TempData.
Mechanizm TempData jest podobny do danych sesji, ale jego wartości są zaznaczane do usunięcia po pierwszym
odczycie i po zakończeniu przetwarzania żądania są usuwane. Dzięki temu idealnie się nadają dla danych o krótkim
czasie życia, które powinny zostać zachowane pomiędzy przekierowaniami. Poniżej przedstawiony jest prosty
przykład metody akcji, która korzysta z metody RedirectToAction:
...
public RedirectToRouteResult RedirectToRoute() {
TempData["Message"] = "Witaj";
TempData["Date"] = DateTime.Now;
return RedirectToAction("Index");
}
...
Gdy metoda ta przetwarza żądanie, ustawia wartość w kolekcji TempData, a następnie przekierowuje
użytkownika do metody akcji Index w tym samym kontrolerze. W docelowej metodzie akcji można znów
odczytać dane TempData i przekazać je do widoku:
...
public ViewResult Index() {
ViewBag.Message = TempData["Message"];
ViewBag.Date = TempData["Date"];
return View();
}
...
Bardziej bezpośrednim podejściem jest odczytanie tych wartości w widoku w następujący sposób:
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
Dzisiejszy dzień to @(((DateTime)TempData["Date"]).DayOfWeek)
<p />
Komunikat: @TempData["Message"]
Odczytanie wartości w widoku powoduje, że nie musimy używać funkcji ViewBag ani ViewData w metodach
akcji. Jednak konieczne jest rzutowanie wyniku z TempData na właściwy typ.
Możliwe jest również pobranie wartości z TempData, bez oznaczania jej do usunięcia, za pomocą metody Peek:
...
DateTime time = (DateTime)TempData.Peek("Date");
...
451
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Za pomocą metody Keep wartość oznaczoną do usunięcia można zachować w następujący sposób:
...
TempData.Keep("Date");
...
Metoda Keep nie chroni wartości na stałe. Przy kolejnym odczycie wartość jest znów oznaczana do usunięcia.
Jeżeli chcesz przechowywać dane, które nie powinny być automatycznie usuwane, skorzystaj z kolekcji Session.
Zwracanie błędów i kodów HTTP
Ostatnia z wbudowanych klas ActionResult, którą przedstawię w tym rozdziale, może być używana do wysyłania
dowolnych komunikatów o błędach oraz kodów HTTP. Większość aplikacji nie wymaga takich funkcji, ponieważ
platforma MVC automatycznie generuje takie odpowiedzi. Może to być jednak przydatne, jeżeli potrzebujemy
bezpośredniej kontroli nad odpowiedziami wysyłanymi do klienta.
Wysyłanie dowolnych kodów wyniku HTTP
Za pomocą klasy HttpStatusCodeResult można wysyłać do przeglądarki dowolne kody HTTP. Nie istnieje
metoda kontrolera tworząca te obiekty, więc należy wykonać to samodzielnie, jak pokazano na listingu 17.24.
Listing 17.24. Wysyłanie dowolnych kodów wyniku HTTP w pliku ExampleController.cs
using System;
using System.Web.Mvc;
namespace ControllersAndActions.Controllers {
public class ExampleController : Controller {
public ViewResult Index() {
ViewBag.Message = "Witaj";
ViewBag.Date = DateTime.Now;
return View();
}
public RedirectToRouteResult Redirect() {
return RedirectToAction("Index", "Basic");
}
public HttpStatusCodeResult StatusCode() {
return new HttpStatusCodeResult(404, "Ten adres URL nie jest obsługiwany");
}
}
}
Parametrami konstruktora klasy HttpStatusCodeResult są numeryczny kod statusu oraz opcjonalny
komunikat. Na listingu zwróciliśmy kod 404, który oznacza, że żądany zasób nie istnieje.
Wysyłanie kodu 404
Zamiast wywołania użytego na listingu 17.24 możemy użyć wygodniejszej klasy HttpNotFoundResult, dziedziczącej
po HttpStatusCodeResult, która może być tworzona za pomocą metody pomocniczej kontrolera, HttpNotFound,
w sposób pokazany na listingu 17.25.
452
ROZDZIAŁ 17.  KONTROLERY I AKCJE
Listing 17.25. Wygenerowanie w pliku ExampleController.cs wyniku 404
...
public HttpStatusCodeResult StatusCode() {
return HttpNotFound();
}
...
Wysyłanie kodu 401
Inną klasą opakowującą dla określonego kodu HTTP jest HttpUnauthorizedResult, która wysyła kod 401 używany
w celu poinformowania o konieczności autoryzacji żądania. Przykład jest zamieszczony na listingu 17.26.
Listing 17.26. Wygenerowanie w pliku ExampleController.cs wyniku 401
...
public HttpStatusCodeResult StatusCode() {
return new HttpUnauthorizedResult();
}
...
W klasie kontrolera nie istnieje metoda pomocnicza do tworzenia obiektów HttpUnauthorizedResult, więc
trzeba wykonać to ręcznie. Efektem zwrócenia obiektu tej klasy jest zwykle przekierowanie na stronę logowania,
jak pokazałem to w rozdziale 12.
Test jednostkowy — kody statusu HTTP
Klasa HttpStatusCodeResult jest zgodna z pokazywanym do tej pory wzorcem wykorzystywanym dla innych
typów wyniku i udostępnia swój stan poprzez zbiór właściwości. W tym przypadku właściwość StatusCode
zwraca numeryczny kod HTTP, a StatusDescription — skojarzony z nim opis. Poniższa metoda testowa jest
przeznaczona dla metody akcji z listingu 17.26:
...
[TestMethod]
public void ControllerTest() {
// przygotowanie — utworzenie kontrolera
ExampleController target = new ExampleController();
// działanie — wywołanie metody akcji
HttpStatusCodeResult result = target.StatusCode();
// asercje — sprawdzenie wyniku
Assert.AreEqual(404, result.StatusCode);
}
...
Podsumowanie
Kontrolery są jedną z podstaw wzorca projektowego MVC. W tym rozdziale pokazałem, w jaki sposób można
tworzyć „surowy” kontroler przez zaimplementowanie interfejsu IController oraz wygodniejsze kontrolery
dziedziczące po klasie Controller. Przedstawiłem również rolę, jaką odgrywają kontrolery na platformie MVC,
oraz wyjaśniłem, dlaczego ułatwiają testowanie jednostkowe. Poznałeś różne sposoby otrzymania danych
wejściowych i wygenerowania danych wyjściowych metody akcji. Ponadto zademonstrowano różne rodzaje
obiektu ActionResult, dzięki którym proces staje się łatwy i elastyczny. W następnym rozdziale zajmiemy się
dokładniej infrastrukturą kontrolerów — poznasz funkcję filtrów, która zmienia sposób przetwarzania żądań.
453
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
454
ROZDZIAŁ 18.

Filtry
Dzięki użyciu filtrów możemy dodawać do potoku przetwarzania żądania dodatkowe funkcje. Są one
prostym i efektywnym sposobem implementacji zadań przekrojowych — termin ten oznacza funkcje, które
są wykorzystywane w całej aplikacji i nie pasują do jednego miejsca, przez co łamią zasadę rozdzielenia zadań.
Klasycznymi przykładami są rejestrowanie danych, autoryzacja i buforowanie. W tym rozdziale pokażę różne
kategorie filtrów obsługiwanych przez platformę MVC, sposoby tworzenia i używania filtrów oraz możliwości
sterowania ich uruchamianiem. W tabeli 18.1 znajdziesz podsumowanie materiału omówionego w rozdziale.
Tabela 18.1. Podsumowanie materiału omówionego w rozdziale
Temat
Rozwiązanie
Listing (nr)
Wstawienie dodatkowej logiki do potoku
przetwarzania żądania
Zastosowanie filtrów w kontrolerze
lub w jego metodach akcji
Od 1. do 8.
Ograniczenie metod akcji dla określonych
użytkowników i grup
Użycie filtrów autoryzacji
Od 9. do 12.
Uwierzytelnianie żądań
Użycie filtrów uwierzytelniania
Od 13. do 19.
Przetwarzanie błędów podczas wykonywania
żądań
Użycie filtrów wyjątków
Od 20. do 30.
Wstawienie logiki ogólnego przeznaczenia
do procesu obsługi żądania
Użycie filtrów akcji
Od 31. do 35.
Analiza lub modyfikacja wyników
wygenerowanych przez metody akcji
Użycie filtrów wyników
Od 36. do 41.
Użycie filtrów bez atrybutów
Użycie wbudowanych metod kontrolerów
42.
Definiowanie filtrów, które mają zastosowanie
dla wszystkich metod akcji w aplikacji
Użycie filtrów globalnych
Od 43. do 46.
Kontrola kolejności wykonywania filtrów
Użycie parametru Order
Od 47. do 49.
Nadpisanie filtrów (globalnych i kontrolera)
dla metody akcji
Użycie możliwości nadpisania filtra
Od 50. do 54.
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Utworzenie przykładowego projektu
W tym rozdziale utworzymy nowy projekt MVC o nazwie Filters. Projekt utwórz na podstawie szablonu
Empty. Nie zapomnij o zaznaczeniu pola wyboru MVC. Następnie dodaj kontroler HomeController wraz
z metodą akcji (listing 18.1). W tym rozdziale skoncentrujemy się na kontrolerach, więc wartościami zwrotnymi
metod akcji będą ciągi tekstowe zamiast obiektów ActionResult. Dzięki temu platforma MVC będzie wysyłała
wartości tekstowe bezpośrednio do przeglądarki internetowej, pomijając silnik widoku Razor.
Listing 18.1. Kod kontrolera HomeController w projekcie Filters
using System.Web.Mvc;
namespace Filters.Controllers {
public class HomeController : Controller {
public string Index() {
return "To jest metoda akcji Index kontrolera Home.";
}
}
}
W dalszej części rozdziału zobaczysz, jak używać nowej funkcji MVC o nazwie filtry uwierzytelniania oraz
do jakich celów można stosować proste uwierzytelnianie użytkowników. Jak już wspomniałem w rozdziale 12.,
w tej książce nie będę omawiał funkcji zabezpieczeń oferowanych przez platformę ASP.NET. Wydawnictwo
Apress zgodziło się na bezpłatne udostępnienie poświęconych temu tematowi rozdziałów z innej mojej książki,
zatytułowanej Pro ASP.NET MVC 5 Platform. Dlatego też w celu zademonstrowania funkcji filtrów
uwierzytelniania (to jest część platformy MVC) zastosuję takie samo podejście jak w rozdziale 12., czyli
zdefiniowanie statycznych danych uwierzytelniających w pliku Web.config. Odpowiednie zmiany
do wprowadzenia w wymienionym pliku przedstawiono na listingu 18.2.
Listing 18.2. Zdefiniowanie w pliku Web.config danych uwierzytelniających użytkownika
...
<system.web>
<compilation debug="true" targetFramework="4.5.1" />
<httpRuntime targetFramework="4.5.1" />
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" timeout="2880">
<credentials passwordFormat="Clear">
<user name="janek" password="sekret"/>
<user name="admin" password="sekret" />
</credentials>
</forms>
</authentication>
</system.web>
...
Zdefiniowaliśmy dwóch użytkowników, janek i admin, oraz przypisaliśmy im takie samo hasło sekret,
co pozwala na zachowanie prostoty przykładu. Ponownie wykorzystujemy uwierzytelnianie formularzy ( forms),
a także atrybut loginUrl wskazujący, że nieuwierzytelnieni użytkownicy powinni być przekierowywani do adresu
URL /Account/Login. Na listingu 18.3 przedstawiono zawartość dodanego do projektu kontrolera Account,
którego akcja Login będzie używana przez domyślną konfigurację routingu.
456
ROZDZIAŁ 18.  FILTRY
Listing 18.3. Zawartość pliku AccountController.cs
using System.Web.Mvc;
using System.Web.Security;
namespace Filters.Controllers {
public class AccountController : Controller {
public ActionResult Login() {
return View();
}
[HttpPost]
public ActionResult Login(string username, string password, string returnUrl) {
bool result = FormsAuthentication.Authenticate(username, password);
if (result) {
FormsAuthentication.SetAuthCookie(username, false);
return Redirect(returnUrl ?? Url.Action("Index", "Admin"));
} else {
ModelState.AddModelError("", "Nieprawidłowa nazwa użytkownika lub hasło.");
return View();
}
}
}
}
W celu utworzenia widoku pobierającego dane uwierzytelniające od użytkownika utwórz katalog
Views/Shared, kliknij prawym przyciskiem myszy, a następnie z menu kontekstowego wybierz opcję Dodaj/Strona
widoku MVC 5 (Razor). Jako nazwę widoku podaj Login, kliknięcie przycisku OK spowoduje utworzenie
pliku Login.cshtml. Teraz w pliku widoku umieść kod przedstawiony na listingu 18.4.
 Uwaga Ponieważ w dalszej części rozdziału dodamy drugi kontroler uwierzytelniania, dlatego tutaj tworzymy
widok współdzielony, co pozwoli na jego ponowne użycie.
Listing 18.4. Zawartość pliku widoku Login.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title></title>
</head>
<body>
@using (Html.BeginForm()) {
@Html.ValidationSummary()
<p><label>Nazwa użytkownika:</label><input name="username" /></p>
<p><label>Hasło:</label><input name="password" type="password"/></p>
<input type="submit" value="Zaloguj" />
}
</body>
</html>
457
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Ustawienie początkowego adresu URL i przetestowanie aplikacji
Podobnie jak w innych przykładowych projektach, chcemy, aby po uruchomieniu aplikacji przez Visual Studio
następowało przejście do jej głównego adresu URL, a nie ustalonego na podstawie ostatniego edytowanego
pliku w projekcie. Z menu Projekt wybierz więc opcję Właściwości Filters…. Przejdź do karty Sieć Web
i w sekcji Uruchom akcję wybierz Określ stronę. Nie trzeba podawać żadnej wartości, wystarczy wybrać
wymienioną opcję. Jeżeli teraz uruchomisz aplikację, otrzymasz odpowiedź pokazaną na rysunku 18.1.
Rysunek 18.1. Uruchomienie przykładowej aplikacji
Użycie filtrów
Przykład filtrów pokazywałem w rozdziale 12., gdy użyliśmy filtra uwierzytelniania do metod akcji kontrolera
z funkcjami administracyjnymi aplikacji SportsStore. Chcieliśmy, aby metody akcji mogły być stosowane
wyłącznie przez uwierzytelnionych użytkowników, co można było zrealizować na kilka sposobów. Mogliśmy
sprawdzać stan uwierzytelniania żądania w każdej metodzie akcji w sposób pokazany na listingu 18.5.
Listing 18.5. Jawne sprawdzanie uwierzytelniania w metodach akcji
namespace SportsStore.WebUI.Controllers {
public class AdminController : Controller {
// … zmienne egzemplarza i konstruktor
public ViewResult Index() {
if (!Request.IsAuthenticated) {
FormsAuthentication.RedirectToLoginPage();
}
// … dalsza część metody akcji
}
public ViewResult Create() {
if (!Request.IsAuthenticated) {
FormsAuthentication.RedirectToLoginPage();
}
// … dalsza część metody akcji
}
public ViewResult Edit(int productId) {
if (!Request.IsAuthenticated) {
FormsAuthentication.RedirectToLoginPage();
}
// … dalsza część metody akcji
}
// … inne metody akcji
}
}
Jak widać, w przypadku zastosowania tego podejścia pojawia się wiele powtórzeń, dlatego zdecydowaliśmy
się na użycie filtrów, jak pokazano na listingu 18.6.
458
ROZDZIAŁ 18.  FILTRY
Listing 18.6. Użycie filtrów
namespace SportsStore.WebUI.Controllers {
[Authorize]
public class AdminController : Controller {
// … zmienne egzemplarza i konstruktor
public ViewResult Index() {
// … dalsza część metody akcji
}
public ViewResult Create() {
// … dalsza część metody akcji
}
public ViewResult Edit(int productId) {
// … dalsza część metody akcji
}
// … inne metody akcji
}
}
Filtry są atrybutami .NET, które powodują dodanie dodatkowych kroków do procesu przetwarzania żądania.
Na listingu 18.6 użyliśmy filtra Authorize, który daje taki sam efekt jak wielokrotne testy z listingu 18.5.
Wprowadzenie do podstawowych typów filtrów
Platforma MVC obsługuje pięć podstawowych typów filtrów. Każdy pozwala dodać logikę w innym
punkcie potoku przetwarzania żądania. Te typy filtrów są opisane w tabeli 18.2.
Tabela 18.2. Typy filtrów na platformie MVC
Typ filtra
Interfejs
Domyślna implementacja
Opis
Authentication
IAuthenticationFilter
-
Uruchamiany jako pierwszy przed
uruchomieniem innych filtrów
lub metod akcji, ale może być
uruchomiony także po filtrach
autoryzacji.
Authorization
IAuthorizationFilter
AuthorizeAttribute
Uruchamiany jako drugi po filtrach
uwierzytelniania, ale może być
również uruchomiony przed innymi
filtrami lub metodami akcji.
Action
IActionFilter
ActionFilterAttribute
Uruchamiany przed metodą akcji
i po niej.
Result
IResultFilter
ActionFilterAttribute
Uruchamiany przed uruchomieniem
wyniku akcji i po nim.
Exception
IExceptionFilter
HandleErrorAttribute
Uruchamiany jedynie wtedy, gdy
inny filtr, metoda akcji lub wynik
akcji zgłasza wyjątek.
459
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Zanim platforma MVC wywoła akcję, sprawdza definicję metody w celu ustalenia, czy posiada atrybuty będące
implementacją interfejsów wymienionych w tabeli 18.2. Jeżeli istnieją, to w odpowiednim punkcie potoku
przetwarzania żądania wywoływane są metody definiowane przez te interfejsy. Platforma zawiera domyślne
klasy atrybutów, które implementują interfejsy filtrów. W dalszej części rozdziału pokażę, jak użyć tych klas.
 Wskazówka Na platformie MVC 5 wprowadzono nowy interfejs IoverrideFilter, który zostanie omówiony
w dalszej części rozdziału.
Klasa ActionFilterAttribute implementuje zarówno interfejs IActionFilter, jak i IResultFilter. Jest
to klasa abstrakcyjna, która wymusza na nas dostarczenie implementacji. Z kolei klasy AuthorizeAttribute oraz
HandleErrorAttribute zawierają użyteczne funkcje, które można wykorzystać bez konieczności tworzenia klas
pochodnych.
Dołączanie filtrów do kontrolerów i metod akcji
Filtry możemy stosować do pojedynczych metod akcji lub do całego kontrolera. Na listingu 18.6 dodaliśmy filtr
Authorize do klasy AdminController, co ma taki sam efekt jak dodanie go do każdej metody akcji w kontrolerze,
jak pokazano na listingu 18.7.
Listing 18.7. Dodawanie filtra do poszczególnych metod akcji
namespace SportsStore.WebUI.Controllers {
public class AdminController : Controller {
// … zmienne egzemplarza i konstruktor
[Authorize]
public ViewResult Index() {
// … dalsza część metody akcji
}
[Authorize]
public ViewResult Create() {
// … dalsza część metody akcji
}
// … inne metody akcji
}
}
Możliwe jest stosowanie wielu filtrów i mieszanie poziomów, na których są używane — czyli na poziomie
kontrolera lub pojedynczych metod akcji. Na listingu 18.8 pokazane jest wykorzystanie różnych filtrów.
Listing 18.8. Stosowanie wielu filtrów w klasie kontrolera
[Authorize(Roles="trader")] // odnosi się do wszystkich akcji
public class ExampleController : Controller {
[ShowMessage]
[OutputCache(Duration=60)]
public ActionResult Index() {
// … treść metody akcji
}
}
460
// odnosi się tylko do tej akcji
// odnosi się tylko do tej akcji
ROZDZIAŁ 18.  FILTRY
Niektóre filtry przedstawione na tym listingu posiadają parametry. Sposób ich działania omówię w punktach
poświęconych tym filtrom.
 Uwaga Jeżeli zdefiniowałeś klasę bazową dla kontrolerów, wszystkie filtry umieszczone w klasie bazowej będą
działały również we wszystkich klasach pochodnych.
Użycie filtrów autoryzacji
Filtry autoryzacji są uruchamiane po filtrach uwierzytelniania, przed filtrami akcji oraz przed wywołaniem
metody akcji. Jak sugeruje nazwa, są to filtry wymuszające politykę autoryzacji i zapewniające, że metody
akcji będą wywoływane wyłącznie przez uprawnionych użytkowników.
Istnieje pewien związek między filtrami uwierzytelniania i autoryzacji, który będzie można łatwiej wyjaśnić,
gdy poznasz sposób działania filtrów autoryzacji. Wspomniany związek zostanie więc omówiony w dalszej części
rozdziału. Filtry autoryzacji implementują interfejs IAuthorizationFilter, zamieszczony na listingu 18.9.
Listing 18.9. Interfejs IAuthorizationFilter
namespace System.Web.Mvc {
public interface IAuthorizationFilter {
void OnAuthorization(AuthorizationContext filterContext);
}
}
Masz możliwość utworzenia klasy implementującej interfejs IAuthorizationFilter i samodzielne
przygotowanie kodu zabezpieczeń. Tekst przedstawiony w ramce poniżej informuje, dlaczego tego rodzaju
podejście jest naprawdę nietrafione.
Ostrzeżenie — tworzenie kodu zabezpieczeń jest ryzykowne
Historia programowania obfituje w przykłady problemów z aplikacjami, których programiści uważali, że potrafią
tworzyć dobry kod zabezpieczeń. W rzeczywistości tę umiejętność posiada niewielu. Zwykle w kodzie znajdują się
zapomniane zakamarki lub nieprzetestowane przypadki brzegowe, które powodują powstanie luk w bezpieczeństwie
aplikacji. Jeśli mi nie wierzysz, to w ulubionej wyszukiwarce internetowej wpisz wyrażenie security bug i zacznij
przeglądać znalezione strony.
Jeżeli jest to tylko możliwe, korzystam z kodu bezpieczeństwa, który został wielokrotnie sprawdzony. W tym
przypadku platforma MVC zapewnia kompletny filtr autoryzacji, którego można użyć przy implementowaniu własnych
zasad autoryzacji. Jeżeli tylko mogę, próbuję go stosować i zalecam takie samo postępowanie. W najgorszym przypadku,
gdy nasze tajne dane aplikacji staną się powszechnie dostępne w internecie, mogę poskarżyć się na Microsoft.
Znacznie bezpieczniejszym rozwiązaniem jest utworzenie podklasy klasy AuthorizeAttribute, która zajmie się
obsługą wszystkich szczegółów i ułatwi przygotowanie własnego kodu odpowiedzialnego za obsługę autoryzacji.
Najlepszy sposób zademonstrowania takiego rozwiązania to utworzenie własnego filtra. Do projektu dodaj
katalog Infrastructure i umieść w nim nowy plik klasy o nazwie CustomAuthAttribute.cs. Kod tego pliku
znajduje się na listingu 18.10.
Listing 18.10. Zawartość pliku CustomAuthAttribute.cs
using System.Web;
using System.Web.Mvc;
461
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
namespace Filters.Infrastructure {
public class CustomAuthAttribute : AuthorizeAttribute {
private bool localAllowed;
public CustomAuthAttribute(bool allowedParam) {
localAllowed = allowedParam;
}
protected override bool AuthorizeCore(HttpContextBase httpContext) {
if (httpContext.Request.IsLocal) {
return localAllowed;
} else {
return true;
}
}
}
}
To jest bardzo prosty filtr autoryzacji. Dzięki niemu można zablokować dostęp do żądań lokalnych
(o żądaniu lokalnym mówimy w sytuacji, gdy przeglądarka internetowa i serwer aplikacji działają w tym
samym urządzeniu, np. używanym przez Ciebie komputerze).
Zastosowane zostało najprostsze podejście w zakresie tworzenia filtra autoryzacji, czyli utworzenie
klasy dziedziczącej po AuthorizeAttribute i nadpisanie jego metody AuthorizeCore. Dzięki temu możemy
skorzystać z funkcji dostępnych w AuthorizeAttribute. Konstruktor filtra pobiera wartość boolowską
wskazującą, czy wykonywanie żądań lokalnych jest dozwolone.
Najbardziej interesującą częścią tej klasy jest implementacja metody AuthorizeCore używanej przez platformę
MVC do sprawdzenia, czy filtr autoryzuje dostęp do żądania. Parametr przekazywany do tej metody jest obiektem
klasy HttpContextBase. Dzięki niemu możemy odwołać się do danych żądania. Wykorzystując wbudowane
funkcje klasy bazowej AuthorizeAttribute, musimy się skoncentrować jedynie na logice uwierzytelniania i zwrócić
wartość true z metody AuthorizeCore, jeśli żądanie ma zostać autoryzowane, i false — w przeciwnym razie.
Zachowaj prostotę atrybutów autoryzacji
Metodzie AuthorizeCore jest przekazywany obiekt HttpContextBase zapewniający dostęp do informacji o żądaniu,
ale nie o kontrolerze lub metodzie akcji, w stosunku do której został zastosowany filtr autoryzacji. Głównym powodem
bezpośredniego implementowania interfejsu IAuthorizationFilter jest uzyskanie dostępu do obiektu
AuthorizationContext przekazanemu metodzie OnAuthorization. Dzięki wymienionemu obiektowi
AuthorizationContext można uzyskać znacznie dokładniejsze informacje, między innymi szczegóły dotyczące
routingu, nazwę aktualnego kontrolera i metody akcji.
Nie zalecam stosowania tego rodzaju podejścia, i to nie tylko dlatego, że samodzielne tworzenie kodu zabezpieczeń
jest ryzykowne. Wprawdzie autoryzacja to zadanie przekrojowe, ale umieszczenie logiki w atrybutach autoryzacyjnych,
które są ściśle powiązane ze strukturą kontrolerów, osłabia zasadę podziału zadań i prowadzi do powstawania
problemów podczas testowania i obsługi aplikacji. Staraj się zachować prostotę atrybutów autoryzacyjnych
i skoncentruj się na autoryzacji opartej na żądaniu — pozwól, aby kontekst tego, co jest autoryzowane,
pochodził z miejsca stosowania atrybutu.
Użycie własnego filtra autoryzacji
Aby użyć własnego filtra autoryzacji, dodajemy atrybut do kontrolera lub metody akcji, którą chcemy chronić,
w sposób pokazany na listingu 18.11. Na listingu pokazano sposób zastosowania filtra dla metody akcji Index
kontrolera HomeController przykładowego projektu.
462
ROZDZIAŁ 18.  FILTRY
Listing 18.11. Użycie własnego filtra autoryzacji w pliku HomeController.cs
using System.Web.Mvc;
using Filters.Infrastructure;
namespace Filters.Controllers {
public class HomeController : Controller {
[CustomAuth(false)]
public string Index() {
return "To jest metoda akcji Index kontrolera Home.";
}
}
}
Argumentowi konstruktora została przypisana wartość false, co oznacza, że żądania lokalne nie mają
dostępu do metody akcji Index. Możesz się o tym przekonać, uruchamiając aplikację — domyślna konfiguracja
routingu powoduje wywołanie metody akcji Index, gdy adresem URL żądanym przez przeglądarkę internetową
jest /. Jeśli przeglądarka internetowa wykonująca żądanie znajduje się w komputerze, w którym zostało
uruchomione narzędzie Visual Studio, wówczas otrzymasz efekt pokazany na rysunku 18.2. Filtr autoryzacji
uniemożliwi wykonanie żądania, a platforma MVC udziela odpowiedzi w jedyny znany jej sposób, czyli prosi
użytkownika o podanie danych uwierzytelniających. Oczywiście podanie nazwy użytkownika i hasła nie
zmienia faktu, że żądanie pochodzi z komputera lokalnego. Na tym etapie nie możesz więc przejść przez etap
uwierzytelniania.
Rysunek 18.2. Brak dostępu dla żądania lokalnego — skutek działania własnego filtra autoryzacji
Jednak filtr autoryzacji zezwoli na wykonanie żądania po zmianie na true parametru konstruktora,
a następnie ponownym uruchomieniu aplikacji. (Nie możesz przetestować aplikacji, wykonując żądanie
z poziomu innego komputera, ponieważ serwer IIS Express, w którym została uruchomiona aplikacja,
jest skonfigurowany w taki sposób, aby odrzucać wszystkie żądania pochodzące z zewnątrz).
Użycie wbudowanego filtra autoryzacji
Wprawdzie klasy AuthorizeAttribute użyliśmy jako klasy bazowej dla własnego filtra, ale wymieniona klasa
również posiada własną implementację metody AuthorizeCore, co czyni ją użyteczną podczas wykonywania
ogólnych zadań autoryzacji. Podczas bezpośredniego korzystania z AuthorizeAttribute możemy określić nasze
zasady autoryzacji przy wykorzystaniu dwóch publicznych właściwości tej klasy, zamieszczonych w tabeli 18.3.
Tabela 18.3. Właściwości klasy AuthorizeAttribute
Nazwa
Typ
Opis
Users
String
Rozdzielana przecinkami lista nazw użytkowników, którzy mogą korzystać z metody akcji.
Roles
String
Rozdzielana przecinkami lista nazw ról. Aby wykonać metodę akcji, użytkownik musi mieć
co najmniej jedną z tych ról.
463
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Na listingu 18.12 pokazałem, w jaki sposób możemy korzystać z jednej z wymienionych właściwości
w celu ochrony metody akcji.
Listing 18.12. Użycie wbudowanego filtra autoryzacji
using System.Web.Mvc;
using Filters.Infrastructure;
namespace Filters.Controllers {
public class HomeController : Controller {
[Authorize(Users="admin")]
public string Index() {
return "To jest metoda akcji Index kontrolera Home.";
}
}
}
Na listingu tym określiliśmy, że użytkownik admin może wywoływać metodę akcji. Istnieje również
niejawny warunek — żądanie musi być uwierzytelnione. Jeżeli nie określimy żadnego użytkownika ani
żadnej roli, to każdy uwierzytelniony użytkownik będzie mógł wykorzystać tę metodę. Zasady autoryzacji
zapewniane przez AuthorizeAttribute są wystarczające w większości zastosowań. Jeżeli chcesz zaimplementować
coś specjalnego, możesz dziedziczyć po tej klasie, jak to przedstawiono we wcześniejszej części rozdziału.
Ewentualnie musisz uzupełnić konfigurację filtrami uwierzytelniania, co zostanie omówione w kolejnym
podrozdziale.
Użycie filtrów uwierzytelniania
Filtry uwierzytelniania są nowością na platformie MVC 5 i mają na celu zapewnić dokładniejszą kontrolę nad
sposobem uwierzytelniania użytkowników dla kontrolerów i akcji w aplikacji.
Filtry uwierzytelniania mają stosunkowo skomplikowany cykl życiowy. Ponieważ są uruchamiane przed
wszystkimi pozostałymi filtrami, zyskujesz możliwość zdefiniowania polityki uwierzytelniania, która będzie
zastosowana jeszcze przed użyciem innego rodzaju filtrów. Filtry uwierzytelniania można łączyć z filtrami
autoryzacji i tym samym zapewnić obsługę uwierzytelniania żądań niezgodnych z polityką autoryzacji. Filtry
uwierzytelniania są uruchamiane także po wykonaniu metody akcji, ale jeszcze przed przetworzeniem wyniku
akcji (ActionResult). Wyjaśnię dokładnie sposób ich działania i przedstawię pewne przykłady.
Interfejs IAuthenticationFilter
Filtry uwierzytelniania implementują interfejs IAuthenticationFilter, który przedstawiono na listingu 18.13.
Listing 18.13. Interfejs IAuthenticationFilter
namespace System.Web.Mvc.Filters {
public interface IAuthenticationFilter {
void OnAuthentication(AuthenticationContext context);
void OnAuthenticationChallenge(AuthenticationChallengeContext context);
}
}
Metoda OnAuthenticationChallenge jest wywoływana przez platformę MVC, gdy żądanie jest niezgodne z
polityką uwierzytelniania lub autoryzacji dla metody akcji. Metodzie OnAuthenticationChallenge zostaje
przekazany obiekt AuthenticationChallengeContext dziedziczący po klasie ControllerContext omówionej
w rozdziale 17. Definiuje także dwie dodatkowe właściwości wymienione w tabeli 18.4.
464
ROZDZIAŁ 18.  FILTRY
Tabela 18.4. Właściwości definiowane przez klasę AuthenticationChallengeContext
Nazwa
Opis
ActionDescriptor
Zwraca obiekt ActionDescriptor opisujący metodę akcji, do której został zastosowany filtr.
Result
Definiuje obiekt ActionResult wyrażający wynik uwierzytelniania.
Najważniejszą właściwością jest Result, ponieważ pozwala filtrowi uwierzytelniania na przekazanie
ActionResult platformie MVC. To jest proces o nazwie short-circuiting, który zostanie wkrótce omówiony.
Najlepszym sposobem wyjaśnienia sposobu działania filtra uwierzytelniania jest posłużenie się przykładem.
Według mnie najbardziej interesującym aspektem filtrów uwierzytelniania jest to, że pozwalają one
pojedynczemu kontrolerowi na zdefiniowanie metod akcji uwierzytelnianych na różne sposoby. Pierwszym
krokiem będzie więc dodanie nowego kontrolera symulującego logowanie do usługi Google. Na listingu
18.14 przedstawiono kod kontrolera GoogleAccountController.
Listing 18.14. Zawartość pliku GoogleAccountController.cs
using System.Web.Mvc;
using System.Web.Security;
namespace Filters.Controllers {
public class GoogleAccountController : Controller {
public ActionResult Login() {
return View();
}
[HttpPost]
public ActionResult Login(string username, string password, string returnUrl) {
if (username.EndsWith("@google.com") && password == "sekret") {
FormsAuthentication.SetAuthCookie(username, false);
return Redirect(returnUrl ?? Url.Action("Index", "Home"));
} else {
ModelState.AddModelError("", "Nieprawidłowa nazwa użytkownika lub hasło.");
return View();
}
}
}
}
Nie chcę implementować rzeczywistego logowania do konta Google, ponieważ oznacza to konieczność
zagłębienia się w kod uwierzytelniania opracowany przez firmę trzecią, co jest obszernym tematem samym w sobie.
Zamiast tego posłużymy się okropną sztuczką polegającą na uwierzytelnianiu każdego użytkownika, którego
nazwa kończy się na @google.com, o ile jego hasło to sekret.
Na tym etapie kontroler GoogleAccountController nie jest w żaden sposób powiązany z aplikacją.
Tutaj do gry wchodzą filtry uwierzytelniania. W katalogu Infrastructure tworzymy plik nowej klasy
GoogleAuthAttribute.cs, którego kod przedstawiono na listingu 18.15. Klasa FilterAttribute, po której
dziedziczy prezentowana GoogleAuthAttribute jest klasą bazową dla wszystkich klas filtrów.
Listing 18.15. Zawartość pliku GoogleAuthAttribute.cs
using
using
using
using
System;
System.Web.Mvc;
System.Web.Mvc.Filters;
System.Web.Routing;
namespace Filters.Infrastructure {
public class GoogleAuthAttribute : FilterAttribute, IAuthenticationFilter {
465
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
public void OnAuthentication(AuthenticationContext context) {
// nie zaimplementowano
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext context) {
if (context.Result == null) {
context.Result = new RedirectToRouteResult(new RouteValueDictionary {
{"controller", "GoogleAccount"},
{"action", "Login"},
{"returnUrl", context.HttpContext.Request.RawUrl}
});
}
}
}
}
Moja implementacja metody OnAuthenticationChallenge sprawdza, czy ustawiona została właściwość Result
argumentu AuthenticationChallengeContext. W ten sposób można uniknąć zmuszania użytkownika do
uwierzytelniania, gdy filtr jest uruchamiany po wykonaniu metody akcji. Teraz się tym nie przejmuj.
W dalszej części rozdziału dowiesz się, dlatego to jest tak ważne.
Teraz najważniejszy jest fakt użycia metody OnAuthenticationChallenge w celu zmuszenia użytkownika
do podania danych uwierzytelniających. W tym celu następuje użycie RedirectToRouteResult, aby przekierować
użytkownika do kontrolera GoogleAccount. Filtry uwierzytelniania mogą używać dowolnego typu wyniku akcji
(ActionResult) z omówionych w rozdziale 17. Jednak wygodne metody klasy Controller przeznaczone do
tworzenia ActionResult są niedostępne i dlatego konieczne jest wykorzystanie obiektu RouteValueDictionary
w celu wskazania wartości segmentu. To pozwala na wygenerowanie trasy do metody akcji uwierzytelniania.
Implementacja sprawdzenia uwierzytelniania
Mój filtr uwierzytelniania jest gotowy do pobierania od użytkowników fikcyjnych danych uwierzytelniających.
Możemy więc przystąpić do konfiguracji pozostałych funkcji. Kontroler będzie wywoływał metodę
OnAuthentication przed uruchomieniem jakiegokolwiek rodzaju filtra i tym samym zapewni możliwość
przeprowadzenia wczesnego sprawdzenia uwierzytelnienia użytkownika. Nie musisz implementować metody
OnAuthentication, ale ja w omawianym przykładzie to zrobię, aby móc sprawdzić, czy na pewno mamy
do czynienia z kontem Google.
Metodzie OnAuthentication jest przekazywany obiekt AuthenticationContext, który — podobnie jak klasa
AuthenticationChallengeContext — jest pochodną ControllerContext i zapewnia dostęp do wszystkich danych
omówionych w rozdziale 17. Klasa AuthenticationContext ponadto definiuje właściwości wymienione
w tabeli 18.5.
Tabela 18.5. Właściwości definiowane przez klasę AuthenticationContext
Nazwa
Opis
ActionDescriptor
Zwraca obiekt ActionDescriptor opisujący metodę akcji, do której został zastosowany filtr.
Principal
Zwraca implementację IPrincipal identyfikującą bieżącego użytkownika, o ile został
uwierzytelniony.
Result
Definiuje obiekt ActionResult wyrażający wynik uwierzytelniania.
Jeżeli metoda OnAuthentication ustawi wartość dla właściwości Result obiektu kontekstu, wówczas
platforma wywoła metodę OnAuthenticationChallenge. Jeżeli metoda OnAuthenticationChallenge w swoim
obiekcie kontekstu nie ustawi wartości dla właściwości Result, wtedy zostanie użyta wartość pochodząca
z metody OnAuthentication.
466
ROZDZIAŁ 18.  FILTRY
Metody OnAuthentication używam do przygotowania wyniku zgłaszającego użytkownikowi błąd powstały
w trakcie uwierzytelniania. Ten wynik można nadpisać metodą OnAuthenticationChallenge w celu zmuszenia
użytkownika do podania danych uwierzytelniających, zamiast wyświetlać mu komunikat błędu. Dzięki temu
mam gwarancję, że użytkownik otrzyma jasny komunikat, nawet jeśli nie zostało przeprowadzone
uwierzytelnianie (muszę przyznać, że jeszcze nie spotkałem się z taką sytuacją). Na listingu 18.16
możesz zobaczyć, jak zaimplementowałem metodę OnAuthentication, aby sprawdzała, czy żądanie
zostało uwierzytelnione przez użycie jakichkolwiek danych uwierzytelniających Google.
Listing 18.16. Implementacja metody OnAuthentication w pliku GoogleAuthAttribute.cs
using
using
using
using
using
System;
System.Security.Principal;
System.Web.Mvc;
System.Web.Mvc.Filters;
System.Web.Routing;
namespace Filters.Infrastructure {
public class GoogleAuthAttribute : FilterAttribute, IAuthenticationFilter {
public void OnAuthentication(AuthenticationContext context) {
IIdentity ident = context.Principal.Identity;
if (!ident.IsAuthenticated || !ident.Name.EndsWith("@google.com")) {
context.Result = new HttpUnauthorizedResult();
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext context) {
if (context.Result == null || context.Result is HttpUnauthorizedResult) {
context.Result = new RedirectToRouteResult(new RouteValueDictionary {
{"controller", "GoogleAccount"},
{"action", "Login"},
{"returnUrl", context.HttpContext.Request.RawUrl}
});
}
}
}
}
Moja implementacja metody OnAuthentication sprawdza, czy żądanie zostało uwierzytelnione z użyciem
nazwy użytkownika kończącej się na @google.com. Jeżeli żądanie nie zostało uwierzytelnione lub jest
uwierzytelnione z użyciem innych danych uwierzytelniających, wtedy właściwości Result obiektu
AuthenticationContext zostaje przypisana wartość HttpUnauthorizedResult.
HttpUnauthorizedResult staje się wartością właściwości Result obiektu AuthenticationChallengeContext
przekazywanego metodzie OnAuthenticationChallenge. Jak możesz zobaczyć, uaktualniłem tę metodę w celu
żądania uwierzytelnienia użytkownika, gdy wystąpi wymieniona sytuacja. W ten sposób akcje dwóch metod
są koordynowane w filtrze. Kolejnym krokiem jest zastosowanie filtra w kontrolerze, jak przedstawiono na
listingu 18.17.
Listing 18.17. Zastosowanie filtra uwierzytelniania w pliku HomeController.cs
using System.Web.Mvc;
using Filters.Infrastructure;
namespace Filters.Controllers {
public class HomeController : Controller {
[Authorize(Users = "admin")]
public string Index() {
return "To jest metoda akcji Index kontrolera Home.";
}
467
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
[GoogleAuth]
public string List() {
return "To jest metoda akcji List kontrolera Home.";
}
}
}
W kodzie zdefiniowano nową metodę akcji o nazwie List udekorowaną filtrem GoogleAuth. Wynikiem jest to,
że dostęp do metody Index został zabezpieczony za pomocą wbudowanego mechanizmu uwierzytelniania
formularzy, natomiast dostęp do metody akcji List chroni nasz fikcyjny system uwierzytelniania Google.
Efekt wprowadzonych zmian możesz zobaczyć po uruchomieniu aplikacji. Domyślnie przeglądarka
internetowa będzie uruchamiała metodę akcji Index, co spowoduje wywołanie standardowego mechanizmu
uwierzytelniania. W tym przypadku trzeba podać jedną z nazw użytkownika, które zdefiniowaliśmy wcześniej
w pliku Web.config. Jeżeli przejdziemy do adresu /Home/List, wówczas istniejące dane uwierzytelniające
zostaną odrzucone i konieczne będzie uwierzytelnienie się z użyciem nazwy użytkownika konta Google.
Połączenie filtrów uwierzytelniania i autoryzacji
Istnieje możliwość połączenia w tej samej metodzie filtrów uwierzytelniania i autoryzacji w celu zawężenia
zasięgu polityki bezpieczeństwa. Platforma MVC będzie wywoływała metodę OnAuthentication filtra
uwierzytelniania, jak w poprzednim przykładzie. Jeżeli żądanie zostanie prawidłowo uwierzytelnione, wtedy
nastąpi uruchomienie filtra autoryzacji. W przypadku nieudanej autoryzacji zostanie wywołana metoda
OnAuthenticationChallenge filtra uwierzytelniania, aby można było zażądać od użytkownika podania danych
uwierzytelniających. Na listingu 18.18 pokazano przykład połączenia filtrów GoogleAuth i Authorize w celu
ograniczenia dostępu do metody akcji List kontrolera Home.
Listing 18.18. Połączenie filtrów uwierzytelniania i autoryzacji w pliku HomeController.cs
using System.Web.Mvc;
using Filters.Infrastructure;
namespace Filters.Controllers {
public class HomeController : Controller {
[Authorize(Users = "admin")]
public string Index() {
return "To jest metoda akcji Index kontrolera Home.";
}
[GoogleAuth]
[Authorize(Users = "[email protected]")]
public string List() {
return "To jest metoda akcji List kontrolera Home.";
}
}
}
Filtr Authorize zapewnia dostęp jedynie użytkownikowi [email protected]. Jeżeli metoda akcji zostanie
wywołana przez użytkownika innego konta Google, metoda OnAuthenticationChallenge filtra uwierzytelniania
otrzyma obiekt AuthenticationChallengeContext, którego właściwość Result będzie miała przypisany
egzemplarz klasy HttpUnauthorizedResult (dlatego w metodzie OnAuthentication użyłem tej samej klasy).
Filtry w kontrolerze Home zapewniają dostęp do metody Index jedynie użytkownikowi admin, który
uwierzytelnia się za pomocą AccountController. Natomiast dostęp do metody List jest zarezerwowany
tylko dla użytkownika [email protected] uwierzytelnianego za pomocą kontrolera GoogleAccount.
468
ROZDZIAŁ 18.  FILTRY
Obsługa ostatniego uwierzytelnienia w żądaniu
Platforma MVC wywołuje metodę OnAuthenticationChallenge po raz ostatni po wykonaniu metody akcji, ale
jeszcze przed zwrotem i wykonaniem ActionResult. W ten sposób filtr uwierzytelniania ma możliwość reakcji
na zakończenie wykonywania metody akcji lub nawet zmiany wyniku (to jest możliwe również za pomocą
filtrów wyniku, które zostaną omówione w dalszej części rozdziału).
Zastosowano takie rozwiązanie, ponieważ w metodzie OnAuthenticationChallenge sprawdzamy właściwość
Result obiektu AuthenticationChallengeContext. W przeciwnym razie użytkownik byłby ponownie proszony
o podanie danych uwierzytelniających, co praktycznie nie ma sensu, skoro metoda akcji została już wykonana.
Jedynym powodem (jaki znalazłem) udzielenia odpowiedzi na ostatnie wywołanie metody jest wyzerowanie
uwierzytelnienia dla żądania. Taka możliwość jest użyteczna, gdy ważna metoda akcji wymaga tymczasowego
zwiększenia uprawnień i chcemy, aby użytkownik musiał podawać dane uwierzytelniające w trakcie każdego
uruchamiania takiej metody akcji. Na listingu 18.19 przedstawiono przykład implementacji wymienionej
funkcjonalności.
Listing 18.19. Obsługa ostatniego uwierzytelnienia w pliku GoogleAuthAttribute.cs
using
using
using
using
using
using
System;
System.Security.Principal;
System.Web.Mvc;
System.Web.Mvc.Filters;
System.Web.Routing;
System.Web.Security;
namespace Filters.Infrastructure {
public class GoogleAuthAttribute : FilterAttribute, IAuthenticationFilter {
public void OnAuthentication(AuthenticationContext context) {
IIdentity ident = context.Principal.Identity;
if (!ident.IsAuthenticated || !ident.Name.EndsWith("@google.com")) {
context.Result = new HttpUnauthorizedResult();
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext context) {
if (context.Result == null || context.Result is HttpUnauthorizedResult) {
context.Result = new RedirectToRouteResult(new RouteValueDictionary {
{"controller", "GoogleAccount"},
{"action", "Login"},
{"returnUrl", context.HttpContext.Request.RawUrl}
});
} else {
FormsAuthentication.SignOut();
}
}
}
}
Efekt wprowadzonych zmian możesz zobaczyć po uruchomieniu aplikacji i przejściu do adresu URL
/Home/List. Zostaniesz poproszony o podanie danych uwierzytelniających. Jeżeli uwierzytelnisz się jako
[email protected], wtedy metoda akcji zostanie wykonana. Po odświeżeniu strony, czyli po ponownym
wywołaniu metody List, zostaniesz ponownie poproszony o podanie danych uwierzytelniających
469
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Użycie filtrów wyjątków
Filtry wyjątków są uruchamiane wyłącznie w przypadku, gdy metoda akcji zgłosi nieobsłużony wyjątek.
Wyjątek może pochodzić z następujących lokalizacji:
 innego rodzaju filtra (autoryzacji, akcji lub wyniku),
 samej metody akcji,
 wyniku akcji (informacje na temat wyników akcji są przedstawione w rozdziale 17.).
Tworzenie filtra wyjątku
Filtry wyjątków implementują interfejs IExceptionFilter, zamieszczony na listingu 18.20.
Listing 18.20. Interfejs IExceptionFilter
namespace System.Web.Mvc {
public interface IExceptionFilter {
void OnException(ExceptionContext filterContext);
}
}
Metoda OnException jest wywoływana w momencie wykrycia nieobsłużonego wyjątku. Parametrem tej metody
jest obiekt ExceptionContext, który dziedziczy po ControllerContext i definiuje kilka użytecznych właściwości
pozwalających na pobranie informacji o żądaniu. Wspomniane właściwości zostały wymienione w tabeli 18.6.
Tabela 18.6. Użyteczne właściwości klasy ControllerContext
Nazwa
Typ
Opis
Controller
ControllerBase
Zwraca obiekt kontrolera dla bieżącego żądania.
HttpContext
HttpContextBase
Zapewnia dostęp do szczegółów żądania oraz do odpowiedzi.
IsChildAction
bool
Zwraca true, jeżeli jest to akcja potomna (przedstawiona będzie
w rozdziale 20.).
RequestContext
RequestContext
Zapewnia dostęp do HttpContext oraz danych routingu,
które są dostępne również poprzez inne właściwości.
RouteData
RouteData
Zwraca dane routingu dla bieżącego żądania.
Oprócz właściwości dziedziczonych po klasie ControllerContext, klasa ExceptionContext definiuje kilka
dodatkowych właściwości (przedstawiono je w tabeli 18.7), które są użyteczne podczas obsługi wyjątków.
Tabela 18.7. Dodatkowe właściwości klasy ExceptionContext
Nazwa
Typ
Opis
ActionDescriptor
ActionDescriptor
Udostępnia dane na temat metody akcji.
Result
ActionResult
Wynik metody akcji; filtr może anulować żądanie przez przypisanie
do tej właściwości wartości różnej od null.
Exception
Exception
Nieobsłużony wyjątek.
ExceptionHandled
bool
Zwraca true, jeżeli inny filtr oznaczył wyjątek jako obsłużony.
470
ROZDZIAŁ 18.  FILTRY
Zgłoszony wyjątek jest dostępny poprzez właściwość Exception. Filtr wyjątku może poinformować o obsłużeniu
wyjątku przez ustawienie właściwości ExceptionHandled na true. Wywoływane są wszystkie filtry wyjątków
dodane do akcji, nawet jeżeli ta właściwość ma wartość true, więc dobrą praktyką jest sprawdzanie, czy inny
filtr obsłużył już problem, dzięki czemu nie będziemy próbować rozwiązywać problemu rozwiązanego już przez
inny filtr.
 Uwaga Jeżeli żaden z filtrów wyjątków metody akcji nie ustawi właściwości ExceptionHandled na true, platforma
MVC użyje domyślnej procedury obsługi wyjątków w ASP.NET. Wyświetla ona żółty „ekran śmierci”.
Właściwość Result jest wykorzystywana przez filtr wyjątku do poinformowania platformy MVC o operacjach
do wykonania. Dwoma podstawowymi zastosowaniami filtrów wyjątków są rejestrowanie wyjątków oraz
wyświetlanie odpowiedniego komunikatu użytkownikowi. Aby zademonstrować całość, utworzymy plik
nowej klasy o nazwie RangeExceptionAttribute.cs, który należy dodać do katalogu Infrastructure projektu.
Kod wspomnianej klasy został przedstawiony na listingu 18.21.
Listing 18.21. Zawartość pliku RangeExceptionAttribute.cs
using System;
using System.Web.Mvc;
namespace Filters.Infrastructure {
public class RangeExceptionAttribute: FilterAttribute, IExceptionFilter {
public void OnException(ExceptionContext filterContext) {
if (!filterContext.ExceptionHandled &&
filterContext.Exception is ArgumentOutOfRangeException)
{
filterContext.Result = new RedirectResult("~/Content/RangeErrorPage.html");
filterContext.ExceptionHandled = true;
}
}
}
}
Filtr ten obsługuje wyjątki ArgumentOutOfRangeException poprzez przekierowanie użytkownika do pliku
RangeErrorPage.html znajdującego się w katalogu Content. Zwróć uwagę, że klasa RangeExceptionAttribute
wywodzi się z klasy FilterAttribute i implementuje interfejs IExceptionFilter. Aby klasa atrybutu .NET była
traktowana jak filtr MVC, klasa musi implementować interfejs IMvcFilter. Wprawdzie możesz to zrobić
bezpośrednio, ale łatwiejszym sposobem utworzenia filtra jest użycie klasy wywodzącej się z klasy FilterAttribute,
która implementuje wymagany interfejs, a poza tym oferuje pewne użyteczne funkcje, np. obsługę domyślnej
kolejności przetwarzania filtrów (więcej na ten temat dowiesz się w dalszej części rozdziału).
Użycie filtra wyjątków
Przed użyciem filtra wyjątku trzeba poczynić pewne przygotowania. Przede wszystkim konieczne jest utworzenie
katalogu Content w projekcie, a następnie umieszczenie w nim pliku o nazwie RangeErrorPage.html. Wymieniony
plik będzie używany do wyświetlania prostego komunikatu. Kod pliku przedstawiono na listingu 18.22.
Listing 18.22. Zawartość pliku RangeErrorPage.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Błąd</title>
471
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
</head>
<body>
<h2>Przepraszamy</h2>
<span>Wartość jednego z argumentów jest spoza oczekiwanego zakresu.</span>
</body>
</html>
Kolejnym krokiem jest dodanie do kontrolera Home metody akcji odpowiedzialnej za zgłoszenie
interesującego nas wyjątku. W kontrolerze Home wprowadź zmiany przedstawione na listingu 18.23.
Listing 18.23. Dodanie nowej akcji do kontrolera Home
using System;
using System.Web.Mvc;
using Filters.Infrastructure;
namespace Filters.Controllers {
public class HomeController : Controller {
[Authorize(Users="admin")]
public string Index() {
return "To jest metoda akcji Index kontrolera Home.";
}
[GoogleAuth]
[Authorize(Users = "[email protected]")]
public string List() {
return "To jest metoda akcji List kontrolera Home.";
}
public string RangeTest(int id) {
if (id > 100) {
return String.Format("Wartość id wynosi: {0}", id);
} else {
throw new ArgumentOutOfRangeException("id", id, "");
}
}
}
}
Domyślną obsługę wyjątku możesz zaobserwować po uruchomieniu aplikacji i przejściu do adresu URL
/Home/RangeTest/50. Tworzona przez Visual Studio dla projektu MVC trasa domyślna posiada zmienną
segmentu o nazwie id, której w podanym adresie URL zostaje przypisana wartość 50. Efekt wywołania podanego
adresu URL pokazano na rysunku 18.3. (Dokładne informacje na temat systemu routingu i segmentów URL
przedstawiono w rozdziałach 15. i 16.).
 Uwaga Visual Studio wykryje zgłoszenie wyjątku i przerwie działanie debugera, aby umożliwić Ci kontrolę nad
przebiegiem wykonywania aplikacji. Naciśnij klawisz F5 lub kliknij przycisk Kontynuuj, aby kontynuować działanie
aplikacji i zobaczyć domyślny sposób obsługi wyjątku.
Przygotowany filtr wyjątku można zastosować wobec kontrolerów bądź ich poszczególnych akcji,
jak pokazano na listingu 18.24.
472
ROZDZIAŁ 18.  FILTRY
Rysunek 18.3. Domyślny sposób obsługi wyjątku
Listing 18.24. Zastosowanie filtra w pliku HomeController.cs
...
[RangeException]
public string RangeTest(int id) {
if (id > 100) {
return String.Format("Wartość id wynosi: {0}", id);
} else {
throw new ArgumentOutOfRangeException("id", id, "");
}
}
...
Efekt zastosowania filtra możesz zaobserwować po ponownym uruchomieniu aplikacji i przejściu
do adresu URL /Home/RangeTest/50, jak pokazano na rysunku 18.4.
Rysunek 18.4. Efekt zastosowania filtra wyjątku
473
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Użycie widoku w celu reakcji na wyjątek
W zależności od rodzaju wyjątku wyświetlenie statycznej strony z treścią może być najprostszym
i najbezpieczniejszym rozwiązaniem — istnieje niewielkie niebezpieczeństwo, że proces wyświetlania komunikatu
nie powiedzie się i spowoduje dodatkowe problemy. Wprawdzie masz pewność, że użytkownik zobaczy komunikat,
ale takie rozwiązanie nie będzie dla niego zbyt użyteczne, ponieważ użytkownikowi zostanie wyświetlony bardzo
ogólny komunikat o błędzie w aplikacji.
Inne podejście polega na wykorzystaniu widoku w celu wyświetlenia szczegółowych informacji o problemie
i przedstawienia użytkownikowi kontekstu oraz opcji pozwalających na rozwiązanie problemu. Wprowadzimy
więc pewne zmiany w klasie RangeExceptionAttribute, które przedstawiono na listingu 18.25.
Listing 18.25. Zwrot widoku przez filtr wyjątku w pliku RangeExceptionAttribute.cs
using System;
using System.Web.Mvc;
namespace Filters.Infrastructure {
public class RangeExceptionAttribute : FilterAttribute, IExceptionFilter {
public void OnException(ExceptionContext filterContext) {
if (!filterContext.ExceptionHandled &&
filterContext.Exception is ArgumentOutOfRangeException) {
int val = (int)(((ArgumentOutOfRangeException)
filterContext.Exception).ActualValue);
filterContext.Result = new ViewResult {
ViewName = "RangeError",
ViewData = new ViewDataDictionary<int>(val)
};
filterContext.ExceptionHandled = true;
}
}
}
}
Utworzony zostaje obiekt ViewResult, następnie przypisywane są wartości właściwości ViewName i ViewData.
W ten sposób wskazujemy obiekty widoku i modelu otrzymujące informacje. Kod nie jest w najbardziej czytelnej
postaci, ponieważ bezpośrednio współpracujemy z obiektem ViewResult, zamiast polegać na metodzie View
zdefiniowanej w klasie Controller używanej w metodach akcji. Nie zastosujemy wspomnianego kodu, ponieważ
widoki omówię dokładnie w rozdziale 20. Ponadto wbudowany filtr wyjątku, który zostanie omówiony
w kolejnym podrozdziale, może być użyty do osiągnięcia tego samego efektu, ale w znacznie bardziej elegancki
sposób. Teraz chcę po prostu pokazać Ci, jak to wszystko działa.
Obiekt ViewResult wskazuje widok o nazwie RangeError i jako argument przekazuje mu wartość int, która
spowodowała zgłoszenie wyjątku. Wspomniany argument jest używany w charakterze obiektu modelu widoku.
Do projektu w Visual Studio trzeba dodać katalog Views/Shared, a następnie umieścić w nim plik
RangeError.cshtml, którego zawartość przedstawiono na listingu 18.26.
Listing 18.26. Kod pliku widoku RangeError.cshtml
@model int
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>RangeError</title>
474
ROZDZIAŁ 18.  FILTRY
</head>
<body>
<h2>Przepraszamy</h2>
<span> Wartość @Model jest spoza oczekiwanego zakresu.</span>
<div>
@Html.ActionLink("Zmień wartość i spróbuj ponownie", "Index")
</div>
</body>
</html>
Widok składa się ze standardowego kodu HTML i Razor i służy do przedstawienia użytkownikowi
komunikatu nieco bardziej użytecznego niż we wcześniejszym przykładzie. Omawiana aplikacja jest bardzo
prosta, więc nie mamy możliwości przedstawienia użytecznych opcji, które mogłyby pomóc w rozwiązaniu
problemu. W kodzie widoku użyto metody pomocniczej ActionLink w celu wygenerowania łącza do innej
metody akcji, aby w ten sposób zademonstrować możliwość użycia pełnego zestawu opcji. Wynik wprowadzonych
zmian możesz zobaczyć po ponownym uruchomieniu aplikacji i przejściu do adresu URL /Home/RangeTest/50,
jak pokazano na rysunku 18.5.
Rysunek 18.5. Użycie widoku w celu wyświetlenia komunikatu błędu z filtra wyjątku
Uniknięcie przechwycenia niewłaściwego wyjątku
Zaletą używania widoku w celu wyświetlania komunikatu błędu jest możliwość wykorzystania układu, aby
komunikat błędu zachował spójność z pozostałą częścią aplikacji. Wygenerowanie treści dynamicznej pomaga
użytkownikowi w zrozumieniu powstałego problemu i znalezieniu dostępnych sposobów jego rozwiązania.
Wadą jest konieczność dokładnego przetestowania widoku i upewnienia się, że nie spowoduje on zgłoszenia
innego wyjątku. Taka sytuacja może się zdarzyć, gdy programista koncentruje się na testowaniu głównych funkcji
aplikacji i nie poświęca wystarczająco dużo czasu na sprawdzenie innych sytuacji, które mogą wystąpić. Przykład
takiej sytuacji przedstawiono na listingu 18.27. W pliku widoku RangeError.cshtml został umieszczony blok
kodu Razor, który spowoduje zgłoszenie wyjątku.
Listing 18.27. Dodanie w pliku RangeError.cshtml kodu, który spowoduje zgłoszenie wyjątku przez widok
@model int
@{
var count = 0;
var number = Model / count;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>RangeError</title>
</head>
475
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
<body>
<h2>Przepraszamy</h2>
<span> Wartość @Model jest spoza oczekiwanego zakresu.</span>
<div>
@Html.ActionLink("Zmień wartość i spróbuj ponownie", "Index")
</div>
</body>
</html>
W trakcie generowania widoku nastąpi zgłoszenie wyjątku DivideByZeroException. Jeżeli ponownie
uruchomisz aplikację i przejdziesz do adresu URL /Home/RangeTest/50, wówczas nastąpi zgłoszenie
wymienionego wyjątku przez kontroler, jak pokazano na rysunku 18.6.
Rysunek 18.6. Wyjątek zgłoszony w trakcie generowania widoku
To nie jest realistyczny scenariusz, ale pokazuje, co się może stać w przypadku występowania problemów
w widoku — użytkownik zobaczy informacje o błędzie, które nawet nie są powiązane z faktycznym problemem
zaistniałym w aplikacji. Kiedy używasz filtra wyjątku wykorzystującego widok, upewnij się o dokładnym
przetestowaniu każdego widoku.
Użycie wbudowanego filtra wyjątków
Pokazałem Ci, w jaki sposób tworzyć filtr wyjątku, ponieważ jestem przekonany, że warto zrozumieć sposób
działania platformy MVC. W rzeczywistych projektach nie będziesz musiał zbyt często tworzyć własnych filtrów,
ponieważ firma Microsoft oferuje na platformie MVC atrybut HandleErrorAttribute będący wbudowaną
implementacją interfejsu IExceptionFilter. Korzystając z właściwości tej klasy, opisanych w tabeli 18.8,
możemy określić wyjątek oraz nazwy widoków i układów.
Tabela 18.8. Właściwości klasy HandleErrorAttribute
Nazwa
Typ
Opis
Exception
Type
Type
Typ wyjątku obsługiwanego przez ten filtr. Obsługiwane są również wyjątki typów
dziedziczących po podanej klasie, a pozostałe są ignorowane. Domyślną wartością
jest System.Exception, co oznacza, że domyślnie obsługiwane są wszystkie standardowe
wyjątki.
View
string
Nazwa szablonu widoku generowanego przez ten filtr. Jeżeli nie podamy tej wartości,
domyślnie przyjmowana jest wartość Error, więc domyślnie są generowane
/Views/<nazwaKontrolera>/Error.cshtml lub /Views/Shared/Error.cshtml.
Master
string
Nazwa układu używanego przy generowaniu wyniku tego filtra. Jeżeli nie podamy tej
wartości, widok będzie korzystał z domyślnego układu.
476
ROZDZIAŁ 18.  FILTRY
Po napotkaniu nieobsłużonego wyjątku wskazanego przez ExceptionType filtr spowoduje wygenerowanie
widoku wskazanego przez właściwość View (używając przy tym układu domyślnego lub określonego we
właściwości Master).
Przygotowanie do użycia wbudowanego filtra wyjątku
Klasa HandleErrorAttribute działa tylko wtedy, gdy włączymy w pliku Web.config opcję niestandardowej obsługi
błędów przez umieszczenie w węźle <system.web> znacznika <customErrors mode="On" />, jak przedstawiono
na listingu 18.28.
Listing 18.28. Włączenie w pliku Web.config obsługi własnych błędów
...
<system.web>
<httpRuntime targetFramework="4.5.1" />
<compilation debug="true" targetFramework="4.5.1" />
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" timeout="2880">
<credentials passwordFormat="Clear">
<user name="janek" password="sekret"/>
<user name="admin" password="sekret" />
</credentials>
</forms>
</authentication>
<customErrors mode="On" defaultRedirect="/Content/RangeErrorPage.html"/>
</system.web>
...
Domyślnie opcja niestandardowej obsługi błędów ma ustawioną wartość RemoteOnly, co oznacza,
że połączenia przychodzące z komputera lokalnego zawsze będą prowadziły do wyświetlenia żółtego
ekranu śmierci. To niewątpliwie problem, ponieważ serwer IIS Express pozwala jedynie na wykonywanie
połączeń lokalnych. Ustawiając wartość On atrybutu customErrors, wskazujemy, że zdefiniowana polityka
obsługi błędów powinna być stosowana zawsze, niezależnie od źródła pochodzenia połączenia. Atrybut
defaultRedirect wskazuje domyślną stronę, która będzie wyświetlana, jeśli wszystkie inne próby rozwiązania
problemu okażą się nieskuteczne.
Implementacja wbudowanego filtra wyjątku
Zastosowanie filtra HandleError w kontrolerze Home przedstawiono na listingu 18.29.
Listing 18.29. Użycie filtra HandleErrorAttribute w pliku HomeController.cs
...
[HandleError(ExceptionType = typeof(ArgumentOutOfRangeException), View = "RangeError")]
public string RangeTest(int id) {
if (id > 100) {
return String.Format("Wartość id wynosi: {0}", id);
} else {
throw new ArgumentOutOfRangeException("id", id, "");
}
}
...
W omawianym przykładzie odtworzyliśmy sytuację, z którą spotkaliśmy się wcześniej podczas pracy
z własnym filtrem. Wyjątek ArgumentOutOfRangeException będzie obsłużony poprzez wyświetlenie
użytkownikowi widoku RangeError.
477
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
W trakcie generowania widoku filtr HandleErrorAttribute przekazuje obiekt modelu widoku
HandleErrorInfo, który jest opakowaniem wyjątku i dostarcza informacji dodatkowych wyświetlanych
użytkownikowi w widoku. W tabeli 18.9 przedstawiono właściwości definiowane przez klasę HandleErrorInfo.
Tabela 18.9. Właściwości klasy HandleErrorInfo
Nazwa
Typ
Opis
ActionName
string
Zwraca nazwę metody akcji, w której nastąpiło zgłoszenie wyjątku.
ControllerName
string
Zwraca nazwę kontrolera, w którym nastąpiło zgłoszenie wyjątku.
Exception
Exception
Zwraca wyjątek.
Uaktualniony kod pliku widoku RangeError.cshtml gotowego do użycia obiektu modelu został
przedstawiony na listingu 18.30.
Listing 18.30. Użycie obiektu modelu HandleErrorInfo w pliku RangeError.cshtml
@model HandleErrorInfo
@{
ViewBag.Title = "Przepraszamy, mamy problem!";
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>RangeError</title>
</head>
<body>
<h2>Przepraszamy</h2>
<span> Wartość @(((ArgumentOutOfRangeException)Model.Exception).ActualValue) jest spoza
oczekiwanego zakresu.</span>
<div>
@Html.ActionLink("Zmień wartość i spróbuj ponownie", "Index")
</div>
</body>
</html>
W kodzie przeprowadzane jest rzutowanie wartości właściwości Model.Exception na typ ArgumentOutOf
RangeException w celu umożliwienia odczytania wartości ActualValue, ponieważ HandleErrorInfo to ogólnego
przeznaczenia klasa modelu używana do przekazywania widokowi dowolnego wyjątku.
Użycie filtrów akcji
Filtry akcji oraz wyniku są filtrami ogólnego przeznaczenia, które mogą być wykorzystane do dowolnych celów.
Wbudowany interfejs dla do tworzenia tych typów filtrów, IActionFilter jest pokazany na listingu 18.31.
Listing 18.31. Interfejs IActionFilter
namespace System.Web.Mvc {
public interface IActionFilter {
void OnActionExecuting(ActionExecutingContext filterContext);
void OnActionExecuted(ActionExecutedContext filterContext);
}
}
478
ROZDZIAŁ 18.  FILTRY
Interfejs definiuje dwie metody. Platforma MVC wywołuje metodę OnActionExecuting przed wywołaniem
metody akcji. Po zakończeniu wykonywania metody akcji wywoływana jest metoda OnActionExecuted.
Implementacja metody OnActionExecuting
Metoda OnActionExecuting jest wywoływana przed wywołaniem metody akcji. Można jej użyć w celu
przejrzenia żądania i jego anulowania, zmodyfikowania lub wywołania aktywności, która będzie
realizowana w czasie wywołania akcji. Parametrem tej metody jest obiekt klasy ActionExecutingContext
dziedziczącej po ControllerContext i definiującej dwie dodatkowe właściwości wymienione w tabeli 18.10.
Tabela 18.10. Właściwości klasy ActionExecutingContext
Nazwa
Typ
Opis
ActionDescriptor
ActionDescriptor
Udostępnia dane na temat metody akcji.
Result
ActionResult
Wynik metody akcji; filtr może anulować żądanie przez przypisanie
do tej właściwości wartości różnej od null.
Możliwe jest selektywne anulowanie żądania przez ustawienie właściwości Result w parametrze na obiekt
wyniku akcji. Aby to zademonstrować, utworzymy w katalogu Infrastructure własną klasę filtra akcji o nazwie
CustomActionAttribute. Kod klasy został przedstawiony na listingu 18.32.
Listing 18.32. Zawartość pliku CustomActionAttribute.cs
using System.Web.Mvc;
namespace Filters.Infrastructure {
public class CustomActionAttribute : FilterAttribute, IActionFilter {
public void OnActionExecuting(ActionExecutingContext filterContext) {
if (filterContext.HttpContext.Request.IsLocal) {
filterContext.Result = new HttpNotFoundResult();
}
}
public void OnActionExecuted(ActionExecutedContext filterContext) {
// jeszcze nie zaimplementowano
}
}
}
W przykładzie tym używamy metody OnActionExecuting do sprawdzenia, czy żądanie było wykonane
z użyciem SSL. Jeżeli nie, użytkownik zobaczy stronę z informacją o błędzie 404.
 Uwaga Na listingu 18.32 można zauważyć, że nie trzeba implementować obu metod zdefiniowanych
w interfejsie IActionFilter, aby utworzyć działający filtr. Jednak należy pamiętać, aby nie zgłaszać wyjątku
NotImplementedException, który Visual Studio dodaje do klasy podczas implementacji interfejsu ponieważ,
w takim przypadku platforma MVC wywoła obie metody w filtrze akcji i jeśli zostanie zgłoszony wyjątek,
wówczas nastąpi uruchomienie filtra wyjątku. Jeżeli nie chcemy dodawać żadnej logiki do metody, po prostu
pozostawiamy ją pustą.
Filtr akcji można zastosować w dokładnie taki sam sposób jak każdy inny atrybut. Aby zademonstrować
użycie filtra akcji utworzonego na listingu 18.32, do kontrolera Home dodajemy nową metodę akcji,
jak przedstawiono na listingu 18.33.
479
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
Listing 18.33. Dodanie nowej metody akcji w pliku HomeController.cs
using System;
using System.Web.Mvc;
using Filters.Infrastructure;
namespace Filters.Controllers {
public class HomeController : Controller {
[Authorize(Users="admin")]
public string Index() {
return "To jest metoda akcji Index kontrolera Home.";
}
[GoogleAuth]
[Authorize(Users = "[email protected]")]
public string List() {
return "To jest metoda akcji List kontrolera Home.";
}
[HandleError(ExceptionType = typeof(ArgumentOutOfRangeException), View = "RangeError")]
public string RangeTest(int id) {
if (id > 100) {
return String.Format("Wartość id wynosi: {0}", id);
} else {
throw new ArgumentOutOfRangeException("id", id, "");
}
}
[CustomAction]
public string FilterTest() {
return "To jest akcja FilterTest";
}
}
}
W celu przetestowania filtra uruchom aplikację i przejdź do adresu URL /Home/FilterTest. Żądanie oczywiście
pochodzi z komputera lokalnego i dlatego nasz filtr akcji spowoduje wygenerowanie w przeglądarce
internetowej błędu 404, jak pokazano na rysunku 18.7.
Rysunek 18.7. Efekt użycia filtra akcji
 Wskazówka Jeżeli chcesz się upewnić co do tego, że to filtr wygenerował komunikat błędu, po prostu usuń
atrybut z metody akcji FilterTest kontrolera Home i ponownie uruchom aplikację.
480
ROZDZIAŁ 18.  FILTRY
Implementacja metody OnActionExecuted
Możliwe jest również użycie filtrów do wykonywania zadań, które przekraczają czas wykonania metody akcji.
W katalogu Infrastructure projektu tworzymy nową klasę o nazwie ProfileActionAttribute, która mierzy
czas potrzebny do wykonania metody akcji. Kod klasy został przedstawiony na listingu 18.34.
Listing 18.34. Zawartość pliku ProfileActionAttribute.cs
using System.Diagnostics;
using System.Web.Mvc;
namespace Filters.Infrastructure {
public class ProfileActionAttribute : FilterAttribute, IActionFilter {
private Stopwatch timer;
public void OnActionExecuting(ActionExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}
public void OnActionExecuted(ActionExecutedContext filterContext) {
timer.Stop();
if (filterContext.Exception == null) {
filterContext.HttpContext.Response.Write(
string.Format("<div>Czas wykonania metody akcji: {0:F6}</div>",
timer.Elapsed.TotalSeconds));
}
}
}
}
W przykładzie tym w metodzie OnActionExecuting uruchamiamy stoper (jest to stoper Stopwatch o dużej
rozdzielczości, zdefiniowany w przestrzeni System.Diagnostics). Metoda OnActionExecuted jest wywoływana
po zakończeniu metody akcji. Na listingu 18.35 atrybut został zastosowany w stosunku do kontrolera Home
(poprzednio utworzony filtr został usunięty, aby uniknąć przekierowywania żądań lokalnych).
Listing 18.35. Zastosowanie filtra akcji w pliku HomeController.cs
...
[ProfileAction]
public string FilterTest() {
return "To jest akcja FilterTest";
}
...
Jeżeli uruchomisz aplikację i przejdziesz do adresu URL /Home/FilterTest, uzyskasz efekt pokazany
na rysunku 18.8.
Rysunek 18.8. Użycie filtra akcji do pomiaru wydajności
481
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
 Wskazówka Zwróć uwagę, że informacje profilowania są wyświetlane w przeglądarce przed wynikiem metody
akcji. Wynika to z faktu wykonania filtra akcji po zakończeniu metody akcji, ale przed przetworzeniem wyniku.
Parametrem przekazywanym do metody OnActionExecuted jest ActionExecutedContext. W klasie tej
zdefiniowane są dwie dodatkowe właściwości opisane w tabeli 18.11. Właściwość Exception zwraca
wyjątki zgłoszone w metodzie akcji, a właściwość ExceptionHandled informuje, czy inny filtr obsłużył wyjątek.
Tabela 18.11. Właściwości klasy ActionExecutedContext
Nazwa
Typ
Opis
ActionDescriptor
ActionDescriptor
Udostępnia dane na temat metody akcji.
Canceled
bool
Zwraca true, jeżeli akcja została anulowana przez inny filtr.
Exception
Exception
Zwraca wyjątek zgłoszony przez inny filtr lub przez metodę akcji.
ExceptionHandled
bool
Zwraca true, jeżeli wyjątek został obsłużony.
Result
ActionResult
Wynik metody akcji; filtr może anulować żądanie przez przypisanie
do tej właściwości wartości różnej od null.
Właściwość Canceled zwraca true, jeżeli inny filtr anulował żądanie (przez ustawienie wartości właściwości
Result) po wywołaniu metody OnActionExecuting bieżącego filtra. Nasza metoda OnActionExecuted jest nadal
wywoływana, ale wyłącznie w celu zwolnienia zasobów używanych przez filtr.
Używanie filtra wyniku
Filtry wyniku to filtry ogólnego przeznaczenia, które operują na wynikach generowanych przez metody akcji.
Filtry wyniku implementują interfejs IResultFilter, zamieszczony na listingu 18.36.
Listing 18.36. Interfejs IResultFilter
namespace System.Web.Mvc {
}
public interface IResultFilter {
void OnResultExecuting(ResultExecutingContext filterContext);
void OnResultExecuted(ResultExecutedContext filterContext);
}
W rozdziale 17. wyjaśniłem, w jaki sposób metody akcji zwracają wyniki akcji. Pozwala nam to oddzielić
intencje metody akcji od jej wykonania. Gdy stosujemy filtr wyniku do metody akcji, metoda OnResultExecuting
jest wywoływana po zwróceniu wyniku akcji przez metodę, ale przed wykonaniem wyniku akcji. Metoda
OnResultExecuted jest wywoływana po zakończeniu wykonywania wyniku akcji.
Parametrami tych metod są odpowiednio obiekty ResultExecutingContext oraz ResultExecutedContext,
które są bardzo podobne do ich odpowiedników z filtra akcji. Definiują one te same właściwości, które dają
ten sam efekt (tabela 18.11). Aby zademonstrować prosty filtr wyniku, w katalogu Infrastructure należy
utworzyć nowy plik klasy o nazwie ProfileResultAttribute.cs i umieścić w nim kod przedstawiony na listingu
18.37.
Listing 18.37. Zawartość pliku ProfileResultAttribute.cs
using System.Diagnostics;
using System.Web.Mvc;
namespace Filters.Infrastructure {
482
ROZDZIAŁ 18.  FILTRY
public class ProfileResultAttribute : FilterAttribute, IResultFilter {
private Stopwatch timer;
public void OnResultExecuting(ResultExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}
public void OnResultExecuted(ResultExecutedContext filterContext) {
timer.Stop();
filterContext.HttpContext.Response.Write(
string.Format("<div>Przetwarzanie wyniku - czas: {0:F6}</div>",
timer.Elapsed.TotalSeconds));
}
}
}
Filtr ten jest uzupełnieniem utworzonego wcześniej filtra akcji i mierzy ilość czasu potrzebnego
na przetworzenie wyniku. Teraz możemy dołączyć nasz filtr do metody akcji w kontrolerze Home,
co zostało przedstawione na listingu 18.38:
Listing 18.38. Zastosowanie filtra wyniku w pliku HomeController.cs
...
[ProfileAction]
[ProfileResult]
public string FilterTest() {
return "To jest akcja FilterTest";
}
...
Po uruchomieniu aplikacji i przejściu do adresu URL /Home/FilterTest zobaczymy wynik pokazany
na rysunku 18.9. Zwróć uwagę, że oba filtry dodały dane do odpowiedzi przekazywanej przeglądarce
internetowej — dane wyjściowe filtra wyniku znajdują się oczywiście po danych wyjściowych metody akcji,
ponieważ metoda OnResultExecuted nie może być wykonana przez platformę MVC przed zakończeniem
przetwarzania wyniku. W omawianym przypadku oznacza to umieszczenie wartości string w wyniku.
Rysunek 18.9. Efekt działania filtra wyniku
Użycie wbudowanych klas filtrów akcji i wyniku
Platforma MVC zawiera klasy, które mogą być użyte do tworzenia zarówno filtrów akcji, jak i filtrów wyniku.
Klasa ta, o nazwie ActionFilterAttribute, jest zamieszczona na listingu 18.39.
Listing 18.39. Klasa ActionFilterAttribute
public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter{
public virtual void OnActionExecuting(ActionExecutingContext filterContext) {
}
483
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
public virtual void OnActionExecuted(ActionExecutedContext filterContext) {
}
public virtual void OnResultExecuting(ResultExecutingContext filterContext) {
}
public virtual void OnResultExecuted(ResultExecutedContext filterContext) {
}
}
}
Jedyną zaletą stosowania tej klasy jest to, że nie musimy implementować metod, których nie będziemy
używać — w przeciwnym razie to rozwiązanie nie ma żadnej przewagi nad bezpośrednim implementowaniem
interfejsów filtrów.
Na listingu 18.40 jako przykład pokazany jest filtr dziedziczący po ActionFilterAttribute, który łączy
nasze pomiary wydajności dla metod akcji oraz wyniku akcji. Kod z listingu należy umieścić w nowym pliku
klasy ProfileAllAttribute.cs, który trzeba utworzyć w katalogu Infrastructure.
Listing 18.40. Zawartość pliku ProfileAllAttribute.cs
using System.Diagnostics;
using System.Web.Mvc;
namespace Filters.Infrastructure {
public class ProfileAllAttribute : ActionFilterAttribute {
private Stopwatch timer;
public override void OnActionExecuting(ActionExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}
public override void OnResultExecuted(ResultExecutedContext filterContext) {
timer.Stop();
filterContext.HttpContext.Response.Write(
string.Format("<div>Całkowity czas wykonania: {0:F6}</div>",
timer.Elapsed.TotalSeconds));
}
}
}
Klasa ActionFilterAttribute implementuje interfejsy IActionFilter oraz IResultFilter, co oznacza,
że platforma MVC będzie traktowała klasy pochodne jako oba rodzaje filtrów, nawet gdy nie wszystkie metody
zostaną nadpisane. W omawianym przykładzie zaimplementowane zostały jedynie metody OnActionExecuting
interfejsu IActionFilter i OnResultExecuted interfejsu IResultFilter. To pozwala nam na kontynuację tematu
profilowania i pomiar czasu zarówno wykonania metody akcji, jak i przetworzenia wyniku. Zastosowanie filtra
w klasie Home przedstawiono na listingu 18.41.
Listing 18.41. Zastosowanie filtra w pliku HomeController.cs
...
[ProfileAction]
[ProfileResult]
[ProfileAll]
public string FilterTest()
{
return "To jest akcja FilterTest.";
}
...
484
ROZDZIAŁ 18.  FILTRY
Efekt zastosowania wszystkich filtrów możesz zobaczyć po uruchomieniu aplikacji i przejściu do adresu URL
/Home/FilterTest. Wynik pokazano na rysunku 18.10.
Rysunek 18.10. Wynik działania połączonych filtrów akcji i wyniku
Użycie innych funkcji filtrów
Poprzednie przykłady zawierają wszystkie informacje, jakich potrzebujemy do efektywnej pracy z filtrami. Oprócz
przedstawionych funkcji istnieje jeszcze kilka interesujących, choć nie tak często używanych mechanizmów.
W kolejnych punktach omówię niektóre zaawansowane możliwości filtrowania dostępne na platformie MVC.
Filtrowanie bez użycia atrybutów
Normalnym sposobem stosowania filtrów jest tworzenie i wykorzystywanie atrybutów, co pokazałem
w poprzednich punktach. Istnieje jednak alternatywa dla użycia atrybutów. Klasa Controller implementuje
interfejsy IActionFilter, IResultFilter, IAuthenticationFilter, IAuthorizationFilter oraz IExecutionFilter.
Dostarcza ona również pustych, wirtualnych implementacji każdej z metod OnXXX, które tu prezentowałem,
takich jak OnAuthorization czy OnException. Na listingu 18.42 zamieszczony jest uaktualniony kontroler
Home używający omawianej funkcji i tworzący klasę kontrolera mierzącego swoją wydajność.
Listing 18.42. Użycie metod kontrolera filtra w pliku HomeController.cs
using
using
using
using
System;
System.Web.Mvc;
Filters.Infrastructure;
System.Diagnostics;
namespace Filters.Controllers {
public class HomeController : Controller {
private Stopwatch timer;
[Authorize(Users = "admin")]
public string Index()
{
return "To jest metoda akcji Index kontrolera Home.";
}
[GoogleAuth]
[Authorize(Users = "[email protected]")]
public string List() {
return "To jest metoda akcji List kontrolera Home.";
}
[HandleError(ExceptionType = typeof(ArgumentOutOfRangeException),
View = "RangeError")]
public string RangeTest(int id)
485
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
{
if (id > 100)
{
return String.Format("Wartość id wynosi: {0}", id);
}
else
{
throw new ArgumentOutOfRangeException("id", id, "");
}
}
public string FilterTest()
{
return "To jest akcja FilterTest.";
}
protected override void OnActionExecuting(ActionExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}
protected override void OnResultExecuted(ResultExecutedContext filterContext) {
timer.Stop();
filterContext.HttpContext.Response.Write(
string.Format("<div>Całkowity czas wykonania: {0}</div>",
timer.Elapsed.TotalSeconds));
}
}
}
Z metody akcji FilterTest zostały usunięte filtry, ponieważ nie są już wymagane — kontroler Home będzie
dodawał informacje profilu do odpowiedzi każdej metody akcji. Na rysunku 18.11 pokazano efekt uruchomienia
aplikacji i przejścia do adresu URL /Home/RangeTest/200, który powoduje wywołanie metody akcji RangeTest
bez zgłaszania wyjątku skonfigurowanego do zademonstrowania filtra HandleError.
Rysunek 18.11. Wynik implementacji metod filtrów bezpośrednio w kontrolerze
Technika ta jest najużyteczniejsza, gdy tworzymy klasę bazową, po której dziedziczy wiele kontrolerów z projektu.
Podstawowym zadaniem filtrów jest umieszczenie kodu wykorzystywanego w całej aplikacji w jednej, wspólnej
lokalizacji, dlatego używanie tych metod w kontrolerze, który nie będzie dziedziczony, nie ma większego sensu.
 Wskazówka W swoich projektach preferuję stosowanie atrybutów. Podoba mi się oddzielenie logiki kontrolera
od logiki filtra. Jeżeli szukasz sposobu na zastosowanie filtrów do wszystkich kontrolerów, kontynuuj lekturę,
aby dowiedzieć się, jak korzystać z filtrów globalnych.
486
ROZDZIAŁ 18.  FILTRY
Użycie filtrów globalnych
Filtry globalne są stosowane do wszystkich metod akcji w kontrolerach aplikacji. Istnieje konwencja dotycząca
konfiguracji filtrów globalnych. Wspomniana konwencja jest automatycznie stosowana przez Visual Studio
w projektach opartych na szablonie MVC, ale musi być zdefiniowana ręcznie w projektach opartych
na szablonie Empty.
Konfiguracja na poziomie aplikacji jest przeprowadzana w klasach dodanych do katalogu App_Start. Dlatego
też w rozdziałach 15. i 16. trasy definiowaliśmy w pliku App_Start/RouteConfig.cs. Aby dla filtrów utworzyć
odpowiednik wymienionego pliku, w katalogu App_Start dodaj nowy plik klasy o nazwie FilterConfig.cs i umieść
w nim kod przedstawiony na listingu 18.43.
Listing 18.43. Zawartość pliku FilterConfig.cs
using System.Web;
using System.Web.Mvc;
namespace Filters {
public class FilterConfig {
public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
filters.Add(new HandleErrorAttribute());
}
}
}
Kod przedstawiony na listingu jest identyczny z kodem, jaki Visual Studio tworzy dla projektu opartego
na szablonie MVC. Klasa FilterConfig definiuje metodę statyczną o nazwie RegisterGlobalFilters
otrzymującą kolekcję filtrów globalnych wyrażoną w postaci obiektu GlobalFilterCollection.
Do tego obiektu (kolekcji) można dodawać nowe filtry.
Na listingu 18.43 mamy dwie konwencje, na które warto zwrócić uwagę. Pierwsza polega na tym, że klasa
FilterConfig jest definiowana w przestrzeni nazw Filters, a nie Filters.App_Start używanej przez Visual
Studio po utworzeniu pliku. Druga konwencja polega na tym, że omówiony wcześniej w rozdziale filtr
HandleError zawsze będzie definiowany jako filtr globalny. Odbywa się to przez wywołanie metody Add
w obiekcie GlobalFilterCollection.
 Uwaga Nie ma konieczności globalnej konfiguracji filtra HandleError, ale definiuje ona domyślną politykę obsługi
wyjątków na platformie MVC. W przypadku wystąpienia nieobsłużonego wyjątku zostanie wywołany widok
/Views/Shared/Error.cshtml. Ta domyślna zasada obsługi wyjątków jest zablokowana dla środowiska
programistycznego. Informacja na temat sposobu jej odblokowania w pliku Web.config znajduje się w punkcie
„Tworzenie filtra wyjątku”.
Zastosujemy teraz globalnie nasz filtr ProfileAll i użyjemy tego samego wywołania metody, które jest
odpowiedzialne za konfigurację filtra HandleError. Odpowiednie zmiany w pliku FilterConfig.cs przedstawiono
na listingu 18.44.
Listing 18.44. Dodanie filtra globalnego w pliku FilterConfig.cs
using System.Web;
using System.Web.Mvc;
using Filters.Infrastructure;
namespace Filters {
public class FilterConfig {
public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
filters.Add(new HandleErrorAttribute());
filters.Add(new ProfileAllAttribute());
487
ASP.NET MVC 5. ZAAWANSOWANE PROGRAMOWANIE
}
}
}
 Wskazówka Zwróć uwagę, że rejestracja filtra globalnego odbywa się przez utworzenie egzemplarza klasy filtra.
Oznacza to konieczność odwołania się do nazwy klasy łącznie z przyrostkiem Attribute. Reguła jest następująca:
przyrostek Attribute pomijasz podczas stosowania filtra jako atrybutu, natomiast używasz go w trakcie
bezpośredniego tworzenia egzemplarza klasy.
Kolejnym krokiem jest upewnienie się, że metoda FilterConfig.RegisterGlobalFilters jest wywoływana
z poziomu pliku Global.asax, dzięki czemu mamy pewność, że filtry zostaną zarejestrowane w momencie
uruchomienia aplikacji MVC. Odpowiednie zmiany do wprowadzenia przedstawiono na listingu 18.45.
Listing 18.45. Konfiguracja filtra globalnego w pliku Global.asax
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
System.Web.Routing;
namespace Filters {
public class MvcApplication : System.Web.HttpApplication {
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
}
}
}
Aby zademonstrować filtr globalny, należy utworzyć nowy kontroler o nazwie Customer, którego kod
przedstawiono na listingu 18.46. Tworzymy nowy kontroler, aby móc wykorzystać kod, do którego nie będą
stosowane żadne filtry z wcześniejszych sekcji.
Listing 18.46. Zawartość pliku Customer.cs
using System.Web.Mvc;
namespace Filters.Controllers {
public class CustomerController : Controller {
public string Index() {
return "To jest kontroler Customer.";
}
}
}
To jest bardzo prosty kontroler, którego metoda akcji Index zwraca ciąg tekstowy (string). Na rysunku 18.12
pokazano efekt użycia filtra globalnego, który uzyskano w wyniku uruchomienia aplikacji i przejścia do adresu
URL /Customer. Mimo że filtr nie został zastosowany bezpośrednio wobec kontrolera, to filtr globalny dodaje
informacje profilowania, co możesz zobaczyć na rysunku.
488
ROZDZIAŁ 18.  FILTRY
Rysunek 18.12. Efekt użycia filtra globalnego
Określanie kolejności wykonywania filtrów
Jak już wcześniej wspominałem, filtry są wykonywane według typów. Kolejność jest następująca: filtry
uwierzytelniania, filtry autoryzacji, filtry akcji i filtry wyniku. Platforma wykonuje nasze filtry wyjątków
w dowolnym momencie, gdy zostanie zgłoszony nieobsłużony wyjątek. Jednak wewnątrz każdej z kategorii
można sterować kolejnością wykonywania poszczególnych filtrów. Na listingu 18.47 pokazany jest prosty filtr
akcji, którego użyjemy do zademonstrowania sterowania kolejnością wykonywania filtr
Download