ASP.NET CORE MVC Model réteg bevezetése Model Binding Repository tervezési minta Dependency Injection Transient és Singleton Custom Binding AutoMapper
Hallgatói tájékoztató A jelen bemutatóban található adatok, tudnivalók és információk a számonkérendő anyag vázlatát képezik. Ismeretük szükséges, de nem elégséges feltétele a sikeres zárthelyinek, illetve vizsgának. Sikeres zárthelyihez, illetve vizsgához a jelen bemutató tartalmán felül a kötelező irodalomként megjelölt anyag, a gyakorlatokon szóban, illetve a táblán átadott tudnivalók ismerete, valamint a gyakorlatokon megoldott példák és az otthoni feldolgozás céljából kiadott feladatok önálló megoldásának képessége is szükséges.
Model réteg Webalkalmazásunk működési magjának osztályai Egyed osztályok (autó, személy, hitel, stb.) Egy entitást írnak le Összefogják a jellemzőket és függvényeket, amelyek egy entitás sajátjai Business Logic Pl. egy kártyajáték működését foglalja magában (szabályok, keverés, stb.) Pl. egy üzleti alkalmazás különböző funkciói Megjelenítési rétegtől teljesen független logikai osztályok Mindig úgy kell megírni, hogy akár ConsoleApp-ban is működjön! Repository Elfedi az adatbázist különböző függvényekkel Business Logic fogja használni folyamatosan Erősen javasolt interfészt használni 3
Model feladata Megjelenítés során Összegyűjtjük egy modelben a megjelenítendő információkat (Controller végzi) A modelt átadjuk a View-nak Adatbevitel során A HTML form mezőinek értéke átadásra kerül a Controllernek Controller egy Modelt példányosít belőle 4
Feladat Készítsünk egy egyszerű regisztrációs felületet, ahol egy hallgatót tudunk beiratkoztatni az egyetemre! Egy hallgató adatai: Név Neptunkód Évfolyam Szak A hallgató kezeléséhez készítsünk megfelelő Model osztályt! 5
1. Model osztály elkészítése Készítsük el az alábbi mappaszerkezetet A Model osztály nevébe szokás beleírni, hogy Model public class HallgatoModel public string Nev get; set; public string Neptunkod get; set; public int Evfolyam get; set; public string Szak get; set; 6
2. Controller Osztályszintű tagként kezeljük public class HomeController : Controller List<HallgatoModel> hallgatok; public HomeController() hallgatok = new List<HallgatoModel>(); public IActionResult Index() return View(hallgatok); [HttpGet] public IActionResult Add() return View(); a listát Konstruktorban példányosítjuk a listát Az index Action átadja az összes hallgatót a View-jának Majd itt lesz hozzáadás gomb A hozzáadás gomb hatására idekerülünk, itt lesz a form, a formot ugyanide küldjük el, de POST-al [HttpPost] public IActionResult Add(HallgatoModel model) hallgatok.add(model); return RedirectToAction("Index"); POST változat, ez kapja meg a form eredményét, hozzáadja a hallgatót és visszaküld az Indexre egy View mutatás helyett. 7
3. Index View @model List<ASPKurzus02.Models.HallgatoModel> <body> <h2>hallgatók</h2> <p> <a href="/home/add">új hallgató</a> </p> <table> <tr> <th>név</th> <th>neptunkód</th> <th>évfolyam</th> <th>szak</th> </tr> @foreach (var item in Model) <tr> <td>@item.nev</td> <td>@item.neptunkod</td> <td>@item.evfolyam</td> <td>@item.szak</td> </tr> </table> </body> 8
3. Add View @model ASPKurzus02.Models.HallgatoModel <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>új Hallgató</title> </head> <body> <form method="post" action="add"> Név: <br /><input type="text" name="nev" /> <br /> Neptunkód: <br /><input type="text" name="neptunkod" /> <br /> Évfolyam: <br /><input type="text" name="evfolyam" /> <br /> Szak: <br /><input type="text" name="szak" /> <br /> <input type="submit" value="beiratkozás" /> </form> </body> </html> 9
Model Binding Hogyan kerülnek a form adatok egy objektumba?! Eddigiekben átadtuk az összes form elem értékét paraméterként Ott is megfigyeltük, hogy a típusátalakítás automatikus (int.parse()) A Model Binder mechanizmus képes a form adatokból objektumot példányosítani 10
Model Binding Működési követelmények Az osztálynak legyen paraméternélküli konstruktora Tulajdonságai írhatók és publikusak legyenek A HTML Form name attribútumai egyezzenek meg a Model osztály tulajdonság neveivel Pozitívumok Nem szükséges konstruktort írnunk rengeteg this.valami = valami jellegű sorral 11
Tesztelés Nem jelennek meg a felvitt hallgatók az Indexen Debug a Post Action-re! Láthatjuk, hogy megérkezett a model, és a Model Binder is tökéletesen beillesztette a form értékeit az egyes tulajdonságokba Probléma: A Controller minden egyes Action híváskor újrapéldányosul. Teljesen új listánk van az Index hívásakor. 12
Megoldás Repository tervezési minta Készítünk egy osztályt, ami a tárolásért felelős Ennek az osztálynak a metódusait fogjuk hívogatni a Controllerből 13
2. HallgatoRepository.cs Osztályszintű tagként public class HallgatoRepository List<HallgatoModel> hallgatok; public List<HallgatoModel> GetAll get return hallgatok; public HallgatoRepository() hallgatok = new List<HallgatoModel>(); public void AddHallgato(HallgatoModel model) hallgatok.add(model); kezeljük a listát A listát tulajdonságként visszaadjuk Később majd interfésszel Konstruktorban példányosítjuk a listát Biztosítunk metódust arra, hogy új hallgatót lehessen felvenni 14
2. Módosított Controller Osztályszintű tagként public class HomeController : Controller HallgatoRepository tarolo; public HomeController() tarolo = new HallgatoRepository(); kezeljük repót Példányosítjuk a repót public IActionResult Index() return View(tarolo.GetAll); [HttpGet] public IActionResult Add() return View(); Továbbra is fennáll a probléma, hogy a Controller újrapéldányosul, és új repónk lesz így [HttpPost] public IActionResult Add(HallgatoModel model) tarolo.addhallgato(model); return RedirectToAction("Index"); 15
Megoldás Valahol máshol példányosítjuk a repót! Startup.cs? Hogy adjuk át? Sehol nem találjuk a Controller konstruktorhívását, hogy beletegyük Dependency Injection + Singleton minta Egy függőség-beszúrás nevezetű mechanizmus példányosít nekünk egyet a repóból és automatikusan átadja a Controller konstruktorának Singleton minta: az osztályunkból egy példány létezik, és bárhol példányosítjuk le, az mindig ugyanarra a memóriában lévő objektumra mutat. 16
Megvalósítás Controller módosítása Startup.cs: ConfigureServices() kiegészítése 17
Transient vs Singleton services.addtransient<hallgatorepository, HallgatoRepository>(); Mindig új példányt ad át Hogyha az osztály egy adatbáziskapcsolatot fed el, akkor ez bőven elegendő services.addsingleton<hallgatorepository, HallgatoRepository>(); Hogyha az átadott osztály fontos, hogy megőrizze a változói, tagjai értékét, akkor ezt kell használni Tipikusan ha nincs adatbázis, hanem valamilyen adatszerkezet a tároló 18
Repository függetlenítés Repository osztály variációk Az osztályban van egy globális tömb, lista, dictionary, stb. Az osztály konstruktorában fájlt olvas, módosításkor fájlt ír Az osztály egy adatbázisba ír/olvas Az osztály API-n keresztül pl. google drive-ba dolgozik Repository függetlenítés Készítünk egy interfészt a repó funkciókkal Pl: Hozzáadás, Törlés, Módosítás, Minden lekérése, Minden törlése, stb. Az interfészt és az adott implementációt összerendeljük Új fajta tárolásra áttérve ezt az egy sort kell más implementációra cserélni 19
Nagy feladat: CRUD alkalmazás CRUD: Create, Read, Update, Delete Készítsünk nyilvántartó alkalmazást, amelyben dolgozókat tudunk menedzselni! Készítsünk egy olyan Repositoryt, amely XML fájlból tölti be a dolgozóinkat és minden módosítás után frissíti a fájlt (későbbiekben a fájlkezelést adatbázisra cseréljük) A Repository-t interfészen át használjuk, hogy cserélhető legyen 20
Probléma Az update és a delete mint akciógombok megjelennek minden dolgozó mellett GET kérés: elküldjük az adott elem hash-ét Object őstől örökölt függvény: GetHashCode() felüldefiniálható A view-ban a törlő és módosító linknél megadhatjuk közvetlenül a hash-t (ekkor id névvel érjük el a Controllerből) Vagy beparaméterezzük a GET kérés URL-ét Repositoryban adunk lehetőséget hash alapján keresni 21
Custom binding A formon a fizetés ezer forintra kerekítve jelenik meg (pl: 100 e Ft) Érdemes lenne a Modelben ténylegesen 100000-et tárolni mindenféle belső folyamatok (bérkalkuláció, stb.) miatt Egyszerű probléma, de rengeteg ilyenbe fogunk belefutni később (pl: radiobutton -> enum konverzió) A Model Bindert utasítjuk, hogy a saját konvertáló osztályunkat hívja meg és azzal végezze a konverziót Teljesen testre tudjuk szabni, hogy a form elemei hogyan kerüljenek az objektumba WPF-ben a Converter osztályok működéséhez nagyon hasonló! 22
Custom binding Data mappa -> WorkerModelBinder.cs public class WorkerModelBinder : IModelBinder public Task BindModelAsync(ModelBindingContext bindingcontext) if (bindingcontext == null) throw new ArgumentNullException(nameof(bindingContext)); var name = bindingcontext.valueprovider.getvalue("name"); var job = bindingcontext.valueprovider.getvalue("job"); var salary = bindingcontext.valueprovider.getvalue("salary"); WorkerModel model = new WorkerModel() Name = name.firstvalue, Salary = int.parse(salary.firstvalue) * 1000, Job = job.firstvalue ; bindingcontext.result = ModelBindingResult.Success(model); return Task.CompletedTask; 23
Custom binding Attribútummal jelezzük a Model osztályunkon, hogy melyik Custom Binder osztályt használja fel a konverzióra 24
Custom binding fordítottja Szimplán a View-ban visszaalakítjuk <input type="number" name="salary" value="@(model.salary/1000)" 25
Custom binder alkalmazása Olyan esetekben, amikor valami formátum probléma van az űrlapelem tartalma -> osztály között, amit manuálisan kell megoldani Példa: Megadunk egy évet text inputban Kiválasztunk egy oktatási hetet legördülő listából De a Modelben nekünk egy kezdő és egy vég DateTime kell Ezt két DateTime-ot a két mezőből kell kiszámoltatni Ezt nyilván a Model Binder magától nem tudja 26
AutoMapper Hasonló, mint a Model Binder, de mégsem Használati eset: Vannak adatok, amelyek repó-ba kellenek, view-ba nem Vannak adatok, amelyek view-ba kellenek, repó-ba nem Vannak adatok, amelyek úgy kellenek a view-ba, hogy a Modelt ki kell egészíteni valamivel (pl. aktuális idő) Megvalósítás: ViewModel készítés CarModel Repository AutoMapper CarViewModel View 27
ViewModel (eml.) 3. Repository Models Busines Logic Controller 4. Model ViewModel Car Model Person Model View Amikor a Controller adatot továbbít a View számára, tipikusan egy darab objektumot adunk át. Abban az esetben, hogyha egy Autó objektumot és a rendelkezésünkre álló egyenleget akarjuk megjeleníteni együtt, akkor nem az adott Autó Model osztályt adjuk át, hanem létrehozunk egy ViewModelt, amely egy átmeneti megjelenítési célú összetett objektum. 28
Feladat Egészítsük ki a dolgozó CRUD alkalmazást azzal, hogy a felvett dolgozónak megadjuk a felettesét is! Egy legördülő listából engedjük kiválasztani View-nak egy objektumot adunk át Ez az új dolgozónál nem probléma, mert átadjuk a meglévő dolgozókat Módosításnál? Mi kell a view-ba? A meglévő dolgozó objektum Az összes dolgozó listája Megoldás: WorkerViewModel Tartalmazza a dolgozót, meg a többieket is 29
1. AutoMapper telepítése Tools -> NuGet Package Manager -> Manage NuGet Packages for Solution 30
2. AutoMapper használata Egyszerű bemutató példa (nem a nagyfeladat) Kutya osztály Név Életkor KutyaDetails osztály Név Életkor Feladat: Azonosító (név hossza * életkor) Írni egy automatizmust, ami bármikor Kutya osztályból KutyaDetails osztályra tér át, vagy fordítva 31
3. Modelosztályaink public class Kutya public string Nev get; set; public int Eletkor get; set; public class KutyaDetails public string Nev get; set; public int Eletkor get; set; public int Azonosito get; set; 32
4. Konverter osztály Azonos nevű propertyk: maguktól átkerülnek public class KutyaProfile : Profile public KutyaProfile() //Kutya -> KutyaDetails áttérés CreateMap<Kutya, KutyaDetails>().ForMember(dest => dest.azonosito, options => options.mapfrom( src => src.nev.length * src.eletkor )).AfterMap((kutya, kutyadetails) => //itt lehet bonyolultabb kódot írni kutyadetails.nev = kutya.nev.toupper(); ); A -> B property ForMember részben A -> B bonyolult kód AfterMap részben //KutyaDetails -> Kutya áttérés CreateMap<KutyaDetails, Kutya>().ForMember(dest => dest.eletkor, options => options.mapfrom( src => src.eletkor + 1 )); 33
5. Felhasználás Controllerben osztályszintű tag, példányosítás 34
5. Felhasználás Action-ökben felhasználás 1: jön egy KutyaDetails, átalakítjuk Kutyára 2: jön egy Kutya, átalakítjuk KutyaDetails-re 35
5. AutoMapper mindenhol Controllerünkben mindenhol elérhető a mapper Jó lenne, ha további Controllerek is tudnák használni Kivisszük a Mappert a Startup osztályba és Singletonként használjuk bárhol 36