Skip to content

BMEVIAUBB04/gyakorlat-server-side-rendering

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 

Repository files navigation

ASP.NET Core MVC

Célkitűzés

Egyszerű szerver oldalon renderelt felületek készítésének alapszintű elsajátítása ASP.NET Core technológia segítségével.

Előfeltételek

A labor elvégzéséhez szükséges eszközök:

  • Microsoft SQL Server (LocalDB vagy Express edition, Visual Studio telepítővel telepíthető)
  • Visual Studio 2022 .NET 6 SDK-val telepítve

Amit érdemes átnézned:

  • EF Core előadás anyaga
  • ASP.NET Core Web API (kliens renderelt) előadás anyaga
  • ASP.NET Core MVC, Razor Pages (szerver rendererelt) előadás anyaga
  • A használt adatbázis sémája

Feladat 0: Kiinduló projekt letöltése, indítása

Az előző laborokon megszokott adatmodellt fogjuk használni MS SQL LocalDB segítségével. Az adatbázis sémájában néhány mező a .NET-ben ismeretes konvencióknak megfelelően átnevezésre került, felépítése viszont megegyezik a korábban megismertekkel.

  1. Töltsük le a korábban már használt GitHub repository-t a repository főoldaláról (https://github.com/BMEVIAUBB04/gyakorlat-rest-web-api > Code gomb, majd Download ZIP) vagy a közvetlen letöltő link segítségével.
  2. Csomagoljuk ki
  3. Nyissuk meg a kicsomagolt mappa AcmeShop alkönyvtárban lévő solution fájlt.

A kiinduló solution egyelőre egy projektből áll:AcmeShop.Data: EF modellt, a hozzá tartozó kontextust (AcmeShopContext) tartalmazza. Hasonló az EF Core gyakorlaton generált kódhoz, de ez Code-First migrációt is tartalmaz (Migrations almappa).

Feladat 1: Webes projekt elkészítése

  1. Adjunk a solutionhöz egy új web projektet

    • Típusa: ASP.NET Core Web App (Model-View-Controller) (nem Web Api!, nem sima Web App, fontos a zárójeles rész!)
    • Neve: AcmeShop.Mvc
    • Framework: .NET 6.0
    • Authentication type: None
    • HTTPS, Docker: kikapcsolni
  2. Függőségek felvétele az új projekthez

    • adjuk meg projektfüggőségként az AcmeShop.Data-t
    • adjuk hozzá a Microsoft.EntityFrameworkCore.Design NuGet csomagot
  3. Adatbáziskapcsolat, EF beállítása

    • connection string beállítása a konfigurációs fájlban (appsettings.json). A nyitó { jel után
     "ConnectionStrings": {
       "AcmeShopContext": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=AcmeShop"
     },
    • connection string kiolvasása a konfigurációból, AcmeShopContext példány konfigurálása ezen connection string alapján, AcmeShopContext példány regisztrálása DI konténerbe. Program.cs-be, a builder.Build() sor elé:
    builder.Services.AddDbContext<AcmeShopContext>(
        options => options.UseSqlServer(
            builder.Configuration.GetConnectionString(nameof(AcmeShopContext))));
  4. Ha van már adatbázis AcmeShop néven, töröljük le.

  5. Fordítsuk a solutiont.

  6. Adatbázis inicializálása Package Manager Console (PMC)-ban

    • Indítandó projekt az AcmeShop.Mvc projekt legyen (jobbklikk az AcmeShop.Mvc-n > Set as Startup Project)
    • A PMC-ben a Defult projekt viszont az AcmeShop.Data legyen
    • PMC-ből generáltassuk az adatbázist az alábbi paranccsal
    Update-Database
  7. Projekt indítása. Próbáljuk ki a jelenleg elérhető oldalakat.

Feladat 2: Adatbázis objektumok lekérdezése és megjelenítése

Az eddig legenerált MVC oldalak nem használták az adatbázisunkat. Vegyünk fel új kontrollereket és nézeteket, melyek segítségével le tudjuk kérdezni az adatbázist (a kontroller feladata) és az eredményt HTML-be tudjuk formázni (ez a nézetek feladata)! A leggyorsabb módja ennek a kódgenerálás (scaffolding).

  1. Adjunk hozzá az MVC projekthez a Microsoft.VisualStudio.Web.CodeGeneration.Design NuGet csomagot.
  2. Az AcmeShopContext.cs alján (Data projekt) kommentezzük vissza az AcmeShopContextFactory osztályt. (Erre nem kellene szükség legyen, valószínűleg a generátorban lévő bug miatt kell mégis.)
  3. Fordítsuk az MVC projektet.
  4. PMC-ben telepítsük az ASP.NET Core kódgeneráló eszközt, ha még korábban nem telepítettük az adott gépen
    dotnet tool install -g dotnet-aspnet-codegenerator
  5. Lépjünk be a projekt könyvtárába
    cd .\AcmeShop.Mvc
  6. Generáljunk a kódgenerálóval kontrollert és kapcsolódó nézeteket a Termek entitáshoz (-m), mely a AcmeShopContext kontextushoz (-dc) tartozik. A generált kontroller neve legyen TermekController (-name), az AcmeShop.Mvc.Controllers névtérbe (-namespace) kerüljön. A generált fájl a Controllers mappába (-outDir) kerüljön. A generált nézetek használják az alapértelmezett (projektben már meglévő) layoutot (-udl).
    dotnet aspnet-codegenerator controller -m AcmeShop.Data.Termek -dc AcmeShop.Data.AcmeShopContext -outDir Controllers -name TermekekController -namespace AcmeShop.Mvc.Controllers -udl
  7. Kommentezzük ki az AcmeShopContextFactory osztályt.
  8. Vegyünk fel egy új navigációs lehetőséget a Views\Shared_Layout.cshtml-be. Másoljuk le a Home menüpontot reprezentáló <li> címkét saját maga alá és írjuk át az asp-controller értékét az <a> gyerekcímkében Termekek-re és az <a> címkék közötti szöveget is a kívánt menüpont névre, pl. Termékek. A teljes <li> valami az alábbihoz hasonló lesz:
<li class="nav-item">
    <a class="nav-link text-dark" asp-area="" asp-controller="Termekek" asp-action="Index">Termékek</a>
</li>

Vegyük észre, hogy nem állítottuk be az <a> elem href tulajdonságát, helyette az asp-page TagHelpert használtuk, ami a kontroller és a kontrolleren belüli művelet neve alapján az az URL-t fogja nekünk generálni az elkészülő HTML-be, amit böngészőben navigálva a megadott action hívódik meg.

Teszteljük az így létrejött alkalmazást, teszteljük a termékekkel kapcsolatos funkciókat! A tesztelt oldalak forráskódját is tekintsük át (a kontrollert és a nézeteket és is)!

Scaffolding eredménye

A legtöbb funkció alapvetően működőképes, de lehet találni hibákat, kényelmetlenségeket. Például:

  • A törlés általában csak akkor működik, ha általunk felvett termékről van szó (például a Create New link segítségével). A legtöbb alapból felvett terméknek ugyanis van már valamilyen érintettsége idegen kulcs kényszerben.
  • A kapcsolódó elemeknek (kategória, áfakulcs) csak az azonosítójukkal szerepelnek, ez elég kényelmetlenné tesz minden funkciót ahol megjelennek - pl. nem tudjuk melyik kategóriát jelentik az egyes azonosítók.

Feladat 3: Listázó nézet szépítése

A Views/Termekek/Index.cshtml felelős a termékek listájának megjelenítéséért. A TermekekController.Index ugyan nem adja meg, hogy ez a nézet legyen a szerencsés, de a nézet felderítés algoritmusa (Views/[Kontroller név]/[Függvény név]) ezt jelöli ki.

Vizsgáljuk meg ezen nézet kódját és végezzük el az alábbi módosításokat.

  1. Írjuk át az oldal címét (HTML <title> címke). Ez a böngésző címsorában vagy a böngészőfülön megjelenő felirat. Ezt MVC-ben általában nem a nézet írja ki közvetlenül (<title> tag-et használva), hanem valamelyik layout nézet. Esetünkben konkrétan a Views/Shared/_Layout.cshtml. Figyeljük meg ez utóbbi nézet kódjában a <title>@ViewData["Title"] - AcmeShop.Mvc</title> sort. Ebből látható, hogy a ViewData szótárban a Title kulcshoz tartozó érték kerül a <title> címkébe. Szerencsére ezt az értéket az Index.cshtml-ben is állít(hat)juk. A nézet tetején írjuk át a vonatkozó C# blokkban a beállított értéket.

    @{
        ViewData["Title"] = "Terméklista";
    }
  2. Rögtön ezalatt egy <h1> címke található, ami az oldal címsorát adja. Ezt is írjuk át, pl.

    <h1>Termékek listája</h1>
  3. Ez a nézet ne jelenítse meg a képet és a leírást. Töröljük/kommentezzük ki (@*...*@) HTML táblázat fejlécéből és törzséből is.

    @*<th>
        @Html.DisplayNameFor(model => model.Leiras)
    </th>
    <th>
        @Html.DisplayNameFor(model => model.Kep)
    </th>*@
    @*<td>
        @Html.DisplayFor(modelItem => item.Leiras)
      </td>
      <td>
        @Html.DisplayFor(modelItem => item.Kep)
      </td>*@
  4. A táblázatban a fejléc feliratok egy az egyben a property-k nevei, ami felhasználói szemmel elég ronda. Ezen feliratokat a DisplayNameFor HTML helper generálja. Ez a helper alapvetően csak utolsó lehetőségként használja a property nevét, előnyben részesíti a property-re tett Display attribútumot (a System.ComponentModel.DataAnnotations névtérből). Tegyük fel ezen attribútumot első körben a Kategoria property-re (a Data projekt Termek osztályában).

    [Display(Name = "Kategória")]
    public Kategoria? Kategoria { get; set; }

    Éles felhasználásra készülő projektekben gyakran inkább resource fájlokba szervezzük a feliratszövegeket és onnan hivatkozzuk, mert úgy könnyebb a többnyelvűsítés.

  5. A kapcsolódó elemekből (áfakulcs, kategória) a táblázatban csak az azonosítók szerepelnek. A megjelenítendő propertyt a DisplayFor HTML helper jelöli ki. Írjuk át a kategória megjelenítéséért felelős DisplayFor hívást, hogy az azonosító helyett a nevet használja.

    @*@Html.DisplayFor(modelItem => item.Kategoria.Id)*@
    @Html.DisplayFor(modelItem => item.Kategoria.Nev)
  6. Próbáljuk ki a változásokat, ellenőrizzük, hogy a következők megváltoztak-e:

  • oldal címe
  • oldal címsora
  • leírás, kép oszlopok
  • kategória oszlop fejléce
  • kategória oszlop értéke

Scaffolding eredménye

Bónuszként láthatjuk, hogy a termék részletes oldalon (Details link) is a kategória neve megváltozott, mert ugyanazon Display attribútumot használja a HTML helper (@Html.DisplayNameFor(model => model.Kategoria)).

Feladat 4: Termék létrehozási folyamat továbbfejlesztése

A termékek szerkesztő és létrehozás (Edit, Create) oldalain láthatjuk, hogy a generált kódunk felismerte a kapcsolódó entitásokat is, amiket legördülő listából választhatunk ki. Ez nagyon jó, de az azonosító alapján nehezen tudjuk megmondani a helyes termékkategóriát és ÁFA-kulcsot, így helyesebb volna a nevüket megjeleníteni a felületen.

  1. Vizsgáljuk meg az új termék létrehozásához tartozó műveletet. Igazából két műveletről van szó, a Create() művelet a /Termekek/Create címre navigáláskor hívódik meg, míg a másik HttpPost attribútummal ellátott változat ugyanezen címre egy űrlapot visszaküldenek. (HTML űrlapok visszaküldése tipikusan HTTP POST üzenettel történik). Az előbbi művelet felelős a létrehozó űrlap megjelenítéséért: feltölt néhány ViewData adatot, a többi a nézet (Views/Termekek/Create.cshtml) feladata. A ViewData-ba minden kapcsolódó elemhez egy-egy SelectList kerül. A SelectList a legördülő menükhöz használható modellként, összefogja a legördülő menühöz kapcsolódó listát, hogy a listaelemtípus melyik propertyjét kell használni feliratként, illetve elemazonosítóként. Pont a feliratot szeretnénk állítani, ezért módosítsuk a kategóriához létrehozott SelectList-et:

    ViewData["KategoriaId"] = new SelectList(_context.Kategoria, nameof(Kategoria.Id), nameof(Kategoria.Nev));
  2. A kapcsolódó nézetből (Views/Termekek/Create.cshtml) töröljük/kommentezzük ki a leírás és a kép propertykhez generált részeket.

    @*<div class="form-group">
        <label asp-for="Leirasclass="control-label"></label>
        <input asp-for="Leirasclass="form-control" />
        <span asp-validation-for="Leirasclass="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Kepclass="control-label"></label>
        <input asp-for="Kepclass="form-control" />
        <span asp-validation-for="Kepclass="text-danger"></span>
    </div>*@
  3. A másik (POST-os) Create műveletben a paraméter Bind attribútummal van ellátva. Ebben azt a lehető legszűkebb tulajdonsághalmazt érdemes megadni, ami a művelet elvégzéséhez kell. Ez amiatt kell, hogy vicces kedvű vagy támadó szándékú kolléga ne tudjon olyan HTTP POST kérést készíteni, amiben olyan tulajdonságokat is kitölt, amiket nem akarunk a HTTP kérésben érkező adatok alapján tölteni. Innen is érdemes kivenni a kép és leírás tulajdonságokat, hiszen ezeket már eleve meg se lehet adni az űrlapon.

    public async Task<IActionResult> Create([Bind("Id,Nev,  NettoAr,Raktarkeszlet,AfaId,KategoriaId")] Termek termek)
  4. Tegyünk töréspontot mindkét Create műveletre.

  5. Debug módban indítva a projektet új termék felvételével próbáljuk ki, hogy a kategória legördülő menü jól működik-e. Kövessük végig a folyamatot debuggerrel ([F10] gombbal léptetve).

Feladat 5: Validáció

Az adatbázisban a termék neve legfeljebb 50 karakter hosszú lehet. Bár az adatbázis nem kényszeríti ki, az ár és a raktárkészlet esetében csak pozitív (vagy nulla) értékeknek van létjogosultsága. Érvényesítsük ezeket a validációs szabályokat!

  1. A Termek típus (Data projekt) vonatkozó property-jeire helyezzünk el modell validációs attribútumokat.

    [MaxLength(50)]
    public string? Nev { get; set; }
    
    [Range(0, double.MaxValue, ErrorMessage = "A termék ára nem lehet negatív")]
    public double? NettoAr { get; set; }
    
    [Range(0, int.MaxValue, ErrorMessage = "A termék raktárkészlete nem lehet negatív")]
    public int? Raktarkeszlet { get; set; }

    Az EF ugyan eddig is tudta, hogy a név maximum 50 hosszú lehet, a kontext OnModelCreating függvényében van leírva, de ezt az ASP.NET Core MVC nem veszi figyelembe, csak bizonyos C# attribútumokat. Ha ez zavar minket, érdemes a kliens renderelt esethez hasonlóan külön modell réteget létrehozni az ASP.NET Core számára, így az EF és az MVC modell beállításai nem keverednek.

  2. Próbáljuk ki a validációt valamelyik termék szerkesztésével. Az oldal HTML forrásában ellenőrizhetjük, hogy a maximális szöveghosszt a HTML input mezőre tett maxlength attribútummal oldotta meg az ASP.NET Core. A raktárkészlet és az ár ellenőrzése viszont csak a mentés gomb hatására szerveroldalon történik meg. Ezt is ellenőrizhetjük, ha a HTTP POST kérésre reagáló Edit kontrollerműveletre teszünk töréspontot. A művelet kódjában látható, hogy az ellenőrzés eredményét a ModelState.IsValid property lekérdezésével kapjuk meg.

  3. A legtöbb ellenőrzés annyira egyszerű, hogy böngészőben futó JavaScript kóddal is ellenőrizhető, a szerverhez fordulni ezért felesleges. Az ASP.NET Core számos beépített validációs attribútumhoz legenerálja a kliensoldali ellenőrzéshez szükséges kódot, HTML attribútumokat. Az ellenőrzést a böngészőben a jQuery űrlap validációs könyvtár végzi. Azon nézetekben, ahol kliensoldali validációt akarunk használni ezt a JavaScript könyvtárat hivatkozni kell. A hivatkozások már meg vannak írva a _Views/Shared/_ValidationScriptsPartial.cshtml nézetfájlban. A layout nézetünk másrészről eleve definiál egy helyet (section) ahová a nézeteknek ezeket a hivatkozásokat elhelyezni érdemes. A _Views/Shared/_Layout.cshtml végén látható a szekció definííciója:

    @await RenderSectionAsync("Scripts", required: false)

    Így nincs is más dolgunk, mint a nézetünkben (Edit.cshtml) kitöltsük a Scripts nevű szekciót a _ValidationScriptsPartial.cshtml tartalmával. A nézet végére:

    @section Scripts {
        @{await Html.RenderPartialAsync ("_ValidationScriptsPartial"); }
    }
  4. Próbáljuk ki újra a szerkesztést. Így már az adott érték (ár, raktárkészlet) szerkesztése után, ha a szövegmező elveszti a fókuszt, de még a mentés gomb megnyomása előtt is kapunk visszajelzést, ha nem megfelelő a bevitt érték. Törésponttal ellenőrizhetjük, hogy a mentés gomb nyomogatása nem okoz szerver hívást, ha nem megfelelő a bevitt érték.

Önálló feladatok

Feladat 1: Nézetek szépítése

Listázó nézet (Index.cshtml)

Cseréld le a műveletek linkfeliratát emoji-kra. Az emojikat HTML entitásokként adjuk meg. Ahhoz, hogy az emoji ne legyen aláhúzva (ami szövegeknél jól mutat, képeknél nem annyira) a text-decoration-none CSS osztályt alkalmazhatjuk. Az alábbi példa az Edit művelethez tartozó linkre alkalmazza a fentieket, és egy ceruza emojit használ feliratként.

<a asp-action="Edit" asp-route-id="@item.Id" class="text-decoration-none">&#9999;</a>

További műveletekhez használható emoji-k. A HTML entitás kódját nézzük meg a linkelt dokumentációban:

  • Új (Create) - +
  • Részletek (Details) - Memó
  • Törlés (Delete) - X

Készítő nézet (Create.cshtml)

A korábbi feladatok alapján módosítsuk az oldalt az alábbiak szerint.

  • Oldal címe (HTML title): Termékfelvétel
  • Címsor (HTML h1): Új termék
  • Alcímsor (HTML h4): Termék
  • Létrehozás gomb felirat (HTML input submit): Új
  • Vissza a listázó oldalra link (Index művelet): <- emoji

Törlés nézet (Delete.cshtml)

A korábbi feladatok alapján módosítsuk az oldalt az alábbiak szerint.

  • Oldal címe (HTML title): Terméktörlés
  • Címsor (HTML h1): Termék törlése
  • Alcímsor (HTML h3): Biztosan törli?
  • Alcímsor2 (HTML h4): Termék
  • Kép és leírás propertykhez tartozó részek törlése/kommentezése (2 db. <dt>-<dd> pár)
  • Vissza a listázó oldalra link (Index művelet): <- emoji
  • Törlés gomb felirat (HTML input submit): Törlés

Részletes nézet (Details.cshtml)

A korábbi feladatok alapján módosítsuk az oldalt az alábbiak szerint.

  • Oldal címe (HTML title): Termékadatok
  • Címsor (HTML h1): Termék részletek
  • Alcímsor2 (HTML h4): Termék
  • Kép és leírás propertykhez tartozó részek törlése/kommentezése (2 db. <dt>-<dd> pár)
  • Vissza a listázó oldalra link (Index művelet): <- emoji
  • Szerkesztő oldalra link (Edit művelet): ceruza emoji

Szerkesztés nézet (Edit.cshtml)

A korábbi feladatok alapján módosítsuk az oldalt az alábbiak szerint.

  • Oldal címe (HTML title): Termék módosítás
  • Címsor (HTML h1): Termék módosítás
  • Alcímsor (HTML h4): Termék
  • Kép és leírás propertykhez tartozó részek törlése/kommentezése (2 db. <div class="form-group"> rész)
  • Mentés gomb felirat (HTML input submit): Mentés
  • Vissza a listázó oldalra link (Index művelet): <- emoji

Feladat 2: Feliratok megadása modellattribútumokkal

A korábbi feladatok alapján a Display C# attribútum módosítsuk az alábbi property-k nevének megjelenítését.

  • Nev -> Név
  • NettoAr -> Nettó ár
  • Raktarkeszlet -> Raktárkészlet
  • Afa -> Áfakulcs

Feladat 3: Áfakulcs megjelenítése

A korábbi feladatok alapján a termék listázó oldalon (Index.cshtml) a termékhez kapcsolódó Afa példány azonosítója (Id) helyett az áfakulcs értékét írjuk ki.

Feladat 4: Törlés biztonságosabbá tétele

A törlés funkció általában hibára fut, mert a termék idegen kulcs kényszerben érintett és alapesetben ilyenkor a terméket nem engedi törölni az adatbázis.

A törlés funkció adatkezelő alkalmazásokban egy erősen átgondolandó művelet, sok féle hozzáállás lehetséges, a megrendelői igényektől és a komplexitástól is függ, hogy melyik irány a legmegfelelőbb. Néhány lehetséges irány:

  • (bizonyos) idegen kulcs kényszerek átállítása, hogy az adatbázis engedje a kaszkád törlést
  • logikai törlés (soft delete) - igazi törlés helyett csak egy oszlop értékének átállításával jelezzük, hogy az adott rekordot törölték
  • korlátozni a törlést csak függőséggel nem rendelkező elemekre

Most az utóbbi irányt kövesd: a törlés jóváhagyó oldalra (Delete.cshtml a kapcsolódó nézet) navigáláskor vizsgáld meg, hogy az adott terméknek van-e függősége. Ezt is többféleképp lehet megtenni. EF lekérdezéssel meg lehet nézni minden olyan kapcsolatot, amiben Termek entitás érintett. Másik lehetőség, hogy egy nem commitolódó tranzakcióban töröljük a terméket. Ha nem keletkezik kivétel, engedjük az igazi törlést. Egyik módszer sem 100%-os, mert a törlés engedélyezése és a törlés gomb megnyomása között megváltozhat a helyzet, például egy másik felhasználó függőséget létrehozó műveletet végezhet.

A megoldás vázlata:

  1. A törlést jóváhagyó oldalra (műveletre) navigáláskor a kontrollerműveletben már most is van pár ellenőrzés (azonosító meg van-e adva, az adott azonosítóval van-e termék). Ezen meglévő ellenőrzések után valósítsd meg a törlést előellenőrző logikát. Az alábbi kódrészlet segítség a tranzakciós módszerhez:

    try
    {
        using var tr = await _context.Database. BeginTransactionAsync();
        _context.Termek.Remove(termek);
        await _context.SaveChangesAsync();
        tr.Rollback();
    }
    catch(DbUpdateException)
    {
        
    }

    A fenti kódrészletben ha a catch ágba akkor jut a végrehajtás, akkor a terméket nem szabad törölni. A tranzakció sosem fog commitolni, akkor sem, ha kivétel keletkezik.

  2. Ha az ellenőrzés alapján nem törölhető a termék, adj át a nézetnek erről egy hibaszöveget.

  3. A kapcsolódó nézetben, ha a hibaszöveg be van állítva, jelenítsd meg a szöveget egy figyelmeztető sávban.

  4. Ugyanezen nézeten a tényleges törlést elvégző gombot tiltsd le, ha a hibaszöveg be van állítva. A letiltáshoz elég a disabled HTML attribútumot generálni érték nélkül. Segítségképp alább egy példa az attribútum felhelyezésére:

    <input type="submit" value="Felirat" @(feltetel ? "disabled" : "")/>

    A feltetel helyére egy logikai kifejezést kell behelyettesíteni, ami akkor igaz, ha a gombot le kell tiltani.

Feladat 5: Legördülő menük felhasználóbarátabbá tétele

A korábbi feladatok alapján a kontrollerben lévő minden SelectList konstruktorhívást módosítsd, hogy a legörülő menük felhasználóbarátabbak legyenek. Az áfa azonosító helyett a felirat az áfakulcs legyen, kategória azonosító helyett pedig a kategória neve. Csak a string konstans (macskakörmös) paramétereket kell módosítani. string konstans (macskaköröm) helyett használj nameof operátort.


Az itt található oktatási segédanyagok a BMEVIAUBB04 tárgy hallgatóinak készültek. Az anyagok oly módú felhasználása, amely a tárgy oktatásához nem szorosan kapcsolódik, csak a szerző(k) és a forrás megjelölésével történhet.

Az anyagok a tárgy keretében oktatott kontextusban értelmezhetőek. Az anyagokért egyéb felhasználás esetén a szerző(k) felelősséget nem vállalnak.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published