RoutingCzęść NR.3 Nadszedł w końcu ten czas, aby na tym blogu napisać kurs o ASP.NET MVC. Wiem, że jest to kurs ASP.NET Core, ale przez większą cześć tego kursu będziemy mówili o ASP.NET MVC i o rzeczach, które już istnieją w starszych jego wersjach.

To co chcemy ostatecznie osiągnąć, to napisać aplikację WEB korzystając z ASP.NET MVC. W teorii jest możliwe stworzenie aplikacji ASP.NET używając tylko Middelware, ale po co wybierać taką trudna drogę.

Ustawmy więc odpowiednie biblioteki tak, byśmy mogli skorzystać ze środowiska MVC. Co więc musimy zrobić, aby zacząć zabawę? Oto co zrobimy:

  • Dodamy paczkę ASP.NET MVC z NuGet
  • Po zainstalowaniu paczki musimy dodać odpowiednie elementy środowiska MVC
  • Na końcu musimy dodać Middelware MVC

Zrób my więc TO.

Ustawienia ASP.NET MVC

Używając NuGeta pobierz paczkę MVC.

ASP.NET MVC

Alternatywnie możesz pobrać paczkę dopisując zależności do pliku project.json w sposób podany na obrazku.

dependencies MVC

Gdy piszę ten wpis najnowsza wersja paczki MVC ma nazwę 6.0.0-rc1-final . U ciebie będzie zapewne inaczej. Pobierz więc najnowszą wersję paczki ASP.NET MVC 6.

Oto obecna lista zależności naszej aplikacji.

"dependencies": {
  "Microsoft.AspNet.Diagnostics": "1.0.0-rc1-final",
  "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
  "Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
  "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
  "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final",
},

Mamy więc już odpowiednie biblioteki. Teraz stwórzmy prosty kontroler, aby potwierdzić, że wszystko ustawiliśmy poprawnie. Teraz nie będę tłumaczył czym jest kontroler i dlaczego on tak działa. Zrobię to później.

Do projektu dodaj nowy folder i nazwij go koniecznie “Controllers”. W tym folderze będziemy umieszczać kontrolery.

New Folder

Do tego folderu dodaj klasę.

Class

Nazwij tę klasę “HomeController”. W nazwie tej klasy musi być człon “Controller”. Na podstawie tej konwencji nazewniczej kod MVC będzie wiedział, że ta klasa reprezentuje kontroler.

Class DNX

W oknie dodawania nowego pliku możesz zauważyć, że istnieją gotowe szablony do utworzenia klas, które są kontrolerami. Na razie to zignoruj i stwórz normalną klasę, która będzie się nazywać “HomeController”.

mvc Controller

Klasa powinna się znajdować w folderze Controllers. Ten fakt, też jest istotny bo taka jest konwencja.

HomeController

Do klasy kontrolera dodam prostą metodę publiczną, która zwróci napis potwierdzający to, że rzeczywiście właśnie ten kod się uruchomił.

namespace GamersPoems.Controllers
{
    public class HomeController
    {
        public string Index()
        {
            return "This is a message from HomeController";
        }
    }
}

Metoda ta musi nazywać się “Index”. Dlaczego tak musi być omówię później.

Chcę, aby mój kontroler się uruchomił i zwrócił ten napis, gdy uruchomię stronę. Obecnie tak jeszcze nie jest ponieważ nie dodałem jeszcze middelware ASP.NET MVC w metodzie Configure.

UseMvcWithDefaultRoute

W pliku Startup.cs dodaj middelware ASP.NET MVC po deklaracji middelware statycznych plików, ale przed uruchomieniem komponentu RUN.

W trakcie dodawania middelware ASP.NET upewnij się, że wybrałeś opcję z domyślnymi ścieżkami. Na razie wybierz UseMvcWithDefaultRoute zamiast UseMvc.

Jeśli uruchomisz teraz aplikację, to zobaczysz ten ekran błędu i w sumie słusznie bo zapomnieliśmy jeszcze o jednej rzeczy.

error 500

Ten błąd informuje, że nie ma usługi związanej z frameworkiem ASP.NET MVC. Sam wyjątek mówi nawet jak mam tę usługę dodać. Dlaczego muszę to robić? Otóż kontrolery ASP.NET MVC, jak się jeszcze przekonasz w tym wpisie, potrafią korzystać z techniki wstrzykiwania zależności oferowanych przez ASP.NET Core.

Jednak, aby z tego korzystać same klasy ASP.NET MVC muszą być zadeklarowane jako usługi.

W pliku Startup.cs w metodzie ConfigureServices dodaj w następujący sposób usługę MVC.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddSingleton<IMessages, Messages>();
}

Teraz, gdy odświeżysz stronę zobaczysz treść tekstu, który jest zwrócony przez nasz kontroler.

MVC works

Udało nam się więc zainstalować ASP.NET MVC i go poprawnie skonfigurować. A teraz omówmy jaka jest filozofia frameworku ASP.NET MVC.

ASP.NET MVC : co to jest?

Framework ASP.NET MVC zawdzięcza swoją nazwę wzorcowemu projektowi MVC.

  • Litera M reprezentuje Model
  • Litera V reprezentuje Widok czyli View
  • Litera C reprezentuje kontroler

Wzorzec projekt MVC jest bardzo popularnym rozwiązaniem w aplikacjach z interfejsem użytkownika. Można nawet się kłócić, że w roku 2009, gdy powstała pierwsza wersja ASP.NET MVC została ona głównie zainspirowana przez framework Ruby on Rails, który także korzystał i korzysta z tego wzorca. Mówiąc krótko wzorzec MVC na przestrzeni lat stał się czymś naturalnym dla stron internetowych.

Zastosowanie wzorca MVC i wbudowanie go we framework było zdecydowanie dobrym pomysłem. Kiedyś strony internetowe w platformie .NET były pisane w ASP.NET Web Forms, który ostatecznie stał się chaotyczny i nawet łamiący zasady projektowe na start. ASP.NET Web Form przykładowo już na początku tworzenia aplikacji cierpiał na antywzorzec zwany Smart UI.

ASP.NET Web Form oficjalnie zostało porzucone i całe szczęście. Ja z tą technologią musiałem żyć i pracować przez dwa lata.

ASP.NET MVC został stworzony z myślą o wzorcach projektowych. Oznacza to, że łatwo w nim zastosować inne wzorce służące do dostępu danych i do przesytu informacji. Razem to wszystko tworzy ładnie ułożoną aplikację.

Na razie skupimy się na fundamentach frameworku MVC czyli na samym wzorcu.

Controller Model View 1

Co robi kontroler? Jak się już przekonaliśmy kontroler przy pomocy swoich metod obsługuje określone zapytania HTTP. Powiedzmy więc, że użytkownik wysłał zapytanie o stronę “poems”.

Mechanizm ścieżek, który omówimy za chwilę ustala, że właśnie ten kontroler i ta metoda powinna obsłużyć to zapytanie HTTP.

Controller Model View 2

Wewnątrz metody kontrolera kod napisany w C# musi zwrócić jakąś odpowiedź na to żądanie. Jak się już przekonaliśmy, może to być zwykły napis.

public class HomeController
{
    public string Index()
    {
        return "This is a message from HomeController";
    }
}

Oczywiście w profesjonalnej aplikacji to o wiele za mało. Kontroler do wyświetlenia elementu w tym wypadku poematu potrzebuje jakiegoś źródła danych, by ta informacja mogła zostać zwrócona.

W tym przykładzie chce zwrócić użytkownikowi listę poematów. A czym jest sam poemat jak nie obiektem. Poemat jest więc modelem. Kontroler w jakiś sposób musi uzyskać ten model bądź jego listę elementów.

Model ma zadanie przetrzymywać informację o samym obiekcie, który reprezentuje. Nie robi on nic więcej. Logika pobierania elementu bądź jego transformacji znajduje się zupełnie gdzieś indziej.

Controller Model View 3

Nie pisz tego kodu. Hipotetycznie nasz obecny kontroler mógłby zwrócić model w ten sposób i go tak wyświetlić dla użytkownika.

public class HomeController
{
    public Poem Index()
    {
        return new Poem();
    }
}

public class Poem
{ }

Powstaje jednak pytanie skąd ASP.NET ma wiedzieć jak ma wyświetlić ten model na ekranie użytkownika. Czy to ma być zwykły tekst? Czy to ma być strona internetowa? Jak mam to wyświetlić i skąd ma to wiedzieć?

Dlatego kontroler wtedy odwołuje się do warstwy widoku we wzorcu MVC.  

Controller Model View 4

Widok zabiera wtedy model ze sobą i go w odpowiedni sposób aranżuje na stronie HTML. Korzystając z specjalnej składni językowej Razor możemy się odwoływać w widoku do modelu, który otrzymaliśmy. O czym w następnym wpisie.

<body>
    @Model

Sformatowana strona przez nasz kod Razor później jest zwracana użytkownikowi w postaci odpowiedzi HTTP. Kontroler, aby zadziałał tak jak omówiliśmy musiał się zmienić do takiej formy. Jak widzisz kontroler naglę zaczął dziedziczyć po klasie Controller i teraz zamiast napisu metoda ma zwracać interfejs IActionResult.

Sama metoda teraz zwraca widok i przekazuje w konstruktorze model poematu.  Nie martw się za chwilę omówimy wszystko bardziej szczegółowo.

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View(new Poem());
    }
}

public class Poem
{ }

Tak właśnie prześledziliśmy całe żądanie HTTP. Oto podstawy frameworka MVC. Filozofia tego wzorca polega na przestrzeganiu zasady projektowej Separation of Concern. Ta zasada projektowa pilnuje nas, aby każdy element aplikacji miał tylko jedną odpowiedzialność.

Kontroler obsługuję żądanie HTTP i konstruuje model, a potem przesyła go do widoku. Model zawiera logię i opis swojego przedmiotu. Widok ma natomiast układać w odpowiedni sposób tę informację na stronię HTML.

Mamy trzy komponenty. Każdy z nich koncentruje się na innym aspekcie aplikacji. Aby to wszystko działało musimy jednak na początku w odpowiedni sposób przekierowywać zapytania HTTP do odpowiedniego kontrolera i jego metody.

W ASP.NET MVC ten proces nazywa się “Routing”. Przeczytajmy więc jak przekierować zapytania HTTP do odpowiednich kontrolerów.

Routing

Middelware ASP.NET MVC, który zainstalowaliśmy musi mieć sposób na zdeterminowanie czy właśnie to zapytanie HTTP powinno pójść do odpowiedniego kontrolera

Middelware ASP.NET MVC będzie podejmować tę decyzję bazując na adresie URL i kodzie w C#, który zaraz napiszemy.

Istnieją dwa sposoby na konfiguracje ścieżek kontrolerów do odpowiednich żądań HTTP. Pierwszy z nich polega na zdefiniowaniu ich w trakcie konfiguracji komponentów Middleware. Ten styl polega na stworzeniu szablonu ścieżki.

Drugi styl konfiguracji ścieżek leży w atrybutach, które są umieszczane w kontrolerach i w jego metodach. Omówimy oba style.

Mamy na razie jeden kontroler, który utworzyliśmy. Jak na razie ten nasz kontroler nie musi mieć specjalnych atrybutów, ani dziedziczyć po innych klasach aby działać.

public class HomeController
{
    public string Index()
    {
        return "This is a message from HomeController";
    }
}

Nie używamy jeszcze modeli, ani widoków. Na razie skupimy się na definicji ścieżek kontrolerów.

Cała magia pierwszego stylu konfiguracji siedzi w pliku Startup.cs. W metodzie Configure zastąp użycie Middleware UseMvcWithDefaultRoute na UseMvc.

UseMvcWithDefaultRoute stworzył za nas domyślne mapowanie zapytań HTTP na kontrolery. Teraz jednak chcemy to zrobić bardziej jawnie.

Wewnątrz metody UseMvc zadeklaruj metodę SetRoutes. Korzystając z pomocy Visual Studio pozwól aby ten edytor za ciebie utworzył kod tej metody.

Use MVC

Fragment mojego kodu w klasie Startup wygląda teraz tak.

public void Configure(IApplicationBuilder app,
    IHostingEnvironment enviroment,
    IMessages messages)
{
    app.UseIISPlatformHandler();

    if (enviroment.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();

        app.UseRuntimeInfoPage("/info");
        app.UseWelcomePage("/IIS");
    }

    app.UseStaticFiles();

    app.UseMvc(SetRoute);

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync(messages.Hello());
    });
}

private void SetRoutes(IRouteBuilder obj)
{
    throw new NotImplementedException();
}

Mamy więc metodę SetRoutes i w niej będziemy musieli zadeklarować nasze mapowania. Stwórzmy więc domyślnie mapowanie.

private void SetRoutes(IRouteBuilder routeBuilder)
{
    routeBuilder.MapRoute("MyDefault", "{controller}/{action}");
}

Każda zmapowana ścieżka jako swój pierwszy parametr potrzebuje swojej nazwy. Ja postanowiłem tę ścieżkę nazwać “MyDefault”. Drugi parametr metody przyjmuje właśnie szablon, który jest tylko napisem. Szablon opisuje MVC jak rozdzielić adres URL.

Gdy wyślemy do przeglądarki zapytanie /Home/Index chce wtedy odwołać się do kontrolera Home i wywołać jego metodę Index.

HomeController

Publiczne metody w kontrolerze nazywamy akcjami. Dlaczego? Publiczna metoda kontrolera jest czymś, co mogę użyć w adresie URL.

Adres URL nigdy nie będzie zawierał słowa Controller. ASP.NET MVC ma konwencję, w której ignoruje wyrażenie Controller w nazwach klas kontrolera.

Pierwsza część naszej mapy zawiera więc odwołanie do kontrolera {controller}. Jest to specjalne odwołanie do wszystkich nazw kontrolera. Analogicznie wyrażenie {action} jest odwołaniem do wszystkich akcji wszystkich kontrolerów.

Dodatkowo do mapy mogę dodać definicję parametrów w tym wypadku parametru ID. Wartość tego parametru zostanie przekazana do metody kontrolera w formie parametru metody.

public class HomeController
{
    public string Index(int? Id)
    {
        return "This is a message from HomeController";
    }
}

Ścieżka URL do tej metody wtedy wyglądałby tak “/Home/Index/2” zakładając, że parametr jest liczbą. Parametr może też być stringiem “/Home/Index/qwerty”.

Do definicji parametru dodałem znak zapytania zaznaczając w ten sposób, że nie jest on wymagany.

private void SetRoutes(IRouteBuilder routeBuilder)
{
    // Home / Index
    routeBuilder.MapRoute("MyDefault", "{controller}/{action}/{id?}");
}

Zobaczmy jak nasz routing działa. Jeśli teraz odświeżę stronę bez żadnych dodatkowych kresek zobaczysz nasz stary napis Hello.

Dlaczego tak się stało? Faktycznie ustawialiśmy ścieżki dla kontrolerów. Nie mamy jednak żadnego mapowania dla domyślnego adresu URL jakim jest po prostu nazwa naszej strony.

image

Napis Hello się wyświetlił ponieważ Middelware nie znalazł żadnego kontrolera, który mógłby obsłużyć to zapytanie HTTP. Przeszedł więc on dalej i wykonał się nasz stary Middelware Run. 

Middelware

Routing oczywiście działa jeśli wpiszesz kompletną ścieżkę odwołującą się do kontrolera i jego metody.

HomeController

Jak więc dodać domyślną ścieżkę, gdy żaden inny kontroler bądź akcja nie zostaną znalezione.

private void SetRoutes(IRouteBuilder routeBuilder)
{
    routeBuilder.MapRoute("MyDefault", 
        "{controller=Home}/{action=Index}/{id?}");
}

Mogę to zrobić poprzez znaki równa się w sposób pokazany powyżej.

default route homecontroller

Teraz po odświeżeniu strony widzisz, że ścieżka wskazuje na nasz kontroler Home.  Sprawdźmy czy nasza mapa działa dla każdego kontrolera i każdej akcji. Dodajmy więc kolejny kontroler do folderu Controllers. 

contactController

Ja utworzyłem kontroler Contact, ale będę szczery nie ma to większego znaczenia. Mój kontroler ma dwie metody Phone i Email.

public class ContactController
{
    public string Phone()
    {
        return "555-BUT";
    }

    public string Email()
    {
        return "franko@gmail.com";
    }
}

Sprawdźmy czy mogę wywołać metodę Phone wpisując odpowiedni adres URL odwołujący się do tego kontrolera i metody.

contact phone action controller

Jak widać tak. Sprawdźmy metodę email. 

contact email action controller

A teraz sprawdźmy co się stanie jak się odwołam tylko do kontrolera Contact.

contact

Jak widać uruchomił się mój Middelware Run. Dlaczego tak się stało? MVC szukał domyślnej akcji Index w kontrolerze Contact. Kontroler Contact jednak nie ma metody Index.

ASP.NET MVC nie znalazł sposobu na tę odpowiedź, przekazał więc zapytanie HTTP do kolejnego Middelware.

Atrybuty

Zanim skończę tę część kursu, omówmy drugi styl mapowania ścieżek URL do odpowiednich kontrolerów i akcji.

Poprzedni styl pozwolił nam zmapować wiele kontrolerów i wiele akcji naraz. Zdarzają się jednak pewne przypadki, w których te domyślne reguły, które ustaliliśmy wcześniej muszą zostać złamane dla wyjątkowych elementów naszej aplikacji.

W takich momentach warto więc odwołać się do atrybutów.

using Microsoft.AspNet.Mvc

Aby użyć atrybutów musisz dodać przestrzeń nazw Microsoft.ASPNET.MVC do aplikacji. Pamiętaj aby skorzystać z pomocy Visual Studio.

Atrybuty Route pozwalają mi przykładowo nadpisać ścieżkę dla tego kontrolera. Mimo iż kontroler nazywa się Contact teraz będę mógł się do niego odwołać poprzez adres URL Email.

[Route("Email")]
public class ContactController
{
    public string Phone()
    {
        return "555-BUT";
    }

    [Route("")]
    public string Email()
    {
        return "franko@gmail.com";
    }
}

Atrybuty Route mogą też by zastosowane do akcji. Teraz domyślną akcją dla tego kontrolera będzie metoda i akcja Email. Jeśli jednak teraz odświeżysz aplikację wpisując adres URL “/Email” zobaczysz ten błąd.

Aplikacja MVC nie wie, co jest domyślną akcją dla wywołania samego kontrolera Contact. Jaki błąd nas informuje, nie może zdecydować się pomiędzy akacją Phone a Email.

Exception ambiguousActionException

Błąd ten wynika z tego, że jak już zmapowaliśmy jedną metodę przy pomocy atrybutów oznacza to, że musimy zmapować wszystkie akcje tego kontrolera. A mówiąc dokładniej ten jeden atrybut dla tej jednej akcji zmapował wszystkie akcje w kontrolerze.

Dlatego musimy dodać kolejne atrybuty do innych akcji.

[Route("Email")]
public class ContactController
{
    [Route("Phone")]
    public string Phone()
    {
        return "555-BUT";
    }

    [Route("")]
    public string Email()
    {
        return "franko@gmail.com";
    }
}

Dodajmy więc atrybut też do drugiej metody. Teraz już wszystko powinno działać.

email path

Atrybuty też mają specjalne parametry. Mogę się odwołać do nazwy samego kontrolera wpisując w nawisach kwadratowych [controller]. Analogicznie mogę się odwołać do akcji metody wpisując [action].

[Route("[controller]")]
public class ContactController
{
    [Route("[action]")]
    public string Phone()
    {
        return "555-BUT";
    }

    [Route("")]
    public string Email()
    {
        return "franko@gmail.com";
    }
}

Do atrybutów mogę dodawać także inne literały i bardziej skomplikowane ścieżki.

[Route("me/[controller]")]
public class ContactController

Mogę sprawić przykładowo, że mój kontroler zostanie wywołany dopiero, gdy wpiszę jeszcze w adresie URL przedrostek “me”.

me contact controller

Oto szybka demonstracja dwóch styli mapowania ścieżek. W tym kursie wiele razy będziemy wracać do mapowania ścieżek. Teraz jednak wiesz jak można łączyć oba style tworzenia map, aby wywołać odpowiednie kontrolery i akcje.

Mając tę wiedze, możemy śmiało przejść dalej do kontrolerów, modeli i widoków.