20170810 artyku%c5%82 ilustracja 2
Jak szybko i przyjemnie stworzyć aplikację webową w środowisku .NET i Ext JS?

Pierwsza część artykułu napisanego przez Senior Developera Michała Koźlika.

1. Wstęp

Systemy internetowe stanowią obecnie dominującą część produktów wytwarzanych przez firmy software’owe. Nie może zatem budzić zdziwienia fakt, że większość narzędzi wytwarzania oprogramowania mocno wspiera ten trend. Widoczne jest to także w przypadku środowiska .NET. W tym artykule postaramy się przedstawić przykładowe narzędzia, biblioteki i mechanizmy, których znajomość jest niezbędna do stworzenia aplikacji webowej. W tym celu wykorzystamy środowiska IDE Visual Studio Community 2017. Ta wersja Visual Studio udostępniana jest nieodpłatnie i można ją pobrać bezpośrednio ze strony firmy Microsoft. Użyty zostanie framework .NET w wersji 4.5.2.

Szkielet backend’u przykładowej aplikacji napisany zostanie z zastosowaniem Web Api 2. Ten mechanizm będący częścią rozwiązania ASP.NET umożliwia nam efektywną implementację serwisów typu RESTful. Dla przypomnienia serwisy RESTful opierają swoje działanie na protokole http. Komunikacja poprzez ten protokół odbywa się za pomocą 4 podstawowych metod tj. get (pobieranie danych), post (zapis danych), put (edycja danych) i delete (usuwanie danych). Tego typu webserwisy udostępniane są jako zasoby. Dostęp do danego zasobu możliwy jest poprzez odpowiedni adres URL wywoływany z poziomu protokołu http za pomocą określonej metody. Przykładowe zasoby mogłyby zatem być następujące:

Adres zasobu                         Metoda        Działanie

http://localhost/api/station         GET​          Pobranie wszystkich elementów

http://localhost/api/station/1      GET          Pobranie elementu o numerze id=1

http://localhost/api/station       POST​       Dodanie nowego elementu

http://localhost/api/station/1     PUT​         Edycja istniejącego elementu (id=1)

http://localhost/api/station/1     DELETE​  Usunięcie elementu (id=1)

Tabela nr 1. Przykładowy zestaw zasobów w ramach serwisu RESTful.

Frontend aplikacji będzie korzystać z przygotowanej infrastruktury Web Api. Dla urozmaicenia pokażemy zastosowanie dwóch frameworków Javascript’owych tj. Ext Js oraz Knockout JS. Ext JS jest zaawansowanym framework’iem z wieloma gotowymi komponentami, których można użyć do budowy GUI przygotowywanej aplikacji (obecnie jest to ponad 115 komponentów, część z nich jest bardzo zaawansowana). Dodatkowo framework ten wspiera paradygmat MVVC (Model, View, ViewModel). Wzorzec ten dokonuje wyraźnej separacji pomiędzy modelem danych, widokiem, a klasą ViewModel, która udostępnia dane dla widoku. Model i widok wiązane są ze sobą za pomocą tzw. data-bind’ingu. W przypadku data-binding’u dwukierunkowego wszystkie dane z modelu zostają odzwierciedlone w powiązanych komponentach widoku, a zmiany poczynione w tych komponentach są automatycznie odwzorowywane w modelu. Oznacza to, że cały czas mamy spójny stan widoku i modelu z którego ten widok korzysta. Framework ten nadaje się do tworzenia skomplikowanych i dużych aplikacji klasy enterprise. Trzeba mieć także na uwadzę fakt, że Ext JS jest rozwiązaniem płatnym.

W mniejszych projektach, gdzie nie ma potrzeby stosowania tak rozbudowanych rozwiązań jak Ext JS lub też po prostu budżet projektu nie pozwala na zakup takich narzędzi rozwiązaniem alternatywnym może być framework Knockout JS. Framework ten nie posiada co prawda wbudowanego zbioru komponentów, jednak świetnie nadaje się do organizacji struktury projektu z zastosowaniem wcześniej opisanego wzorca MVVC i bindowania danych. Same zaś komponenty można obecnie łatwo stworzyć z wykorzystaniem takich narzędzi jak Bootstrap i jQuery.

2.Czym będziemy się zajmować ?

Przykładowa aplikacja, którą przygotujemy będzie umożliwiać przeglądanie oraz edycję rozkładu jazdy pociągów. Jak widać na mockupie poniżej główne informacje dotyczące tras i rozkładu pociągów przedstawione zostaną za pomocą hierarchicznego grida. Elementy nadrzędne zawierać będą informację na temat nazwy pociągu i trasy, a elementy podrzędne to stacje pośrednie między początkiem trasy, a miejscem docelowym.

Rysunek nr 1. Mockup GUI tworzonej aplikacji.

W dolnej części widzimy formatkę, która będzie służyła do konfiguracji nowego połączenia kolejowego oraz grid z listą stacji pośrednich. Gridy zostaną zaimplementowane z użyciem komponentów TreeGrid oraz Grid z ExtJS. Formularz edycyjny składać będzie się ze zwykłych komponentów HTML5 obsługiwanych za pomocą Knockout JS.

3.Pierwsze kroki – utworzenie projektu w Visual Studio

Tak jak wspomnieliśmy na początku, do utworzenia aplikacji zostanie użyte Visual Studio 2017 Community. Klikamy opcję Plik/Nowy projekt, a następnie z szablonów sieci Web wybieramy Aplikacja sieci Web platformy ASP.NET (.NET Framework).

Rysunek nr 2. Tworzenie nowego projektu – krok 1.

W kolejnym kroku określamy typ aplikacji, którą chcemy utworzyć – w naszym przypadku będzie to Pusty szablon, ale z dodanymi folderami i odwołaniami do Web API.

Rysunek nr 3. Tworzenie nowego projektu – krok 2.

Po wygenerowaniu projektu będzie on miał strukturę jak poniżej:

Rysunek nr 4. Struktura projektu.

4.Model danych

Załóżmy dla potrzeb naszego przykładu, iż otrzymaliśmy wymagania dotyczące informacji jakie mają być przechowywane w aplikacji w postaci schematu relacyjnego bazy danych (rysunek nr 5). Model ten jest stosunkowo prosty i zawiera w sobie tylko dwie encje, które przechowują odpowiednio dane połączenia kolejowego i informacje o stacjach pośrednich.

Rysunek nr 5. Model bazy danych.

Aby zamapować model relacyjny (bazodanowy) do model obiektowego (aplikacyjnego) zastosujemy Entity Framework 6. Użyjemy podejścia „code-first”. Oznacza to, że najpierw stworzony zostanie model danych w postaci klas, a następnie na podstawie tego modelu wygenerowana zostanie struktura bazy danych. To podejście w przypadku aplikacji budowanych od podstaw wydaje się mieć najwięcej zalet. Inne dostępne metody tworzenia bazy danych z Entity Framework to:

  • Database first – najpierw tworzymy schemat bazy danych, a następnie na tej podstawie Entity Framework generuje model domenowy klas i mapowania pomiędzy tabelami i klasami. To podejście sprawdza się jeśli np. otrzymujemy istniejącą bazę danych z którą ma integrować się powstająca aplikacja.
  • Model first – tworzymy model danych za pomocą designera. Entity Framework na tej podstawie generuje zestaw klas i tabel bazodanowych. Takie rozwiązanie może sprawdzić się w przypadku, gdy za projekt bazy danych odpowiedzialni są ludzie niekoniecznie znający szczegóły techniczne języka C#, platformy .NET czy też samego Entity Framework. Mogą oni wówczas zaprojektować bazę danych w designerze, która po generacji modelu obiektowego i bazodanowego zostanie użyta w projekcie przez programistów.

Stwórzmy zatem w folderze Models dwie klasy tj. TrainConnection oraz Station:

public class TrainConnection
{

        [Key]
        public int Id { get; set; }

        [Required]
        public String Name { get; set; }

        [Required]
        public String Start { get; set; }

        [Required]
        public String Destination { get; set; }

        [Required]
        public TrainConnectionType ConnectionType { get; set; }

        [JsonIgnore]
        public virtual ICollection<Station> Stations { get; set; }
}

Atrybut Key pozwala oznaczyć pole, które zostanie użyte jako klucz główny tabeli. Z kolei atrybut Required spowoduje, iż pole to nie będzie mogło przyjąć wartości null w bazie danych. TrainConnectionType jest typem wyliczeniowym. Jego wartości określają typ  połączenia kolejowego.

public enum TrainConnectionType { Intercity, Pendolino, Regio };

public class Station
{

        [Key]
        public int Id { get; set; }

        [Required]
        public String Name { get; set; }

        public DateTime? ArrivalTime { get; set; }

        public DateTime? DepartureTime { get; set; }

        // Foreign Key               
        public int TrainConnectionId { get; set; }

        // Navigation property       
        [ForeignKey("TrainConnectionId")]

        public TrainConnection TrainConnection { get; set; }
}

Atrybut ForeignKey określa powiązanie z encją TrainConnection poprzez klucz obcy przechowywany w polu TrainConnectionId.

5.Kontroler

Uwaga! Czasami przed wygenerowaniem szkieletu kontrolera konieczne jest skompilowanie projektu za pomocą opcji Kompilowanie/Kompiluj rozwiązanie.

Kolejny krok to wygenerowanie kontrolera z metodami WebAPI na podstawie klasy domenowej TrainConnection. W tym celu postępujemy w następujący sposób:

  1. Klikamy prawym klawiszem myszy na folderze Controllers i wybieramy opcję Dodaj kontroler.
  2. Z dostępnych rodzajów kontrolerów wybieramy „Kontroler Web API2 z akcjami używający narzędzia Entity Framework”.

Rysunek nr 6. Tworzenie kontrolera – etap 1.

W kolejnym oknie określamy klasę modelu (TrainConnection) oraz klikamy przycisk +, aby wygenerować nowy kontekst danych (jego nazwa może pozostać domyślna). Zaznaczamy również opcję „Użyj asynchronicznych akcji kontrolera”.​

Rysunek nr 7. Tworzenie kontrolera – etap 2.

Wygenerowany kontroler zawiera szkielet gotowych funkcji Web API, które obsługują encje TrainConnection z wykorzystaniem wcześniej przedstawionych metod protokołu http tj. GET, POST, PUT oraz DELETE. Kod ten może być z powodzeniem użyty, jednak ze względu na specyficzne wymagania framework’ów Javascript, które zastosujemy, będziemy musieli dokonać modyfikacji tych metod w kolejnych krokach implementacji.

W podobny sposób proszę wygenerować kontroler StationController na podstawie encji Station.

6.Migracja i inicjalizacja danych

Entity Framework dba o to, aby kod modelu danych był spójny z modelem bazodanowym. Odbywa się to za pomocą odpowiednich skryptów migracyjnych, które mogą zostać wygenerowane automatycznie na podstawie zmian dokonanych w kodzie i porównania stanu modelu obiektowego z modelem bazodanowym. Aby uaktywnić migrację wybieramy opcję Narzędzia/Menedżer pakietów NuGet/Konsola menedżera pakietów. W konsoli wpisujemy polecenie Enable-Migrations. Proces ten powinien zakończyć się wyświetleniem komunikatu „Code First Migrations enabled for project TimeApp”. W nowopowstałym katalogu Migrations wygenerowane zostały dwa pliki:

  1. data_InitialCreate.cs – skrypt migracyjny tworzący odpowiednie tabele bazodanowe
  2. Configuration.cs – plik w którym można dopisać kod inicjalizujący dane

Dodajmy zatem do pliku Configration.cs w metodzie Seed następujący kod:Wywołanie powyższego kodu spowoduje utworzenie przykładowego połączenia kolejowego (encja TrainConnection) z listą stacji pośrednich (encje Station).

DateTime dateNow = DateTime.Now;

context.TrainConnections.AddOrUpdate(x => x.Id,
new Models.TrainConnection() { Id = 1, Name = "IC 6306 (KOSSAK)", Start = "Wroclaw Glowny", Destination = "Krakow Glowny", ConnectionType = Models.TrainConnectionType.Pendolino });

context.Stations.AddOrUpdate(x => x.Id,
new Station() { Id = 1, Name = "Wroclaw Glowny", DepartureTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 18, 20, 0), TrainConnectionId =1 },
new Station() { Id = 1, Name = "Olawa", ArrivalTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 18, 36, 0), DepartureTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 18, 37, 0), TrainConnectionId = 1 },
new Station() { Id = 1, Name = "Brzeg", ArrivalTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 18, 46, 0), DepartureTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 18, 47, 0), TrainConnectionId = 1 },
new Station() { Id = 1, Name = "Opole Glowne", ArrivalTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 19, 08, 0), DepartureTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 19, 10, 0), TrainConnectionId = 1 },
new Station() { Id = 1, Name = "Lubliniec", ArrivalTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 19, 41, 0), DepartureTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 19, 42, 0), TrainConnectionId = 1 },
new Station() { Id = 1, Name = "Czestochowa Stradom", ArrivalTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 20, 05, 0), DepartureTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 20, 06, 0), TrainConnectionId = 1 },
new Station() { Id = 1, Name = "Koniecpol", ArrivalTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 20, 50, 0), DepartureTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 20, 52, 0), TrainConnectionId = 1 },
new Station() { Id = 1, Name = "Krakow Glowny", ArrivalTime = new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, 21, 35, 0), TrainConnectionId = 1 });

context.SaveChanges();

Aby powyższy kod został wykonany ponownie otwieramy Konsolę menedżera pakietów (Narzędzia/Menedżer pakietów NuGet/Konsola menedżera pakietów) i wydajemy polecenie Update-Database. Spowoduje to wykonanie powyższej metody Seed.

Możemy teraz sprawdzić czy struktura bazy danych oraz dane początkowe zostały dobrze zapisane w bazie danych. Wybierzmy zatem z menu Widok opcję SQL Server Object Explorer. W gałęzi localdb odnajdziemy węzeł Databases a w nim wcześniej stworzony kontekst (np. TimeAppContext).W ramach kontekstu możemy zobaczyć stworzone tabele (TrainConnections, Stations).

Rysunek nr 8. Struktura utworzonej bazy danych.

Możliwe jest także kliknięcie prawym klawiszem myszy na danej tabeli i wywołanie opcji View Data co spowoduje wyświetlenie wszystkich danych zawartych w danej tabeli jak na rysunku nr 9.

Rysunek nr 9. Dane w tabeli Stations.

Część druga artykułu zostanie opublikowana 25 sierpnia br.