# 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 {
}
```
> 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
@Html.DisplayFor(model => model.FirstName)
```
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
Update User Profile
```
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 `47`
- 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 `
` 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#
```
3. U `AjaxHtml.cshtml`:
Prvi način je korištenje obrasca:
```HTML
```
Drugi način je korištenje samo poveznice (možete je stilizirati kao npr. Bootstrap gumb):
```HTML
Unobtrusive update 2
```
_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 `
` iz `ProfileDetails.cshtml` u `_ProfileDetailsPartial.cshtml`.
Ideja je imati `ProfileDetails.cshtml` koji koristi `` 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
Ajax HTML Refresh
```
## 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 `` referencu pomoću `
```
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 `` 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/