# MVC arhitektura

Ovaj materijal dio je ishoda učenja 4 (minimum).

## 11 Pregled

-   Korištenje query stringa
    -   Filtriranje
    -   Sortiranje
    -   Straničenje (paging)
-   Korištenje kolačića: https://positiwise.com/blog/how-to-use-cookies-in-asp-net-core
-   Korištenje sessiona: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-8.0
-   Korištenje TempData

### 11.1 Postavljanje vježbe

**Postavljanje SQL poslužitelja**

U SQL Server Management Studiju učinite sljedeće:

-   koristite skriptu za stvaranje baze podataka, njezine strukture i nekih testnih podataka: https://pastebin.com/jtJfak9E

**Starter projekt**

> Sljedeće je već dovršeno kao projekt starter:
>
> -   Postavljeni modeli i repozitorij
> -   Podešen "Launch settings"
> -   Stvoreni osnovni CRUD prikazi i funkcionalnost (Genre, Artist, Song)
> -   Implementirana validacija i označavanje korištenjem viewmodela
>
> Za detalje pogledajte prethodne vježbe.

Raspakirajte starter arhivu i otvorite rješenje u Visual Studiju.
Postavite connection string i pokrenite aplikaciju.
Provjerite radi li aplikacija (npr. navigacija, popis pjesama, dodavanje nove pjesme).

> U slučaju da ne radi, provjerite jeste li ispravno slijedili upute.

### 11.2 Query stringovi: prosljeđivanje podatka kao dijela query stringa u URL-u

Kada očekujete da Vaš URL sadrži stanje, niz upita obično je dobar kandidat za pohranu tog stanja.  
Na primjer, stvorimo krajnju točku koja će vratiti samo pjesme s trajanjem između dvije zadane vrijednosti.  
Koristite metodu `Index()` kao predložak, jer vraća popis pjesama, a to nam treba.  
Također su Vam potrebne dvije vrijednosti za ograničenje trajanja pjesme:

-   min
-   max

Učinite ih nulabilnim, tako da korisnik može odlučiti odabrati samo minimalno ili samo maksimalno trajanje.

> Napomena: ovdje "posuđujemo" prikaz `Index`.

```
public ActionResult GetSongsByDuration(int? min, int? max)
{
    try
    {
        IEnumerable<Audio> songs = _context.Audios
            .Include(x => x.Genre)
            .Include(x => x.Artist);

        if (min.HasValue) {
            songs = songs.Where(x => x.Duration >= min.Value);
        }

        if (max.HasValue)
        {
            songs = songs.Where(x => x.Duration <= max.Value);
        }

        var songVms =
            songs.Select(x => new SongVM
            {
                Id = x.Id,
                Title = x.Title,
                Year = x.Year,
                ArtistId = x.ArtistId,
                ArtistName = x.Artist.Name,
                GenreId = x.GenreId,
                GenreName = x.Genre.Name,
                Duration = x.Duration,
                Url = x.Url
            })
            .ToList();

        return View("Index", songVms);
    }
    catch (Exception ex)
    {
        throw ex;
    }
}
```

Testirajte:

-   http://localhost:6555/Song/GetSongsByDuration?min=194&max=249
-   http://localhost:6555/Song/GetSongsByDuration?min=194
-   http://localhost:6555/Song/GetSongsByDuration?max=249

### 11.3 Primjer: filtriranje, sortiranje i osnovno straničenje

Isti princip se može koristiti za filtriranje rezultata, pa čak i njihovo sortiranje u istom zahtjevu. Također, nakon filtriranja i sortiranja možete uzeti samo jedan isječak rezultata kako biste izbjegli dohvaćanje cijelog skupa rezultata. To zovemo straničenje (engl. paging).

Primjeri:

-   http://localhost:6555/Song/GetSongsByDuration?q=best
-   http://localhost:6555/Song/GetSongsByDuration?sortby=name
-   http://localhost:6555/Song/GetSongsByDuration?page=1&count=10
-   http://localhost:6555/Song/GetSongsByDuration?q=the&sortby=genre&page=1&count=10

Za ulazni parametar filtriranja `string q`, možete jednostavno filtrirati naslov pjesme ovako:

```
if (!string.IsNullOrEmpty(q))
{
    songs = songs.Where(x => x.Title.Contains(q));
}
```

Parametar unosa redoslijeda (sortiranja) `string sortby` mogao bi se koristiti ovako:

```
switch (orderBy.ToLower())
{
    case "id":
        songs = songs.OrderBy(x => x.Id);
        break;
    case "title":
        songs = songs.OrderBy(x => x.Title);
        break;
    //...year, duration, genre, artist...
}
```

Parametri straničenja `page` i `count` mogu se koristiti ovako:

```
songs = songs.Skip((page - 1) * size).Take(size); // if pages start from 1
```

Kako biste postupno radili na kolekciji pjesama, morate koristiti LINQ sučelje koje pruža metode za postavljanje upita bazi podataka: `IQueryable<T>`

```
public ActionResult Search(string q, string orderBy = "", int page = 1, int size = 10)
{
    try
    {
        IQueryable<Audio> songs = _context.Audios
            .Include(x => x.Genre)
            .Include(x => x.Artist);

        if (!string.IsNullOrEmpty(q))
        {
            songs = songs.Where(x => x.Title.Contains(q));
        }

        //...sorting, filtering...

        return View("Index", songVms);
    }
    catch (Exception ex)
    {
        throw ex;
    }
}
```

> Kako bi proces ispravno funkcionirao, morate primijeniti filtriranje, sortiranje i straničenje upravo tim redoslijedom.  
> Možete pokušati primijeniti neki drugi redoslijed i vidjeti što će se dogoditi.

Konačno, stvorimo vlastiti prikaz za filtriranje i sortiranje, `Search.cshtml`:

-   kopirajte kod iz `Song/Index.cshtml` (možete ukloniti gumbe Create, Edit i Delete, ovdje nam ne trebaju)
-   dodajte vezu `Search` u layout
-   dodajte model `SearchVM` s odgovarajućim svojstvima (`string Q`, `string OrderBy`, `int Page`, `int Size`) i proslijedite ga kao parametar akciji `Search()`
-   Modelu `SearchVM` također je potrebna zbirka pjesama za prikaz podataka: `IEnumerable<SongVM> Songs`

    ```
    public ActionResult Search(SearchVM searchVm)
    {
        try
        {
            // ...change existing code - assign model members, including song collection...

            return View(searchVm);
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
    ```

-   koristite `SearchVM` kao model za `Search.cshtml`

    -   morat ćete zamijeniti razne reference
    -   npr. `@Html.DisplayNameFor(model => model.Title)` neće raditi
    -   možete koristiti npr. `@Html.DisplayNameFor(model => model.Songs.FirstOrDefault().Title)` umjesto toga

-   neposredno prije `<table>` potreban nam je obrazac koji će koristiti `GET` (!) HTTP metodu za prosljeđivanje parametara upita u `Search()`
    ```
    <form asp-action="Search" method="GET">
        <div class="row">
            <div class="col-8">
                <input asp-for="Q" class="form-control" placeholder="Search song" />
            </div>
            <div class="col-auto">
                <input type="submit" value="Go" class="btn btn-primary" />
            </div>
        </div>
    </form>
    ```
-   testirajte
-   kada se uvjerite da radi, dodajte padajući izbornik za sortiranje i veličinu unutar `<div>`
    ```
    <div class="col-auto">
        <label class="form-label mt-1">Sort by:</label>
    </div>
    <div class="col-auto">
        <select asp-for="OrderBy" class="form-select">
            <option value="id">(default)</option>
            <option>Title</option>
            <option>Year</option>
            <option>Duration</option>
            <option>Genre</option>
            <option>Artist</option>
        </select>
    </div>
    <div class="col-auto">
      <select asp-for="Size" class="form-select">
          <option>10</option>
          <option>20</option>
          <option>50</option>
      </select>
    </div>
    <div class="col-auto">
      <input type="submit" value="Go" class="btn btn-primary" />
    </div>
    ```

### 11.4 Primjer: Implementacija potpunog pagera

Trebamo više podataka u bazi podataka:

-   koristite ovu skriptu za popunjavanje dodatnih podataka: https://pastebin.com/Ms752UY3

Za potpuni pager potrebne su nam neke izmjene:

-   potreban nam je `int FromPager` u modelu da bismo znali koji je broj stranica od kojeg treba krenuti pager
-   potreban nam je `int ToPager` u modelu da bismo znali koji je broj stranica na kojem pager treba stati
-   također trebamo `int LastPage` u modelu za neke vizualne oznake
-   dodajte gumbe za straničenje (vidi https://getbootstrap.com/docs/5.0/components/pagination/)
-   dodajte konfiguraciju `Paging:ExpandPages`, broj stranica koje se mogu prikazati prije i poslije trenutne stranice
    ```
    "Paging": {
      "ExpandPages": 5
    }
    ```
-   popunite potrebne podatke straničenja u model
    ```
    // BEGIN PAGER
    var expandPages = _configuration.GetValue<int>("Paging:ExpandPages");
    searchVm.LastPage = (int)Math.Ceiling(1.0 * filteredCount / searchVm.Size);
    searchVm.FromPager = searchVm.Page > expandPages ?
      searchVm.Page - expandPages :
      1;
    searchVm.ToPager = (searchVm.Page + expandPages) < searchVm.LastPage ?
      searchVm.Page + expandPages :
      searchVm.LastPage;
    // END PAGER
    ```
-   za ovo trebate dohvatiti broj pjesama odmah nakon filtriranja
    ```
    var filteredCount = songs.Count();
    ```
-   konačno možete koristiti sljedeći HTML za navigaciju (istražite ovaj kod!)

    ```
    <nav>
        <ul class="pagination">
            @for (int i = Model.FromPager; i <= Model.ToPager; i++)
            {
                var linkText = @i.ToString();
                if (i != 1 && i == Model.FromPager)
                {
                    linkText = "«";
                }
                else if (i != Model.LastPage && i == Model.ToPager)
                {
                    linkText = "»";
                }

                var linkClass = "page-item";
                if (i == Model.Page)
                {
                    linkClass = "page-item active";
                }
                <li class="@linkClass">
                    @Html.ActionLink(
                        @linkText,
                        "Search",
                        new {
                            q = Model.Q,
                            orderby = Model.OrderBy,
                            page = i,
                            size = Model.Size },
                        new { @class = "page-link" })
                </li>
            }
        </ul>
    </nav>
    ```

### 11.5 Kolačić: Pohranjivanje podataka u prilagođeni kolačić

-   čitanje prilagođenih kolačića iz zahtjeva: `string value = Request.Cookies["CookieKey"]`
    -   ovdje je `CookieKey` zapravo naziv kolačića
-   pisanje prilagođenih kolačića u odgovor: `Response.Cookies.Append("CookieKey", value)`
-   pisanje prilagođenog kolačića s opcijama:
    ```
    var option = new CookieOptions { Expires = DateTime.Now.AddDays(14) };
    Response.Cookies.Append("CookieKey", "10", option);
    ```
-   brisanje kolačića: `Response.Cookies.Delete("CookieKey");`

-   kreirajte kolačić `SongYear` kada se stvori nova pjesma
    ```C#
    // POST Action
    var option = new CookieOptions { Expires = DateTime.Now.AddDays(14) };
    Response.Cookies.Append("SongYear", song.Year.ToString(), option);
    ```
-   pročitajte kolačić `SongYear` kada se prikaže obrazac za stvaranje pjesme

    ```C#
    // GET Action
    var song = new SongVM();
    int.TryParse(Request.Cookies["SongYear"], out int year);
    song.Year = year == 0 ? null : year;

    return View(song);
    ```

    ```C#
    // View
    ViewBag.SongYear = Request.Cookies["SongYear"] ?? "";
    ```

-   test: sljedeći put kada želite dodati novu pjesmu, vrijednost kolačića treba se koristiti za godinu

### 11.6 Primjer kolačića: spremanje upita za pretraživanje

Koristite istu tehniku ​​da biste spremili upit za pretraživanje.   
Let the cookie last for 15 minutes.

```C#
// Start of Search() method
if (string.IsNullOrEmpty(searchVm.Q))
{
    searchVm.Q = Request.Cookies["query"];
}
```

```C#
// Before returning the view
var option = new CookieOptions { Expires = DateTime.Now.AddMinutes(15) };
Response.Cookies.Append("query", searchVm.Q ?? "", option);
```

Primijetite da ne možete poništiti polje uklanjanjem sadržaja polja. Razlog je taj što vaša akcija ne razaznaje učitavanje prve stranice od učitavanja stranice kada kliknete na gumb za pretraživanje.
Lijek: dodajte naziv (i vrijednost ako nedostaje) gumbu kako biste razlikovali klik gumba od učitavanja stranice pomoću URL-a:

```C#
// In viewmodel
public string Submit { get; set; }
```

```HTML
<input type="submit" value="Go" name="submit" class="btn btn-primary" />
```

```C#
// Replacement code for action
if (string.IsNullOrEmpty(searchVm.Q) && string.IsNullOrEmpty(searchVm.Submit))
{
    searchVm.Q = Request.Cookies["query"];
}
```

### 11.7 Session: Korištenje sessiona

Za korištenje sessiona trebate konfigurirati usluge i dodati ih u redoslijed međuprograma.

```C#
// Program.cs
builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(10);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});
```

```C#
// Program.cs - middleware, after authorization
app.UseSession();
```

Na primjer, jedna od mogućnosti korištenja sessiona je izbjegavanje slanja zahtjeva bazi podataka. Podaci se spremaju u varijablu sessiona, koja je obično dio memorijskog prostora poslužitelja, ali se može i drukčije konfigurirati i tako optimizirati.

```C#
private List<SelectListItem> GetGenreListItems()
{
    var genreListItemsJson = HttpContext.Session.GetString("GenreListItems");

    List<SelectListItem> genreListItems;
    if (genreListItemsJson == null)
    {
        genreListItems = _context.Genres
            .Select(x => new SelectListItem
            {
                Text = x.Name,
                Value = x.Id.ToString()
            }).ToList();

        HttpContext.Session.SetString("GenreListItems", genreListItems.ToJson());
    }
    else
    {
        genreListItems = genreListItemsJson.FromJson<List<SelectListItem>>();
    }

    return genreListItems;
}

// ...

ViewBag.GenreDdlItems = GetGenreListItems();
```

> Isto možete učiniti za padajuće stavke za odabir izvođača

### 11.8 TempData: Prikaz rezultata spremanja podataka na preusmjerenoj stranici

Vrijednost `TempData` ostaje u memoriji dok se podaci ne prenesu u drugu akciju.
Vrijednosti su obično stringovi, pa je potrebna JSON (de)serializacija.

```C#
// In GenreController, POST Create
TempData["newGenre"] = newGenre.ToJson();
```

```C#
// In GenreController, GET Index
if (TempData.ContainsKey("newGenre"))
{
  var newGenre = ((string)TempData["newGenre"]).FromJson<GenreVM>();
}
```

Vrijednost se može koristiti izravno u Razor predlošku kao dio logike prikaza.

```C#
// In Genre/Index.cshtml
GenreVM newGenre = null;
if (TempData.ContainsKey("newGenre"))
{
  newGenre = ((string)TempData["newGenre"]).FromJson<GenreVM>();
}

...

@if (newGenre != null)
{
    <div class="alert alert-primary" role="alert">
        A new genre @newGenre.Name has been created.
    </div>
}
```
