# 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 ``` 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 `
`. ```HTML
``` 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 `` 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 { } ``` ### 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 '' oznake umjesto veza ili renderirajte '' oznake unutar veza ### Implementacija 1. **Implementirajte obrazac za učitavanje datoteka** ```C# // Viewmodel public class UploadFilesVM { public IEnumerable 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 `` za učitavanje datoteke.** Koristite sljedeći HTML kôd za unos datoteke za upload: ```C#
``` **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 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

Download files


``` 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/