Tworzenie obszarów w .Net Core

Obszary aplikacji to byty, które mają nam oddzielić jedną logikę aplikacji od drugiej, np:

Mamy panel administratora oraz obszar aplikacji dla klienta. Jeżeli chcielibyśmy to zrobić w klasyczny sposób, to widoki oraz logika aplikacji dla klienta i administratora byłyby w jednym katalogu głównym aplikacji i razem ze sobą pomieszane, przez co struktura aplikacji staje się mniej czytelna i trudniejsza do zarządzania.

Do przykładu użyję wcześniejszego projektu z wpisu: https://antyper.pl/2019/04/02/sposoby-wyszukiwan-widokow-w-net-core-mvc/
Oto link do repozytorium:
https://github.com/pnkp/Tutorials/tree/master/ViewsTutorials/SearchingViews

Zaczniemy od dodania folderu o nazwie Admin w naszym katalogu głównym projektu. Będę bazował na przykładzie aplikacji administratora oraz klienta. Następnie musimy skonfigurować routing w metodzie Configure, która znajduję w klasie Startup. Metoda po modyfikacji wygląda tak:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseDeveloperExceptionPage();
    app.UseStatusCodePages();
    app.UseStaticFiles();
    app.UseMvc(options =>
    {
        options.MapRoute(
            name: "areas",
            template: "{area:exists}/{controller=Home}/{action=Index}"
        ); //dodanie nowego routingu
        
        options.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}"
        );
    });
}

Dodałem nowy routing dla naszej aplikacji.

Uwaga!
Musimy pamiętać, że kolejność routingu jest ważna. Framework dobiera adres url do pierwszego dostępnego wzoru. Jeżelibyśmy zamienili kolejność zdefiniowanych routingów, aplikacja może zadziałać nieoczekiwanie.

Najbardziej może nas zaciekawić nietypowy wzór dla naszego routingu:

"{area:exists}/{controller=Home}/{action=Index}"

Dodany jest nowy parametr {area:exists}. Oznacza on, że jeżeli wystąpi nazwa zdefiniowanego obszaru, wtedy framework zaczyta kontroler i akcję z wybranego obszaru. Reszta parametrów została bez zmian. Dla domyślnego kontrolera zdefiniowany jest kontroler Home a dla akcji –Index. Oczywiście można dowolnie modyfikować domyślne wartości dla kontrolera i akcji.

Przejdźmy dalej i utwórzmy nowy plik o nazwie
HomeController.cs w lokalizacji Areas/Admin/Controllers. Oto treść naszego nowego pliku:

using Microsoft.AspNetCore.Mvc;

namespace ViewsProject.Areas.Admin.Controllers
{
    [Area("Admin")]
    public class HomeController : Controller
    {
        public ViewResult Index() => View();
    }
}

Główną różnicą między kontrolerem, który wcześniej zdefiniowałem w projekcie jest to, że występuję atrybut [Area(„Admin”]. Bez tego znacznika nasz obszar nie mógłby istnieć.
Jak można zauważyć, w atrybucie jest przypisana wartość Admin. Jest to nazwa naszego obszaru, według której framework będzie szukał naszych kontrolerów oraz akcji.

Dla celów dydaktycznych dodałem jeden model oraz repozytorium.
W lokalizacji Areas/Models utworzyłem plik o nazwie Client.cs:

namespace ViewsProject.Areas.Admin.Models
{
    public class Client
    {
        public int ClientId { get; set; }
        public string ClientName { get; set; }
    }
}

Jest to prosty model do przechowywania danych o pewnej strukturze.

Następnie utworzyłem plik MemoryRepository.cs w lokalizacji Areas/Admin/Repository, oto ciało klasy:

using System.Collections.Generic;
using ViewsProject.Areas.Admin.Models;

namespace ViewsProject.Areas.Admin.Repository
{
    public class MemoryRepository
    {
        private readonly IDictionary<int, Client> _clients = new Dictionary<int, Client>();

        public MemoryRepository()
        {
            AddClient(new Client{ClientName = "Paweł"});
            AddClient(new Client{ClientName = "Krzysztof"});
        }
        
        public Client this[int id] => _clients[id];

        public IEnumerable<Client> Clients => _clients.Values;

        public Client AddClient(Client client)
        {
            var index = _clients.Count;
            client.ClientId = index;

            _clients[index] = client;

            return client;
        }
    }
}

Klasa MemoryRepository ma na celu przetrzymania danych w pamięci. Będziemy ją wykorzystywać do wyświetlania danych o klientach.

Musimy jeszcze zmodyfikować nasz kontroler, tak aby mógł operować na klasie MemoryRepository. Ciało kontrolera po modyfikacji:

using Microsoft.AspNetCore.Mvc;
using ViewsProject.Areas.Admin.Repository;

namespace ViewsProject.Areas.Admin.Controllers
{
    [Area("Admin")]
    public class HomeController : Controller
    {
        private readonly MemoryRepository _repository = new MemoryRepository();

        public ViewResult Index()
        {
            return View(_repository.Clients);
        }
    }
}

Jak widać, w kontrolerze zainicjowałem klasę MemoryRepository w zmiennej prywatnej. Później do metody View() przekazałem dane z klasy MemoryRepository do modelu widoku.

Celowo zainicjowałem klasę MemoryRepository bezpośrednio w zmiennej w klasie HomeController. Później będę modyfikował ten przykład do pokazania mechanizmu wstrzykiwania i cyklu życia zależności. 

Do naszego projektu potrzebujemy jakiegoś widoku. W katalogu Areas/Admin/Views utworzyłem plik o nazwie Index.cshtml.

@model IEnumerable<Client>
@{ Layout = null;}

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Obszar administratora</title>
</head>
<body>
Obszar administratora.
Klienci:
<table>
  <tr>
    <th>Id klienta</th>
    <th>Nazwa klienta</th>
  </tr>
  @foreach (var client in Model)
  {
    <tr>
      <th>@client.ClientId</th>
      <th>@client.ClientName</th>
    </tr>
  }
</table>
</body>
</html>

Zwróćcie uwagę na kluczowe słowo, którym jest: @model. Po słowie kluczowym definiujemy typ modelu jaki ma być wykorzystany do wyświetlenia widoku. Dzięki takiemu zabiegowi nasze IDE bardzo dobrze radzi sobie z podpowiadaniem składni modelu, który jest wykorzystany w widoku.

Kolejną rzeczą jest wyświetlenie listy klientów w tabeli. Wykorzystałem do tego celu pętlę @foreach. Silnik Razor iteruje po modelu a my możemy dowolnie formatować nasze dane w widoku.

Ważna sprawa!
Słowo kluczowe "model" z małej litery służy do definiowania naszego modelu w widoku. Natomiast słowo "Model" z wielkiej litery przechowuje dane naszego modelu. Dlatego w pętli foreach użyłem słowa kluczowego "Model", ponieważ znajdują się tam potrzebne dane.

Małym usprawnieniem jest dodanie pliku _ViewImports.cshtml. Musimy go umieścić w lokalizacji Areas/Admin/Views.
Jest to plik odpowiedzialny za importowanie między innymi specjalnych znaczników pomocniczych oraz modelu.
Ja użyłem go do zdefiniowania modelu, dzięki temu nie muszę wpisywać całej ścieżki przestrzeni nazw. Oto wygląd pliku _ViewImports.cshtml:

@using ViewsProject.Areas.Admin.Models;

Podsumowanie

Jak można zauważyć na przykładzie, obszary są przydatne w podzieleniu projektu na odpowiedzialności oraz porządkuje strukturę projektu.

Cały zmodyfikowany kod jest dostępny pod adresem:

https://github.com/pnkp/Tutorials/tree/master/ViewsTutorials/SearchingViews

Sposoby wyszukiwań widoków w .NET Core MVC

W dzisiejszym wpisie chciałbym poruszyć temat wbudowanego wyszukiwania widoków w frameworku .Net Core.

Materiał tworzony jest w celach edukacyjnych oraz utrwalania sobie wiedzy

Do ćwiczeń potrzebujemy pusty projekt .Net Core MVC. Generujemy go w wybranych folderze za pomocą narzędzia CLI.

dotnet new web

Po chwili utworzy nam się nowy projekt aplikacji webowej. Możemy go uruchomić za pomocą komendy:

dotnet watch run

Komenda uruchamia nam wewnętrzny serwer środowiska .Net Core. A parametr watch , nasłuchuje zmian w plikach, które podlegają kompilacji i automatycznie buduje i uruchamia serwer.

Do ćwiczeń potrzebujemy skonfigurować odpowiednio klasę Startup.cs, która znajduję się w głównym folderze projektu. Dokładnie musimy zmodyfikować dwie metody: ConfigureServices oraz Configure. Metody te odpowiadają za konfigurację naszej aplikacji.

Ciało naszej funkcji wygląda teraz tak:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
}

W metodzie ConfigureServices włączyliśmy framework .Net Core przy użyciu metody AddMvc(), która jest dziedziczona z interfejsu IServiceCollection.

Kolejnym krokiem jest skonfigurowanie metody Configure,

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
     app.UseDeveloperExceptionPage();
     app.UseStatusCodePages();
     app.UseStaticFiles();
     app.UseMvc(options =>
     {
          options.MapRoute(
              name: "default",
              template: "{controller=Home}/{action=Index}/{id?}"
         );
     });
}
  • UseDeveloperExceptionPage – odpowiada za wyświetlanie strony podczas wywołania niespodziewanego wyjątku w naszej aplikacji, pozwala ona nam namierzyć w którym miejscu i dlaczego pojawił się nieoczekiwany problem,
  • UseStatusCodePages – odpowiada za wyświetlanie strony z kodami błędów HTTP, np: jeżeli system routingu nie znajdzie odpowiedniej ścieżki, wtedy dostaniemy stronę z zawartością „Status Code: 404; Not Found” oraz statusem HTTP 404,
  • UseStaticFiles – odpowiada za statyczne pliki dołączane do naszej witryny, np: jeżeli chcemy z poziomu projektu dołączyć pliku ze kodem JavaScriptu lub z plikami CSS.
  • UseMvc – metoda udostępnia nam funkcje lambda, w tym ćwiczeniu wykorzystam tą metodę do wystawienia routingu,

Teraz trochę dłużej o metodzie MapRoute:

W tym przeciążeniu metody MapRoute dodałem dwa parametry. Name odpowiada za nazwę naszego routingu, a parametr template jest odpowiedzialny za wzór naszego routingu. Według tego wzoru zostają dopasowane adresy url do naszych kontrolerów.

Jak możemy zauważyć, wzór naszego routingu wygląda jakoś nietypowo. W nawiasach klamrowych mamy właściwości controller oraz action. Są to słowa kluczowe, które odpowiadają za generowanie linków oraz przypisanie wartości domyślnych. Dla przykładu, w folderze projektu utworzę folder Controllers i umieszczę w nich plik HomeController, a o to ciało pliku:

using Microsoft.AspNetCore.Mvc;

namespace ViewsProject.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index()
        {
            ViewBag.Controller = nameof(HomeController);
            ViewBag.Action = nameof(Index);
            return View();
        }
    }
}

Konwencja nazewnictwa frameworka .Net Core MVC wymaga, żeby wszystkie kontrolery były umieszczone w folderze Controllers w katalogu głównym projektu, wtedy framework znajdzie sobie bez problemu ścieżkę do pliku kontrolera, dzięki zachowaniu konwencji. Konwencja także wymaga, żeby nazwy klas i plik kontrolera zawierała po słowie kluczowym słowo Controller, np: jak w naszym przykładzie HomeController albo ViewController. Jak można zauważyć, HomeController dziedziczy po bazowym kontrolerze w przestrzeni nazw Microsoft.AspNetCore.Mvc, który udostępnia szereg bardzo przydatnych metod i właściwości. Dzięki niemu nie musimy ręcznie implementować potrzebnych podstawowych metod.

Następnym krokiem będzie utworzenie ścieżki folderów w naszym projekcie Views/Home oraz utworzenie w nim pliku Index.cshtml.

@{ Layout = null;}

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Hello</title>
</head>
<body>
Wnętrze Kontrollera @ViewBag.Controller i akcji @ViewBag.Action
</body>
</html>

Rozszerzenie pliku .cshtml jest to rozszerzenie pliku dla silnika generowania HTML Razor, który jest wbudowany do frameworka .Net Core.

Teraz spróbuj uruchomić aplikację i wpisać adres url, który wyświetli Ci się w konsoli.

W twojej przeglądarce powinien się pojawić taki wynik:

Wnętrze Kontrollera HomeController i akcji Index

Metoda View(), która generuje widoki występuje w czterech przeciążeniach:

  • View()
  • View(Object)
  • View(String)
  • View(String, Object)

Pierwsze przeciążenie metody View() wyszukuje widok po schemacie Views/NazwaKontrolera/NazwaAkcji.cshtml lub w ścieżce Views/Shared/NazwaAkcji.cshtml . W przypadku kontrolera Home i akcji Index, będzie to ścieżka Views/Home/Index.cshtml. Dlatego jest ważna konwencja nazewnictwa oraz konwencja utrzymywania określonych ścieżek do plików aplikacji.

Drugie przeciążenie metody View(Object) wyszukuje identycznie ścieżkę do widoku jak w przykładzie wyżej, tylko dodatkowo dołącza model, który ma być wyświetlony w widoku.

Przeciążenie View(String) jest bardziej złożone, ponieważ potrafi znaleźć plik widoku, który dotyczy innego kontrolera. Wtedy wywołanie tej metody będzie wyglądać tak:
View(Views/Test/List.cshtml)
Musimy pamiętać, żeby podać rozszerzenie pliku, inaczej nasz widok nie zostanie wczytany oraz zostanie rzucony wyjątek.
Kolejnym przykładem jest użycie tego samego widoku w innej akcji kontrolera:

 HomeController
...
public ViewResult List()
 {
      ViewBag.Controller = nameof(HomeController);
      ViewBag.Action = nameof(List);
      return View("Index");
 }

Metoda View zacznie nam przeszukiwać ścieżkę Views/Home. Jeżeli znajdzie widok w folderze Views/Home to nam go wyświetli, jeżeli nie znajdzie pliku to zacznie szukać folderze Views/Shared. Jeżeli taki widok będzie się znajdował to go wyświetli. W sytuacji gdy chcemy skorzystać widoków z folderu Views/Shared, nie musimy podawać pełnej ścieżki ani rozszerzenia pliku. Wystarczy sama nazwa widoku. W lokalizacji Views/Shared powinny się znajdować widoki, które będą współdzielone między innymi kontrolerami.
Oto przykład takiej metody:

 HomeController
...
public ViewResult Shared()
 {
      ViewBag.Controller = nameof(HomeController);
      ViewBag.Action = nameof(List);
      return View("SharedView");
 }

Czwarte przeciążenie metody, to połączenie przeciążenia metody View(string) oraz View(object). Metoda w tym przeciążeniu potrafi nam dołączyć model do wybranego widoku.

Myślę, że pora zakończyć ten wpis, bo już się zrobił bardzo długi a to tylko widoki 🙂 Następny wpis będzie poświęcony obszarom w frameworku .Net Core MVC.

Wszelkie pliki związane z tutorialem można znaleźć pod adresem:
https://github.com/pnkp/Tutorials
oraz dotyczące tego wpisu pod adresem:
https://github.com/pnkp/Tutorials/tree/master/ViewsTutorials/SearchingViews