# MVC arhitektura

Ovaj materijal dio je ishoda učenja 5 (minimalni i željeni).

### 14.1 Postavke 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
-   u skripti, **promijenite naziv baze podataka u Exercise14 i upotrijebite ga**
-   izvršite ga da biste stvorili bazu podataka, njezinu strukturu i neke testne podatke
-   izvršite dodatnu skriptu 1: https://pastebin.com/SeHBs1BA
-   izvršite dodatnu skriptu 2: https://pastebin.com/7qHM2h2d

**Starter projekt**

Raspakirajte arhivu startera 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 ispunili upute.

**Korisnici aplikacije**  

Admin: korisničko ime je admin, zaporka je 12345678  
User: korisničko ime je user1, zaporka je 12345678  

### Priprema: Izradite stranicu za ažuriranje profila i stranicu s pojedinostima o profilu

Izradite stranicu profila za korisnika:
- u `UserController`, kreirajte akciju detalja profila (automatski generirajte prikaz detalja iz `UserVM`)
  - akcija može detektirati koji je korisnik prijavljen i vratiti korisničke podatke prema njegovom korisničkom imenu

  ```C#
  [Authorize]
  public IActionResult ProfileDetails()
  {
      var username = HttpContext.User.Identity.Name;

      var userDb = _context.Users.First(x => x.Username == username);
      var userVm = new UserVM
      {
          Id = userDb.Id,
          Username = userDb.Username,
          FirstName = userDb.FirstName,
          LastName = userDb.LastName,
          Email = userDb.Email,
          Phone = userDb.Phone,
      };

      return View(userVm);
  }
  ```

- u `UserController`, kreirajte `ProfileEdit` GET i POST akcije (automatski generirajte `Edit` prikaz iz `UserVM`)

  ```C#
  [Authorize]
  public IActionResult ProfileEdit(int id)
  {
      var userDb = _context.Users.First(x => x.Id == id);
      var userVm = new UserVM
      {
          Id = userDb.Id,
          Username = userDb.Username,
          FirstName = userDb.FirstName,
          LastName = userDb.LastName,
          Email = userDb.Email,
          Phone = userDb.Phone,
      };

      return View(userVm);
  }

  [Authorize]
  [HttpPost]
  public IActionResult ProfileEdit(int id, UserVM userVm)
  {
      var userDb = _context.Users.First(x => x.Id == id);
      userDb.FirstName = userVm.FirstName;
      userDb.LastName = userVm.LastName;
      userDb.Email = userVm.Email;
      userDb.Phone = userVm.Phone;

      _context.SaveChanges();

      return RedirectToAction("ProfileDetails");
  }
  ```

- u layout prikazu predstavite korisničko ime kao vezu na `ProfileDetails` (koristite gumb Bootstrap umjesto badge/značke)
- u `ProfileDetails` spojite gumb `Edit Profile` na akciju `ProfileEdit`

## 14.2 Jednostavni Ajax zahtjevi

Ajax zahtjevi su važni kada želimo ažurirati samo dio stranice.  
  
Moramo implementirati 2 dijela:  
- klijentsku stranu koja koristi Ajax zahtjev  
- serversku stranu koja prihvaća Ajax zahtjev i vraća HTML kod  

> U "sretnom" (manje složenom) slučaju, možemo koristiti HTML atribute koji automatski pokreću Ajax zahtjev, zbog podrške postojećeg nenametljivog (engl. unobtrusive) JavaScripta. To znači da u tim okolnostima možemo samo napisati kod na strani poslužitelja, što znači akcije i prikaze koji sadrže potrebne atribute povezane s Ajaxom.


### Koristite Ajax zahtjev i vraćene JSON podatke za ažuriranje stranice 

Implementirat ćete Ajax zahtjev na stranici `ProfileDetails`.  

Prvo implementirajte akciju koja vraća podatke o korisničkom profilu u JSON obliku:

  ```C#
  public JsonResult GetProfileData(int id)
  {
      var userDb = _context.Users.First(x => x.Id == id);
      return Json(new {
          userDb.FirstName,
          userDb.LastName,
          userDb.Email,
          userDb.Phone,
      });
  }
  ```

Zatim dodajte gumb "Ajax Refresh Data" i implementirajte JavaScript koji ažurira profil pomoću Ajax zahtjeva na klik na gumb:

  ```JavaScript
  @section Scripts {
      <script>
          const modelId = "@Model.Id";
          $("#ajaxUpdate").click(() => {
              $.ajax({
                  url: `/User/GetProfileData/${modelId}`,
                  method: "GET"
              })
                  .done((data) => {
                      //debugger;
                      $("#FirstName").text(data.firstName);
                      $("#LastName").text(data.lastName);
                      $("#Email").text(data.email);
                      $("#Phone").text(data.phone);
                  });
          })
      </script>
  }
  ```

> Imajte na umu da morate dodijeliti `id` svakom elementu koji ažurirate. Također imajte na umu korištenje "debugger" direktive koja vam može pomoći da zaustavite izvršavanje koda kada rezultat dohvatite s poslužitelja, kako biste ga ispitali taj rezultat.

  ```HTML
  <dd class="col-sm-10" id="FirstName">
      @Html.DisplayFor(model => model.FirstName)
  </dd>
  ```

Da biste ovo testirali, uredite profil u zasebnoj kartici preglednika i spremite ga. Zatim testirajte Ajax-osvježavanjem podataka u prvoj kartici.

### Koristite Ajax zahtjev za izmjenu podataka 

Implementirati ćete Ajax zahtjev na stranici `ProfileEdit`:
- stvoriti modalni dijalog za ažuriranje
- stvoriti funkciju koja hrani modalni dijalog podacima i prikazuje ga
- stvoriti funkciju koja šalje podatke poslužitelju i zatvara modalni dijalog

Preduvjet je imati dinamički modalni dijalog kao spremnik za vaš obrazac.
Vidi: https://getbootstrap.com/docs/5.2/components/modal/

```HTML
<!--Paste this at the end of the HTML, just before "Scripts" section-->
<div class="modal fade" id="AjaxEditModal" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Update User Profile</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <form id="profileForm">
                    <div class="mb-3">
                        <label for="FirstNameInput" class="form-label">First name</label>
                        <input id="FirstNameInput" class="form-control">
                    </div>
                    <div class="mb-3">
                        <label for="LastNameInput" class="form-label">Last name</label>
                        <input id="LastNameInput" class="form-control">
                    </div>
                    <div class="mb-3">
                        <label for="EmailInput" class="form-label">E-mail</label>
                        <input type="email" id="EmailInput" class="form-control">
                    </div>
                    <div class="mb-3">
                        <label for="PhoneInput" class="form-label">Phone</label>
                        <input type="tel" id="PhoneInput" class="form-control">
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                <button id="SaveProfileButton" type="button" class="btn btn-primary">Save profile</button>
            </div>
        </div>
    </div>
</div>
```

Prikažite taj modalni dijalog kada korisnik klikne gumb `Edit Profile`.
- izmijenite gumb `Edit Profile`: postavite njegov `id` na `ajaxEdit`
- na korisnikov klik, prikupite podatke sa stranice i ubacite ih u unose u modalnom dijalogu
- prikažite dijalog pomoću metode `show()`

  ```JavaScript
  const ajaxEditModalEl = $("#AjaxEditModal")[0];
  const ajaxEditModal = new bootstrap.Modal(ajaxEditModalEl);

  $("#ajaxEdit").click((e) => {
      e.preventDefault();

      const firstName = $("#FirstName").text().trim();
      const lastName = $("#LastName").text().trim();
      const email = $("#Email").text().trim();
      const phone = $("#Phone").text().trim();

      $("#FirstNameInput").val(firstName);
      $("#LastNameInput").val(lastName);
      $("#EmailInput").val(email);
      $("#PhoneInput").val(phone);

      ajaxEditModal.show();
  });
  ```

  > Testirajte dijalog. Trebali biste vidjeti korisničko sučelje za uređivanje podataka profila.    

Kada korisnik spremi profil, morate poslati odgovarajući PUT zahtjev. Ako sve završi dobro, samo zatvorite prozor.
- implementirajte `click` događaj na `SaveProfileButton`
- prikupite podatke i koristite ih unutar `$.ajax()` zahtjeva
- koristite radnju `/User/SetProfileData/{id}` koju ćete kasnije implementirati
- za osvježavanje podataka unutar stranice, samo upotrijebite jQuery za "trigeriranje" klika na gumb `ajaxUpdate`
- ako nešto pođe po zlu, pokažite grešku

  ```JavaScript
  $("#SaveProfileButton").click(() => {
      const profile = {
        firstName: $("#FirstNameInput").val(),
        lastName: $("#LastNameInput").val(),
        email: $("#EmailInput").val(),
        phone: $("#PhoneInput").val(),
      };

      $.ajax({
        url: `/User/SetProfileData/${modelId}`,
        method: "PUT",
        contentType: "application/json",
        data: JSON.stringify(profile)
      })
        .done((data) => {
          ajaxEditModal.hide();

          $("#ajaxUpdate").trigger("click");
        })
        .fail(() => {
          alert("ERROR: Could not update profile");
        })
  })
  ```

  > Pokušajte apremiti podatke. Trebali biste vidjeti grešku.

Implementirajte HTTP PUT akciju `SetProfileData()` koja ažurira podatke korisničkog profila:

  ```C#
  [HttpPut]
  public ActionResult SetProfileData(int id, [FromBody]UserVM userVm)
  {
      var userDb = _context.Users.First(x => x.Id == id);
      userDb.FirstName = userVm.FirstName;
      userDb.LastName = userVm.LastName;
      userDb.Email = userVm.Email;
      userDb.Phone = userVm.Phone;

      _context.SaveChanges();

      return Ok();
  }
  ```

> Ovdje smo "posudili" UserVM, što nije neočekivano u aplikacijama, ali bi bolji način bio implementirati
> model ProfileVM posebno za ovu operaciju.

## 14.3 Ajax i ažuriranje dijela HTML stranice

U MVC-u je ažuriranje dijela HTML DOM-a češće od ažuriranja podataka pomoću JSON-a. Izvršimo jednostavno ažuriranje stranice.

1. U novom kontroleru `AjaxTestController` stvorite akciju pod nazivom `AjaxHtml()` koja vraća prikaz bez layouta (na početku prikaza postavite `Layout = null`)
2. U prikazu vratite nasumični broj između 0 i 100, omotan u Bootstrap značku - nešto kao `<span class="badge bg-info text-dark">47</span>`
    - koristite `Random.Shared.Next(100)`
3. Testirajte stranicu
    - Otvorite izvor stranice i promatrajte generirani HTML
    - Trebali biste vidjeti ažurirani broj svaki put kada osvježite stranicu
4. U novom `AjaxTestController` implementirajte `Index` prikaz, vratite samo `<div>` s `id="ajaxPlaceholder"`. Također dodajte gumb "Update HTML" s `id="ajaxUpdateHtmlButton"`.
5. Zakačite jQuery klik događaj na gumb. U funkciji pokrenite Ajax zahtjev prema novoj akciji AjaxHtml. Kada je zahtjev gotov, ažurirajte HTML placeholder vraćenim HTML sadržajem.
    ```JavaScript
    $("#ajaxUpdateHtmlButton").click(() => {
        $.ajax({
            url: "/AjaxTest/AjaxHtml"
        })
            .done((resultHtml) => {
                $("#ajaxPlaceholder").html(resultHtml);
            })
    })
    ```
    > Postoji jQuery prečac umjesto `$.ajax()` zahtjeva i ažuriranja:
    ```JavaScript
    // Replace this
    $.ajax({
        url: "/AjaxTest/AjaxHtml"
    })
        .done((resultHtml) => {
            $("#ajaxPlaceholder").html(resultHtml);
        })

    // With this
    $("#ajaxUpdateHtmlButton").load("/AjaxTest/AjaxHtml");
    ```

Trebali biste vidjeti ažurirani broj na stranici svaki put kada kliknete gumb.  

To je u biti način na koji ažurirate dio HTML stranice pomoću Ajax zahtjeva.

## 14.4 Ajax and unobtrusive JavaScript

Postoje i drugi načini ažuriranja dijela stranice. Kada koristite "nenametljivo" (engl. "unobtrusive") ažuriranje, i dalje trebate na serveru učiniti sve korake za stvaranje HTML-a pomoću akcije, ali ne morate izričito koristiti jQuery kod. Ključ je korištenje biblioteke "jQuery Unobtrusive AJAX" koja može sama obaviti Ajax poziv i izvršiti ažuriranje na temelju običnih HTML atributa.

Upute:

1. Pronađite klijentsku skriptu "jQuery Unobtrusive AJAX" u CdnJS-u (vidi: https://cdnjs.com/) i instalirajte je pomoću LibMana (Add > Client Side Library, naziv je `jquery-ajax-unobtrusive`)
2. U layout prikazu: 

    ```C#
    <script src="~/lib/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js"></script>
    ```

3. U `AjaxHtml.cshtml`:

    Prvi način je korištenje obrasca:

    ```HTML
    <form asp-controller="User" asp-action="AjaxHtml" data-ajax="true" data-ajaxmethod="GET" data-ajax-update="#ajaxPlaceholder" >
      <input type="submit" class="btn btn-primary" value="Unobtrusive update" />
    </form>
    ```

    Drugi način je korištenje samo poveznice (možete je stilizirati kao npr. Bootstrap gumb):

    ```HTML
    <a href="#" class="btn btn-info"
        data-ajax="true" 
        data-ajaxmethod="GET" 
        data-ajax-url="/User/AjaxHtml" 
        data-ajax-update="#ajaxPlaceholder">Unobtrusive update 2</a>
    ```

_Vidi: https://www.learnrazorpages.com/razor-pages/ajax/unobtrusive-ajax_

## 14.5 Primjer: koristite Ajax za osvježavanje HTML stranice korisničkog profila

Možete koristiti ono što ste do sada naučili za izvođenje Ajax ažuriranja.  
Postoji još jedna stvar na koju morate paziti - izbjegavanje dupliciranja cshtml-a. U tu svrhu možete koristiti **djelomične prikaze** (engl. partial views).

_Vidi: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/partial?view=aspnetcore-8.0_

1. Premjestite cijeli dio `<dl>` iz `ProfileDetails.cshtml` u `_ProfileDetailsPartial.cshtml`.
   Ideja je imati `ProfileDetails.cshtml` koji koristi `<partial name="_ProfileDetailsPartial"></partial>` da bi se uključio dio koji se može osvježiti Ajaxom. Na taj način jednostavno dijelimo prikaz na 2 dijela - `statični` i onaj koji se može ažurirati pomoću Ajaxa.  
   Provjerite radi li stranica i dalje ispravno.

2. Stvorite `ProfileDetailsPartial()` akciju tako da kopirate kod iz `ProfileDetails()`, ali vratite `PartialView("_ProfileDetailsPartial", userVm)` umjesto `View(userVm)`.  
   Provjerite vraća li `/User/ProfileDetailsPartial` dio HTML-a za ažuriranje stranice.

3. Izvršite Ajax nenametljivo ažuriranje kada se klikne novi gumb "Ajax HTML Refresh".
    ```HTML
    <a id="ajaxUpdate" class="btn btn-primary mt-3"
      data-ajax="true"
      data-ajaxmethod="GET"
      data-ajax-url="/User/ProfileDetailsPartial"
      data-ajax-update="#profileDetailsPlaceholder">
        Ajax HTML Refresh
    </a>
    ```

## 14.6 Primjer: koristite Ajax za implementaciju stranica popisa pjesama

Ovo je zanimljiviji problem jer se stranica ovdje mora osvježavati korištenjem liste gumba za straničenje - straničnika (engl. pager).  
Prvo dodajmo neke testne podatke: https://pastebin.com/Ms752UY3  

Ideja je ažurirati samo tablicu. I tablica i straničnik (ažurira se na klik) tada bi trebali biti u djelomičnom prikazu.

1. Premjestite tablicu i straničnik u odvojeni djelomični prikaz pod nazivom `_SearchPartial` i referencirajte djelomični prikaz iz `Search` prikaza. Umotajte `<partial>` referencu pomoću `<div>`.
    ```HTML
    <div id="searchPartialPlaceholder">
        <partial name="_SearchPartial"/>
    </div>
    ```
3. U kontroleru duplicirajte akciju `Search()` u `SearchPartial()` i iz nje vratite `PartialView("_SearchPartial", searchVm)`.  

    Testirajte ju.  

    > Sada možete vidjeti priliku za izdvajanje koda iz akcije u zasebnu privatnu funkciju ili čak u uslugu. Na taj način ćete izbjeći dupliciranje koda zbog kopiranja/lijepljenja.

4. Veze u straničniku imaju query string parametre, tako da ne možemo samo koristiti nenametljivi Ajax - on ne podržava prosljeđivanje query string parametara. Najlakši način bi bio presresti klik na `<a>` i umjesto toga poslati Ajax upit. Dodavanje skripti nije podržano u djelomičnim prikazima, ali možemo skriptu dodati u prikazu `Search.cshtml`.
    ```HTML
    @section Scripts {
        <script>
            $("body").on("click", "a.page-link", function (e) {
                e.preventDefault();

                const urlParts = e.target.href.split("?");
                const base = urlParts[0];
                const query = urlParts.length > 1 ? urlParts[1] : null;
                if (query) {
                    $("#searchPartialPlaceholder").load(`${base}Partial?${query}`);
                } else {
                    $("#searchPartialPlaceholder").load(`${base}Partial`);
                }
            })
        </script>
    }
    ```

### 14.7 Dodatni sadržaj: Klasično učitavanje datoteka

Uobičajeni prijenos datoteka u modernim preglednicima podržava slanje više datoteka pomoću dijaloškog okvira "Odaberi datoteke..." ili funkcije "povuci i ispusti". CSS i JavaScript mogu omogućiti vizualni prikaz učitanih datoteka. U većini slučajeva s razumnom veličinom datoteke, nema potrebe koristiti Ajax tehniku ​​za funkciju učitavanja datoteke. Međutim, ako je potrebno (velike datoteke, veliki broj učitanih datoteka itd.), Ajax pristup ipak treba implementirati.

Opći postupak bez Ajaxa:
- kreirajte obrazac za učitavanje datoteke (akcija GET)
- pohraniti učitane datoteke, na primjer na disk (akcija POST)
  - također zapišite informacije o učitanim datotekama u bazu podataka
- u prvoj akciji GET, koristite podatke iz baze za prikaz poveznica koje mogu preuzeti datoteke
- dodatno - ako su učitane datoteke slike, renderirajte '<img>' oznake umjesto veza ili renderirajte '<img>' oznake unutar veza

### Implementacija

1. **Implementirajte obrazac za učitavanje datoteka**

    ```C#
    // Viewmodel
    public class UploadFilesVM
    {
        public IEnumerable<IFormFile> Files { get; set; }
    }

    // GET action
    public IActionResult Upload()
    {
        return View();
    }

    // POST action
    [HttpPost]
    public IActionResult Upload(UploadFilesVM uploadVm)
    {
        return RedirectToAction();
    }
    ```

    Generirajte prikaz za akciju `Upload` (predložak "Create").  

    **Imajte na umu da generator ne zna kako stvoriti `<input>` za učitavanje datoteke.**  

    Koristite sljedeći HTML kôd za unos datoteke za upload:
    ```C#
    <div class="form-group">
        <label asp-for="Files" class="control-label"></label>
        <input type="file" asp-for="Files" class="form-control" multiple />
        <span asp-validation-for="Files" class="text-danger"></span>
    </div>
    ```

    **Primijetite da obrascu nedostaje atribut `enctype="multipart/form-data"`.**  

    Dodajte atribut `enctype="multipart/form-data"`, inače POST akcija neće dobiti nikakve učitane datoteke.

    Dodajte "Upload files" stavku izbornika u prikaz `_LayoutAdmin.cshtml` - administrator može učitavati datoteke.

    Testirajte dobiva li POST akcija učitane podatke.

2. **Spremite učitane datoteke na disk**

    Datoteke ćete morati negdje uploadati, a `wwwroot` je jedna od opcija kako biste omogućili pristup datoteci. _Imajte na umu da na ovaj način ne možete ograničiti pristup slici samo nekim korisnicima, pristup je dopušten svim korisnicima. Postoje i drugi problemi s ovim pristupom, poput imenovanja datoteka i prepisivanja. Međutim, ovaj pristup koristimo za prikaz upload koncepta._  

    Stvorite mapu `upload` unutar mape `wwwroot`.  
    Implementirajte `POST Upload`.  

    ```C#
    [HttpPost]
    public IActionResult Upload(UploadFilesVM uploadVm)
    {
        // Create upload folder for user
        var baseUploadPath = Path.GetFullPath("wwwroot/upload");
        var userName = HttpContext.User.Identity.Name;
        var userUploadPath = Path.Combine(baseUploadPath, userName);

        if (!System.IO.Directory.Exists(userUploadPath))
        {
            System.IO.Directory.CreateDirectory(userUploadPath);
        }

        // Upload file to temp file
        // Then copy temp file to the destination file
        foreach (var uploadFile in uploadVm.Files)
        {
            var tempFilePath = Path.GetTempFileName();

            using (var stream = System.IO.File.Create(tempFilePath))
            {
                uploadFile.CopyTo(stream);
            }

            var targetUploadPath = Path.Combine(userUploadPath, uploadFile.FileName);

            // !Will overwrite if names are the same!
            System.IO.File.Copy(tempFilePath, targetUploadPath, true);
        }

        return RedirectToAction();
    }
    ```

    Istražite kako radi kod.  

    Testirajte i provjerite jesu li datoteke stvarno učitane u `wwwroot` mapu.

3. **Pohranite učitane reference datoteka u bazu podataka**

    Sada ćemo u bazu podataka upisati podatke o učitanim datotekama.  
    Najprije upotrijebite refaktoriranje za izdvajanje logike unutar petlje `foreach` kako biste odvojili metodu `UploadToUserFolder()`. Na taj način je lakše razumjeti kod.  

    ```C#
    private static void UploadToUserFolder(string userUploadPath, IFormFile uploadFile)
    {
        var tempFilePath = Path.GetTempFileName();

        using (var stream = System.IO.File.Create(tempFilePath))
        {
            uploadFile.CopyTo(stream);
        }

        var targetUploadPath = Path.Combine(userUploadPath, uploadFile.FileName);

        // !Will overwrite if names are the same!
        System.IO.File.Copy(tempFilePath, targetUploadPath, true);
    }
    
    // ...

    foreach (var uploadFile in uploadVm.Files)
    {
        UploadToUserFolder(userUploadPath, uploadFile);
    }
    ```

    Stvorite tablice baze podataka za učitavanje datoteka.  

    ```SQL
    CREATE TABLE Document (
      [Id] [int] IDENTITY(1,1) NOT NULL,
      [AudioId] [int] NOT NULL,
      [FileName] [nvarchar](256) NOT NULL,
      [Contents] [nvarchar](max),
      CONSTRAINT [PK_Document] PRIMARY KEY CLUSTERED (
        [Id] ASC
      ),
      CONSTRAINT [FK_Document_Audio] FOREIGN KEY([AudioId])
      REFERENCES [Audio] ([Id])
    )
    GO

    CREATE TABLE [Image] (
      [Id] [int] IDENTITY(1,1) NOT NULL,
      [AudioId] [int] NOT NULL,
      [FileName] [nvarchar](256) NOT NULL,
      [Contents] [nvarchar](max),
      CONSTRAINT [PK_Image] PRIMARY KEY CLUSTERED (
        [Id] ASC
      ),
      CONSTRAINT [FK_Image_Audio] FOREIGN KEY([AudioId])
      REFERENCES [Audio] ([Id])
    )
    GO
    ```

    Napravite reverse engineering db konteksta baze podataka i modela kako biste dobili najnovije promjene.  

    ```
    dotnet ef dbcontext scaffold "Name=ConnectionStrings:ex14cs" Microsoft.EntityFrameworkCore.SqlServer -o Models --force
    ```

    Nakon završetka učitavanja, spremite podatke u bazu podataka. Upotrijebite hardkodirani `AudioId = 2`.

    ```
    _context.Documents.Add(new Document
    {
        AudioId = 2,
        FileName = uploadFile.Name
    });
    _context.SaveChanges();
    ```

    Testirajte jesu li podaci pohranjeni u bazi podataka.

4. **Implementirajte preuzimanje**

    Implementirajte akcije i modele prikaza...  

    ```C#
    public IActionResult Download()
    {
        var basePath = "/upload";
        var userName = HttpContext.User.Identity.Name;
        var userPath = Path.Combine(basePath, userName);

        var fileNames = _context.Documents.Select(x => new DownloadFileVM { 
            Filename = x.FileName,
            DownloadUri = Path.Combine(userPath, x.FileName)
        });
        var downloadFiles = 
            new DownloadFilesVM
            {
                Files = fileNames.ToList()
            };
        return View(downloadFiles);
    }

    public class DownloadFilesVM
    {
        public IEnumerable<DownloadFileVM> Files { get; set; }
    }

    public class DownloadFileVM
    {
        public string Filename { get; set; }
        public string DownloadUri { get; set; }
    }
    ```

    ...i automatski generirajte prikaz (`Create` predložak). To neće stvoriti očekivani rezultat, ali možete upotrijebiti iteraciju putem člana `FileNames` da biste prikazali poveznice za preuzimanje.

    ```HTML
    <h1>Download files</h1>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <ul class="list-group list-group-flush">
            @if(Model != null)
            {
                foreach (var file in Model.Files)
                {
                    <li class="list-group-item">
                        <a href="@file.DownloadUri">@file.Filename</a>
                    </li>
                }
            }
            </ul>
        </div>
    </div>
    ```

    Dodajte poveznicu u navigaciju i testirajte preuzimanje datoteke.

### 14.8 Upload/download slike

Prilagodite učitavanje dokumenata učitavanju slika.  
Omogućite da se učitane slike prikazuju kao oznake `<img>` s njihovim atributima `src`.

### 14.9 Učitaj/preuzmi u potpunosti iz/u bazu podataka

Prilagodite učitavanje dokumenta/slike u bazu podataka.  
Pohranite datoteke u Base64 kodiranom obliku.  

### 14.10 Vježba: "Autocomplete" funkcionalnost

Koristite `Select2` za automatsko dovršavanje `Genre` kada stvarate novu pjesmu.

### 14.11 Vježba (napredno!): Koristite sličnost stringa i unesenog pojma pretraživanje

Ovo je eksperimentalni pristup. Obično se za to koristi "full-text index" funkcionalnost u bazi podataka, ali u određenim slučajevima to možemo učiniti i na ovaj način.  
Međutim, zabavno je učiti nove stvari pa istražimo :)  

- dotnet add package F23.StringSimilarity
- na primjer, koristite u autocomplete akciji:
  ```C#
  var dstMeasure = new NormalizedLevenshtein();
  var similarItems = _context.Genres
    .ToList()
    .Select(x => new { dst = dstMeasure.Distance(x.Title, searchTerm), item = x })
    .OrderBy(x => x.dst)
    .Take(10)
    .Select(x => x.item);
  ```
- pokušajte također s `JaroWinkler()` ili `NGram()` umjesto `NormalizedLevenshtein()`

> Ako želite da se takva vrsta rješenja skalira, morate prvo spremiti zbirku u predmemoriju u npr. varijablu sesije, a zatim filtrirajte tu varijablu sesije.
>  
> Drugi, ozbiljniji skalabilni pristup bila bi integracija algoritama sličnosti stringa u SQL Server CLR. Već postoje neki pokušaji u tom smjeru, ali nijedan nije široko prihvaćen.  
> Primjer: https://www.reddit.com/r/SQL/comments/ahvpl1/anyone_ever_done_fuzzy_matching_in_ms_sql/
