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