Model RazorCzęść NR.4W poprzednim wpisie omówiliśmy jak ASP.NET MVC przy pomocy Routingu ustala, jaki kontroler powinien zostać użyty w zależności od adresu URL. W tym wpisie omówię kontrolery oraz rezultaty, jakie mogą być zwracane przez metody w kontrolerze. Omówimy krótko model oraz widok.

Mówiąc krótko po tym wpisie powinieneś zobaczyć, jak wszystkie elementy MVC ze sobą współpracują. 

Controller i IActionResult

Jak do tej pory w naszym projekcie korzystaliśmy z prostych klas w C#. Jak widać proste klasy mogą być kontrolerami. Mimo to, ten styl programowania nie jest zalecany.

Prawdziwy kontroler jednak powinien dziedziczyć po klasie Controller, która znajduje się w przestrzeni nazw “Microsoft.AspNet.Mvc”.

“Microsoft.AspNet.Mvc”.

Ta klasa bazowa daje mi dostęp do wielu podstawowych informacji na temat wywołanego zapytania HTTP. Poprzez właściwość HttpContext mogę uzyskać wszystkie potrzebne informacje na temat obecnego przetwarzanego zapytania HTTP.

HttpContext

Gdybym w kodzie potrzebował na przykład logiki sprawdzenia, czy dane żądanie HTTP ma odpowiednie nagłówki, to mógłbym to zrobić właśnie w taki sposób.

HttpContext Headers

Alternatywnie mógłbym dodać odpowiednie nagłówki HTTP odpowiedzi, jeśli istniałaby taka potrzeba. Oczywiście to, co mówię jest tylko sugestią w MVC istnieją lepsze sposoby na dodawanie nagłówków do odpowiedzi HTTP. Akurat jeśli coś robimy z obiektem reprezentującym odpowiedź HTTP oznacza to, że coś zrobiliśmy źle.

HttpContext Headers Response

Spójrzmy na inne właściwości, które dziedziczymy po klasie Controller.

Korzystając z właściwości ActionContext mogę uzyskać informację na temat tego, dlaczego właśnie ta akcja została wybrana przez system ścieżek.

ActionContext RouteData

Wewnątrz metody w kontrolerze wpisz słówko this, by zobaczyć dokładnie to samo, co jest pokazane na tych obrazkach.

Poza tym teraz moja klasa kontrolera posiada gotowe metody, które potrafią mi ułatwić zwrócenie jakiejś zawartości dla użytkownika. Jak do tej pory był to zwykły napis string.

ContentResult

Oto przykład metody Index, która tak jak wcześnie zwróci pewien napis, jeśli zostanie ona wywołana. Tym razem jednak korzystam z metody Content, która zwraca obiekt ContentResult.

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return Content("This is a message from HomeController");
    }
}

Istnieje wiele rezultatów, które kontroler może zwrócić dla użytkownika. Patrząc na wbudowane metody w klasie Controler można zobaczyć przeróżne typy zwracane. Przykładowo możesz zwrócić sam status HTTP jako formę odpowiedzi dla użytkownika.

Możesz też zwrócić strumień pliku do pobrania. Możesz też zwrócić odpowiedni widok czyli VIEW, który zbuduje stronę przygotowaną wcześniej przez ciebie. Możesz też zwrócić dane w formacie XML lub JSON sprawiając, że dane wywołanie adresu URL zachowa się jak usługa sieciowa typu REST.

Controller methods

Wszystkie klasy rezultatów implementują interfejs IActionResult. W metodach najlepiej stosować ten interfejs jako typ zwracany przez akcję, ponieważ w ten sposób uelastyczniasz kod. Z drugiej strony, jeśli jesteś pewny jaki typ powinien być zwrócony nie widzę przeszkód by go z góry nie zadeklarować.

Wracając do różnych sposobów na zwracanie odpowiedzi użytkownikowi. Możesz popatrzeć na inne metody zwracające rezultaty dla użytkownika.

FileResult 1

Przykładowo metoda File zwróci  strumień wirtualnego pliku, który zostanie pobrany przez użytkownika.

FileResult 2

Korzystając z innej metody  możesz zwrócić strumień pliku, który już istnieje.

JsonResult

Mając do dyspozycji metodę JSON jesteś w stanie zwrócić zwartość swojego obiektu w formie notacji JSON.  Jeśli nic to nie mówi, nie martw się za chwilę zwrócimy obiekt klasy w tej notacji.

Na razie kod naszego kontrolera wygląda tak. Jeśli odświeżysz aplikację to zobaczysz, że wszystko działa dokładnie, tak jak wcześniej.

public class HomeController : Controller
{
    public ContentResult Index()
    {
        return Content("This is a message from HomeController");
    }
}

Oczywiście widząc, że niczego nie zmieniliśmy możesz zadać sobie pytanie dlaczego w ogóle korzystamy z klasy ContentResult, skoro wciąż zwracamy goły napis. Dobre praktyki MVC mówią nam by zawsze zwracać obiekt implementujący interfejs IActionResult.

Chodzi tutaj o zasadę hermetyzacji. Istnieje duża różnica pomiędzy mówieniem co dokładnie chcemy zwrócić, a tworzeniem obiektu klasy, która tę decyzję robi za nas.

Dlatego raczej nigdy nie zobaczysz kontrolerów, które zwracają tylko napisy lub typy proste jak INT czy BOOL.

Aby dobrze zapamiętać tę lekcję na temat różnych rezultatów utwórz w projekcie folder Models. Tworzyliśmy foldery wcześniej więc na tym etapie zapewne wiesz jak to zrobić.

image_thumb[107]

Wewnątrz folderu utwórz klasę Poem

public class Poem
{
    public long Id { get; set; }
    public string Name { get; set; }

    public string Text { get; set; }
}

Klasa ta reprezentuje poemat. Poemat obecnie posiada swój identyfikator, nazwę i tekst. Wracając do kontrolera HomeController utwórz teraz instancję klasy Poem wewnątrz metody Index.

Nie zapomnij skorzystać z pomocy Visual Studio, aby dodać odpowiednią definicję użycia przestrzeni nazw gdzie są nasze modele.

using GamerPoems.Models

Oto jak ja uzupełnię swój obiekt poematu.

public class HomeController : Controller
{
    public JsonResult Index()
    {
        var model = new Poem()
        {
            Id = 1,
            Name = "Serce Mortal Kombat 2",
            Text = @"Gdy miałem 5 lat mój brat zrobił mi Fatalitę"
                    + " i wyrwał mi serce." +
                    "postanowiłem zostać programistą"
            };

        return new JsonResult(model);
    }
}

Jak widzisz zmieniłem rezultat metody Index na JsonResult. Nie musiałem tego zrobić, ale w ten sposób widać dokładnie, co jest właściwe zwracane.

Rezultat, który wypluje tekst w formie JSON można utworzyć na wiele sposobów tym razem utworzyłem instancję obiektu JsonResult, ale równie dobrze mógłbym skorzystać z metody Json, którą omówiliśmy wcześniej.

Gdy uruchomisz kod zobaczysz w swojej przeglądarce zawartość obiektu poematu serializowanego do formatu JSON.

JsonResult in action

Jeśli wcześniej nie widziałeś czym jest ten format JSON, to teraz możesz podziwiać dane swojego obiektu poematu sformatowanego do postaci obiektu JavaScript.

Jak widzisz także udowodniłem dlaczego warto korzystać z rezultatów, a nie z gołych napisów string. Informacje o tym jak ten obiekt został serializowanego  są ukryte wewnątrz obiektu JsonResult i w sumie ta logika w żaden sposób nie wycieka do akcji kontrolera.

Gdybym jednak zwracał bezpośrednio napis typu string, to bym musiał napisać kod, który by mi przetworzył ten obiekt na taki format tekstowy.

Oczywiście to by było stratą czasu. Przy okazji pokazałem ci jak w prosty sposób w ASP.NET Core można utworzyć usługę sieciową typu REST. 

Usługi te są ostatnio popularne ponieważ format danych JSON jest łatwo przyswajalny przez większość frameworków i jeżyków programowania. Co to znaczy? Znaczy to, że ktoś tworzący aplikację na telefony z systemem Android mógłbym zrobić wywołanie HTTP i pobrać odpowiednie informacje w tym przykładzie o poematach i wyświetlić je na telefonie użytkownika.

Poruszyliśmy temat modelu, czas więc zobaczyć jak ten model wyredenrować w postaci strony HTML.

View czyli Widok

W frameworku ASP.NET MVC najbardziej popularnym sposobem na tworzenie rezultatu HTLM jest skorzystanie z silnika Razor.

Aby skorzystać z tego silnika kontroler musi zwrócić rezultat typu ViewResult.  Obiekt ViewResult zajmie się przekazaniem modelu do odpowiedniego widoku jak i zadeklaruje jaki widok zostanie użyty na podstawie obecnego kontrolera.

public class HomeController : Controller
{
    public IActionResult Index()
    {
        var po = new Poem()
        {
            Id = 1,
            Name = "Serce Mortal Kombat 2",
            Text = @"Gdy miałem 5 lat mój brat zrobił mi Fatalitę"
                    + " i wyrwał mi serce." +
                    "postanowiłem zostać programistą"
            };

        return View();
    }
}

Brzmi skomplikowanie? Niezupełnie. Wracając do kodu naszego kontrolera, jeśli teraz zmienisz rezultat JSON na rezultat widokowy, to zobaczysz następujący błąd.

Exception when there is no view

Co się stało? Otóż MVC szukał odpowiedniego widoku dla mojej akcji. Domyślna konwencja MVC mówi, że ten widok, który chce zwrócić powinien znajdować się w folderze Views/Home/Index.cshtml lub w folderze /Views/Shared/Index,cshtml.

Widok to pliki o rozszerzaniu cshtml. Dlaczego mają takie rozszerzenie? Otóż oprócz znaczników HTML wewnątrz widoków można też korzystać z kodu napisanego w C#. Co w esencji właśnie jest silnikiem Razor, który buduję stronę HTML na podstawie tego szablonu. Ten szablon będzie zawierał  mix znaczników HTML i prostego kodu C#.

image

Domyślna konwekcja MVC mówi, że wszystkie widoki są i będą w folderze Views. Następny folder jest określony przez nazwę kontrolera. A sam plik widoku ma nazwę jak dana akcja, w tym wypadku jest to akcja o nazwie Index.

Jeśli MVC nie znalazł takiego widoku, to próbuje jeszcze go znaleźć w folderze Shared. W folderze Shared znajdują się widoki, które są używane wielokrotnie przez różne kontrolery.

Poza tym w folderze Shared są widoki, które mogą być użyte w każdym miejscu aplikacji, w tym przez inne widoki.

Aby mój rezultat widokowy działał poprawnie muszę utworzyć plik widoku Index.cshtml i umieścić go odpowiednio w folderze.

Folders

Co też zrobiłem. Plik widoku w oknie “Add new Item” nazywa się “MVC View Page”.

MVC View Page

Mój plik cshtml obecnie wygląda tak. Radzę ci byś skasował wszystko, co masz w tym pliku i skopiował zwartość HTLM, którą napisałem poniżej.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <h1>Hi</h1>
    This is Views/Home/Index
</body>
</html>

Odświeżając aplikację możesz zobaczyć teraz, że rzeczywiście kontroler zwrócił mi stronę HTML.

ViewResult in browser

Wrócimy teraz do kontrolera Home. Jeśli wywołasz intellisense wewnątrz metody View, to zobaczysz, że ta metoda ma kilka przeciążeń.

ViewResult more methods

Patrząc na najbardziej rozbudowaną wersję tej metody możemy zobaczyć, że istnieje możliwość deklaracji nazwy widoku, do którego będziemy się odwoływać.

Jeśli więc nie chciałbym skorzystać z domyślnej konwencji, to zawsze mogę podać nazwę swojego pliku cshtml w parametrach tej metody.

Oprócz nazwy widoku możemy do tej metody przesłać model.

public IActionResult Index()
{
    var model = new Poem()
    {
        Id = 1,
        Name = "Serce Mortal Kombat 2",
        Text = @"Gdy miałem 5 lat mój brat zrobił mi Fatalitę"
                + " i wyrwał mi sercę." +
                "postanowiłem zostać programistą"
        };

    return View(model);
}

W kodzie cshtml przy użyciu Razor i C# możemy do tego modelu w widoku się odnieść.

Razor in cshtml

Aby użyć kodu C# i modelu muszę skorzystać z odpowiedniego wyrażenia. Dodając znak małpy informuję silnik Razor, że w tym miejscu piszę kod a nie składnie strony HTML.

Korzystając z właściwości Model odnoszę się teraz do obiektu, który zostanie mi przesłany w kontrolerze.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <h1>Hi</h1>
    <p>@Model.Name</p>
    <p>@Model.Text</p>
</body>
</html>

Ten kod będzie działać poprawnie tak długo, jak mój model poematu ma nazwę i tekst. Jeśli odświeżysz stronę możesz zobaczyć jak nasz model został wydrukowany do naszej strony HTML.

View with Razor in Browser

To, co zobaczyłeś to esencja wzorca MVC w pełnej swojej krasie. Kontroler tworzy model, model jest wysłany do widoku, a widok ustawia sobie zawartość modelu w odpowiedni sposób do swojej strony HTML.

Na tym etapie wszystko jest proste, ale na razie nam chodzi o zrozumienie działania wzorca MVC bez dodatkowych ulepszeń, które pojawią się później.

Na tym etapie możesz zauważyć, że kontroler nie ma pojęcia, jak widok użyje modelu. Sam model nie ma pojęcia, w którym widoku i w którym kontrolerze zostanie użyty. Sam widok wie tylko jakie modele są mu potrzebne, aby utworzyć swoja stronę. On nie wie nic o kontrolerze.

Zanim przejdziemy dalej warto zaznaczyć pewien ważny szczegół związany z działanie Intellisence wewnątrz pliku cshtml.

Jak zapewne zauważyłeś u siebie, albo w gifie powyżej odwołując do modelu Visual Studio nie podpowiadam jakie właściwości ma nasz model. Dzieje się tak ponieważ w esencji nasz widok nie wie czym jest ten model.

Możemy to zmienić poprzez deklarację i określenie czym dokładnie jest model, w sposób ukazany poniżej.

@model GamersPoems.Models.Poem
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <h1>Hi</h1>
    <p>@Model.</p>
    <p>@Model.Text</p>
</body>
</html>

Teraz powinieneś zobaczyć podpowiadanie Visual Studio w widoku cshtml.

Razor Intellisense

Jest to na pewno pomocne ponieważ, jeśli się dowołasz właściwości, której model nie ma, wtedy zobaczysz wyjątek wygenerowany przez silnik Razor.

Zbliżamy się ku końcowi. Dla utrwalenia wiedzy stworzymy szybko tabelkę przy pomocy Razor i C#.

Stwórzmy tabelkę gier do poematów

Mając już całkiem sporą widzę o tym jak to wszystko przepływa możemy stworzyć coś bardziej poważniejszego. Zmodyfikujemy ponownie kontroler Home i metodę Index tak, aby wyświetlić na stronie tabelkę z grami.

Wiemy jak wyświetlić jeden model, ale jak wyświetlić kolekcje w widoku. Zaraz się przekonasz.

W folderze Model utwórz więc model, który będzie reprezentować gry.

public class Game
{
    public long Id { get; set; }
    public string Name { get; set; }
}

Struktura całego projektu u mnie wygląda tak. Do folderu Services utwórz folder Interface. W tym folderze będziemy umieszczać wszystkie interfejsy usług. W folderze Interface utwórz interfejs IGameData, a w folderze Services utwórz klase GameData.

Folders in MVC Project

W menu Add New Item istnieje szablon Interfejsu.

Interfejs

Po co nam interfejs? W przyszłości utworzymy klasę, która będzie pobierała kolekcję gier i kolekcję poematów z baz danych. Na razie jednak nie musimy iść aż tak do przodu. Utworzymy prostą klasę, która zwróci statyczną kolekcję gier napisaną w twardo w kodzie.

Nasza klasa będzie implementować interfejs. Korzystając z wstrzykiwania zależności, gdy moment podmiany klasy nastąpi będziemy musieli zmienić tylko deklarację czym dokładnie ten interfejs jest.

Nasz interfejs reprezentuje więc usługę, która będzie zwracała kolekcję gier. Pamiętaj by korzystać z pomocy Visual Studio.

using GamerPoems.Models

Moja definicja interfejsu na razie zawiera jedną metodę.

public interface IGameData
{
    IEnumerable<Game> GetAll();
}

Teraz czas uzupełnić klasę, która będzie implementować ten interfejs. Visual Studio potrafi wygenerować odpowiednie metody potrzebne do implementacji interfejsu.

Implement interface

Oto jak moja klasa GamesData wygląda. Moja implementacja metody GetAll zwraca listę 3 gier.

public class GamesData : IGameData
{
    public IEnumerable<Game> GetAll()
    {
        List<Game> games = new List<Game>();

        games.Add(new Game() { Name = "Mortal Kombat 2", Id = 1 });
        games.Add(new Game() { Name = "Mega-lo-Mania", Id = 2 });
        games.Add(new Game() { Name = "Dune II", Id = 3 });

        return games;
    }
}

Aby wstrzykiwanie zależności zadziało musimy w klasie Startup w metodzie ConfigureServices powiedzieć czym dokładnie serwis IGameData będzie.

Robimy to tylko w tym miejscu. Później łatwo będzie podmienić definicję tej usługi ponieważ robi się to tylko w tym miejscu.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        services.AddScoped<IGamesData, GamesData>();
        services.AddSingleton<IMessages, Messages>();
    }

Na tym etapie warto wyjaśnić różnicę pomiędzy deklaracją usługi Scope, a Singleton. Usługa zadeklarowana jako singleton zostanie utworzona tylko raz w życiu całej aplikacji.

Usługa zadeklarowana jako Scope będzie tworzona z każdym połączeniem HTTP. Jest to istotne ponieważ później będziemy korzystać z sesji w bazie danych. Sesja nie powinna być współdzielona pomiędzy połączeniami HTTP dlatego nie może ona być sigletonem.

W kontrolerze musimy dodać konstruktor, który będzie przyjmować naszą usługę IGamesData. Usługa zostanie potem zapisana do pola prywatnego.

Konstruktor tego kontrolera zostanie wywołany przez framework MVC więc nie musisz się martwić tym, że na tym etapie pracujesz z interfejsem gdyż, gdy ten kod zostanie uruchomiony trafi tutaj obiekt klasy, który ustaliliśmy w klasie Startup.

public class HomeController : Controller
{
    private IGamesData _gamesData;

    public HomeController(IGamesData gamesData)
    {
        _gamesData = gamesData;
    }

    public IActionResult Index()
    {
        var result = _gamesData.GetAll();

        return View(result);
    }
}

Korzystając z usługi pobieram kolekcję gier i wysyłam tę kolekcję do widoku. W naszym widoku musimy zmienić typ modelu, gdyż obecnie nie jest to poemat, będzie to kolekcja gier. Interfejs IEnumerable daje nam elastyczność, gdyż kto wie, być może kontroler prześle nam inną kolekcję List.

@model IEnumerable<GamersPoems.Models.Game>

Korzystając z pomocy Visual Studio napisz deklaracje pętli foreach. Napisz słowo foreach a potem wciśnij TAB.

foreach razor

Mając pętlę foreach możesz utworzyć jeden wiersz tabeli dla każdego elementu kolekcji w sposób ukazany poniżej.

<body>
    <h1>Hi</h1>
    <table>
        <tbody>
            <tr>
                <td>Id</td>
                <td>Name</td>
            </tr>
        </tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @item.Id
                </td>
                <td>
                    @item.Name
                </td>
            </tr>
        }
    </table>
</body>

Tak utworzyliśmy tabelkę, która wyświetla nam listę gier. Wracając do kodu razor w pliku cshtml możesz zauważyć to, jak ten silnik jest w stanie wykryć kiedy piszemy kod w C# , a kiedy korzystamy ze znaczników HTML.

Zazwyczaj by skorzystać z kodu w C# w widoku musimy użyć znaku małpy chyba, że znajdujemy się w bloku kodu, który zrobił to już wcześniej.

Jeśli odświeżysz stronę powinieneś zobaczyć następujący rezultat.

HTML table

To by było na tyle w tej części kursu.

Następnym razem utworzymy formularz, który pozwoli nam dodawać gry i poematy.